3. ARMv7 指令集基础:ARM 指令与 Thumb 指令切换、条件执行、常用数据处理指令
ARMv7 的指令集,说白了就是处理器的「母语」。你让它干活,就得用它能听懂的话。ARM 处理器最特别的地方,就是它有两套「母语」——ARM 指令和 Thumb 指令。我刚开始接触时也觉得奇怪,为什么要有两套?后来在项目里吃过亏才明白,这其实是性能和代码密度的平衡艺术。
3.1 ARM 指令 vs Thumb 指令:双面人生
ARM 指令是 32 位定长,每条指令能干很多事。Thumb 指令是 16 位定长(后来 Thumb-2 引入了 32 位混合),指令密度高,但功能相对受限。你想想看,同样的代码,用 Thumb 编译出来体积能小 30% 左右,这对嵌入式系统来说太重要了。
| 特性 | ARM 指令 | Thumb 指令 |
|---|---|---|
| 指令长度 | 固定 32 位 | 16 位 / 32 位混合 |
| 代码密度 | 较低 | 较高(节省 30% 空间) |
| 性能 | 单周期执行更多操作 | 部分指令需多周期 |
| 寄存器访问 | 所有 16 个寄存器 | 受限(通常只访问低 8 个) |
| 条件执行 | 几乎所有指令都支持 | 仅分支指令支持 |
3.2 ARM/Thumb 状态切换:BX 指令的妙用
ARM 处理器通过 CPSR 的 T 位来标记当前状态:T=0 是 ARM 状态,T=1 是 Thumb 状态。切换的核心指令是 BX(Branch and Exchange)。
; 从 ARM 切换到 Thumb
LDR R0, =thumb_func + 1 ; 注意:目标地址最低位写 1
BX R0
; 从 Thumb 切换到 ARM
LDR R0, =arm_func ; 目标地址最低位写 0
BX R0
这里有个坑,我踩过不止一次:目标地址的最低位必须正确设置。ARM 模式下地址最低位必须是 0,Thumb 模式必须是 1。处理器在跳转时会自动清除这个位来获取真实地址,同时根据这个位切换 T 标志。
3.3 条件执行:ARM 指令的灵魂
ARM 指令最让我着迷的特性就是条件执行。几乎所有 ARM 指令都可以带条件后缀,根据 CPSR 中的标志位决定是否执行。这能省掉很多分支跳转,让代码更紧凑。
| 条件码 | 含义 | 标志位条件 |
|---|---|---|
| EQ | 等于 | Z = 1 |
| NE | 不等于 | Z = 0 |
| GT | 有符号大于 | Z = 0 且 N = V |
| LT | 有符号小于 | N != V |
| HI | 无符号大于 | C = 1 且 Z = 0 |
| LS | 无符号小于等于 | C = 0 或 Z = 1 |
; 条件执行示例:求两个数的最大值
CMP R0, R1 ; 比较 R0 和 R1
MOVGT R2, R0 ; 如果 R0 > R1,R2 = R0
MOVLE R2, R1 ; 如果 R0 <= R1,R2 = R1
你看,没有跳转指令,没有标签,三行搞定。这在流水线处理器里能避免分支预测失败的开销。我个人习惯在状态机实现中大量使用条件执行,代码可读性反而更好。
3.4 常用数据处理指令:MOV、ADD、SUB
这些指令是 ARM 汇编的「三原色」。别看简单,用好了能写出非常高效的代码。
3.4.1 MOV 指令
MOV 用于寄存器间传值或加载立即数。但要注意,ARM 的立即数不是随便写的——它必须是 8 位值循环右移偶数位的结果。
MOV R0, #0xFF ; 合法:8 位值
MOV R1, #0x100 ; 非法!0x100 超过 8 位
; 正确做法:
MOV R1, #0x01
MOV R1, R1, LSL #8 ; 通过移位得到 0x100
我记得刚学 ARM 时,被这个立即数规则折磨得不轻。后来我总结了一个规律:能用 8 位表示的立即数直接写,否则用 LDR 伪指令加载。
3.4.2 ADD 和 SUB 指令
加减法指令支持灵活的移位操作数,这是 ARM 的特色。
; 基本用法
ADD R0, R1, R2 ; R0 = R1 + R2
SUB R0, R1, #10 ; R0 = R1 - 10
; 带移位的用法
ADD R0, R1, R2, LSL #2 ; R0 = R1 + (R2 << 2)
SUB R0, R1, R2, ASR #3 ; R0 = R1 - (R2 >> 3) 算术右移
这种带移位的操作数,在数组索引、结构体成员访问时特别有用。比如你要访问一个 int 数组的第 n 个元素,直接用 LDR R0, [R1, R2, LSL #2] 就搞定了,不用先乘 4 再加。
3.5 LDR/STR:内存访问的核心
ARM 是加载/存储架构,所有数据处理都在寄存器里完成,内存访问只能通过 LDR 和 STR 指令。
| 指令 | 含义 | 示例 |
|---|---|---|
| LDR | 从内存加载到寄存器 | LDR R0, [R1] |
| STR | 从寄存器存储到内存 | STR R0, [R1] |
| LDRB | 加载一个字节 | LDRB R0, [R1] |
| STRH | 存储半个字(16 位) | STRH R0, [R1] |
| LDM | 批量加载多个寄存器 | LDMIA R0!, {R1-R4} |
| STM | 批量存储多个寄存器 | STMDB R0!, {R1-R4} |
寻址模式有三种:
- 前索引:
LDR R0, [R1, #4]— 先计算地址再访问 - 后索引:
LDR R0, [R1], #4— 先访问再更新基址 - 前索引并回写:
LDR R0, [R1, #4]!— 先计算地址访问,再更新基址
STR R0, [SP, #-4]! 相当于压栈。但要注意,回写操作(带 !)不能用于 PC 寄存器,否则会出大问题。
3.6 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到 ARM 和 Thumb 是两条并行的指令集路径,通过 BX 指令切换。条件执行是 ARM 指令的「加速器」,而数据处理指令和内存访问指令构成了日常编程的基石。
3.7 实战要点总结
- 状态切换:BX 指令的目标地址 LSB 决定切换方向,ARM 为 0,Thumb 为 1。别搞反了。
- 条件执行:ARM 模式下多用条件执行减少分支,Thumb 模式下老老实实用跳转。
- 立即数:MOV 只能加载 8 位立即数(循环右移偶数位),超范围用 LDR 伪指令。
- 寻址模式:后索引适合栈操作,前索引适合结构体访问,批量指令适合上下文切换。
- 对齐访问:LDR/STR 要求地址按字对齐(4 字节),LDRH/STRH 要求半字对齐(2 字节)。不对齐会触发异常。
__attribute__((aligned(4)))。
好了,ARM 指令集的基础就这些。记住:指令集是工具,怎么用好它取决于你对硬件的理解。多写多试,慢慢就会形成自己的风格。