Cortex-A 系列芯片上电后,SP 指针未初始化,C 语言运行环境尚未建立,无法直接执行 C 代码。必须先用汇编完成以下工作:
当汇编把 C 环境设置好后,才跳转到 C 语言的 main 函数。这与 STM32 的启动文件 startup_stm32f10x_hd.s 原理相同,只是 STM32 的启动文件由 ST 写好,开发者无需修改。
参考文档:
MDK 和 IAR 的汇编语法不同,不能直接移植。本教程使用 GCC 交叉编译器,汇编代码必须符合 GNU 语法。
基本格式:
label:instruction @comment
label:标号,表示地址位置,以 : 结尾instruction:汇编指令或伪操作@:注释符号(也可用 /* */)大小写规则:指令、伪操作、寄存器名可全大写或全小写,但不能混用。
| 伪操作 | 含义 |
|---|---|
.text |
代码段 |
.data |
已初始化数据段 |
.bss |
未初始化数据段 |
.rodata |
只读数据段 |
.section |
自定义段 |
.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 |
定义全局符号 |
函数名:
函数体
返回语句 @ 非必须
示例——Cortex-A7 中断服务函数:
/* 未定义中断 */
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
处理器内部数据传输的三种场景:
| 场景 | 指令 |
|---|---|
| 寄存器 → 寄存器 | MOV |
| 特殊寄存器(CPSR/SPSR)→ 通用寄存器 | MRS |
| 通用寄存器 → 特殊寄存器 | MSR |
将数据从一个寄存器拷贝到另一个寄存器,或加载立即数到寄存器。
MOV R0, R1 @ R0 = R1
MOV R0, #0x12 @ R0 = 0x12
读取特殊寄存器(CPSR / SPSR)的值到通用寄存器。读特殊寄存器只能用 MRS。
MRS R0, CPSR @ R0 = CPSR
将通用寄存器的值写入特殊寄存器。写特殊寄存器只能用 MSR。
MSR CPSR, R0 @ CPSR = R0
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 有两种用法:
@ 用法1:从存储器加载数据到寄存器
LDR R0, =0x0209C004 @ 将地址 0x0209C004 加载到 R0(立即数用 =)
LDR R1, [R0] @ 从地址 0x0209C004 读取数据到 R1
@ 用法2:加载立即数到寄存器
LDR R0, =0x12345678 @ R0 = 0x12345678
关键区别:加载立即数用 =,加载地址值也用 =;加载寄存器中存储的地址指向的数据用 []。
@ 配置 GPIO1_GDIR 寄存器(地址 0x0209C004)的值为 0x20000002
LDR R0, =0x0209C004 @ R0 = 寄存器地址
LDR R1, =0x20000002 @ R1 = 要写入的值
STR R1, [R0] @ 将 R1 写入 R0 指向的地址
函数调用时需要保存/恢复寄存器状态(现场保护/恢复现场),使用 PUSH 和 POP 指令。
| 指令 | 描述 |
|---|---|
PUSH <reg list> |
将寄存器列表压入栈中 |
POP <reg list> |
从栈中恢复寄存器列表 |
STMFD SP!, <reg list> |
PUSH 的另一种写法 |
LDMFD SP!, <reg list> |
POP 的另一种写法 |
@ 将 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
低地址
出栈顺序必须与压栈相反:
POP {LR} @ 先恢复 LR
POP {R0~R3, R12} @ 再恢复 R0~R3, R12
PUSH 和 POP 是 STMFD / LDMFD 的简化写法:
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 是多存储/多加载指令,可一次操作多个连续数据。
| 指令 | 描述 |
|---|---|
B <label> |
跳转到 label(范围 ±2KB,用 B.W 可扩大到 32 位) |
BX <Rm> |
间接跳转到 Rm 中的地址,并切换指令集 |
BL <label> |
跳转到 label,并将返回地址保存到 LR |
BLX <Rm> |
跳转到 Rm 指定地址,保存返回地址到 LR,并切换指令集 |
用于不需要返回的跳转,例如汇编初始化完成后跳转到 C 的 main 函数:
_start:
ldr sp, =0x80200000 @ 设置栈指针
b main @ 跳转到 main,不再返回
用于函数调用,跳转前自动将当前 PC 值保存到 LR(R14):
push {r0, r1} @ 保存 r0, r1
cps #0x13 @ 进入 SVC 模式
bl system_irqhandler @ 调用 C 语言中断处理函数
cps #0x12 @ 进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0x10] @ 写 EOIR,中断处理完成
选择原则:需要返回原处用 BL,跳转后不返回用 B。
| 指令 | 公式 | 说明 |
|---|---|---|
ADD Rd, Rn, Rm |
Rd = Rn + Rm |
加法 |
ADD Rd, Rn, #immed |
Rd = Rn + #immed |
立即数加法 |
ADC Rd, Rn, Rm |
Rd = Rn + Rm + C |
带进位加法 |
SUB Rd, Rn, Rm |
Rd = Rn - Rm |
减法 |
SUB Rd, Rn, #immed |
Rd = Rn - #immed |
立即数减法 |
SBC Rd, Rn, Rm |
Rd = Rn - Rm - !C |
带借位减法 |
MUL Rd, Rn, Rm |
Rd = Rn * Rm |
乘法(32位) |
UDIV Rd, Rn, Rm |
Rd = Rn / Rm |
无符号除法 |
SDIV Rd, Rn, Rm |
Rd = Rn / Rm |
有符号除法 |
嵌入式开发中最常用的是加减指令,乘除基本用不到。
| 指令 | 公式 | 说明 |
|---|---|---|
AND Rd, Rn, Rm |
Rd = Rn & Rm |
按位与 |
AND Rd, Rn, #immed |
Rd = Rn & #immed |
立即数与 |
ORR Rd, Rn, Rm |
Rd = Rn \| Rm |
按位或 |
ORR Rd, Rn, #immed |
Rd = Rn \| #immed |
立即数或 |
BIC Rd, Rn, Rm |
Rd = Rn & (~Rm) |
位清除 |
BIC Rd, Rn, #immed |
Rd = Rn & (~#immed) |
立即数位清除 |
ORN Rd, Rn, #immed |
Rd = Rn \| (~#immed) |
按位或非 |
EOR Rd, Rn, Rm |
Rd = Rn ^ Rm |
按位异或 |
EOR Rd, Rn, #immed |
Rd = Rn ^ #immed |
立即数异或 |
逻辑运算指令常用于配置 I.MX6UL 外设寄存器时的位操作。
Cortex-A7 有 16 个 32 位通用寄存器(R0-R15)和 2 个特殊状态寄存器:
| 寄存器 | 别名 | 用途 |
|---|---|---|
| R0-R3 | 参数传递 / 返回值(AAPCS 调用约定) | |
| R4-R10 | 通用变量,被调用者保存 | |
| R11 | FP | 帧指针(可选) |
| R12 | IP | 过程间调用暂存 |
| R13 | SP | 栈指针,指向当前栈顶 |
| R14 | LR | 链接寄存器,保存子程序返回地址 |
| R15 | PC | 程序计数器,当前正在执行的指令地址 |
| CPSR | 当前程序状态寄存器(标志位、中断使能、处理器模式) | |
| SPSR | 保存的程序状态寄存器(异常返回时恢复 CPSR) |
31 30 29 28 ... 7 6 5 4 3 2 1 0
N Z C V I F T M4 M3 M2 M1 M0
└──处理器模式──┘
| 位 | 名称 | 含义 |
|---|---|---|
| N | 负数 | 运算结果为负置 1 |
| Z | 零 | 运算结果为零置 1 |
| C | 进位 | 加法进位 / 减法借位 |
| V | 溢出 | 有符号运算溢出置 1 |
| I | IRQ | 1 = 禁止 IRQ 中断 |
| F | FIQ | 1 = 禁止 FIQ 中断 |
| T | Thumb | 0 = ARM 状态,1 = Thumb 状态 |
| M[4:0] | 模式 | 处理器模式(SVC=10011, IRQ=10010 等) |
| 类别 | 指令 | 语法 |
|---|---|---|
| 数据传输 | MOV / MRS / MSR | MOV Rd, Operand2 |
| 存储加载 | LDR / STR / LDRB / STRB | LDR Rd, [Rn, #offset] |
| 压栈出栈 | PUSH / POP / STMFD / LDMFD | PUSH {reg list} |
| 跳转 | B / BL / BX / BLX | BL label |
| 算术 | ADD / ADC / SUB / SBC | ADD Rd, Rn, Rm |
| 乘除 | MUL / UDIV / SDIV | MUL Rd, Rn, Rm |
| 逻辑 | AND / ORR / BIC / EOR | AND Rd, Rn, Rm |
@ Cortex-A7 启动汇编示例(适用于 I.MX6ULL)
@ 文件:start.S
.global _start
_start:
@ ============================================
@ 第一阶段:汇编初始化
@ ============================================
@ 1. 关闭 IRQ 和 FIQ
mrs r0, cpsr
orr r0, r0, #0xc0 @ 设置 I=1, F=1,禁止中断
msr cpsr, r0
@ 2. 设置 SVC 模式
mrs r0, cpsr
bic r0, r0, #0x1f @ 清除 M[4:0] 位
orr r0, r0, #0x13 @ 设置为 SVC 模式(10011)
msr cpsr, r0
@ 3. 设置栈指针(SVC 模式栈)
ldr sp, =0x80200000 @ DDR 起始地址 + 2MB 作为栈顶
@ 4. 清零 .bss 段
ldr r0, =__bss_start @ .bss 起始地址(链接脚本定义)
ldr r1, =__bss_end @ .bss 结束地址
mov r2, #0
bss_loop:
cmp r0, r1 @ 比较当前地址与结束地址
bge bss_done @ 如果 >= 结束地址,跳转完成
str r2, [r0] @ 写零
add r0, r0, #4 @ 地址 +4
b bss_loop @ 继续循环
bss_done:
@ 5. 跳转到 C 语言 main 函数
bl main @ 调用 main,保存返回地址到 LR
@ main 返回后(理论上不会到这里)
hang:
b hang @ 死循环
@ 中断向量表(放在代码段最前面)
.section .text.startup, "ax"
.global _vector_table
_vector_table:
ldr pc, =_start @ 复位
ldr pc, =Undefined_Handler
ldr pc, =SWI_Handler
ldr pc, =PrefAbort_Handler
ldr pc, =DataAbort_Handler
nop @ 保留
ldr pc, =IRQ_Handler
ldr pc, =FIQ_Handler
@ IRQ 中断服务函数示例
IRQ_Handler:
push {r0, r1} @ 保存 r0, r1
cps #0x13 @ 切换到 SVC 模式
bl system_irqhandler @ 调用 C 语言中断处理函数
cps #0x12 @ 切换回 IRQ 模式
pop {r0, r1}
str r0, [r1, #0x10] @ 写 EOIR 清中断
subs pc, lr, #4 @ 返回到中断前的下一条指令
@ 汇编函数示例:调用 C 函数并保存寄存器
my_function:
push {r4-r11, lr} @ 保存 callee-saved 寄存器和返回地址
@ 调用 C 函数
mov r0, #10 @ 第一个参数
mov r1, #20 @ 第二个参数
bl c_add_func @ 调用 C 函数,返回值在 r0
pop {r4-r11, pc} @ 恢复寄存器并返回
LDR R0, =0x12345678 @ 加载立即数到 R0(伪指令,编译器生成文字池)
LDR R0, [R1] @ 从 R1 指向的地址读取数据
MOV R0, #0x12 @ 加载小立即数(8 位以内)
= 用于加载任意 32 位立即数(编译器自动处理),# 用于直接立即数(有限范围)。
@ 错误!出栈顺序与压栈相反
PUSH {r0, r1, r2}
POP {r2, r1, r0} @ 正确但不推荐
@ 推荐写法:POP 按原顺序写,硬件自动按正确顺序恢复
PUSH {r0, r1, r2}
POP {r0, r1, r2} @ 推荐!
| 场景 | 使用 | 原因 |
|---|---|---|
| 初始化完成后跳 main | B |
不需要返回汇编 |
| 调用 C 函数 | BL |
需要返回继续执行 |
| 中断处理调用 C 函数 | BL |
必须返回完成中断恢复 |
bx lr:返回到 LR 保存的地址pop {pc}:从栈中恢复 PC(等价于返回)mov pc, lr:简单返回(不切换指令集)@ 以下两种写法等价
MOV R0, R1
mov r0, r1
@ 但不能混用
Mov r0, R1 @ 错误!
Q:为什么 Cortex-A 芯片上电后必须先执行汇编?
A:上电时 SP 指针未初始化,C 语言的函数调用依赖栈(出栈入栈),没有栈 C 语言无法运行。汇编不依赖栈,可以先初始化 DDR、设置 SP,建立 C 运行环境后再跳转到 C 代码。
Q:MOV 和 LDR 加载立即数有什么区别?
A:MOV 只能加载 8 位立即数(通过循环右移偶数位得到的值),LDR 伪指令可以加载任意 32 位立即数(编译器会将值放在代码段附近的文字池中,用 PC 相对寻址加载)。
Q:B 和 BL 指令的区别?
A:B 只做跳转,不保存返回地址;BL 跳转前自动将当前 PC+4(下一条指令地址)保存到 LR 寄存器,支持子程序调用后返回。
Q:PUSH/POP 与 STMFD/LDMFD 的关系?
A:PUSH 等价于 STMFD SP!,POP 等价于 LDMFD SP!。FD 表示 Full Descending(满递减堆栈),ARM 处理器默认使用这种堆栈模型。
Q:什么是现场保护?为什么需要它?
A:函数调用或中断发生时,需要先保存当前寄存器值(R0-R15),称为现场保护;函数返回或中断处理完成后,再恢复寄存器值,称为恢复现场。这是保证程序正确执行的基础。
Q:CPSR 寄存器的作用?
A:CPSR(Current Program Status Register)保存当前处理器的状态信息,包括:条件标志位(N/Z/C/V)、中断禁止位(I/F)、处理器模式位(M[4:0])、Thumb 状态位(T)等。通过 MRS/MSR 指令读写。