Appearance
ARM 体系结构
概念
前两章用的是 x86 那一路的口径(8086、段式寻址、独立编址)。但嵌入式世界的默认处理器是 ARM:手机、路由器、STM32 单片机、树莓派,里面全是 ARM 核。
一句话说清它是什么:ARM 是一种 RISC 风格的处理器体系结构,用"定长指令 + 规整寄存器"换来"简单、省电、好流水线"。
它和 lang 里学的 MIPS32 是同一类的两种写法:
| 维度 | MIPS32 | ARM |
|---|---|---|
| 风格 | RISC | RISC |
| 通用寄存器 | 32 个 | 16 个(R0–R15) |
| 指令长度 | 定长 32 位 | A32 定长 32 位;Thumb 可 16 位 |
| 返回地址 | $ra($31) | R14(LR) |
| 条件执行 | 靠分支指令 | 每条指令自带条件码 |
| 访存 | load/store 架构 | load/store 架构 |
为什么嵌入式学员要专门学一篇 ARM:因为写裸机 C 代码时你看到的所有编译产物、启动文件、__attribute__((interrupt)),都是 ARM 的规矩。看不懂 R0–R3 与 R13,就看不懂为什么函数的第 5 个参数会跑到栈上。
这一层回答了上一层什么问题:上一章说清了"中断怎么打断 CPU",但那是 x86 的视角。换到 ARM,你要重新回答同一批问题——寄存器叫什么、现场怎么保存、函数怎么调用。这一篇把 x86 换成 ARM,把上一章没讲的"寄存器级别的细节"补齐。
原理
一、RISC 的选择:为什么嵌入式选 ARM
ARM 属 RISC(精简指令集),与 x86 的 CISC 是两条路。取舍如下:
| 对比项 | RISC(ARM) | CISC(x86) |
|---|---|---|
| 指令长度 | 定长 | 变长 |
| 指令条数 | 少而规整 | 多而复杂 |
| 寻址方式 | 少 | 多 |
| 访存 | 只有 load/store 能访存 | 大部分指令能直接访存 |
| 译码 | 简单、规则 | 复杂 |
| 适合 | 低功耗、嵌入式、流水线 | 桌面、兼容历史 |
关键在"规整"二字:指令都是定长,译码器就能在固定位置取到操作码;只有 load/store 访存,流水线就不必处理"算一半又要等内存"的复杂情况。省下来的晶体管,换来的是能效比——这正是嵌入式和移动设备最在意的指标。
二、寄存器组:16 个通用寄存器
ARM 的通用寄存器只有 16 个(R0–R15),每个 32 位;另有一个状态寄存器 CPSR。
| 寄存器 | 别名 | 用途 |
|---|---|---|
| R0–R3 | — | 传参、返回值、临时 |
| R4–R11 | — | 被调用者保存的局部变量 |
| R12 | IP | 临时(过程内调用用) |
| R13 | SP | 栈指针 |
| R14 | LR | 连连接寄存器(存返回地址) |
| R15 | PC | 程序计数器 |
三个特殊寄存器要专门记:
- SP(R13):栈顶指针。ARM 的栈通常满递减(Full Descending)——压栈时 SP 先减后存。
- LR(R14):保存返回地址。这是 ARM 与 MIPS 最大的差异点:MIPS 用
$31,ARM 用 R14。调用子程序时BL指令自动把返回地址放进 LR。 - PC(R15):程序计数器。ARM 里 PC 可以被当作普通寄存器直接读写,所以"PC 相对寻址"就是一条普通指令,不需要专门指令。
为什么只有 16 个:寄存器越少,指令里用来编码寄存器号的位数越少,指令可以更短、译码更快。代价是编译器的寄存器分配更紧张,更多变量要落到栈上。
三、AAPCS 调用约定
AAPCS(ARM Architecture Procedure Call Standard)规定了函数之间怎么交接。四条要记住:
text
第 1~4 个整数参数 -> R0, R1, R2, R3
第 5 个参数起 -> 栈(由调用者放入)
返回值 -> R0(64 位用 R0:R1)
R4~R11 -> 被调用者保存(callee-saved,用前先存、返回前恢复)与 MIPS 的对照(lang 里的 o32 约定):
| 项 | MIPS32 (o32) | ARM (AAPCS) |
|---|---|---|
| 参数寄存器 | $a0–$a3(4 个) | R0–R3(4 个) |
| 返回值 | $v0($v1 辅助) | R0(R1 辅助) |
| 返回地址 | $ra | R14(LR) |
| 被调用者保存 | $s0–$s7 | R4–R11 |
| 第 5 个参数 | 落栈,16($sp) | 落栈 |
两套约定的第 5 个参数都落栈,这不是巧合:4 个参数寄存器是 RISC 的通用选择,第 5 个开始只能走内存。
四、Thumb:用 16 位换代码密度
ARM 的定长 32 位指令叫 A32。但它还有一套 Thumb 指令集:指令长度 16 位。
| 指令集 | 长度 | 特点 |
|---|---|---|
| A32(ARM) | 32 位定长 | 功能全、性能好 |
| T16(Thumb) | 16 位定长 | 代码密度高,指令是 A32 的常用子集 |
| Thumb-2 | 16/32 位混合 | 两者兼得,Cortex-M 的主流 |
代价与收益:Thumb 用"更少的指令、更少的寄存器字段"换来一半的指令长度,所以同样的程序体积更小。嵌入式设备的 Flash 很贵,代码密度直接决定芯片选型。
还要注意一条:Thumb 状态下 PC 的最低位有特殊含义——用来标记"切换到 ARM 还是 Thumb",所以真实跳转地址的 bit0 不代表地址的 0 位。这个细节在写汇编跳转时最容易踩。
五、条件执行:把分支变成前缀
这是 ARM 最有辨识度的特征:几乎每条指令都可以带一个条件码后缀,只有条件成立才执行。
对比一下 C 语句 if (a == b) x = 1;:
| 做法 | 指令序列 | 说明 |
|---|---|---|
| 用分支 | CMP r0, r1 → BNE L → MOV r2, #1 → L: | 3 条,且预测失败要清流水线 |
| 条件执行 | CMP r0, r1 → MOVEQ r2, #1 | 2 条,没有分支 |
"没有分支"就是收益:分支是流水线的敌人(预测失败要冲刷),而条件执行把"跳不跳"变成"执不执行",流水线一路不停。常用的条件码有 EQ(相等)、NE(不等)、GT(大于)、LT(小于)等,共 15 个真条件加上恒真的 AL。
六、三级流水线
ARM7TDMI 的经典流水线是三级:取指 → 译码 → 执行。
text
周期: 1 2 3 4 5 6 ...
取指: I1 I2 I3 I4 I5 I6
译码: I1 I2 I3 I4 I5
执行: I1 I2 I3 I4理想情况下每个周期流出一条指令。N 条指令总共用 N + 2 个周期(前两级要"灌满"):
text
非流水线:每条指令 3 周期 -> 3N 周期
三级流水:N 条指令 -> N + 2 周期
加速比 :3N / (N + 2) -> N 很大时趋近 3加速比的上限就是流水线级数,而且指令越少越吃亏(N=1 时加速比只有 1)。这就是流水线的"灌满与排空"代价。
PC 在流水线里的位置:因为取指比执行早两级,ARM 里读 PC 得到的是"当前指令地址 + 8"(A32)。这个偏差值是面试与考试里常出的细节,写 PC 相对寻址时要减掉。
七、ARM 与 MIPS32 的对照
把两套体系放在一起,差异集中在四点:
| 差异点 | ARM | MIPS32 |
|---|---|---|
| 返回地址寄存器 | LR(R14),可嵌套时自动换 bank | $ra,递归要自己压栈 |
| 条件执行 | 每条指令可带条件码 | 只有分支指令有条件 |
| 指令长度 | A32 定长 32 位、Thumb 16 位 | 定长 32 位 |
| 寄存器数 | 16 个通用 | 32 个通用 |
| 读 PC 的值 | 当前地址 + 8(三级流水) | 延迟槽后的地址 |
两者是同一族的两种取舍:ARM 用"少寄存器 + 条件执行 + 可选 16 位"换代码密度与能效;MIPS 用"多寄存器 + 规整三操作数"换编译器友好。学会一个,另一个看半小时就能读写。
示例
例 1:AAPCS 参数分配
同一个函数,参数个数不同,落位就不同:
3 个参数 -> 3 个走 R0-R3,0 个走栈
4 个参数 -> 4 个走 R0-R3,0 个走栈
5 个参数 -> 4 个走 R0-R3,1 个走栈
6 个参数 -> 4 个走 R0-R3,2 个走栈
8 个参数 -> 4 个走 R0-R3,4 个走栈第 5 个参数是分水岭。写驱动时如果一个函数参数超过 4 个,就会被编译器悄悄塞到栈上,中断里调用这类函数要留够栈空间。
例 2:Thumb 代码密度
设一段程序共 1000 条指令:
0% 编为 16 位 Thumb -> 4000 B,省 0.0%
30% 编为 16 位 Thumb -> 3400 B,省 15.0%
50% 编为 16 位 Thumb -> 3000 B,省 25.0%省的比例恰好是"Thumb 占比的一半":因为 16 位对 32 位就是省一半,实际省下 = 占比 × 50%。所以"30% 的指令用 Thumb"→ 省 15%,这条换算可以直接心算。
例 3:流水线加速比
N=1 条:非流水 3 周期,流水 3 周期,加速比 1.0000
N=3 条:非流水 9 周期,流水 5 周期,加速比 1.8000
N=10 条:非流水 30 周期,流水 12 周期,加速比 2.5000
N=100 条:非流水 300 周期,流水 102 周期,加速比 2.9412
N=1000 条:非流水 3000 周期,流水 1002 周期,加速比 2.9940N 越大越接近 3,但永远到不了 3。多选题里"三级流水线一定能把性能提高 3 倍"是错的。
例 4:C 实现——AAPCS 参数分配模拟
用一个数组模拟 R0–R3,一个数组模拟栈,看参数怎么分流。
/* aapcs.c —— 用 C 模拟 AAPCS 下的参数分配 */
#include <stdio.h>
static int reg_args[4]; /* 代表 R0-R3 */
static int stack_args[16]; /* 代表栈 */
static int stack_depth;
static void call_sim(const char *name, int argc, const int *argv)
{
int i;
stack_depth = 0;
for (i = 0; i < argc; i++) {
if (i < 4) {
reg_args[i] = argv[i]; /* 前 4 个进寄存器 */
} else {
stack_args[stack_depth++] = argv[i]; /* 其余进栈 */
}
}
printf("%s(%d args): R0-R3 =", name, argc);
for (i = 0; i < argc && i < 4; i++) {
printf(" %d", reg_args[i]);
}
printf(" ; stack =");
if (stack_depth == 0) {
printf(" (empty)");
} else {
for (i = 0; i < stack_depth; i++) {
printf(" %d", stack_args[i]);
}
}
printf("\n");
}
int main(void)
{
const int a3[] = {10, 20, 30};
const int a5[] = {10, 20, 30, 40, 50};
const int a8[] = {10, 20, 30, 40, 50, 60, 70, 80};
printf("regs: R0-R3 args, R0 return, R4-R11 callee-saved, R13=SP R14=LR R15=PC\n");
call_sim("add3", 3, a3);
call_sim("add5", 5, a5);
call_sim("add8", 8, a8);
printf("args 5+ go on stack -> 8 args means %d words on stack\n", 8 - 4);
printf("3-stage pipeline: 1 insn/cycle ideal, N insns take N+2 cycles\n");
printf("N=1000 -> 1002 cycles, speedup 3.0000*1000/1002 = %.4f\n", 3.0 * 1000 / 1002);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
regs: R0-R3 args, R0 return, R4-R11 callee-saved, R13=SP R14=LR R15=PC
add3(3 args): R0-R3 = 10 20 30 ; stack = (empty)
add5(5 args): R0-R3 = 10 20 30 40 ; stack = 50
add8(8 args): R0-R3 = 10 20 30 40 ; stack = 50 60 70 80
args 5+ go on stack -> 8 args means 4 words on stack
3-stage pipeline: 1 insn/cycle ideal, N insns take N+2 cycles
N=1000 -> 1002 cycles, speedup 3.0000*1000/1002 = 2.9940三行函数调用的输出把 AAPCS 说完了:前 4 个参数进寄存器,第 5 个开始进栈。真实硬件上"进栈"由调用者在 BL 之前完成,这里用数组模拟了同一件事。
例 5:Python——寄存器、密度与流水线验算
# ARM 体系结构:寄存器组、AAPCS、Thumb、流水线
print("=== 一、寄存器组(32 位)===")
regs = [("R0-R3", "参数/返回值/临时"), ("R4-R11", "被调用者保存的局部变量"),
("R12", "临时(IP)"), ("R13", "SP 栈指针"), ("R14", "LR 连接寄存器(存返回地址)"),
("R15", "PC 程序计数器")]
for r, use in regs:
print(f"{r:8s} {use}")
print(f"共 16 个通用寄存器 x 32 位 = {16 * 32 // 8} B,另有 CPSR 状态寄存器")
print("\n=== 二、AAPCS 参数分配 ===")
for k in (3, 4, 5, 6, 8):
print(f"{k} 个整数参数 -> {min(k, 4)} 个走 R0-R3,{max(0, k - 4)} 个走栈")
print("返回值 -> R0(64 位用 R0:R1)")
print("\n=== 三、Thumb 代码密度 ===")
total = 1000
for ratio in (0.0, 0.3, 0.5):
size = int(total * (1 - ratio) * 4 + total * ratio * 2)
print(f"{ratio * 100:2.0f}% 指令编为 16 位 Thumb: {total * 4} B -> {size:5d} B,省 {(1 - size / (total * 4)) * 100:.1f}%")
print("\n=== 四、三级流水线的加速比(取指-译码-执行)===")
for N in (1, 3, 10, 100, 1000):
print(f"N={N:5d} 条: 非流水 {3 * N:6d} 周期,流水 {N + 2:6d} 周期,加速比 {3 * N / (N + 2):.4f}")
print(f"N 很大时加速比上限 = {3:.4f}")
print("\n=== 五、条件执行 vs 分支 ===")
print("C: if (a == b) x = 1;")
print("ARM: CMP r0, r1 ; MOVEQ r2, #1 -> 2 条,无分支")
print("对照: CMP r0, r1 ; BNE L ; MOV r2,#1 ; L: -> 3 条,且预测失败要清流水线")
print("\n=== 六、ARM / Thumb / MIPS 指令长度 ===")
for name, ln, stride in [("ARM (A32)", 32, 4), ("Thumb (T16)", 16, 2), ("MIPS32", 32, 4)]:
print(f"{name:12s} {ln} 位 = {ln // 8} B 对齐,{stride} 字节步长")
print(f"4 KB 指令空间:A32 可放 {4096 // 4} 条,T16 可放 {4096 // 2} 条")
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
=== 一、寄存器组(32 位)===
R0-R3 参数/返回值/临时
R4-R11 被调用者保存的局部变量
R12 临时(IP)
R13 SP 栈指针
R14 LR 连接寄存器(存返回地址)
R15 PC 程序计数器
共 16 个通用寄存器 x 32 位 = 64 B,另有 CPSR 状态寄存器
=== 二、AAPCS 参数分配 ===
3 个整数参数 -> 3 个走 R0-R3,0 个走栈
4 个整数参数 -> 4 个走 R0-R3,0 个走栈
5 个整数参数 -> 4 个走 R0-R3,1 个走栈
6 个整数参数 -> 4 个走 R0-R3,2 个走栈
8 个整数参数 -> 4 个走 R0-R3,4 个走栈
返回值 -> R0(64 位用 R0:R1)
=== 三、Thumb 代码密度 ===
0% 指令编为 16 位 Thumb: 4000 B -> 4000 B,省 0.0%
30% 指令编为 16 位 Thumb: 4000 B -> 3400 B,省 15.0%
50% 指令编为 16 位 Thumb: 4000 B -> 3000 B,省 25.0%
=== 四、三级流水线的加速比(取指-译码-执行)===
N= 1 条: 非流水 3 周期,流水 3 周期,加速比 1.0000
N= 3 条: 非流水 9 周期,流水 5 周期,加速比 1.8000
N= 10 条: 非流水 30 周期,流水 12 周期,加速比 2.5000
N= 100 条: 非流水 300 周期,流水 102 周期,加速比 2.9412
N= 1000 条: 非流水 3000 周期,流水 1002 周期,加速比 2.9940
N 很大时加速比上限 = 3.0000
=== 五、条件执行 vs 分支 ===
C: if (a == b) x = 1;
ARM: CMP r0, r1 ; MOVEQ r2, #1 -> 2 条,无分支
对照: CMP r0, r1 ; BNE L ; MOV r2,#1 ; L: -> 3 条,且预测失败要清流水线
=== 六、ARM / Thumb / MIPS 指令长度 ===
ARM (A32) 32 位 = 4 B 对齐,4 字节步长
Thumb (T16) 16 位 = 2 B 对齐,2 字节步长
MIPS32 32 位 = 4 B 对齐,4 字节步长
4 KB 指令空间:A32 可放 1024 条,T16 可放 2048 条考点
- 寄存器编号与别名:R13 = SP、R14 = LR、R15 = PC。LR 存的是返回地址,这是 ARM 与 MIPS(
$ra)的唯一对应关系,别答成"R14 是栈指针"。 - AAPCS 四条:R0–R3 传参(64 位返回用 R0:R1)、R0 返回、R4–R11 被调用者保存、第 5 个参数起走栈。与 MIPS o32 参数个数相同(都是 4 个)。
- Thumb 密度换算:省下的比例 = Thumb 指令占比 × 50%。别记成"Thumb 指令全用就省一半"以外的东西。
- 条件执行是 ARM 的招牌:
CMP+ 带条件后缀的指令,省掉分支也省掉流水线冲刷。要能写出if (a==b) x=1;的两种编译结果。 - 三级流水线加速比
3N/(N+2):上限是 3,N 越小越吃亏。判断题"流水线一定提升 3 倍"必错。 - 读 PC 得"当前地址 + 8":A32 状态下,因为取指领先执行两级。这个常量 8 常被拿来出选择题。
- 易错:把 ARM 当成 CISC;把 LR 与 SP 弄混;以为 Thumb 是"另一种处理器"(它只是同一处理器的另一套指令编码);把"被调用者保存"记成"调用者保存"。
小结
- ARM 是 RISC 的嵌入式代表:定长指令、load/store 访存、少量通用寄存器,用规整换能效。
- 16 个通用寄存器 + 别名:R13=SP、R14=LR、R15=PC;LR 保存返回地址,是 ARM 的
$ra。 - AAPCS:R0–R3 传参、R0 返回、R4–R11 被调用者保存、第 5 个参数起落栈。
- Thumb 用 16 位换代码密度,省下的比例是占比的一半;Thumb-2 混合两者。
- 条件执行把分支变前缀,是 ARM 压制流水线冲刷的核心手段。
- 三级流水线的加速比是
3N/(N+2),上限 3;读 PC 得到的是当前地址加 8。
回到主线:这一篇是 embed 的"处理器底座"。它把 RISC 与 CISC 的取舍、指令编码、流水线 这三篇的结论落到了具体的一颗核上;同时它复用了 lang 里 函数调用与栈帧 的整套口径,只是把寄存器名字换了一遍——换成 ARM 之后,上一章的中断现场保护、下一章的寄存器配置,用的都是这 16 个寄存器。
这一章只答了"处理器内部有哪些寄存器、函数怎么调用",还没答"怎么让一个引脚变高变低"。下一章从 CPU 出来,走到芯片的边缘——引脚。
下一篇:GPIO 与最小系统
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。