ARM 指令集的设计哲学
Table of Contents
在复习嵌入式系统设计时,发现 ARM 指令集中有一些令人费解的设计细节。
比如为什么立即数只能移动偶数位?为什么减法要有“反向版”?为什么清除位要单独设计指令?
这好像违背了 RISC 的设计原则:“如果一个功能可以用现有的指令组合出来,就绝对不浪费硅片去设计一个新的电路。”
但其实都是工程师们在有限指令空间内的巧妙设计。
一:步长为 2 —— 立即数编码
在 32 位的定长指令中,留给数据的空间只有 12 位。如何在这么小的空间里表示出尽可能多的 32 位整数?
ARM 的答案是“循环右移”。
但这里藏着一个极易被忽视的细节:移位器的步长被强制设定为 2。
为什么是 2?
这 12 位空间被拆解为:
- 8 位:作为基础数值(0-255)。
- 4 位:作为移位控制(rotate_imm)。
4 位二进制最多只能表示 16 种状态(0-15)。而我们的目标是让这 8 位数据能在 32 位的寄存器中遍历(覆盖 0-31 位)。
这是一道简单的数学题:
- 如果步长是 1:只能覆盖一半的寄存器空间。
- 如果步长是 2:完美覆盖所有位域。
为什么不分给 rotate 5 位?
但难免会有疑问:为什么不直接用 5 位来表示 rotate,这样就能直接覆盖 0-31 位了?
这是一个经典的设计取舍:
- 如果移位用 5 位:剩下数据位只剩 7 位。
- 7 位能存什么?:0 ~ 127。
- 8 位能存什么?:0 ~ 255(刚好一个字节)。
结论
ARM 工程师觉得,“保证 8 位数据完整性” 比 “能移动到奇数位置” 更重要。
二:RSB —— 反向减法指令
RSB 指令的基本语法如下:
RSB{S}{cond} Rd, Rn, Operand2
Rd:目标寄存器,存储结果。Rn:第一个操作数寄存器。Operand2:第二个操作数,可以是立即数或寄存器。
公式实际上等同于 Rd = Operand2 - Rn。
解决减法局限性(核心)
这是最最最重要的原因。
在 ARM 指令集中,立即数只能作为第二个操作数使用,无法直接作为第一个操作数参与减法运算。
场景: 计算 100 - R0。
-
尝试用 SUB:
SUB R1, #100, R0Error!,因为#100是立即数,不能放在第一个位置。
-
尝试用 SUB(换位置):
SUB R1, R0, #100- 算出来是
R0 - 100,不是我们要的结果。
- 算出来是
救世主 RSB: 允许我们把寄存器放在第一个位置(Rn),把立即数放在第二个位置(Op2),但是执行反向减法。
- 代码:
RSB R1, R0, #100 - 含义:
R1 = #100 - R0 - 结果: 解决了“常数减变量”的问题。
快速求负数(取反)
如果想把 R0 变成 -R0(比如把 5 变成 -5)。
- 用 SUB: 先找个寄存器存 0,然后
SUB R1, R2(存了0), R0。太麻烦。 - 用 RSB: 一行代码搞定。
RSB R0, R0, #0- 含义:
R0 = 0 - R0 - 标准的数学取负操作。
- 含义:
乘法优化(进阶)
配合移位指令,可以实现一些乘法优化。
场景:计算 R0 = R1 * 7。常规思路是使用 MUL 指令。
- 用 RSB 优化:
RSB R0, R1, R1, LSL #3 - 解析:
R1, LSL #3相当于R1 * 8RSB实际上计算R1 * 8 - R1,等同于R1 * 7
这条指令实现了一个周期计算 R1 * 7,比传统的乘法指令更高效。
三:BIC —— 位清除指令
BIC 在硬件底层做了两步操作:
-
取反:先把 Op2(掩码)里的每一位都黑白颠倒。
-
相与:再拿着这个颠倒后的数,和 Rn 进行 AND 运算。
BIC 使用情景
假设 R0 是 8 个灯的状态,Op2 是一个灭灯清单。
-
R0 (当前状态):
1 1 1 1 0 0 1 1(灯亮为 1,灭为 0) -
Op2 (灭灯清单):
0 0 0 0 1 1 1 1(希望把最后 4 位灭掉)
BIC 执行过程
-
取反 Op2:
1 1 1 1 0 0 0 0 -
AND 运算:
1111 0011 (R0 原来的值) & 1111 0000 (取反后的 Op2) ----------- 1111 0000 (结果)
为什么需要 BIC?
难免有疑惑:直接用 AND R0, R0, #0xF0 (1111 0000) 不就好了?
-
符合编码直觉:
- 使用 AND 清零: 需要在掩码里把想保留的写成 1,把想清除的写成 0。
- 使用 BIC 清零: 直接把想清除的写成 1,想保留的写成 0。更符合“清除”的直觉。
-
合法立即数的瓶颈: 现在我需要把 R0 的 第 0 位清零(其他位保持不变)。
- 使用 AND:掩码需要是
1111 1110,但这个数无法通过 ARM 的立即数编码规则表示出来。 - 使用 BIC:掩码是
0000 0001,合法的立即数。
- 使用 AND:掩码需要是
多讲两句
而且加一个 BIC 看似浪费了硬件电路,其实并没有。
ARM 的 ALU(算术逻辑单元)前面本来就挂着一个桶形移位器(Barrel Shifter)。
这个移位器不仅能做移位,其内部还很容易实现“取反”功能。
AND令走的是:A & BBIC令走的是:A & ~B
在电路设计上,这只是在 B 的输入端加了一排反相器(NOT gate),用极低的成本换来了巨大的代码效率提升。
它是 ARM “实用主义” 战胜 “教条主义” 的经典案例。