type
slug
status
summary
icon
category
date
tags
password
1 嵌入式硬件系统的基本组成

- Processor
- ARM (Advanced RISC Machine)
- 32bit RISC (Reduced Instruction Set Computer)
- 启动模式
- 寄存器
- 指令集
- Bus
- ARM AMBA (Advanced Microcontroller Bus Architecture) bus
- AHB (Advanced High-performance Bus)
- 高性能的总线接口,用于连接CPU和需要高速数据传输的设备,如SRAM、LCD控制器等。
- APB (Advanced Peripheral Bus)
- 连接外围设备的总线接口,通常用于连接速度要求较低的设备,如定时器、UART等。
- Bridge:这是一个桥接器,用于连接AHB总线和APB总线,允许不同速度的设备之间进行通信。
- PCI bus(外围部件互连总线)
- CPCI
- PCIE

1.1 7种模式运行下的寄存器

1.2 CPSR布局

1.2.1 控制”大端”和”小端”之间的条件
- 什么是大端和小端?
- 当你把一个多字节的数据(比如 0x12345678)放入内存时,有两种存法:
存储方式 | 高字节在哪? | 举例(0x12345678) |
大端(Big Endian) | 高位在低地址 | 0x12 0x34 0x56 0x78 |
小端(Little Endian) | 低位在低地址 | 0x78 0x56 0x34 0x12 |
举个例子:内存从地址
0x1000
开始存储 0x12345678
大端:
0x1000=0x12
, 0x1001=0x34
, 0x1002=0x56
, 0x1003=0x78
小端:
0x1000=0x78
, 0x1001=0x56
, 0x1002=0x34
, 0x1003=0x12


- 为什么会有两种方式?
- 不同的CPU架构采用不同的存储方式:Intel x86、ARM 默认使用小端,部分网络协议使用 大端(如 TCP/IP 中的“网络字节序”)
1.2.2 CPSR各位详解

Bit 范围 | 含义 | ㅤ |
31 | N(负号标志) | 结果为负 |
30 | Z(零标志) | 结果为0 |
29 | C(进位标志) | 有进位/借位 |
28 | V(溢出标志) | 有溢出 |
27-8 | 保留位 | ㅤ |
7 | I(IRQ中断禁止位) | IRQ(普通中断)屏蔽位,1=禁止IRQ中断 |
6 | F(FIQ中断禁止位) | FIQ(快速中断)屏蔽位,1=禁止FIQ中断 |
5 | T(Thumb状态标志) | T=1:Thumb 模式(使用16位指令) T=0:ARM 模式(使用32位指令) |
4-0 | 模式位(M4~M0,表示当前CPU模式) | ㅤ |
模式名 | 值(M4~M0) | 描述 |
User | 10000 (0x10) | 普通用户模式 |
FIQ | 10001 (0x11) | 快速中断模式 |
IRQ | 10010 (0x12) | 普通中断模式 |
Supervisor | 10011 (0x13) | 管理模式(启动后进入) |
Abort | 10111 (0x17) | 异常模式(数据访问错误) |
Undefined | 11011 (0x1B) | 未定义指令异常 |
System | 11111 (0x1F) | 特权用户模式 |

- R15在ARM状态下通常用作程序计数器(PC,Program Counter)。THUMB指令集使用16位的指令,因此地址需要对齐到半字(16位)。
- 在ARM状态下,R15的低2位总是0,而高30位用于存储下一条指令的地址。
- 在THUMB状态下,R15的最低位总是0,而高31位用于存储下一条指令的地址。
1.3 字节/半字/字对齐
- 对齐规则:
- 字节对齐:无限制。
- 半字对齐:地址是偶数。
- 字对齐:地址是4的倍数。
1.4 中断返回时的指令(涉及流水线)


2 Practice1: 代码拷贝

- 内存布局图(从 LOW 到 HIGH):Norflash(低地址)、……(中间是代码段)、RAM(高地址)
r1
: ResetHandler 起始地址(Norflash),r2
: 目标地址 text_start(RAM 中),r2
: 目标地址 text_start(RAM 中)- 所以目标是把 Flash 中的代码段从
r1
复制到 RAM 中r2
到r3
之间。
- 背景知识:为什么要复制代码段到 RAM?
- Flash是非易失存储器,只能执行代码,访问速度较慢,不能被频繁写入/修改。而RAM 是易失存储器,访问速度快,可读写。为了更快执行,有些代码会在启动时从Flash复制到 RAM,再从RAM运行。
- 汇编代码逐句解释
ADR r1, ResetHandler
:这条指令的目的是获取ResetHandler
的地址。由于ResetHandler
是一个标签,它的地址是相对于当前指令的。因此,使用ADR
伪指令可以直接计算出这个相对地址。LDR r2, =text_start
和LDR r3, =bss_start
:这两条指令的目的是获取text_start
和bss_start
的地址。这两个地址通常是在链接器脚本中定义的,它们表示代码段和数据段的开始位置。由于这些地址是绝对的,而不是相对于当前指令的,因此需要使用LDR
伪指令来加载这些地址。- 因为在拷贝代码之前ResetHandler是存放在Flash中的,而此时PC在Flash中取址,所以此时所有的在Flash中的标号都应该使用相对寻址获取(所有的标号函数在编译的时候都是一个相对地址,而在经过连接之后就会变成一个绝对地址。但是就算是绝对地址也还是使用ADR指令来获取,因为如果使用LDR指令的话就要在缓冲池中存储这个标号的地址了)
- 而text与bss都是在连接脚本中指定的绝对地址,所以这里需要使用LDR指令获取
- 说白了就是,ADR指令用于获取相对地址(如函数名,标号啥的);而LDR指令用于获取立即数或者绝对地址(如链接脚本中指定的标号)
- 或者换一句话说,LDR是完全根据编译后生成的绝对地址确定的标号地址(就是完全相对于链接脚本中指定的运行域来确定的),而ADR指令是要区分当前代码的位置(就是需要关心当前代码是在运行域还是加载域的,在这两个域上使用ADR指令的结果是不一样的。所以如果运行域与加载域完全相同,那么就会出现LDR指令与ADR指令计算地址相同的情况)
- 循环复制copyloop
步骤 | 地址 | 动作 |
1 | r1 是 Flash 中代码段的起始地址(例如 ResetHandler ) | 从 Flash 中读出 4 字节放入 r0 |
2 | r2 是 RAM 中复制目标区域的起始地址(例如 .text 段) | 把 r0 的数据写入 RAM 中的 r2 地址 |
3 | 然后 r1 和 r2 各自自动 +4,表示处理下一个字 | ㅤ |
4 | 如果 r2 达到了 bss_start,就说明复制完了 | ㅤ |
5 | 否则继续循环,直到全部复制完成 | ㅤ |
- 为什么需要这样做?
- CPU复位后从 Flash 执行,但有些代码必须在 RAM 执行:
- RAM,执行更快;有些函数必须是可读写的(如全局变量初始化、BSS清零等)
- 有些代码(如中断向量表、C库函数)必须在 RAM 中运行。
- Flash 可能是只读,不能动态修改执行位置的数据。
图下方红字问题Why? If the code segment is in the 0 sector of a FLASH disk...?如果代码已经在Flash的0地址扇区了,为什么还要复制?
- 拓展知识点
- BSS段(bss_start):未初始化的全局/静态变量区,通常也在启动代码中清零。
- .text 段(text_start):程序代码段,通常是 Flash 中复制过来的。
- ResetHandler:复位处理函数,是 ARM 启动的第一个执行点。
- ADR vs LDR
ADR
用于计算地址(当前PC附近的标签)ADR
是 PC 相对地址计算(PC + offset)LDR
用于加载常量地址(全局变量地址/远距离符号)LDR r0, =Label
是访问常量池(literal pool)- 这不是字面意思“从label地址加载数据”,而是一个汇编语法糖,它实际上:在常量池中保存了
label
的地址(一个32位的立即数),然后让r0
去读取这个值。 - 指令中并没有直接包含
Label
的值或地址。而是访问一块内存(叫做 literal pool)中事先放好的数据。这个值是是链接器(linker)根据**链接脚本中“运行地址”**决定的! - 可以访问任何内存地址或常量,几乎无限制。会引入一次实际的内存读取(和 ADR 不同)。
- 什么时候用哪个?
- 想要得到一个地址?
ADR
更快更省空间(如果地址在范围内)。 - 想要加载一个常量或者远距离地址?
LDR =xxx
更通用。 - 总结对比
将
Label
的地址(而不是内容) 计算出来,存入 r0。这个地址是通过当前 PC 和 Label 之间的**偏移量(offset)**计算的。假设:当前 PC = 0x1000
, Label
距离 PC 往后偏移 0x20,那么执行完这一条语句之后相当于r0 = PC + 0x20; // 即 r0 = 0x1020。不会访问内存,纯靠PC和偏移计算。只能访问距离 PC 一定范围内的地址(例如 ±4KB 之内,取决于平台)。特性 | ADR | LDR =xxx |
本质 | PC + offset | 访问常量池(literal pool) |
获取内容 | 获取地址(Label的地址) | 获取地址或数据 |
是否访问内存 | 否(纯寄存器操作) | 是(一次内存读取) |
地址范围 | 有限(±4KB左右) | 无限(取决于常量池放在哪) |
编译器优化 | 高效 | 灵活但开销略大 |
- 总的来说就是需要注意:获取ResetHandler的地址是通过ADR指令获取的
- ADR r1, ResetHandler

- 中断向量表有两张:一级中断向量表中存放的是跳转指令,而二级中断向量表中存放的是ISR的地址。这段代码应该在启动文件中,并且上面的这两张表合起来才能被称为中断向量表(跳转表+注册表)
- 另外,这里的LDR PC,地址实际上就是从该地址处取出值存放在PC中(这里的LDR指令就不是伪指令了),而VECTORTABLE中存放的就是ISR的地址。当一个中断或者异常发生了,这里就会跳转到Hal那个表中对应的中断向量(所以这个HALVECTR_START才是真正的中断向量表,下面那张表就只是在注册中断,或者说像是一个文字缓冲池,如果没有注册表的话,标号跳转又需要使用ADR指令,而ADR指令又不能将地址读取到PC寄存器中)
- 这里使用LDR指令+注册表的形式就相当于是手动实现了LDR伪指令的PC长跳转功能

- 中断返回需要结合流水线分析
3 Practice2: 中断向量表拷贝

- 背景:为什么要复制中断向量表?
- ARM 启动后,中断向量表必须在地址
0x00000000
/或者某些处理器可配置的位置(如0xFFFF0000
),但这块内存可能是Flash(可执行不可改)/或者 SRAM(可执行可改) - 你的中断向量表(放在
.vector
段)链接脚本中设置的运行地址是 0x00000000,但程序实际运行时是从Flash(比如0x08000000
)中加载的(即链接器把中断向量表加载进了 Flash 的0x08000000
)。所以,中断向量表最初并不在目标地址(运行地址)处,你必须手动把它从 Flash中复制到运行地址。
- 分析这段代码:
LDR r3, [r0], #4
:LDR是从内存中读取一个字(4字节)到寄存器。[r0]
:从r0指向的地址读数据(例如 Flash 地址)。#4
:这是“后递增”寻址模式:先用 r0 当前地址访问内存,然后让 r0 自动加 4(指向下一个地址)r0 = 0x08000000
,该地址在 Flash 中,Flash 中0x08000000
的值是0xDEADBEEF
- 执行这句后:r3 = 0xDEADBEEF。
r0 = 0x08000004
(地址增加了 4)
STR r3, [r2], #4
:STR:把寄存器里的数据写入内存。[r2]
:往 r2 当前地址写入 r3 的值。#4
:写完之后,r2 += 4,移动到下一个目标地址。r2 = 0x00000000
(SRAM 中,运行地址)r3 = 0xDEADBEEF- 执行这句后:把
0xDEADBEEF
写入地址0x00000000
,然后r2 = 0x00000004
- 拓展知识点
操作 | 语法 | 含义 |
不变地址 | [r0] | 只访问,不变 |
后递增 | [r0], #4 | 访问 r0 → 然后 r0 += 4 |
前递增 | [r0, #4]! | 先 r0 += 4 → 然后访问新地址 |
偏移访问,不改变地址 | [r0, #4] | 访问 r0+4 ,但 r0 本身不变 |
- 首先需要知道的是,中断向量表的拷贝是在代码拷贝之后的(从连接脚本中看到的,32的拷贝是先拷贝了数据段,然后再将中断向量表放在最低地址处)
- 经过代码拷贝之后,加载域中的代码就被拷贝到运行域了,这个时候LDR与ADR指令获取的地址就是完全相同的了(此时PC也跳转到了运行域中),这个时候就可以直接使用LDR指令了:

- 需要注意的是这里还多了一步判断,就是判断中断向量表是不是已经被放在0地址处了,如果是的话就没有必要再次进行拷贝了,就可以直接执行real_code了。
4 ARM指令集和应用
4.1 常见的ARM指令集

- 分支指令(Branch)
- 功能:
B
指令用于无条件跳转到指定标签(Label)的地址。 - 地址范围:跳转范围为当前程序计数器(PC)的 ±32MB(ARM模式下,指令编码支持26位偏移量,左移2位后覆盖此范围)。
- 编码格式:
- 位 31-28:条件码(如
EQ
、NE
,无条件跳转时默认为AL
)。 - 位 27-24:操作码(
1010
表示B
指令) - 位 23-0:24 位有符号偏移量,左移 2 位后得到实际偏移地址。
- 数据处理指令(Data Process)
- SUB 指令:减法运算,
r0 = r1 - 5
。语法:SUB <目标寄存器>, <操作数1>, <操作数2>
。 - ANDS 指令: 功能:按位与操作,并更新条件标志(
S
后缀(CPSR 中的N、Z、C、V 位)。 - ADDEQ 指令:条件加法(EQ表示相等时执行),若条件满足(Z=1),则
r5 = r5 + r6
。
- 数据处理指令(Data Process)
- MRS 指令 : 将 SPSR(Saved Program Status Register)的值读取到通用寄存器。
- MSR 指令 : 将通用寄存器的值写入 SPSR/CPSR(Current Program Status Register)。
CPSR_C
表示仅修改控制位(特权模式下允许修改)。操作状态寄存器需在特权模式(如内核态)下执行。

- 内存访问(Memory access)
LDR r0,[r1]
:这条指令用于从内存中加载一个字(word)到寄存器r0。具体来说,它将地址r1处的字加载到r0中。STRNEB r2,[r3,r4]
:这条指令在条件码NE(Not Equal,不等)为真时执行。它将寄存器r2的低字节(bottom byte)存储到由r3和r4相加得到的地址中,并将结果存储在r3中(Z=0表示不更新标志位)。STMFD sp!,{r0-r7,lr}
:这条指令将寄存器r0到r7以及链接寄存器lr(Link Register)存储到堆栈中,然后更新堆栈指针sp。sp!
表示在存储后自动更新sp。LDMFD sp!,{r0-r7,pc}
:这条指令从堆栈中加载寄存器r0到r7以及程序计数器pc的值,然后更新堆栈指针sp。sp!
表示在加载后自动更新sp。
- 协处理器(Companion processor)
LDC p3, c4, [r0]
:这条指令用于加载协处理器寄存器。它从内存地址r0处加载数据到协处理器p3的寄存器c4中。
- 异常处理(Exception process)
SWI 0x02
:这条指令用于触发一个软件中断(Software Interrupt)。SWI(Software Interrupt)指令允许程序请求操作系统的服务,0x02是中断号,用于指定请求的具体服务。

- 算术逻辑单元(ALU)操作与移位操作
MOV r1, r0, LSL #3
:这条指令展示了如何在一条指令中同时进行ALU操作和移位操作。MOV
指令通常用于将数据从一个寄存器移动到另一个寄存器,或者从寄存器移动到内存,或从内存移动到寄存器。在这个例子中,LSL #3
表示逻辑左移(Logical Shift Left)3位。因此,这条指令将寄存器r0的内容逻辑左移3位,并将结果存储在寄存器r1中。
- 其他类型的指令
BKPT 0x??
:BKPT
是断点指令(Breakpoint),用于在调试过程中暂停程序执行。0x??
是一个占位符,代表一个具体的16位立即数,这个数值可以指定断点的类型或相关的调试信息。当程序执行到这条指令时,它会触发一个断点异常,允许调试器介入并检查程序的状态。
4.2 应用案例

- 原来的代码:循环执行10次,每次将r2累加r3的值。
- 若去掉SUBS和BNE :r1不再递减,循环无法终止,导致无限循环。无分支条件判断,程序流无法跳转。类比于去掉C语言中的i++和i<2023,循环条件无法检查和更新,同样导致无限循环。
- 标志位和流水线机制
- NZCV 是 ARM 架构中 CPSR(Current Program Status Register)程序状态寄存器 的 4 个重要标志位,用于记录最近一次算术或逻辑运算的结果,用来影响条件执行(比如条件跳转指令)。
- 这些位会被如
ADD
、SUB
、CMP
、MOVS
、ANDS
等带S
后缀的指令自动更新。 - ARM 指令可带 条件后缀,判断 NZCV 结果后执行。
位名 | 英文全称 | 含义 |
N | Negative | 结果为负时置 1(最高位为 1)【只关注第31位】 |
Z | Zero | 结果为 0 时置 1 |
C | Carry | 加法产生进位,或减法未借位时置 1 |
V | Overflow | 有符号运算溢出时置 1 |
条件后缀 | 含义 | 等价条件 |
EQ | 相等 | Z = 1 |
NE | 不等 | Z = 0 |
MI | 负数 | N = 1 |
PL | 正数 | N = 0 |
CS/HS | ≥(无符号) | C = 1 |
CC/LO | <(无符号) | C = 0 |
GT | >(有符号) | Z = 0 且 N = V |
LT | <(有符号) | N ≠ V |
SUBS r1, r1, #0x01
:将寄存器r1的值减1,并更新程序状态寄存器(CPSR)中的标志位,包括Z标志位。如果r1减1后的结果为0,则Z标志位被设置为1。
BNE LOOP
:检查Z标志位。如果Z标志位为0(表示r1减1后的结果不为0,NE),则跳转回LOOP标签处,继续执行循环。如果Z标志位为1(表示r1减1后的结果为0),则不跳转,继续执行后面的指令,循环结束。

ORR r0, r0, #(PSR_I_BIT | PSR_F_BIT)
:- 这条指令对r0进行逻辑或操作,设置PSR中的I位(中断禁止位)和F位(快速中断禁止位)。这两个位用于控制中断的使能状态。
PSR_I_BIT
和PSR_F_BIT
通常分别对应于CPSR的第7位和第6位。
MSR CPSR_c, r0
- 这条指令将r0中的值写回到CPSR的控制位(C位)。

- 这个是协处理器指令,这张幻灯片展示了如何在ARM处理器中通过一系列指令来禁用缓存和内存管理单元(MMU)。和MRS和MSR有异曲同工之妙。
MCR p15, 0, r0, c7, c5, 0
- 使用协处理器指令(MCR)来使指令缓存(I Cache)失效。
p15
是协处理器的编号,0
是协处理器的类型,r0
是要写入的数据(在这里是0),c7
是协处理器寄存器的编号,c5
是该寄存器内的偏移量,0
是操作的选项。
MCR p15, 0, r0, c7, c6, 0
- 同样使用MCR指令来使数据缓存(D Cache)失效。
MRC p15, 0, r0, c1, c0, 0
- 使用协处理器指令(MRC)来获取控制寄存器的值。
c1
是控制寄存器的编号,c0
是该寄存器内的偏移量。
BIC r0, r0, #(CTRL_M_BIT | CTRL_C_BIT)
- 使用位清除指令(BIC)来清除控制寄存器中的内存管理单元(MMU)和数据缓存(D Cache)使能位。
CTRL_M_BIT
和CTRL_C_BIT
分别是控制寄存器中MMU和D Cache使能位的位置。
BIC r0, r0, #CTRL_I_BIT
- 再次使用BIC指令来清除控制寄存器中的指令缓存(I Cache)使能位。
MCR p15, 0, r0, c1, c0, 0
- 将修改后的控制寄存器值写回,从而禁用MMU、D Cache和I Cache。
CTRL_M_BIT
:位0,用于控制MMU的使能。|TRL_C_BIT
:位2,用于控制D Cache的使能。|CTRL_I_BIT
:位12,用于控制I Cache的使能。
5 Boot of ARM
- Internal boot (from ROM)
- External boot (from Norflash or ……)
- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/EOS/Chapter3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。