Return

ARM 指令集的设计哲学

在复习嵌入式系统设计时,发现 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, R0

    • Error!,因为 #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 * 8
    • RSB 实际上计算 R1 * 8 - R1,等同于 R1 * 7

这条指令实现了一个周期计算 R1 * 7,比传统的乘法指令更高效。


三:BIC —— 位清除指令

BIC 在硬件底层做了两步操作:

  1. 取反:先把 Op2(掩码)里的每一位都黑白颠倒。

  2. 相与:再拿着这个颠倒后的数,和 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 执行过程

  1. 取反 Op2: 1 1 1 1 0 0 0 0

  2. AND 运算:

         1111 0011  (R0 原来的值)
       & 1111 0000  (取反后的 Op2)
       -----------
         1111 0000  (结果)

为什么需要 BIC?

难免有疑惑:直接用 AND R0, R0, #0xF0 (1111 0000) 不就好了?

  1. 符合编码直觉

    • 使用 AND 清零: 需要在掩码里把想保留的写成 1,把想清除的写成 0。
    • 使用 BIC 清零: 直接把想清除的写成 1,想保留的写成 0。更符合“清除”的直觉。
  2. 合法立即数的瓶颈: 现在我需要把 R0 的 第 0 位清零(其他位保持不变)。

    • 使用 AND:掩码需要是 1111 1110,但这个数无法通过 ARM 的立即数编码规则表示出来。
    • 使用 BIC:掩码是 0000 0001,合法的立即数。

多讲两句

而且加一个 BIC 看似浪费了硬件电路,其实并没有。

ARM 的 ALU(算术逻辑单元)前面本来就挂着一个桶形移位器(Barrel Shifter)。

这个移位器不仅能做移位,其内部还很容易实现“取反”功能。

  • AND 令走的是: A & B
  • BIC 令走的是: A & ~B

在电路设计上,这只是在 B 的输入端加了一排反相器(NOT gate),用极低的成本换来了巨大的代码效率提升。

它是 ARM “实用主义” 战胜 “教条主义” 的经典案例。