1. 嵌入式系统概述
前面我们完成了C语言、数据结构与算法、文件IO、并发程序设计、网络编程等课程的学习。我们的开发环境是这样的

而底层开发课程是在以下硬件的基础上完成的。


前面课程不用关心底层硬件,直接通过open、read、write就可以读写磁盘上文件,通过调用socket、bind、listen、accept、 recv、send就通过网卡进行数据的收发,这些完全得益于操作系统的存在。是操作系统帮我们屏蔽了底层硬件的细节,使得我 们可以专注于上层应用业务逻辑的设计。如果我们把操作系统拿掉,那作为一个软件工程师就要直面硬件,搞清楚软件是如何 驱动硬件的、软件是如何在特定硬件平台上运行的,如何把Linux系统在我们特定的硬件平台上运行起来...,这些问题搞定了,那 你也就是一名优秀的嵌入式底层软件开发工程师了。
而我们接下来的三门课程:ARM体系结构与接口技术、Linux系统移植、Linux驱动程序开发就是为了达成这个目标而设计的。
其中ARM体系结构与接口技术这门课程重点在于告诉大家软件是如何控制硬件的,它是后续两门课程的基础;Linux系统移植课 程的重点是教大家如果把Linux内核源码移植、编译通过,能够让Linux系统在我们的开发板上运行起来;而Linux驱动程序开发 课程是教大家如何在Linux环境下编写诸如LCD、触摸屏、网卡、各种传感器等设备的驱动程序的一门课程。如果你的Linux系 统在我们的平台上可以正常运行了,各种外设硬件也可以正常驱动了,你会发现我们前面学习到的文件IO、多任务、网络编程 这些内容,在我们的开发板同样可以玩了。
1.1 嵌入式的定义
- 嵌入式系统是以应用为中心,以计算机技术为基础,软、硬件可裁剪,适应应用系统对功能、可靠性、成本、体积及 功耗严格要求的专用计算机系统。
1.1.1 硬件系统
- 处理器,arm、51、mips、powerpc、intel、龙芯、GD32
- 电源电路,为器件提供能源
- 复位电路
- 时钟电路
- 存储电路
— 掉电数据不丢失的存储器件:ROM、PROM 、EPROM、EEPROM、nandflash、norflash、 SD卡、 EMMC
— 掉电数据丢失的存储器件: RAM、SRAM、DRAM、DDRAM(电脑上主流)
1.1.2 软件系统
- OS, 操作系统
— linux / uclinux/ Rtlinux
linux属于非实时操作系统。 任务的调度策略:
在多任务的操作系统中,CPU只有一个 多个任务都准备就绪,CPU分配给哪个任务 以什么样的原则分配CPU
该原则就叫做调度策略 linux系统中使用的主要调度策略:时间片轮转 计算机中常见的调度策略: 时间片轮转、基于优先级的调度、先来先服务
— HarmonyOS
— FreeRTOS、RT-Thread
开源免费。RT-Thread中国人自己搞的。
— μcos-II
商业使用需要收费。
— Vxworks
工具多,实时性、稳定性毋庸置疑(美国的火星探测器上用的就这玩意)。但是收费,而且很贵(几十万RMB)。
— 无操作系统
裸板开发。
1.1.3 开发模式

2. ARM寄存器组织与工作模式
2.1 SoC组成结构
2.1.1 物理结构
还是以上那块板子,最重要的元器件就是SoC (system on chip)了。


2.1.2 逻辑结构

2.2 ARM基本概念
2.2.1 ARM的含义
- ARM,Advanced RISC Machines。有着三层含义: 英国一家电子公司的名字 https://www.arm.com/ 成立于1990年。2016年被日本软银收购。2020年英伟达收购未成功。
- ARM开发的RISC处理器。而现在我们把这一类的处理器称为ARM。
- ARM还可以理解为是一种技术
ARM公司是专门从事基于RISC技术芯片设计开发的公司,作为知识产权供应商,本身不直接从事芯片生产,世界各大半 导体生产商从ARM公司购买其设计的ARM微处理器核,根据各自不同的应用领域,加入适当的外围电路,从而形成自己 的ARM微处理器芯片进入市场。
注意,ARM公司只做芯片设计,不生产芯片。
2.2.2 指令集架构与内核架构
指令集,指令集主要是指Cpu硬件和软件之间的接口描述,它本质上是一段二进制机器码,cpu只能识别机器码,但是机器码是 一串无意义的字符串,程序员很难看看懂这些语句,用它来开发软件,所以后面就发明了汇编语言,汇编语言本质上跟机器码 一一对应的,现在有很多不同版本的汇编语言,本质上就是有不同的指令集。ISA(Instruction Set Architecture)指令集架 构。ARM架构通常指指令集架构。
-
目前,世界上的指令集架构宏观上分为CISC和RISC。
CISC
Complex Instruction Set Computer, 复杂指令集。 指令多、变长指令、多周期指令。RISC
Complex Instruction Set Computer,精简指令集。 指令少、定长指令、单周期指令。处理器内核(微架构),括了寄存器组、指令集、总线、存储器映射规则、中断逻辑和调试组件等。 内核是由ARM公司设计并 以销售方式授权给个芯片厂商使用的(ARM公司本身不做芯片)。 比如为高速度设计的Cortex A8、A9都是ARMv7a 指令 集;Cortex M3、M4是ARMv7m指令集。集成电路工程师在设计处理器时,会按照指令集规定的指令,设计具体的译码和运算电 路来支持这些指令的运行;指令集在CPU处理器内部的具体硬件电路的实现,我们就称为微架构,一套相同的指令集,可以由 不同形式的电路实现,可以有不同的微架构。以ARMV7指令集为例,基于该套指令集,面向高性能、低功耗等不同的市场定 位,ARM公司设计出了Cortex-A7、Cortex-A8、CortexA9、Co1tex-A15、Cortex-Al7等不同的微架构。内核架构

2.3 ARM编程模型
- 官网:www.arm.com 官方资料:
- ARMv7指令集手册
https://developer.arm.com/documentation/ddi0406/cd?lang=en
本地:datasheet\30-ARM\DDI0406C_d_armv7ar_arm.pdf - Cortex-A7手册
https://developer.arm.com/documentation/ddi0464/latest
本地:datasheet\30-ARM\DDI0464F_cortex_a7_mpcore_r0p5_trm.pdf 经典书籍:经典书籍\ARM体系结构与编程.pdf
2.3.1 ARM工作模式

- ARMv7架构中增加的两种工作模式:
Monitor模式,安全监控模式,为了安全而扩展出的用于执行安全监控代码的模式。
Hpy模式,针对虚拟化技术的支持。
- 前5种称为异常模式,用来处理系统异常
- 前6种称为特权模式。
- system模式是为了解决同类型异常嵌套而设计的。例如IRQ模式下执行了子函数调用,会把返回地址保存到LR_寄存器。此时如 果又产生了IRQ异常,硬件自动会把异常执行完的返回地址保存到LR_,覆盖掉了子函数调用后的返回地址。
为此,ARM v4及之后的版本提供了system mode这样一种处理器模式来解决这个问题。System mode是一种privileged的模 式,而且共用User模式的所有寄存器。Privileged模式的程序可以运行在这个模式,而不用但是处理器异常会擦除LR。
注意:System mode不是因为发生了某种异常处理器自动进入的模式,而是异常处理函数通过修改CPSR来进入的。
2.3.2 工作状态(指令集状态)
- 四种工作状态:
ARM 状态, 执行ARM指令(32bit)
Thumb状态, 执行Thumb指令(16bit)
ThumbEE状态, 16bit、32bit的混合指令
Jazelle状态, 对JAVA代码执行加速 - 其中重点记忆两种工作状态:
ARM状态、Thumb状态。
程序执行时可以完成ARM状态和Thumb的切换。
执行异常处理时,应该工作与ARM状态。
2.3.3 寄存器组织结构

- 通用:
- ARM核中有37个32bit的寄存器
- 其中31个通用寄存器,分别被命名为r0 r1 r2 ... r15
- 1个cpsr寄存器
- 5个spsr寄存器
- 每种工作模式下只能访问37个寄存器的一个子集
- 关于通用寄存器的说明
r13(sp, 栈指针寄存器)
r14(lr, 保存函数的返回地址)
r15(pc, 保存取指令的地址)- 关于cpsr/spsr的说明
cpsr, currented Program Statued Register :当前程序状态寄存器 spsr, Saved Program Statued Register: cpsr的备份寄存器
cpsr 和 spsr的格式:

[4:0] 模式位, 标识当前ARM核工作于哪种工作模式
[5] T=1, 处于Thumb工作状态
=0,处于ARM工作状态
[6] F =1/0 屏蔽/不屏蔽FIQ异常
[7] I =1/ 0 屏蔽/不屏蔽IRQ异常
[28] V位:Overflow, 对于加减法指令,在操作数和结果是有符号的整数时,如果发生溢出,则V=1, 反之V=0
[29] C位:Carry,运算过程中最高位有进位或者借位
[30] Z位:Zero,运算结果为0 Z=1 反之Z=0
[31] N位:Negative,运算结果为负值 N=1 反之N=0
2.3.4 三级流水线
- 为了提高指令的执行效率,ARM汇编指令采用流水线的方式执行。
- ARM流水线可以分为3级流水线,5级流水线,7级流水线,8级流水线,13级流水线。 cortex-A7中采用的是8级流水线。
- ARM的官方文档并没有对它的技术实现细节做过多描述。 对于大家来说,搞清三级流水线就够用了。
-
三级流水线:取指令、解码指令、执行指令
3. ARM异常处理
3.1 异常与异常向量表
什么是异常?
异常是指在计算机系统运行过程中发生的不正常情况,由于内部或者外部的一些事件所导致的,例如:IO错误、缓冲区溢出等都属于异常。-
计算机中对异常的处理套路
异常产生时,CPU就要将当前的程序暂停下来转而去处理这个异常的事件,异常事件处理完成之后再返回到被异常打断的点继续执行程序
-
ARM中的异常
-
如何处理异常
硬件上
ARM收到异常信号,首先介入处理异常的就是硬件(人家ARM就是这么设计的)。
硬件上收到异常信号,会执行啥操作呢?归纳起来就是:四大、三小。
— 拷贝CPSR中的内容到对应异常模式下的SPSR_<mode>
— 修改CPSR
按照需要CPSR.I=1 CPSR.F=1
CPSR.T=0(异常处理代码必须为ARM 指令,不能是Thumb指令)
修改CPSR.mode, 使其进入对应的异常工作模式
— 保存返回地址到对应异常模式下的LR_<mode>
— 给PC寄存器赋值
软件上
3.2 存储模型
支持的数据类型
Byte ---> 字节 ---> 8bits(1字节)
Half word ---> 半字 ---> 16bits(2字节)
word ---> 字 ---> 32bits(4字节)
Doubleword --->双字 ---->64bits(8字节)-
对齐方式
数据本身是多少字节在内存存储时就应该多少字节对齐。(超过4字节的按4字节对齐) 例如半字是2字节数据,就应该放置在2字节对齐的内存中 字是4字节数据,就应该放置在4字节对齐的内存中。 双字放置在4字节对齐的内存中(比较特殊)。
大小端
int a = 0x12345678;
地址 大 小
0x100 12 78
0x101 34 56
0x102 56 34
0x103 78 12
— 不同架构的芯片在存储以上int类型数据时,采用的策略是不同的。 X86采用小端模式,PowerPC采用大端模式。
— ARM默认是小端模式,但可以通过操作CP15协处理器切换为大端模式。
4. ARM指令集仿真环境搭建
4.1 为什么要学ARM汇编编程
- 从课程的角度来看
— C语言是操作不到ARM核的
— C语言是无法精准实现一条指令实现跳转的 - 从实际工作情况看,汇编有着以下使用场景
— 嵌入式系统需要初始化和中断服务程序中
— 所有的系统都需要调试,可能汇编指令级调试,深层次的BUG需要汇编级的调试
— 可以通过汇编语言编程来提升系统性能(优化)
— 有些指令编译器无法产生,只能通过汇编完成 - 综上,玩嵌入式软件开发,ARM汇编还是要了解一些的,尤其是做底层软件开发。
4.2 第一个ARM汇编程序
/*这是我的第一个汇编程序
比较简单
*/
.text
.global _start @将_start 声明为全局的
_start:
mov r0, #100 @r0 = 100
mov r1, #200 @r1=200
add r2, r0, r1 @r2=r0+r1
nop
b .
.end
汇编程序中一般由三部分内容组成: 指令、伪指令、伪操作。
ARM汇编程序中单行注释使用@符号, 多行注释 /* */
4.3 linux开发环境搭建
交叉编译工具:本身运行在PC(X86-64)的平台上,但它可以把程序翻译为ARM处理器能够认识的机器指令。
1)建立window和Linux的共享目录:

注意,如果看不到共享的文件夹,可以通过如下方式解决:
sudo apt-get autoremove open-vm-tools
sudo apt-get install open-vm-tools-desktop
sudo reboot
#重启后如果还看不到,可以执行
sudo /usr/bin/vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other -o uid=1000 -o gid=1000 -o umask=022
2)在ubuntu系统中安装gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.bz2
cd
mkdir toolchain/
cd toolchain
cp /mnt/hgfs/swap/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.bz2 ./
tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.bz2
vim ~/.bashrc
export PATH=$PATH:/home/linux/toolchain/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
source ~/.bashrc
#验证
arm-linux-gnueabihf-gcc -v
- 成功返回版本号
3)编译前面写好的汇编程序
arm-linux-gnueabihf-as first.s -o first
file first

4.4 Windows仿真环境搭建
4.4.1 安装keil

4.4.2 Keil破解
- 以管理员身份运行Keil
- 复制CID
- 关闭你电脑上的杀毒软件 自行关闭。
-
关闭windows系统的“病毒和威胁防护”"实时保护"
使用快捷键 WIN(键盘上的windows图标) + i 键启动,启动“Windows设置”
如果你的电脑Windows版本和笔者不同,请自行百度完成以上设置。 -
运行破解软件
做如下设置:
右键以管理员身份运行运行解压出的keygen.exe (若未关闭杀毒软件、实时保护会报病毒,自动删除该文件)
4.4.3 安装 ARM7/9 等设备支持包
双击运行MDK79525.EXE 一路下一步完成安装。
4.4.4 在 window 上安装 gcc 编译工具
在arm-2011.09-70-arm-none-linux-gnueabi.exe右键选择属性
做如下设置:

双击运行arm-2011.09-70-arm-none-linux-gnueabi.exe 保持默认,一路下一步完成安装即可。
4.4.5 创建keil汇编工程
-
创建工程
— 启动Keil uVision5, 菜单中依次选择 “Project” ----> "New μVision Project"。
-
为该项目配置编译器。
4.4.6 添加汇编文件

5. 数据处理指令
5.1 ARM汇编的本质和特点
汇编程序的本质:助记符语言。
助记符语言,类似于人类语言,人容易看懂,相对来说编写、维护方便。
机器语言,CPU能看懂并执行,但人类不容易看懂。
汇编工具,用于将汇编语言(人类容易看懂的), 翻译为机器能看懂的机器语言。
ARM汇编的特点:
- 大多数指令都是单周期指令
像加法运行、减法运算、位运算都是单周期指令。而乘法指令则是多周期指令。 -
大多数指令都是可以条件执行的
if (r0 == 0)
{
r1 = r1 + 1;
}
else
{
r2 = r2 + 1;
}
@无条件执行 5条指令解决问题
cmp r0, #1
bne else
add r1, r1, #1
b end
else:
add r2, r2, #1
end:
@无条件执行 3条指令解决问题
cmp r0, #0
addeq r1, r1, #1
addne r2, r2, #1
5.2 ARM汇编指令
5.2.1 数据处理指令
5.2.1.1 数据传输指令
- 语法格式
- mov{cond}{s} <Rd>, <shifter_operand>
实例,
mov r0, #1 @r0=1
cond, 条件码
moveq r0, #1 @if(Z==1) mov r10, #1
s, 操作结果影响NZCV的取值
movs r0, #0 @ s, 操作结果影响CPSR NZ
@ r0 = 0
@ cpsr.N = r0[31]
@ if r0==0 CPSR.Z=1 else CPSR.Z=0
Rd, 通用寄存器
shifter_operand, 第二个操作数 有三种表现形式
立即数, mov r3, #1
寄存器, mov r3, r4
寄存器移位之后的值, mov r3, r4, lsl #2 @r3 = r4 * 4
注意,使用立即数时要关注立即数的合法性问题如果给定的立即数可以通过一个8bit的数据循环右移偶数位得到,那么该累计数就合法- mvn{cond}{s} <Rd>, <shifter_operand> @第二个操作数取反后赋值
实例: mvn r0, #0xff @r0=0xffffff00


- 实例练习
mov r3, #1
mov r4, r3
mov r5, r4, lsl #2
mvn r6, r5
5.2.1.2 移位操作指令
- 语法格式
lsl{cond}{S} <Rd>, <Rm>, 立即数/寄存器 #逻辑左移 logical shift left
lsl{cond}{S} <Rd>, <Rm> Rd逻辑左移Rm位,存于Rd
lsl{cond}{S} <Rd>, 立即数 Rd逻辑左移立即数位,存于Rd
lsr{cond}{S} <Rd>, <Rm>, 立即数/寄存器 #逻辑右移 logical shift right
lsr{cond}{S} <Rd>, <Rm> Rd逻辑右移Rm位,存于Rd
lsr{cond}{S} <Rd>, 立即数 Rd逻辑右移立即数位,存于Rd
asr{cond}{S} <Rd>, <Rm>, 立即数/寄存器 #算术右移 arithmetic shift right
asr{cond}{S} <Rd>, <Rm> Rd算术右移Rm位,存于Rd
asr{cond}{S} <Rd>, 立即数 Rd算术右移立即数位,存于Rd
ror{cond}{S} <Rd>, <Rm>, 立即数/寄存器 #循环右移 Rotate right
ror{cond}{S} <Rd>, <Rm> Rd循环右移Rm位,存于Rd
ror{cond}{S} <Rd>, 立即数 Rd循环右移立即数位,存于Rd
rrx{cond}{S} <Rd>, <Rm> #带扩展位的循环右移 这里是Rm右移1位给Rd ,跟以上比如ror{cond}{S} <Rd>, <Rm> 逻辑有所不同

- 练习
mov r0, #1
lsl r1, r0, #1
lsl r2, r0, r1
mov r5, #0x8f000000
lsr r6, r5, #1
asr r7, r5, #1
ror r8, r5, #1
lsr r6, r0
lsr r6, #1
asr r7, r0
asr r7, #1
ror r8, r0
ror r8, #1
mov r9, #0xff
ror r9, #1
rrx r9, r8
- 更多使用形式:
mov r0, r1, lsl #3 @r0=r1<<3
mov r0, r2, lsr #1 @r0=r2>>1
5.2.1.3 算数运算指令
- 语法格式
- add{cond}{s} <Rd>, <Rn>, <shifter_operand>
Rd,通用寄存器
Rn,通用寄存器
shifter_operand, 三种形式: 立即数 寄存器 寄存器移位之后的值
s, 操作结果影响 NZCV
N = Rd[31]
if Rd ==0 then Z=1 else Z=0
C 最高位有进位 C=1 反之 C=0
V 有符号算术操作是否发生了溢出。溢出V=1, 反之V=0
两个最高位为0的数据(正整数)运算结果最高位为1,则溢出 V=1
两个最高位为1的数据(负整数)运算结果最高位为0,则溢出 V=1
- adc{cond}{s} <Rd>, <Rn>, <shifter_operand>
adc r0, r1, r2 @r0 = r1 + r2 + C
- sub/sbc/rsb/rsc{cond}{s} <Rd>, <Rn>, <shifter_operand>
C, 够减无借位 C= 1 反之 C=0
sbc r0, r1, r2 @r0=r1-r2-NOT(C)
rsb r0, r1, r2 @r0=r2 - r1
rsc r0, r1, r2 @r0=r2 - r1 - NOT(C)
- mul{cond}{s} <Rd>, <Rn>, <Rs> @32位乘法指令
@div 除法指令, arm-v7架构不支持除法指令,arm-v8之后的架构支持除法指令
- 练习
mov r1, #0xff
mov r2, #0xff
add r0, r1, r2
add r0, r1, #3
add r0, r1, r2, lsl #3
adds r0, r1, #0xff000000
mov r3, #0xffffffff
adds r3, r3, #0x08
addeq r0, r1, r2 @if z==1 add r0, r1, r2
sub r0, r1, r2 @r0=r1-r2
sub r0, r1, #8 @r0=r1-8
subs r0, r1, r2, lsl #1 @r0=r1+r2*2
sbc r0, r1, r2 @r0=r1-r2-NOT(c)
rsb r0, r1, r2 @r0=r2-r1
rsc r0, r1, r2 @r0=r2-r1-NOT(c)
mul r0, r1, r2 @r0=r1*r2
- 课后练习
64位加法运算
被加数高32bit存储在r1, 低32bit存储在r0
加数高32bit存储在r3, 低32bit存储在r2
和高位存储到r5, 低位存储到r4
adds r4, r0, r2 @r0+r2 最高位有进位 会体现c=1 无进位c=0
adc r5, r1, r3
64位减法运算
被减数高32bit存储在r1, 低32bit存储在r0
减数高32bit存储在r3, 低32bit存储在r2
差高位存储到r5, 低位存储到r4
subs r4, r0, r2 @r0-r2 最高位有借位 会体现c=0 无借位c=1
subc r5, r1, r3 @5=r1-r3-NOT(0)
5.2.1.4 位运算指令
- 语法格式
- and{cond}{s} <Rd>, <Rn>, <shifter_operand> @ 按位与 &
- and{cond}{s} <Rd>, <Rn>
- and{cond}{s} <Rd>, <shifter_operand>
- orr{cond}{s} <Rd>, <Rn>, <shifter_operand> @ 按位或 |
- orr{cond}{s} <Rd>, <Rn>
- orr{cond}{s} <Rd>, <shifter_operand>
- eor{cond}{s} <Rd>, <Rn>, <shifter_operand> @ 按位异或 ^
- eor{cond}{s} <Rd>, <Rn>
- eor{cond}{s} <Rd>, <shifter_operand>
- bic{cond}{s} <Rd>, <Rn>, <shifter_operand> @ 按位清0
- bic{cond}{s} <Rd>, <Rn>
- bic{cond}{s} <Rd>, <shifter_operand>
s, 操作结果影响 NZC
N = Rd[31]
if Rd ==0 then Z=1 else Z=0
cond, 可以条件执行

- 练习
mov r1, #0xf0
and r0, r1, #0xff
mov r0, #0xf0
and r0, #0x0f @r0=r0&0x0f
mov r2, #0x0f
and r0, r1, r2, lsl #3 @r0=r1&(r2*8)
orr r0, r1, r2 @r0=r1|r2
bic r0, #0xff
orr r0, #0x07
orr r0, r1
eor r0, r1, r2 @r0=r1 ^ r2
bic r0, r0
eor r0, r2
eor r0, #0x01
- 课后练习
将r0寄存器的bit3清0
mov r0, #0xff
mov r1, #1
mov r2, r1, lsl #3
mvn r3, r2
and r0, r0, r3
或者
mov r0, #0xff
mov r1, #1
bic r0, r1, lsl #3
将r0寄存器的bit3置1
mov r0, #0xf0
mov r1, #1
orr r0, r1, lsl #3
将r0寄存器的bit3取反
mov r0, #0xff
mov r1, #1
eor r0, r1, lsl #3
5.2.1.5 比较测试指令
- 语法格式
- cmp{cond} <Rn>, <shifter_operand> @比较大小 按照减法运算结果影响NZCV
- tst{cond} <Rn>, <shifter_operand> @位测试指令 按照与运算结果影响NZ
- teq{cond} <Rn>, <shifter_operand> @测试是否相等 按照异或运算影响NZ
特点
不用+s 默认就影响NZCV的取值
操作结果不保存练习
cmp r0, r1
addeq r2, r2, #1
subeq r3, r5, #6
movs r0, #0xf0
tst r0, #0x01
addeq r2, r2, #1
teq r2, r3
addeq r3, r3, #6
6. 跳转与加载存储访问指令
6.1 分支跳转指令
顺序执行:硬件自动修改PC寄存器, PC = PC+4
在ARM中有两种方式可以跳转:
- 直接向PC寄存器写入目标地址值
mov pc, #0xff000000 @跳转到0xff000000的位置去执行 - 专门的跳转指令
B/BL
BX/BLX
6.1.1 B/BL
- 语法格式
- b{l}{cond} <target>
l, 自动将下一条指令的地址保存到LR寄存器, 子程序调用的一个基本但常用的手段。
cond, 条件码
target,目标地址 只能是地址标号,不能是寄存器
- 练习
.text
.global _start
_start:
mov r0, #10
mov r1, #20
cmp r0, r1
bllt swap
add r5, r6, r7
b stop
swap:
mov r2, r0
mov r0, r1
mov r1, r2
mov pc, lr @这里还有另一种方法 bx lr
stop:
b .
.end
- 注意
b/bl 指定后面直接给定了要跳转的地址, 包括地址在内的所有信息会被翻译为一条32bit的机器指令。具体格式如下所示
那么注定跳转范围受限:+-32M
0~23位 最高位是符号位,也就是2^23=8M, 按这样计算寻址范围应该是+-8M,为啥是+-32M呢?
因为每个指令是4字节的,因此需要4自己对齐,那么地址最低2位一定是0,因此可以省略。所以可以寻找+-32M
6.1.2 BX/BLX
X, 带状态切换的分支跳转指令。
- 实例
.CODE 32
ARM_code:
ADRR0, THUMB_code + 1
BX R0
……
.CODE 16
THUMB_code:
ADR R0, ARM_code
BX R0
……
- 语法格式
- b{l}x{cond} <Rm>
Rm,通用寄存器, 存放的是要跳转的地址
注意
跳转范围不受限 0~4G随便跳
- 练习
.text
.global _start
_start:
mov r0, #10
mov r1, #20
cmp r0, r1
bllt swap
add r5, r6, r7
b stop
swap:
mov r2, r0
mov r0, r1
mov r1, r2
bx lr
stop:
b .
.end
- 课外练习
求1到10的累加和结果保存到r0
mov r0, #0
mov r1, #10 @循环次数
loop:
add r0, r0, r1
sub r1, r1, #1
cmp r1, #0
bne loop
求两个整数的最大公约数
mov r0, #8
mov r1, #36
loop:
cmp r0, r1
sublt r1, r1,r0
subgt r0, r0, r1
bne loop
6.2 程序状态寄存器访问指令
程序状态寄存器通常遵循“读取---》修改---》写回”的操作序列来实现。
- 语法格式
mrs{cond} <Rd>, cpsr @Rd = cpsr
msr{cond} cpsr{_fileds}, <Rm> / msr{cond} cpsr{_fileds}, <立即数> @向cpsr写入

- 重点:cpsr默认只能向f和c域写内容,s和x域是受保护的,如果需要向sx域写内容需添加_fileds
- 练习
mrs r0, cpsr
bic r0, r0, #(1<<7)
msr cpsr, r0 @msr cpsr_fc, r0
6.3 加载存储指令
ARM中所有的运算都是在通用寄存器中完成的。
这就需要将需要运算的数据从内存加载到通用寄存器,运算完毕后再把运算结果由通用寄存器写回内存。
加载指令:将数据由内存拿到寄存器
存储指令:将数据由寄存器写回内存
6.3.1 单寄存器加载存储指令
看一个简单例子:
mov r0, #0x48000000
ldr r1, [r0] @[r0]------>r1
str r1, [r0] @r1------->[r0]

- 语法格式
- ldr{cond} <rd>, 地址模式
ldr r1, [r0] @[r0]---->r1 [r0]等价于p----------------------------------------
ldr r1, [r0,#0x08] @[r0+0x08]--->r1
ldr r1, [r0,r2]
@[r0+r2]----->r1
ldr r1, [r0,r2, lsl #2] @[r0+r24]---->r1------------------------------------------
ldr r1, [r0,#0x08]! @[r0+0x08]--->r1 r0=r0+0x08
ldr r1, [r0,r2]!
@[r0+r2]----->r1
r0=r0+r2
ldr r1, [r0,r2, lsl #2]! @[r0+r24]---->r1 r0=r0+r24----------------------------------------------------
ldr r1, [r0], #0x08 @[r0]----->r1 r0=r0+0x08
ldr r1, [r0], r2
@[r0]----->r1 r0=r0+r2
ldr r1, [r0], r2, lsl #2 @[r0]--->r1 r0=r0+r2*4- ldr{cond}b r1, [r0] @将0x48000000内存中一个字节加载到r1,高位补0
- ldr{cond}sb r1, [r0] @将0x48000000内存中一个字节加载到r1,高位补符号位
- ldr{cond}h r1, [r0] @将0x48000000内存中两个字节加载到r1,高位补0
- ldr{cond}sh r1, [r0] @将0x48000000内存中两个字节加载到r1,高位补符号位
- 练习
.text
.global _start
_start:
mov r0, #0x100
ldr r1, [r0]
mov r2, #0xfc
add r1, r1, r2
str r1, [r0]
ldrb r3, [r0]
ldrsb r4, [r0]
mov r2, #0x80
strb r2, [r0, #1]
ldrh r5, [r0]
ldrsh r6, [r0]
b .
.data
.space 1024
.end
- 将地址0x100开始的连续64字节数据拷贝到地址0x200位置处。
.text
.global _start
_start:
mov r0, #0x100
mov r1, #16
mov r2, #1
init:
str r2, [r0], #4
add r2, r2, #1
subs r1, r1, #1
bne init
mov r0, #0x100
mov r3, #0x200
mov r1, #16
memcpy:
ldr r2, [r0], #4
str r2, [r3], #4
subs r1, r1, #1
bne memcpy
b .
.data
.space 1024
.end
6.3.2 多寄存器加载存储指令
- 语法格式
- ldm{cond}XX <Rd>{!} <registers>{^}
- stm{cond}XX <Rd>{!} <registers>{^}
cond, 可以条件执行
XX:地址模式 默认IA
IA, Increment After(先操作,后增加)
IB, Increment Before(先增加,后操作)
DA, Decrement After (先操作,后递减)
DB, Decrement Before (先递减,后操作)
!, 更新基址寄存器Rd
registers, 寄存器列表 {r0,r2, r4-r8}
^, 指令中使用的寄存器都是用户模式下的寄存器
若特权模式下的ldm指令, 且寄存器列表中包含PC寄存器, cpsr = spsr
规律:编号低的寄存器对应低地址内存单元, 编号高的寄存器对应高地址内存单元

mov r10, #0x100
mov r0, #0x11
mov r1, #0x22
mov r4, #0x33
@stmia r10, {r0,r1,r4}
@stmib r10, {r0,r1,r4}
@stmda r10, {r0,r1,r4}
stmdb r10, {r0,r1,r4}
- 练习
使用多寄存器加载、存储指令将地址0x100开始的连续64字节数据拷贝到地址0x200位置处。
.text
.global _start
_start:
mov r0, #0x100
mov r1, #16
mov r2, #1
init:
str r2, [r0], #4
add r2, r2, #1
subs r1, r1, #1
bne init
mov r0, #0x100
mov r1, #4
mov r3, #0x200
memcpy:
ldmia r0!, {r4-r7}
stmia r3!, {r4-r7}
subs r1, r1, #1
bne memcpy
b .
.data
.space 1024
.end
7. 栈的种类与应用
7.1 栈操作指令
栈操作指令类似于多寄存器加载存储指令。
- 语法格式
- ldm{cond}XX SP{!} <registers>{^}
- stm{cond}XX SP{!} <registers>{^}
XX, 地址模式 默认EA
E, empty A, ascend F, full D, descend
EA
FA
ED
FD, 满减栈 ARM中默认使用。
push {r0, r3} 等价与 stmfd sp!, {r0,r3} 压栈
pop {r0, r3} 等价与 ldmfd sp!, {r0,r3} 弹栈
- 对C语言中局部变量在栈中分配空间的重新认识
void func(void){
int a = 0x123;
int b = 0x456;
int c = 0x789;
}
int main(void){
func();
return 0;
}
arm-linux-gnueabihf-gcc test.c -o test -marm
arm-linux-gnueabihf-objdump -D test >1.asm
vim 1.asm

5.2.4.4 软中断指令
- ARM中异常中断指令主要有两条:
SVC(早期版本中是SWI)
BKPT , 用于产生软件断点,用于调试程序使用 - 语法格式
svc{cond} #<immed_24>

svc #1
关于svc指令的具体使用,我们后边讲异常处理时再展开。
5.2.4.5 协处理器指令(了解)
ARM架构通过支持协处理器来扩展处理器的功能。ARM架构的处理器支持最多16个协处理器,通常称为CP0~CP15。
CP15,提供系统控制功能,主要用于配置MMU、TLB和Cache、异常向量表的地址设置。
我们只需要了解此部分5条指令中的两条就可以了
- 语法格式
- MRC{cond} coproc, opc1, Rd, CRn, CRm{, opc2} #mov reg cooperation 协处理器寄存器内容传输到寄存器
- MCR{cond} coproc, opc1, Rt, CRn, CRm{, opc2} #mov cooperation reg 寄存器内容传输到协处理器寄存器
cond, 条件码
coproc, 协处理器 cp1 cp2 ... cp15
opc1, 协处理器要执行的操作码,取指范围为0~7
Rd, 通用寄存器
CRn, 协处理器中的寄存器
CRm, 附加的协处理器寄存器
opc2, 有可能出现的第二个操作码

8. 伪指令与伪操作
- ARM中伪指令并不是真正的汇编指令(cpu不认识),但这些伪指令在汇编编译器对源程序进行汇编处理时被替换成对应的一条或多条汇编指令。用于简化汇编编码工作。
- ARM中的汇编伪指令包括ADR、ADRL、LDR和NOP。
8.1 NOP伪指令
NOP伪指令在汇编时将会被代替成ARM中的空操作,比如可能是“MOV R0,R0”指令等。NOP可用于延时操作。
8.2 ADR伪指令
小范围地址读取伪指令。
- 语法格式
adr{cond} register, expr
-
作用
将基于PC的地址值读取到寄存器中。获得expr表达式对应代码在内存中的运行位置
8.3 ADRL伪指令
-
中等范围地址读取伪指令。相较于ADR伪指令,可以读取更大范围的地址。
一般ADR伪指令读取范围为-1020~1020
而ADRL伪指令读取范围为-256K~256K。
ADRL伪指令在汇编时会被编译器替换成两条汇编指令。
8.4 LDR伪指令
大范围地址读取伪指令。
它可以将一个32bit的常数或者一个地址值读取到寄存器中。
它的表现形式有两种,对应的用途也是两种。第一种形式: 带 '=', 可以将非法的立即数放入寄存器,比如mov指令无法放入寄存器中的立即数,ldr可以解决
- 语法格式
LDR{cond} register, = expr | label_expr
.text
.global _start
ldr r1, =0x1ff
b .
.end

- 第二种形式: 不带'=', 将标号代表的内存中的数据放入寄存器
- 语法格式
LDR{cond} register, label_expr
.text
.global _start
ldr r1, =0x1ff
ldr r2, start
b .
start:
.word 0x12345678
.end
- 练习
猜猜r1, r2寄存器中存储了什么内容?
.text
.global _start
ldr r0, =0x1ff
add r5,r6,r7
ldr r1, =test
ldr r2, test
b .
test:
.word 0x12345678
.end
r1中存的是test的地址, r2中存的是test的内容0x12345678
8.5 伪操作
汇编伪操作是给编译器提供某些必要的信息,以帮助编译器正确完成程序的编译。
也就是说,这些伪操作只在汇编过程中起作用,汇编结束,伪操作也就消失了。
伪操作比较好认: 大部分都是以'.'开头。段定义伪操作
.text, 告诉编译器后续内容放入代码段
.data, 告诉编译器后续内容放入数据段
.bss, 告诉编译器后续内容放入BSS段
- 指令集类型标识伪操作
.arm
.code 32, 后续指令汇编为32bit的机器指令
.thumb
.code 16, 后续指令汇编为16bit的机器指令
- 数据定义伪操作
.byte 20 分配一字节空间存储数字20
.short 0x11ff 分配两字节空间存储0x11ff
.word 0x22222222 分配4字节空间存储0x22222222 效果等同于.int
.space 1024 分配1024字节内存空间 初始化为0
.string "abcdef", 分配7个字节空间存储"abcdef" + \0, 等价于.asciz
.ascii "abcdef", 分配6个字节空间存储"abcdef"
- 符号声明伪操作
.global _start 将一个符号声明为全局的,这样其它模块可以引用该符号 等价与.globl
.extern _start 将一个符号声明为外部定义的
- 汇编控制伪操作
.if 0
add r0, r1,r2
.else
sub r0, r1, r2
.endif
- 常量定义伪操作
.equ MAX, 100
- 宏定义伪操作
@.macro 宏名称,参数1, 参数2
.macro swap, x, y
eor \x, \x,\y
eor \y, \x,\y
eor \x, \x,\y
.endm @宏定义结束
mov r0, #100
mov r1, #200
swap r0, r1 @使用宏
- 标号的声明
.global 将一个符号声明为全局的
.extern 将一个符号声明为外部的
- 练习:汇编编程实现strcmp的逻辑
.text
.global _start
_start:
ldr r0, =str1
ldr r1, =str2
loop:
ldrb r2, [r0], #1
ldrb r3, [r1], #1
cmp r2, #0
beq cmp_end
cmp r2, r3
beq loop
cmp_end:
sub r0, r2, r3
b .
str1:
.string "abce"
str2:
.string "abcd"
.end
9. 汇编和C的相互调用
- 为了实现C和汇编的相互调用,这里有一个ATPCS规则(ARM公司制定的),大家需要关注是以下三点内容:
1.程序中统一使用满减栈, 该栈是8字节对齐
2.传递参数时,前4个参数使用r0 r1 r2 r3来传递,剩余参数使用栈来传递
3.函数的返回值使用r0
9.1 汇编调用C
int myadd(int x, int y, int z, int m, int n){
return x + y + z + m + n;
}
汇编中如何才能调用myadd函数呢?四步。
步骤一: 使用.extern 在汇编文件中声明myadd是外部符号
步骤二:调用前准备好传递给myadd的参数到 r0、 r1、r2、r3寄存器(遵循ATPCS规则)
步骤三:通过bl myadd 完成调用
步骤四:调用结束后,直接读取r0获取返回值 (遵循ATPCS规则)
.text
.global _start
.extern myadd
_start:
@先初始化要使用的栈空间
ldr sp, =0x400
mov r0, #10
mov r1, #20
mov r2, #30
mov r3, #40
mov r4, #50
push {r4}
bl myadd
b .
.data
.align 4 @按2^4字节对齐
.space 4096
.end
9.2 C调用汇编
C中如何调用汇编中实现的my_add函数?三步
步骤一:定义汇编函数my_add, 注意传递的参数、返回值满足ATCPS规则
步骤二:汇编文件中通过.global 把my_add声明为全局的
步骤三:C语言中通过extern声明外部函数my_add
.text
.global _start
.extern myadd
_start:
@先初始化要使用的栈空间
ldr sp, =0x400
bl main
b .
.data
.align 4 @按2^4字节对齐
.space 4096
.end
.text
.global my_add
my_add:
add r0, r0, r1
add r0, r0, r2
add r0, r0, r3
pop {r4}
add r0, r0, r4
bx lr
.end
extern int my_add(int a, int b, int c, int d, int e);
int main(){
int ret = my_add(10, 20, 30, 40, 50);
return ret;
}
9.3 C中嵌套汇编
- 语法格式
asm volatile (
"汇编指令\n\t"
....
:输出列表
:输入列表
:破坏列表
);
asm, 编译器将不检查后面的内容,而是直接交给汇编器处理
volatile, 向GCC声明不允许对该内联汇编进行优化
- 实例
/*r 表示通用寄存器*/
int func(int a, int b){
int c = 0;
asm volatile(
"add %0, %1, %2"
:"=r"(c)
:"r"(a),"r"(b)
:"memory"
);
return c;
}
/*cc, 表示cpsr*/
int enable_irq(){
int status;
asm volatile(
"mrs r0, cpsr\n\t"
"bic r0, r0, #(1<<7)\n\t"
"msr cpsr_c, r0\n\t"
"mrs %0, cpsr\n\t"
:"=r"(status)
:
:"r0", "cc", "memory"
);
return status&(1<<7)? 1:0 ;
}
10. MP157开发环境搭建
10.1 硬件相关专业术语
- 原理图
原理图(Sch), Sch是Schematic的简写。
硬件设计工程师,根据不同的元器件设计出具有不同功能的电路图纸,这种电路图纸就可以称为原理图。 - 印制电路图
就是我们常说的PCB图,PCB的英文全称是Printed Circuit Board,即印制电路图。
PCB板设计工程师根据不同的原理图设计对应的PCB,交给PCB生成的厂家生成对的PCB板,然后焊接上对应的器件。
交由软件工程师编写驱动程序。 - 丝印
丝印就是PCB板上的白色的边框或者白色的器件的编号。
丝印用于表示器件的边框:对器件进行分类,或者表示器件的边框方便焊接。
丝印用于标识器件的编号:PCB板上每个器件都有一个唯一的编号。
在PCB板的原理图上通过器件的编号可以找到器件对应的原理图。
Ux --> 集成电路芯片 Rx --> 电阻 Cx--> 电容 Lx --> 电感 Dx --> 二极管 Qx --> 三极管 Jx --> 接插件 Yx-->晶振

- 网络标号
原理图中导线上边红色的字表示网络标号。
如果两个导线的网络标号相同,说明两个引脚具有相同的电气连接属性,即在PCB板上这两条导线是连接在一起的。

10.2 LED电路原理图分析
10.2.1 扩展板电路原图



-
结论:
控制LED1亮灭转换为了控制STM32MP157A 芯片上的PE10管脚输出高低电平了。输出高电平,LED1就亮。
套路1: 看电路原理图,找出硬件和CPU的联系
如何通过软件控制CPU上的管脚?
套路2: 看芯片的数据手册(datasheet)
套路3:软件驱动硬件的媒介就是特殊功能寄存器
套路4:只修改寄存器中关注的bit,其它的bit要保持不变

10.2.2 GPIO控制器中的特殊功能寄存器


通过查手册可以设置以下寄存器
- GPIOx_MODER
GPIOE_MODER, 0x50006000
[21:20]:01, PE10就配置成了输出模式
- GPIOx_OTYPER
GPIOE_OTYPER, 0x50006004
[10]: 0, PE10就配置成了推挽输出
- GPIOx_OSPEEDR
GPIOE_OSPEEDR, 0x50006008
[21:20]: 00, 低速模式
- GPIOx_PUPDR
GPIOE_PUPDR, 0x5000600C
[21:20]: 00, 禁止上拉下拉
- GPIOx_IDR
GPIOE_IDR, 0x50006010
[10]: 读取引脚的电平状态
- GPIOx_ODR
GPIOE_ODR, 0x50006014
[10]: 1/0, 对应的引脚输出高/低电平
- RCC_MP_AHB4ENSETR
RCC_MP_AHB4ENSETR, 0x50000A28
[4], 写入1 使能外设时钟
10.3 编写LED驱动代码
10.3.1 汇编语言版本
/* LED1灯接到PE10引脚,对应的总线为AHB4
操作对应的寄存器,只修改对应的位,其他位保持不变
LED2 LED3 灯 自己分析电路图\芯片手册\代码实现
*/
.text
.global _start
.equ AHB4ENSETR, 0x50000A28
.equ GPIOE_MODER, 0x50006000
.equ GPIOE_OTYPER, 0x50006004
.equ GPIOE_OSPEEDR, 0x50006008
.equ GPIOE_PUPDR, 0x5000600C
.equ GPIOE_IDR, 0x50006010
.equ GPIOE_ODR, 0x50006014
_start:
/*1. 使能GPIOE外设的时钟源
RCC_MP_AHB4ENSETR[4] = 0b1 寄存器地址 = 0E50000A28 */
ldr r0, =AHB4ENSETR
ldr r1, [r0]
orr r1, r1, #(1<<4)
str r1, [r0]
/*2.配置PE10引脚为输出模式
GPIOE_MODER[21:20] = 0b01 寄存器地址 = 0E50006000 */
ldr r0, =GPIOE_MODER
ldr r1, [r0]
bic r1, r1, #(0x03 << 20)
orr r1, r1, #(0x01 << 20)
str r1, [r0]
/*3. 配置PE10引脚为推挽输出
GPIOE_OTYPER[10] = 0b0 寄存器地址 = 0E50006004*/
ldr r0, =GPIOE_OTYPER
ldr r1, [r0]
bic r1, r1, #(0x01 << 10)
str r1, [r0]
/*4. 配置PE10引脚为低速模式
GPIOE_OSPEEDR[21:20] = 0b00 寄存器地址 = 0E50006008 */
ldr r0, =GPIOE_OSPEEDR
ldr r1, [r0]
bic r1, r1, #(0x03 << 20)
str r1, [r0]
/*5. 配置PE10引脚禁止上和核下拉电阻
GPIOE_PUPDR[21:20] = 0b00 寄存器地址 = 0E5000600C */
ldr r0, =GPIOE_PUPDR
ldr r1, [r0]
bic r1, r1, #(0x03 << 20)
str r1, [r0]
loop:
/* 循环点亮或者熄灭LED灯*/
/*1. 配置PE10引脚输出高电平, LED1灯亮
GPIOE_ODR[10] = 0b1 寄存器地址 = 0E50006014*/
ldr r0, =GPIOE_ODR
ldr r1, [r0]
orr r1, r1, #(0x01 << 10)
str r1, [r0]
bl delay_1s
/*2. 配置PE10引脚输出底电平, LED1灯亮
GPIOE_ODR[10] = 0b0 寄存器地址 = 0E50006014**/
ldr r0, =GPIOE_ODR
ldr r1, [r0]
bic r1, r1, #(0x01 << 10)
str r1, [r0]
bl delay_1s
b loop
delay_1s:
ldr r3, =0x10000000
mm:
subs r3, r3, #(0x01)
bne mm
bx lr
.end
- 编译程序
arm-linux-gnueabihf-gcc -c led.s -o led.o
arm-linux-gnueabihf-ld led.o -o led.elf -Ttext=0xC0008000
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
10.3.2 C语言版本
typedef struct{
volatile unsigned int MODLER;
volatile unsigned int OTYPER;
volatile unsigned int OSPEEDR;
volatile unsigned int PUPDR;
volatile unsigned int IDR;
volatile unsigned int ODR;
}gpio_t;
#define GPIOE ((gpio_t *)0x50006000)
#define RCC_MP_AHB4ENSET *(volatile unsigned int *)0x50000A28
void delay_ms();
void led_flashing(){
// 1. 使能GPIOE外设的时钟源 RCC_MP_AHB4ENSETR[4] = 0b1
RCC_MP_AHB4ENSET |= (0x01 << 4);
// 2. 设置为输出模式 GPIOE_MODER[21:20] = 0b01
GPIOE->MODLER &= ~(0x03 << 20);
GPIOE->MODLER |= (0x01 << 20);
// 3. 设置PE10引脚为推挽输出 GPIOE_OTYPER[10] = 0b0
GPIOE->OTYPER &= ~(0x01 << 10);
// 4. 设置PE10引脚为低速模式 GPIOE_OSPEEDR[21:20] = 0b00
GPIOE->PUPDR &= ~(0x03 << 20);
// 5. 设置PE10引脚禁止上下拉电阻 GPIOE_PUPDR[21:20] = 0b00
GPIOE->PUPDR &= ~(0x03 << 20);
while(1){
//亮
GPIOE->ODR |= (0x01 << 10);
delay_ms();
//灭
GPIOE->ODR &= ~(0x01 << 10);
delay_ms();
}
}
void delay_ms(){
int count = 0x10000000;
while(count--);
}
- 编译程序
arm-linux-gnueabihf-gcc -c led.c -o led.o -marm
arm-linux-gnueabihf-ld led.o -o led.elf -Ttext=0xc0008000 -eled_flashing
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
10.4 下载运行代码
- 开发板连接(这里不介绍了)
- 安装 stlink 驱动
- 安装串口工具软件(SecureCRT工具)
- 下载运行代码
10.4.1 安装 stlink 驱动
64位的windows系统安装如下图所示,如果你的是32位的windows系统(已经很少了),双击dpinst_x86.exe安装。
10.4.2 安装SecureCRT

10.4.3 使用SecureCRT

-
进入uboot命令模式
重新启动开发板,倒数读秒计时结束前按下电脑键盘上的空格键,进入uboot命令行模式。
10.4.4 下载运行代码

10.4.5 作业
- 编程实现蜂鸣器控制
typedef struct{
volatile unsigned int MODLER;
volatile unsigned int OTYPER;
volatile unsigned int OSPEEDR;
volatile unsigned int PUPDR;
volatile unsigned int IDR;
volatile unsigned int ODR;
}gpio_t;
#define GPIOB ((gpio_t *)0x50003000)
#define RCC_MP_AHB4ENSET *(volatile unsigned int *)0x50000A28
void delay_ms();
void led_flashing(){
// 1. 使能GPIOB外设的时钟源 RCC_MP_AHB4ENSETR[1] = 0b1
RCC_MP_AHB4ENSET |= (0x01 << 1);
// 2. 设置为输出模式 GPIOB_MODER[12:13] = 0b01
GPIOB->MODLER &= ~(0x03 << 12);
GPIOB->MODLER |= (0x01 << 12);
// 3. 设置PB6引脚为推挽输出 GPIOB_OTYPER[6] = 0b0
GPIOB->OTYPER &= ~(0x01 << 6);
// 4. 设置PB6引脚为低速模式 GPIOB_OSPEEDR[12:13] = 0b00
GPIOB->PUPDR &= ~(0x03 << 12);
// 5. 设置PB6引脚禁止上下拉电阻 GPIOB_PUPDR[12:13] = 0b00
GPIOB->PUPDR &= ~(0x03 << 12);
while(1){
//亮
GPIOB->ODR |= (0x01 << 6);
delay_ms();
//灭
GPIOB->ODR &= ~(0x01 << 6);
delay_ms();
}
}
void delay_ms(){
int count = 0x10000000;
while(count--);
}






















