# 9 ARM汇编基础 ## 9.1 完整概念讲解 ### 9.1.1 为什么需要汇编 Cortex-A 系列芯片上电后,SP 指针未初始化,C 语言运行环境尚未建立,无法直接执行 C 代码。必须先用汇编完成以下工作: 1. 初始化 DDR 控制器(芯片内部 RAM 不开放给用户代码时) 2. 设置 SP 指针(栈顶地址) 3. 初始化其他必要外设 当汇编把 C 环境设置好后,才跳转到 C 语言的 `main` 函数。这与 STM32 的启动文件 `startup_stm32f10x_hd.s` 原理相同,只是 STM32 的启动文件由 ST 写好,开发者无需修改。 **参考文档**: - 《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition.pdf》— A4 章讲解全部指令 - 《ARM Cortex-A(armV7)编程手册 V4.0.pdf》— 编程模型与寄存器说明 ### 9.1.2 GNU 汇编语法 MDK 和 IAR 的汇编语法不同,不能直接移植。本教程使用 GCC 交叉编译器,汇编代码必须符合 GNU 语法。 **基本格式**: ```asm label:instruction @comment ``` - `label`:标号,表示地址位置,以 `:` 结尾 - `instruction`:汇编指令或伪操作 - `@`:注释符号(也可用 `/* */`) **大小写规则**:指令、伪操作、寄存器名可全大写或全小写,但不能混用。 #### 段定义伪操作 | 伪操作 | 含义 | | ---------- | -------------- | | `.text` | 代码段 | | `.data` | 已初始化数据段 | | `.bss` | 未初始化数据段 | | `.rodata` | 只读数据段 | | `.section` | 自定义段 | ```asm .section .testsection @ 定义自定义段 ``` #### 常用伪操作 | 伪操作 | 用法示例 | 含义 | | --------- | ------------------ | ------------------ | | `.byte` | `.byte 0x12` | 定义单字节数据 | | `.short` | `.short 0x1234` | 定义双字节数据 | | `.long` | `.long 0x12345678` | 定义 4 字节数据 | | `.equ` | `.equ num, 0x12` | 赋值(num = 0x12) | | `.align` | `.align 4` | 4 字节对齐 | | `.end` | `.end` | 源文件结束 | | `.global` | `.global _start` | 定义全局符号 | #### 函数格式 ```asm 函数名: 函数体 返回语句 @ 非必须 ``` 示例——Cortex-A7 中断服务函数: ```asm /* 未定义中断 */ Undefined_Handler: ldr r0, =Undefined_Handler bx r0 /* SVC 中断 */ SVC_Handler: ldr r0, =SVC_Handler bx r0 /* 预取终止中断 */ PrefAbort_Handler: ldr r0, =PrefAbort_Handler bx r0 ``` ### 9.1.3 数据传输指令 处理器内部数据传输的三种场景: | 场景 | 指令 | | ----------------------------------- | ----- | | 寄存器 → 寄存器 | `MOV` | | 特殊寄存器(CPSR/SPSR)→ 通用寄存器 | `MRS` | | 通用寄存器 → 特殊寄存器 | `MSR` | #### MOV 指令 将数据从一个寄存器拷贝到另一个寄存器,或加载立即数到寄存器。 ```asm MOV R0, R1 @ R0 = R1 MOV R0, #0x12 @ R0 = 0x12 ``` #### MRS 指令 读取特殊寄存器(CPSR / SPSR)的值到通用寄存器。**读特殊寄存器只能用 MRS**。 ```asm MRS R0, CPSR @ R0 = CPSR ``` #### MSR 指令 将通用寄存器的值写入特殊寄存器。**写特殊寄存器只能用 MSR**。 ```asm MSR CPSR, R0 @ CPSR = R0 ``` ### 9.1.4 存储加载指令 ARM 不能直接访问存储器(包括寄存器映射的 RAM),需要通过 LDR/STR 指令间接访问。 | 指令 | 描述 | | ------------------------ | ------------------------------------ | | `LDR Rd, [Rn, #offset]` | 从存储器 `Rn+offset` 读取数据到 `Rd` | | `STR Rd, [Rn, #offset]` | 将 `Rd` 的值写入存储器 `Rn+offset` | | `LDRB Rd, [Rn, #offset]` | 按字节读取 | | `STRB Rd, [Rn, #offset]` | 按字节写入 | | `LDRH Rd, [Rn, #offset]` | 按半字(16 位)读取 | | `STRH Rd, [Rn, #offset]` | 按半字写入 | #### LDR 指令详解 LDR 有两种用法: ```asm @ 用法1:从存储器加载数据到寄存器 LDR R0, =0x0209C004 @ 将地址 0x0209C004 加载到 R0(立即数用 =) LDR R1, [R0] @ 从地址 0x0209C004 读取数据到 R1 @ 用法2:加载立即数到寄存器 LDR R0, =0x12345678 @ R0 = 0x12345678 ``` **关键区别**:加载立即数用 `=`,加载地址值也用 `=`;加载寄存器中存储的地址指向的数据用 `[]`。 #### STR 指令详解 ```asm @ 配置 GPIO1_GDIR 寄存器(地址 0x0209C004)的值为 0x20000002 LDR R0, =0x0209C004 @ R0 = 寄存器地址 LDR R1, =0x20000002 @ R1 = 要写入的值 STR R1, [R0] @ 将 R1 写入 R0 指向的地址 ``` ### 9.1.5 压栈出栈指令 函数调用时需要保存/恢复寄存器状态(现场保护/恢复现场),使用 PUSH 和 POP 指令。 | 指令 | 描述 | | ----------------------- | -------------------- | | `PUSH ` | 将寄存器列表压入栈中 | | `POP ` | 从栈中恢复寄存器列表 | | `STMFD SP!, ` | PUSH 的另一种写法 | | `LDMFD SP!, ` | POP 的另一种写法 | #### 压栈示例 ```asm @ 将 R0~R3 和 R12 压栈 PUSH {R0~R3, R12} @ 将 LR 压栈 PUSH {LR} ``` 堆栈变化示意(SP 初始值 0x80000000,向下增长): ``` 高地址 0x80000000 ┌─────────┐ │ (空) │ ← 压栈前 SP 0x7FFFFFEC ├─────────┤ │ R12 │ │ R3 │ │ R2 │ │ R1 │ │ R0 │ ← PUSH {R0~R3,R12} 后 SP 0x7FFFFFE8 ├─────────┤ │ LR │ ← PUSH {LR} 后 SP 低地址 ``` #### 出栈示例 出栈顺序必须与压栈相反: ```asm POP {LR} @ 先恢复 LR POP {R0~R3, R12} @ 再恢复 R0~R3, R12 ``` #### STMFD / LDMFD 写法 `PUSH` 和 `POP` 是 `STMFD` / `LDMFD` 的简化写法: ```asm STMFD SP!, {R0~R3, R12} @ 等价于 PUSH {R0~R3, R12} STMFD SP!, {LR} LDMFD SP!, {LR} @ 等价于 POP {LR} LDMFD SP!, {R0~R3, R12} ``` **FD = Full Descending**(满递减):ARM 使用 FD 类型堆栈,SP 指向最后一个入栈值,堆栈由高地址向低地址增长。`STM` / `LDM` 是多存储/多加载指令,可一次操作多个连续数据。 ### 9.1.6 跳转指令 | 指令 | 描述 | | ------------ | ----------------------------------------------------- | | `B