Return

ARM 指令的设计研究

在复习嵌入式系统设计时,对 ARM 指令集的设计有一些自己的思考。

初接触 ARM 指令时,发现一些指令有着特别的设计。

感觉违背了我对 RISC 精简的设计认知。

但再深入思考一些就发现,这些设计都有其用意。

一:步长为 2 —— 立即数编码

在 32 位的定长指令中,留给数据的空间只有12 位。那如何可以最大化地利用空间来表示数字呢?

ARM 给出的答案是循环右移:通过把 8 位数据(0-255)循环右移偶数位(0, 2, 4, …, 30),实现 32 位数的全覆盖。

但难免让人产生疑问:为什么只右移偶数位?

为什么是 2?

这 12 位空间被 ARM 拆解为:

  • 前 8 位:作为基础数值(0-255)。
  • 后 4 位:作为移位控制(rotate_imm)。

4 位二进制最多只能表示 16 种状态(0-15)。而我们的目标是让这 8 位数据能在 32 位的寄存器中遍历(覆盖 0-31 位)。

这是一道简单的数学题:

  • 如果步长是 1:只能覆盖一半的寄存器空间。
  • 如果步长是 2:完美覆盖所有位域

为什么不分给 rotate 5 位?

那新的疑问随之而来:为什么不直接用 5 位来表示 rotate 呢?这样就能真正地直接覆盖 0-31 位,而不是像现在这样只能覆盖 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
  • 结果: 解决了常数减变量的问题。

快速求负数(取反)

除了解决刚需问题,RSB 的提出也实现了一些操作的简化:

如果想对 R0 取反(比如把 5 变成 -5)。

  • 如果是 SUB:
    1. 先找个寄存器存 0。
    2. SUB R1, R2(存了0), R0
    3. 要么总是保有一个零寄存器;要么总是需要临时做一个零寄存器,这是一个非常笨重的思路。
  • 用 RSB 可以一行代码搞定:
    RSB R0, R0, #0
    • 含义: R0 = 0 - R0
    • 标准的数学取负操作。

乘法优化(进阶)

RSB 配合移位指令后还可以实现一些乘法指令的优化。

场景:计算 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?

理解了 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(算术逻辑单元)前面本来就挂着一个桶形移位器,这个移位器不仅能做移位,其内部还很容易实现“取反”功能。

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

    在电路设计上,这只是在 B 的输入端加了一排反相器

    换而言之,设计师对“为什么需要 BIC”的两个原因的解决几乎是零成本的。