--- tags: [嵌入式系统, 复习, ARM指令集] create time: 2026-06-10 11:11 --- # ARM指令集与汇编 ## 概述 本文档详细讲解ARM指令集体系,包括ARM/Thumb指令集对比、指令格式、核心指令详解、条件码、寻址方式以及汇编程序设计。这是嵌入式系统课程中需要动手实践的重点部分。 ## 正文 ### 1. ARM指令集 vs Thumb指令集 | 比较维度 | ARM指令集 | Thumb指令集 | |----------|-----------|-------------| | 指令宽度 | **32位** | **16位** | | 功能 | 完整功能 | ARM的子集 | | 代码密度 | 低(占用空间大) | 高(占用空间小) | | 性能 | 高 | 略低 | | 状态标志 | CPSR.T = 0 | CPSR.T = 1 | > [!warning] Thumb状态不能执行ARM指令 > 在Thumb状态下,处理器只能执行16位的Thumb指令。如果需要执行32位ARM指令,必须先切换到ARM状态(通过BX指令)。两种状态不能在同一时刻混合执行。 ### 2. 指令格式 ARM指令的基本格式为: ``` opcode{condition}{S} Rd, operand1, operand2 ``` | 字段 | 说明 | |------|------| | opcode | 操作码,如MOV, ADD, LDR | | {condition} | 条件码,如EQ, NE, GT(可选) | | {S} | 是否影响CPSR标志位(可选) | | Rd | 目标寄存器 | | operand1 | 第一操作数(通常是寄存器) | | operand2 | 第二操作数(灵活,可带移位) | ```mermaid graph LR subgraph format["Instruction Format"] direction LR OP["opcode"] --> CON["condition"] --> S["S flag"] --> RD["Rd"] --> OP1["operand1"] --> OP2["operand2"] end style OP fill:#4fc3f7 style CON fill:#81d4fa style S fill:#b3e5fc style RD fill:#ffcc80 style OP1 fill:#ffab91 style OP2 fill:#a5d6a7 ``` ### 3. 桶形移位器(Barrel Shifter) ARM指令的一个强大特性:**第二操作数可以在送入ALU之前先进行移位操作**。这意味着一条指令就能完成"移位+运算",无需额外的移位指令。 移位类型: | 移位操作 | 全称 | 说明 | |----------|------|------| | LSL | Logical Shift Left | 逻辑左移,低位补0 | | LSR | Logical Shift Right | 逻辑右移,高位补0 | | ASR | Arithmetic Shift Right | 算术右移,高位补符号位 | | ROR | Rotate Right | 循环右移 | ```arm ; 示例:利用桶形移位器 ADD R0, R1, R1, LSL #2 ; R0 = R1 + R1 * 4 = R1 * 5 SUB R2, R3, R3, LSR #1 ; R2 = R3 - R3/2 = R3/2 MOV R4, R5, ROR #8 ; R4 = R5循环右移8位 ``` > [!tip] 桶形移位器的价值 > 在没有桶形移位器的架构中,`R1 * 5` 需要两条指令:先LSL #2再ADD。ARM用一条指令就完成了,这体现了RISC架构中"简单指令的巧妙组合"的设计哲学。 ### 4. 核心指令详解 #### 4.1 数据传送指令 MOV ```arm MOV R0, #10 ; R0 = 10(立即数传送) MOV R1, R0 ; R1 = R0(寄存器传送) MOV R2, R0, LSL #3 ; R2 = R0 << 3(带移位传送) ``` **MOV只能在寄存器之间传送数据,不能访问内存!** 这是与LDR的关键区别。 #### 4.2 加载/存储指令 LDR/STR ```arm LDR R0, [R1] ; 从R1指向的内存地址加载数据到R0 STR R0, [R1] ; 将R0的值存储到R1指向的内存地址 LDR R0, [R1, #4] ; 从R1+4地址加载(基址+偏移) STR R0, [R1], #4 ; 先存储,再更新R1 = R1 + 4(后索引) LDR R0, [R1, #4]! ; 先更新R1 = R1 + 4,再加载(前索引) ``` > [!question] MOV R0, #0xFF 和 LDR R0, =0x12345678 有什么区别? > `MOV R0, #0xFF` 是真正ARM指令,0xFF是合法的8位立即数(通过循环右移编码)。 > `LDR R0, =0x12345678` 是**伪指令**,汇编器会将0x12345678放入文字池(literal pool),然后用一条LDR从该地址加载。因为0x12345678无法编码为8位立即数。 #### 4.3 算术运算指令 ```arm ADD R0, R1, R2 ; R0 = R1 + R2 ADD R0, R1, #5 ; R0 = R1 + 5 ADDS R0, R1, R2 ; R0 = R1 + R2,并更新CPSR标志位 SUB R0, R1, R2 ; R0 = R1 - R2 SUBS R0, R1, #1 ; R0 = R1 - 1,更新标志位 MUL R0, R1, R2 ; R0 = R1 * R2 ``` 注意带`S`后缀的指令会更新CPSR的N、Z、C、V标志位。 #### 4.4 比较指令 CMP ```arm CMP R0, R1 ; 比较R0和R1(计算R0-R1,但不保存结果) BEQ label ; 如果相等(Z=1),跳转到label BNE label ; 如果不相等(Z=0),跳转到label ``` CMP本质上是一条**不保存结果的减法指令**,只设置标志位。 #### 4.5 分支指令 B / BL ```arm B label ; 无条件跳转到label BL function ; 跳转到function,同时将返回地址保存到LR ``` - **B**:简单跳转,用于循环、条件分支 - **BL**:带链接的跳转,用于**函数调用**(LR自动保存返回地址) #### 4.6 软中断指令 SWI ```arm SWI #0x123456 ; 触发软中断,进入SVC模式 ``` SWI用于**从用户模式请求操作系统服务**(系统调用)。执行SWI后: 1. 进入SVC模式 2. CPSR保存到SPSR_svc 3. 返回地址保存到LR_svc 4. PC跳转到向量地址0x00000008 #### 4.7 批量加载/存储 STMFD / LDMFD ```arm STMFD SP!, {R0-R3, LR} ; 将R0-R3和LR入栈(满递减栈) LDMFD SP!, {R0-R3, PC} ; 从栈中恢复R0-R3,并将PC出栈实现返回 ``` 这两个指令常用于**函数入口保存现场**和**函数出口恢复现场**。FD表示Full Descending(满递减栈),是ARM的默认栈类型。 #### 4.8 状态寄存器读写 MRS / MSR ```arm MRS R0, CPSR ; 将CPSR读入R0 MSR CPSR_c, R0 ; 将R0写入CPSR的控制位域 MSR CPSR_f, R0 ; 将R0写入CPSR的标志位域 ``` MRS/MSR用于在**特权模式下修改CPSR**,例如开关中断: ```arm ; 关中断 MRS R0, CPSR ORR R0, R0, #0x80 ; I位(bit7)置1 MSR CPSR_c, R0 ; 开中断 MRS R0, R1 BIC R0, R0, #0x80 ; I位(bit7)清0 MSR CPSR_c, R0 ``` #### 4.9 测试等价指令 TEQ ```arm TEQ R0, R1 ; 按位异或(EOR),只设置标志位,不保存结果 ``` TEQ用于测试两个值是否相等(或测试某些位),与CMP类似但使用异或运算。 ### 5. 条件码 ARM支持15种条件码,基于CPSR的标志位进行判断: | 条件码 | 含义 | 标志位条件 | |--------|------|------------| | EQ | 相等 | Z=1 | | NE | 不相等 | Z=0 | | GT | 大于(有符号) | Z=0且N=V | | LT | 小于(有符号) | N!=V | | GE | 大于等于(有符号) | N=V | | LE | 小于等于(有符号) | Z=1或N!=V | | HI | 无符号大于 | C=1且Z=0 | | LS | 无符号小于等于 | C=0或Z=1 | | CS/HS | 无符号大于等于 | C=1 | | CC/LO | 无符号小于 | C=0 | | PL | 正数或零 | N=0 | | MI | 负数 | N=1 | | VS | 溢出 | V=1 | | VC | 无溢出 | V=0 | | AL | 无条件(默认) | 任意 | > [!question] CMP R0, #0 后接 BMI label,什么时候会跳转? > BMI是"负数"条件跳转。CMP R0, #0 实际上是R0 - 0,结果就是R0本身。如果R0 < 0(最高位为1),N标志位被置位,BMI条件成立,跳转到label。 ### 6. 寻址方式 | 寻址方式 | 示例 | 说明 | |----------|------|------| | 立即数寻址 | `MOV R0, #10` | 操作数就在指令中 | | 寄存器寻址 | `ADD R0, R1, R2` | 操作数在寄存器中 | | 寄存器间接寻址 | `LDR R0, [R1]` | R1中存放的是内存地址 | | 基址+偏移寻址 | `LDR R0, [R1, #4]` | R1为基地址,4为偏移量 | | 基址+索引寻址 | `LDR R0, [R1, R2]` | R2的值作为索引偏移 | ### 7. 立即数编码 ARM指令是32位固定的,立即数编码只占用**12位**(4位旋转量 + 8位立即数值)。规则: ``` 实际值 = 8位立即数 循环右移 (旋转量 * 2) 位 ``` 这意味着只有**特定的值**才能作为立即数。例如: - `#0xFF`:合法,8位全1,旋转0位 - `#0x100`:合法,`#1`循环右移24位 - `#0x12345678`:**不合法**,无法用8位循环右移表示 > [!tip] 立即数合法性判断 > 判断一个数是否是合法的ARM立即数:将它写成二进制,看是否能通过循环右移偶数位变成8位以内的数。 ### 8. 字对齐与大小端 **数据宽度**: | 类型 | 位数 | 字节数 | |------|------|--------| | Word(字) | 32位 | 4字节 | | Halfword(半字) | 16位 | 2字节 | | Byte(字节) | 8位 | 1字节 | **对齐要求**:Word访问的地址必须是4的倍数(地址 mod 4 = 0),Halfword访问地址必须是2的倍数。 **大小端**: - **Little-Endian(小端)**:低字节存低地址(ARM常用) - **Big-Endian(大端)**:高字节存低地址 ```arm ; 假设内存地址0x1000存放值 0x12345678 ; 小端模式: ; 0x1000: 0x78 (最低字节) ; 0x1001: 0x56 ; 0x1002: 0x34 ; 0x1003: 0x12 (最高字节) ; 大端模式: ; 0x1000: 0x12 (最高字节) ; 0x1001: 0x34 ; 0x1002: 0x56 ; 0x1003: 0x78 (最低字节) ``` ### 9. 完整示例:C if-else 对应的ARM汇编 以下是C语言if-else语句及其对应的ARM汇编实现: ```c // C代码 int max(int a, int b) { if (a > b) return a; else return b; } ``` ```arm ; ARM汇编实现 max: CMP R0, R1 ; 比较a和b(R0-R1,设置标志位) MOVGT R0, R0 ; 如果a > b(GT条件成立),R0保持不变 MOVLE R0, R1 ; 如果a <= b(LE条件成立),R0 = b BX LR ; 返回,LR中保存了调用者的返回地址 ``` 更高效的写法: ```arm ; 优化版本:只用两条核心指令 max: CMP R0, R1 ; 比较a和b MOVLT R0, R1 ; 如果a < b,则R0 = b BX LR ; 返回 ``` > [!question] 为什么优化版本只判断了LT一种情况? > 因为如果a >= b,R0的值本来就是a,不需要任何操作。只有当a < b时才需要将b写入R0。这体现了"条件执行"的强大——ARM的每条指令都可以条件执行,避免了不必要的跳转。 ## 关联笔记 - [[嵌入式系统/试题册/index|试题册索引]]