ARM 架构核心知识笔记(整理与补充版)
一、ARM CPU 与内存、外设的交互逻辑
核心原理
ARM CPU 通过地址(addr) 与外部设备交互,所有交互需经过内存控制器(memory controller) ;内存控制器根据 CPU 发出的地址范围,选择访问目标设备(内存、UART、USB 控制器等)。关键特性:内存与 I/O(外设)访问方式统一------ 在 CPU 视角,操作内存和操作外设无本质区别,均通过 "发送地址→内存控制器译码→访问目标设备" 的流程实现,所有设备共享同一地址空间。
补充说明
- 地址范围由芯片厂商预先划分,例如:0x00000000~0x0FFFFFFF 分配给 Flash 内存,0x20000000~0x2000FFFF 分配给 SRAM,0x40000000~0x4000FFFF 分配给 UART 外设。
- 内存控制器的核心作用是 "地址译码",确保不同地址对应唯一设备,避免访问冲突。
二、ARM 的 RISC 架构特点
ARM 芯片属于精简指令集计算机(RISC) ,指令设计简洁,核心特点与运算流程如下:
1. RISC 核心特点
- 对内存仅支持 "读(Load)" 和 "写(Store)" 两类指令,无直接对内存的运算指令。
- 数据运算(加减乘除、逻辑运算)均在 CPU 内部的ALU(算术逻辑单元) 中完成,需先将内存数据读入 CPU 寄存器。
- CPU 硬件复杂度低,易于设计和优化,指令执行周期固定,适合流水线操作。
2. 乘法运算(a = a * b)指令流程(用户代码补充注释)
以 "a 和 b 存储在内存,结果写回 a" 为例,需 4 条汇编指令,流程如下:
arm
; 1. 从内存地址a处,读取数据到CPU寄存器(暂存,为运算做准备)
LDR R0, [a] ; R0 = 内存[a](即a的原始值)
; 2. 从内存地址b处,读取数据到另一寄存器
LDR R1, [b] ; R1 = 内存[b](即b的值)
; 3. CPU内部ALU执行乘法运算,结果存入R0(覆盖原a的值)
MUL R0, R0, R1 ; R0 = R0 * R1(a*b的结果)
; 4. 将运算结果从寄存器写回内存地址a,更新a的值
STR R0, [a] ; 内存[a] = R0(完成a = a*b的更新)
三、ARM CPU 内部寄存器结构
ARM CPU(如 Cortex-M3/M4/A7)内部包含R0~R15 共 16 个通用寄存器(32 位宽),部分寄存器具有特殊功能,需按 "普通寄存器" 和 "特殊功能寄存器" 分类理解。
1. 通用寄存器(R0~R15)
(1)普通数据寄存器
- 低寄存器(Low Registers):R0~R7,所有指令均支持访问,是最常用的临时数据存储寄存器(如传递函数参数、暂存运算中间结果)。
- 高寄存器(High Registers) :R8~R12,仅部分指令支持访问(如 32 位 Thumb2 指令),通常用于存储局部变量或不常用数据。补充:R12 又称IP(Internal Procedure Register,内部过程寄存器),在函数调用时临时保存中间结果,用户代码未提及,需注意其辅助作用。
(2)特殊功能寄存器(R13~R15)
寄存器 | 别名 | 核心功能 | 补充说明 |
---|---|---|---|
R13 | SP | 栈指针,管理栈空间的当前地址 | 分主栈指针(SP_main)和进程栈指针(SP_process),适配操作系统多任务场景 |
R14 | LR | 链接寄存器,保存函数返回地址 | 函数调用(如 BL 指令)时,自动将 "下一条指令地址" 存入 LR;返回时需通过 "MOV PC, LR" 或 "BX LR" 恢复执行流程 |
R15 | PC | 程序计数器,指示下一条要执行的指令地址 | PC 的值始终比当前执行指令地址大 4(ARM 指令)或 2(Thumb 指令),修改 PC 即可实现程序跳转 |
2. 加法运算(a = a + b)指令示例(用户代码补充注释)
arm
; 场景:a(内存地址0x12)、b(内存地址0x34),结果写回a
LDR R0, [a] ; 1. 读取内存a的值(0x12处数据)到R0
LDR R1, [b] ; 2. 读取内存b的值(0x34处数据)到R1
ADD R0, R0, R1 ; 3. CPU内部ALU执行加法:R0 = R0 + R1(a+b结果)
STR R0, [a] ; 4. 结果写回内存a:0x12处数据更新为R0的值
四、Cortex-M3/M4 的程序状态寄存器(xPSR)
xPSR(Program Status Register)是记录 CPU 运行状态的核心寄存器,并非单个寄存器,而是由APSR、IPSR、EPSR 三个子寄存器组合而成,支持单独或整体访问。
1. xPSR 的结构拆分
子寄存器 | 全称 | 核心功能 |
---|---|---|
APSR | Application PSR(应用状态寄存器) | 记录应用层运算状态,如结果正负(N)、是否为零(Z)、进位(C)、溢出(V)等标志 |
IPSR | Interrupt PSR(中断状态寄存器) | 记录当前正在处理的中断 / 异常编号(0~255),CPU 通过该编号找到中断服务函数入口 |
EPSR | Execution PSR(执行状态寄存器) | 记录指令执行相关状态,如 Thumb 模式标志(T 位,Cortex-M 系列始终为 1,清除会触发异常)、条件执行标志(ICI/IT 位) |
2. xPSR 的访问方式(MRS/MSR 指令)
xPSR 需通过MRS(读特殊寄存器) 和MSR(写特殊寄存器) 指令访问,支持 "单独访问子寄存器" 和 "整体访问组合寄存器" 两种方式:
(1)单独访问子寄存器
arm
; 读取APSR到通用寄存器R0(获取运算标志)
MRS R0, APSR
; 读取IPSR到R0(获取当前中断编号)
MRS R0, IPSR
; 将R0的值写入APSR(修改运算标志,如清除溢出标志)
MSR APSR, R0
(2)整体访问组合寄存器(PSR)
arm
; 一次性读取APSR+IPSR+EPSR的组合状态到R0
MRS R0, PSR
; 一次性将R0的值写入组合PSR(整体配置CPU状态,需谨慎使用)
MSR PSR, R0
3. xPSR 关键位域含义(补充用户表格细节)
位域 | 符号 | 含义 | 应用场景 |
---|---|---|---|
31 | N | 负标志:运算结果为负时置 1 | 判断减法结果是否为负(如比较两个数大小) |
30 | Z | 零标志:运算结果为 0 时置 1 | 循环判断(如 SUBS R0,R0,#1 后,BNE Loop) |
29 | C | 进位 / 借位标志:加法有进位或减法无借位时置 1 | 多字节加法(如 64 位数据运算)、无符号数比较 |
28 | V | 溢出标志:运算结果超出数据表示范围时置 1 | 有符号数运算(如 int8_t 类型计算 127+1 时置 1) |
8 | T | Thumb 模式标志:Cortex-M 系列始终为 1 | 清除 T 位会触发 "未定义指令异常",不可修改 |
4~0 | - | 异常编号:IPSR 中的位,指示当前中断编号 | 中断服务函数中判断中断来源(如 UART 中断编号为 28) |
补充说明
- MSR 指令仅能修改 APSR 和 EPSR 的部分位,IPSR(中断编号)由硬件自动更新,软件无法手动修改。
- xPSR 的状态会影响条件指令的执行(如 BEQ、BNE 等),是程序分支控制的核心依据。
五、ARM CPU 核心操作类型
ARM CPU 的所有功能可归纳为 4 类核心操作,覆盖数据处理、存储交互、流程控制的全场景:
操作类型 | 核心功能 | 常用指令 / 模块 | 补充说明 |
---|---|---|---|
内存 R/W | 与内存进行数据读写 | LDR(读)、STR(写)、LDM(批量读)、STM(批量写) | 所有内存操作均需通过寄存器中转,无直接内存运算指令(RISC 特性) |
运算 | 数据的数学 / 逻辑运算 | 数学运算:ADD、SUB、MUL;逻辑运算:AND、ORR、EOR | 运算均在 CPU 内部 ALU 完成,运算对象必须是寄存器(或立即数) |
跳转 / 分支 | 改变程序执行顺序 | 无条件跳转:B;带返回跳转:BL;指令集切换跳转:BX、BLX | 跳转地址可通过标签(如 B Loop)或寄存器(如 BX R0)指定 |
比较 | 数据比较,更新 xPSR 标志 | CMP(比较)、CMN(负数比较)、TST(位测试) | 比较指令本质是 "执行减法但不保存结果,仅更新 xPSR 标志"(如 CMP R0,R1 等价于 SUBS R2,R0,R1 但不保存 R2) |
六、ARM 指令集演进(ARM/Thumb/Thumb2)
ARM 最初支持两类指令集,后续通过 Thumb2 优化切换复杂度,不同指令集的特性与切换逻辑如下:
1. 早期两类指令集对比
指令集 | 指令长度 | 核心优势 | 核心劣势 | 适用场景 |
---|---|---|---|---|
ARM | 32 位 | 运行效率高,单条指令功能强 | 占用存储空间大(相同功能下比 Thumb 多占 50% 空间) | 对性能要求高、存储空间充足的场景(如早期高端处理器) |
Thumb | 16 位 | 存储空间占用小,代码密度高 | 部分复杂操作需多条指令实现,效率略低 | 存储空间有限的场景(如嵌入式微控制器) |
2. 指令集识别与切换(T 位控制)
CPU 通过xPSR 中的 T 位识别当前指令集状态:
- T=0:ARM 状态,执行 32 位 ARM 指令;
- T=1:Thumb 状态,执行 16/32 位 Thumb/Thumb2 指令。
手动切换指令集(用户代码逻辑补充)
调用不同指令集编写的函数时,需修改 PC 寄存器最低位(BIT0)实现切换:
arm
; 场景1:调用ARM指令集函数B(地址0x1000)
MOV PC, #0x1000 ; PC的BIT0=0,切换为ARM状态,跳转到0x1000执行
; 场景2:调用Thumb指令集函数A(地址0x2000)
MOV PC, #0x2001 ; PC的BIT0=1,切换为Thumb状态,跳转到0x2000执行(BIT0仅用于状态识别,地址实际为0x2000)
3. Thumb2 指令集的优化(关键补充)
为解决 "手动切换麻烦" 的问题,ARM 推出Thumb2 指令集,核心优势:
- 兼容 16 位 Thumb 指令和 32 位 "Thumb 风格" 指令,无需手动切换状态;
- 代码密度与 Thumb 相当,性能接近 ARM 指令集,兼顾空间与效率;
- 现代 ARM 芯片(如 Cortex-M3/M4/M7)仅支持 Thumb/Thumb2 指令集,不再支持 ARM 指令集,用户代码未提及此点,需重点注意。
七、ARM 汇编指令分类与格式
1. 汇编指令五大类(参考 ARM 官方文档)
指令类别 | 核心功能 | 代表指令 | 补充说明 |
---|---|---|---|
数据处理 | 寄存器间数据运算、立即数操作 | ADD(加法)、SUB(减法)、AND(与)、MOV(数据移动) | 运算对象可为 "寄存器 + 寄存器""寄存器 + 立即数",部分指令带 S 后缀(如 SUBS)可更新 xPSR |
内存访问 | 寄存器与内存的数据传输 | LDR(读内存到寄存器)、STR(写寄存器到内存)、LDM(批量读)、STM(批量写) | 支持多种寻址方式(如立即数偏移、寄存器偏移、堆栈寻址) |
跳转 | 改变程序执行流程 | B(基础跳转)、BL(带返回跳转)、BX(跳转 + 指令集切换)、BLX(带返回 + 切换) | 跳转范围受指令长度限制(如 Thumb 指令的 B 指令跳转范围为 ±2KB) |
饱和运算 | 处理数值溢出,避免结果错乱 | QADD(饱和加法)、QSUB(饱和减法)、QDADD(双倍饱和加法) | 适用于信号处理、传感器数据采集等场景(如 8 位数据 127+1 时,饱和结果为 127 而非 - 128) |
其他指令 | 系统控制、中断管理等杂项操作 | MRS(读特殊寄存器)、MSR(写特殊寄存器)、SWI(软件中断)、NOP(空操作) | 多为特权指令,仅能在系统模式或特权级下执行 |
2. 数据处理指令格式(UAL 统一汇编语言)
数据处理指令的标准格式为:Operation{cond}{S} Rd, Rn, Operand2
,各字段含义如下:
-
Operation:指令操作码,指定指令功能(如 ADD、MOV、AND);
-
{cond}(可选) :条件后缀,指定指令执行的条件(如 EQ = 相等、NE = 不相等、GT = 大于),省略则无条件执行;补充:常见条件后缀表(用户未提及,需补充)
条件后缀 含义 xPSR 标志依据 EQ 相等 Z=1 NE 不相等 Z=0 GT 大于(有符号) N=V LT 小于(有符号) N≠V CS/HS 有进位(无符号) C=1 CC/LO 无进位(无符号) C=0 -
{S}(可选):状态更新标志,带 S 则指令执行后更新 APSR 的 N/Z/C/V 标志(如 SUBS 会更新标志,SUB 不更新);
-
Rd:目的寄存器,运算结果存储的目标寄存器;
-
Rn:第一个源操作数寄存器,存放参与运算的第一个数据;
-
Operand2:第二个源操作数,可分为 "立即数"(如 #0x12)、"寄存器"(如 R1)、"寄存器移位"(如 R1,LSL#2,即 R1 左移 2 位)。
指令格式示例(补充注释)
arm
; 1. ADD{cond}{S} Rd, Rn, Operand2:无条件加法,不更新APSR(无S)
ADD R0, R1, #0x05 ; R0 = R1 + 5,不更新N/Z/C/V标志
; 2. SUBS{cond} Rd, Rn, Operand2:相等时执行减法,更新APSR(带S)
SUBS EQ R0, R1, R2 ; 若Z=1(前次运算结果相等),则R0 = R1 - R2,同时更新N/Z/C/V标志
; 3. AND{S} Rd, Rn, Operand2:逻辑与,更新APSR
AND S R0, R1, R2 ; R0 = R1 & R2,更新N/Z/C/V标志(Z=1表示结果为0)
八、寄存器赋值:MOV 指令限制与 LDR 伪指令
1. MOV 指令的 "立即数限制"(核心问题)
MOV Rd, #VAL
是寄存器赋值的基础指令,但VAL 必须是 "合法立即数",原因:
- MOV 指令长度为 16 位(Thumb)或 32 位(ARM),其中用于存储立即数的位宽有限(如 Thumb 的 MOV 指令仅支持 8 位立即数 + 4 位移位);
- 合法立即数需满足 "二进制可表示为 8 位数据循环右移偶数位"(如 0x12、0x1200、0x12000000,而 0x12345678 不合法)。
示例(合法与非法 MOV 指令)
arm
MOV R0, #0x12 ; 合法:0x12是8位立即数,无需移位
MOV R0, #0x1200 ; 合法:0x12循环右移16位(偶数位)
; MOV R0, #0x12345678 ; 非法:无法通过"8位数据+偶数移位"表示
2. LDR 伪指令:解决 "任意值赋值" 问题
若需给寄存器赋 "非合法立即数",需使用LDR 伪指令(格式:LDR Rd, =VAL),核心特点:
- "伪指令" 并非 CPU 原生指令,由编译器在编译阶段自动替换为 "合法原生指令";
- 必须带 "=",否则为 "原生 LDR 指令"(用于读内存),二者需严格区分。
3. LDR 伪指令的编译器替换逻辑(用户代码补充)
编译器根据 VAL 的类型自动适配,分两种情况:
(1)VAL 是合法立即数:替换为 MOV 指令
arm
; 原伪指令:LDR R0, =0x12(0x12是合法立即数)
; 编译器替换为原生MOV指令:
MOV R0, #0x12
(2)VAL 是非合法立即数:替换为 "DCD 定义 + 原生 LDR 读内存"
arm
; 原伪指令:LDR R0, =0x12345678(非合法立即数)
; 编译器处理步骤:
; 1. 在程序数据段用DCD指令定义该数值(存入内存)
Data_Sec: DCD 0x12345678 ; 内存地址假设为0x20000000
; 2. 替换为原生LDR指令(从内存读取该数值到R0)
LDR R0, [PC, #offset] ; offset = 0x20000000 - PC当前值(由链接器计算)
补充说明
- LDR 伪指令的偏移范围由 PC 的寻址范围决定(通常为 ±4KB),若 VAL 的地址超出范围,链接器会报错;
- 对于常量地址(如外设寄存器地址),推荐使用 LDR 伪指令(如 LDR R0, =0x40001000),避免手动计算立即数合法性。
九、ADR 伪指令:基于 PC 的地址加载
1. ADR 伪指令的本质
ADR 是 "地址加载伪指令",非 CPU 原生指令,由编译器在编译阶段替换为 "基于 PC 的加法指令"(如 ADD Rd, PC, #offset),核心作用是 "通过标签快速获取内存地址"。
2. 代码示例与替换逻辑(用户代码补充注释)
arm
; 场景:获取Loop标签的地址,存入R0
ADR R0, Loop ; 原伪指令:加载Loop标签地址到R0
; 编译器替换逻辑:
; 1. 链接阶段计算Loop标签与当前PC的偏移量offset(假设PC当前值为0x1000,Loop地址为0x1010,则offset=0x10)
; 2. 替换为原生ADD指令:
ADD R0, PC, #0x10 ; R0 = PC + 0x10 = 0x1000 + 0x10 = 0x1010(即Loop地址)
; 后续可通过R0访问Loop标签处的代码或数据
Loop:
SUBS R1, R1, #1 ; Loop标签处的指令
BNE Loop
3. ADR 与 LDR = label 的区别(补充用户疏漏)
对比维度 | ADR 伪指令 | LDR = label 伪指令 |
---|---|---|
地址来源 | 基于 PC 的偏移(小范围,通常 ±4KB) | 直接加载标签的绝对地址(大范围,无偏移限制) |
替换结果 | 替换为 ADD/ SUB 指令(无内存访问) | 若地址是立即数,替换为 MOV;否则替换为 LDR 读内存 |
适用场景 | 位置无关代码(如 Bootloader) | 位置相关代码(如固定地址的外设寄存器访问) |
十、ARM 汇编的内存操作示例
1. 基础内存写操作(用户代码补充注释)
场景:将数据 0x1234 写入内存地址 0x20000,需 3 条指令:
arm
; 1. 给R0赋值为目标内存地址(0x20000),R0作为"地址寄存器"
MOV R0, #0X20000
; 2. 用LDR伪指令给R1赋值为要存储的数据(0x1234),R1作为"数据寄存器"
LDR R1, =0X1234
; 3. 用STR指令将R1的数据写入R0指向的内存地址(0x20000)
STR R1, [R0]
2. 内存查看窗口验证(用户图示补充)
内存窗口(地址 0x20000~0x21000)显示结果:
- Word Value(32 位数据):0x1234,与 R1 的数据一致,说明写入成功;
- 字节分布(Byte 3~Byte 0) :0x00、0x00、0x12、0x34,符合 ARM小端模式(低字节存低地址)------0x1234 分解为低字节 0x34(存 Byte 0,地址 0x20000)、高字节 0x12(存 Byte 1,地址 0x20001),高位补 0。
3. 多种 STR 寻址方式(用户代码补充注释)
STR 指令支持多种寻址方式,适配不同内存访问场景:
arm
MOV R0, #0x20000 ; 基地址:0x20000
MOV R1, #0x10 ; 偏移寄存器:R1=0x10
MOV R2, #0x12 ; 待存储数据:R2=0x12
; 1. 立即数偏移:存储到R0+4地址(地址=0x20004),R0不变
STR R2, [R0, #4]
; 2. 立即数偏移+R0更新:存储到R0+8地址(0x20008),之后R0=R0+8(0x20008)
STR R2, [R0, #8]!
; 3. 寄存器偏移:存储到R0+R1地址(0x20008 + 0x10 = 0x20018),R0不变
STR R2, [R0, R1]
; 4. 寄存器移位偏移:存储到R0+(R1<<4)地址(0x20008 + (0x10<<4) = 0x20108),R0不变
STR R2, [R0, R1, LSL #4]
; 5. 后递增偏移:先存储到R0地址(0x20008),之后R0=R0+0x20(0x20028)
STR R2, [R0], #0X20
补充说明
- 所有内存访问指令的地址必须4 字节对齐(Cortex-M 系列默认要求),否则会触发 "对齐错误异常";
- 小端模式是 ARM 芯片的默认存储模式,部分芯片支持大端模式(需通过寄存器配置),但嵌入式场景极少使用。
十一、LDM/STM 指令:多寄存器批量内存操作
LDM(Load Multiple)和 STM(Store Multiple)是 "批量读写多寄存器与内存" 的指令,核心用于函数上下文保存、栈操作等场景。
1. 指令语法格式
指令类型 | 语法格式 | 核心作用 |
---|---|---|
LDM(批量读) | LDM{addr_mode}{cond} Rn{!}, reglist{^} | 从 Rn 指向的内存地址,批量读取数据到 reglist 中的寄存器 |
STM(批量写) | STM{addr_mode}{cond} Rn{!}, reglist{^} | 将 reglist 中的寄存器数据,批量写入 Rn 指向的内存地址 |
2. 关键参数解释
(1)addr_mode:地址模式(控制 Rn 增减时机)
地址模式 | 全称 | 核心逻辑 | 适用场景 |
---|---|---|---|
IA | Increment After(后递增,默认) | 先传输数据,再将 Rn += 4(4 字节对齐) | 栈出栈(LDMIA)、数组连续读取 |
IB | Increment Before(前递增) | 先将 Rn += 4,再传输数据 | 仅 ARM 指令支持,嵌入式场景极少用 |
DA | Decrement After(后递减) | 先传输数据,再将 Rn -= 4 | 仅 ARM 指令支持,用于特殊内存布局 |
DB | Decrement Before(前递减) | 先将 Rn -= 4,再传输数据 | 栈入栈(STMDB)、批量数据保存 |
(2)其他参数
- {cond}(可选):条件后缀,如 EQ(相等时执行)、NE(不相等时执行);
- Rn(基址寄存器):内存访问的基准地址,通常为 SP(栈指针)或外设基地址;
- {!}(可选):Rn 更新标志,带!则指令执行后将 "增减后的 Rn" 写回 Rn(如 STMDB SP!, {R0-R3} 会更新 SP);
- reglist(寄存器列表):批量操作的寄存器集合,格式为 {R0-R3, R5}(注意:低编号寄存器对应低内存地址);
- {^}(可选):状态寄存器影响标志,带 ^ 则操作时更新 CPSR(仅用于异常处理,如中断返回)。
3. Rn 按 4 字节增减的原因(补充细节)
- ARM 寄存器为 32 位(4 字节),批量传输时 "一个寄存器对应 4 字节内存",需保证地址对齐;
- 若 Rn 按非 4 字节增减(如 2 字节),会导致内存地址未对齐,触发 "对齐错误异常"。
示例:STMDB 批量保存寄存器(栈入栈)
arm
MOV SP, #0x20000 ; 初始化栈指针SP=0x20000(满减栈)
; 批量保存R0~R3到栈:先SP -= 4×4=16(4个寄存器,每个4字节),再传输数据
STMDB SP!, {R0-R3}
; 执行后:SP=0x20000 - 16=0x1FFFC,R3→0x1FFFC、R2→0x1FFF8、R1→0x1FFF4、R0→0x1FFF0(低寄存器存低地址)
示例:LDMIA 批量恢复寄存器(栈出栈)
arm
; 批量恢复R0~R3从栈:先传输数据,再SP += 16
LDMIA SP!, {R0-R3}
; 执行后:SP=0x1FFFC + 16=0x20000,R0=0x1FFF0数据、R1=0x1FFF4数据、R2=0x1FFF8数据、R3=0x1FFFC数据
十二、ARM 栈操作(满减栈为核心)
栈是 "LIFO(后进先出)" 的临时存储区域,由 SP(栈指针)管理,ARM 支持 4 种栈类型,其中满减栈(Full Descending) 是嵌入式场景的默认选择。
1. 栈的分类规则
栈的类型由 "SP 指向" 和 "SP 增长方向" 共同决定:
分类维度 | 类型 | 核心定义 |
---|---|---|
SP 指向 | 满栈(Full) | SP 指向 "最后一个入栈的数据" |
空栈(Empty) | SP 指向 "下一个待入栈的空地址" | |
SP 增长方向 | 增栈(Ascending) | 入栈时 SP 增大(向高地址扩展) |
减栈(Descending) | 入栈时 SP 减小(向低地址扩展) |
4 种栈类型对比
栈类型 | 入栈逻辑 | 出栈逻辑 | 常用指令 |
---|---|---|---|
满增栈 | 先存数据,再 SP += 4 | 先 SP -= 4,再读数据 | STMIA、LDMDB |
满减栈 | 先 SP -= 4,再存数据 | 先读数据,再 SP += 4 | STMFD(=STMDB)、LDMFD(=LDMIA) |
空增栈 | 先存数据,再 SP += 4 | 先读数据,再 SP -= 4 | STMIB、LDMDA |
空减栈 | 先 SP -= 4,再存数据 | 先 SP += 4,再读数据 | STMDD、LDMEA |
2. 常用满减栈操作(用户代码补充注释)
场景:保存 R1~R3 到栈,修改后恢复,SP 初始值 0x20000:
arm
; 1. 准备数据:R1=1、R2=2、R3=3
MOV R1, #1
MOV R2, #2
MOV R3, #3
; 2. 初始化栈指针SP=0x20000(满减栈基地址)
MOV SP, #0x20000
; 3. 入栈(STMFD = STMDB):批量保存R1~R3到栈
STMFD SP!, {R1-R3}
; 入栈逻辑:SP先减12(3个寄存器×4字节)→0x1FFF4,再存R3→0x1FFF4、R2→0x1FFF8、R1→0x1FFFC
; 4. 模拟中间操作:修改R1~R3的值
MOV R1, #0
MOV R2, #0
MOV R3, #0
; 5. 出栈(LDMFD = LDMIA):从栈恢复R1~R3的值
LDMFD SP!, {R1-R3}
; 出栈逻辑:先读R1→0x1FFFC数据(1)、R2→0x1FFF8数据(2)、R3→0x1FFF4数据(3),再SP加12→0x20000
; 执行后:R1=1、R2=2、R3=3(恢复原始值),SP=0x20000(恢复初始值)
补充说明
- 栈的大小由编译器或程序员配置(如在链接脚本中指定栈空间为 0x1000),超出栈大小会导致 "栈溢出",破坏其他数据;
- 函数调用时,栈用于保存函数参数、返回地址(LR)和局部变量,是函数上下文切换的核心载体。
十三、ARM 跳转指令(4 类核心指令)
跳转指令用于控制程序执行流程,支持 "基础跳转""带返回跳转""指令集切换跳转",是函数调用、循环控制的核心。
1. 基础跳转指令:B(Branch)
- 作用 :无条件或条件跳转至标签,不保存返回地址(跳转后无法直接返回);
- 语法 :
B{cond}{.W} label
;- {cond}:条件后缀(如 EQ、NE),省略则无条件跳转;
- {.W}:指定为 Thumb2 的 32 位指令,扩大跳转范围(±16MB);
- 示例(用户场景补充):
arm
; 无条件跳转至Loop标签(16位Thumb指令,跳转范围±2KB)
B Loop
; 相等时跳转至Exit标签(32位Thumb2指令,跳转范围±16MB)
B.W EQ Exit
Loop:
SUBS R0, R0, #1
BNE Loop ; 不相等则跳回Loop(B指令的条件变体)
Exit:
NOP ; 跳转目标
2. 带返回跳转指令:BL(Branch with Link)
- 作用 :跳转前将 "下一条指令地址" 存入 LR 寄存器,支持跳转后返回(核心用于函数调用);
- 语法 :
BL{cond} label
; - 示例(函数调用与返回):
arm
; 调用func函数:跳转前将"MOV R1, #0"的地址存入LR
BL func
MOV R1, #0 ; 函数返回后执行此指令
func:
ADD R0, R0, #1 ; 函数逻辑
BX LR ; 函数返回:将LR的值赋给PC,回到调用处
3. 跳转 + 指令集切换:BX(Branch and eXchange)
- 作用:根据寄存器 Rm 的 BIT0 切换指令集,再跳转到 Rm 指向的地址;
- 语法 :
BX{cond} Rm
;- Rm 的 BIT0=0:切换为 ARM 状态(执行 32 位 ARM 指令);
- Rm 的 BIT0=1:切换为 Thumb 状态(执行 16/32 位 Thumb 指令);
- 示例(指令集切换):
arm
; R0=0x1001(BIT0=1):切换为Thumb状态,跳转到0x1000(BIT0仅用于状态识别)
BX R0
; R0=0x2000(BIT0=0):切换为ARM状态,跳转到0x2000
BX R0
4. 带返回 + 指令集切换:BLX(Branch with Link and eXchange)
- 作用:结合 BL 和 BX 的功能 ------ 保存返回地址到 LR,同时根据目标地址 BIT0 切换指令集;
- 语法 :两种形式:
BLX{cond} label
:跳转到标签,自动根据标签地址 BIT0 切换指令集;BLX{cond} Rm
:根据 Rm 的 BIT0 切换指令集,跳转到 Rm 指向的地址;
- 示例(跨指令集函数调用):
arm
; 调用Thumb指令集的func函数:保存返回地址到LR,切换为Thumb状态
BLX func
; 调用ARM指令集的func2函数:通过R0指定地址,保存返回地址到LR,切换为ARM状态
MOV R0, #0x3000 ; R0=0x3000(BIT0=0,ARM状态)
BLX R0
补充说明
- Cortex-M 系列仅支持 Thumb/Thumb2 指令集,BX/BLX 的 "ARM 状态切换" 在该系列中无效(会触发异常),仅在 Cortex-A 系列(如 A7/A9)中有用;
- 函数返回优先使用 "BX LR",若需支持协处理器,可使用 "BLX LR"(但嵌入式场景极少)。
十四、延迟循环代码示例
代码实现 "延迟循环 + 函数式返回",核心利用 ADR 伪指令保存返回地址、LR 寄存器存储返回点,整理并补充注释如下:
代码分段与注释
arm
; 1. 保存返回地址:用ADR伪指令将Ret标签地址加载到LR(链接寄存器)
; 作用:记住"延迟逻辑执行完后,要回到Ret处继续执行"
ADR LR, Ret
; 2. 跳转到延迟逻辑入口:用ADR伪指令将Delay标签地址加载到PC(程序计数器)
; 作用:修改PC,让程序跳转到Delay处执行延迟代码
ADR PC, Delay
; 3. 延迟逻辑入口:Delay标签
Delay:
; 初始化R1=1(R1暂未参与核心延迟,可作为临时变量)
MOV R1, #1
; 初始化延迟循环计数器R0=5(循环将执行5次)
MOV R0, #5
; 4. 延迟循环体:Loop标签
Loop:
; R0 = R0 - 1,带S后缀(更新xPSR的Z标志)
SUBS R0, R0, #1
; 若Z=0(R0≠0),则跳回Loop继续循环;Z=1(R0=0)则退出循环
BNE Loop
; 5. 延迟结束后返回:将LR中保存的Ret地址赋给PC,回到初始调用处
MOV PC, LR
; 6. 返回目标地址:Ret标签(延迟结束后执行此指令)
Ret:
MOV R1, #1 ; 示例后续逻辑,可根据需求修改
补充说明
- 延迟时间计算:延迟总时间 = 循环次数 × 单循环指令周期数 × 指令周期(如晶振 16MHz,指令周期 = 62.5ns,5 次循环总延迟≈5×2×62.5ns=625ns);
- ADR 伪指令的偏移范围有限(通常 ±4KB),若 Delay 标签地址超出范围,需改用 LDR 伪指令(如 LDR PC, =Delay)。