title: ARM汇编基础 tags: [ARM, 汇编, 嵌入式Linux, IMX6ULL, STM32, RK3568] created: 2026-09-17 updated: 2026-09-17
前置知识: [[01-Cortex-A7架构详解]] | [[STM32学习笔记/01-STM32开发环境搭建]]
⚠️ 来源说明:本节(指令集对比、ARM 指令编码格式、寻址模式)不属于《I.MX6U嵌入式Linux驱动开发指南》第七章内容,为扩展知识。原书第七章只讲解 GNU 汇编语法与最常用的 Cortex-A7 汇编指令。
| 特性 | ARM指令集 | Thumb指令集 | Thumb-2指令集 |
|---|---|---|---|
| 指令宽度 | 32位 | 16位 | 16/32位混合 |
| 寄存器访问 | 全部R0-R15 | 低寄存器R0-R7 | 全部R0-R15 |
| 条件执行 | 所有指令 | 仅部分指令 | 所有指令 |
| 性能 | 最高 | 较低 | 接近ARM |
| 代码密度 | 低 | 高 | 高 |
| 典型应用 | 启动代码、关键路径 | 低功耗应用 | 通用嵌入式 |
ARM 32位指令格式:
| 31-28 | 27-25 | 24-21 | 20 | 19-16 | 15-12 | 11-0 |
|-------|-------|-------|----|-------|-------|------|
| cond | 001 | opcode| S | Rn | Rd | Operand2 |
| 寻址模式 | 语法 | 示例 | 说明 |
|---|---|---|---|
| 立即数寻址 | #immediate | MOV R0, #0x10 |
立即数在指令中 |
| 寄存器寻址 | Rm | ADD R0, R1, R2 |
操作数在寄存器 |
| 寄存器间接寻址 | [Rm] | LDR R0, [R1] |
地址在寄存器 |
| 基址变址寻址 | [Rn, #offset] | LDR R0, [R1, #4] |
基址+偏移 |
| 前索引寻址 | [Rn, #offset]! | LDR R0, [R1, #4]! |
先更新基址 |
| 后索引寻址 | [Rn], #offset | LDR R0, [R1], #4 |
先访问后更新 |
| 相对寻址 | label | B loop |
PC相对跳转 |
原书第 7.2.1 节重点讲解的三个数据传输指令:
| 指令 | 目的 | 源 | 描述 |
|---|---|---|---|
MOV R0,R1 |
R0 | R1 | 将 R1 里面的数据复制到 R0 中 |
MRS R0,CPSR |
R0 | CPSR | 将特殊寄存器 CPSR 里面的数据复制到 R0 中 |
MSR CPSR,R1 |
CPSR | R1 | 将 R1 里面的数据复制到特殊寄存器 CPSR 里中 |
MOV R0, R1 @ R0 = R1
MOV R0, #0X12 @ R0 = 0x12,立即数传递到寄存器
MRS R0, CPSR @ 读特殊寄存器只能用 MRS:R0 = CPSR
MSR CPSR, R0 @ 写特殊寄存器只能用 MSR:CPSR = R0
要点:读 CPSR/SPSR 只能用
MRS,写 CPSR/SPSR 只能用MSR,不能使用 MOV。
MOV R0, #10 @ R0 = 10
MOV R1, R2 @ R1 = R2
MOV R0, R1, LSL #2 @ R0 = R1 << 2(左移2位)
MVN R0, #0 @ R0 = 0xFFFFFFFF (-1)
MVN R1, #0xFF @ R1 = 0xFFFFFF00
ADD R0, R1, R2 @ R0 = R1 + R2
ADD R0, R1, #10 @ R0 = R1 + 10
ADC R0, R1, R2 @ R0 = R1 + R2 + C(带进位加法)
SUB R0, R1, R2 @ R0 = R1 - R2
SBC R0, R1, R2 @ R0 = R1 - R2 - !C(带借位减法)
MUL R0, R1, R2 @ R0 = R1 * R2(32位乘法)
UDIV R0, R1, R2 @ R0 = R1 / R2(无符号除法)
SDIV R0, R1, R2 @ R0 = R1 / R2(有符号除法)
原书第 7.2.5 节列出的算术指令为:ADD、ADC、SUB、SBC、MUL、UDIV、SDIV;其中乘除指令在嵌入式配置寄存器时基本用不到,最常用的是加减。
AND R0, R1, #0xFF @ R0 = R1 & 0xFF(按位与)
ORR R0, R1, #0xFF @ R0 = R1 | 0xFF(按位或)
ORN R0, R1, #0xFF @ R0 = R1 | (~0xFF)(按位或非)
EOR R0, R1, #0xFF @ R0 = R1 ^ 0xFF(按位异或)
BIC R0, R1, #0xFF @ R0 = R1 & ~0xFF(位清除)
原书第 7.2.6 节列出的逻辑指令为:AND、ORR、BIC、ORN、EOR。
CMP R0, R1 @ 更新标志位:R0 - R1
CMN R0, R1 @ 更新标志位:R0 + R1(负数比较)
TST R0, #0xFF @ 更新标志位:R0 & 0xFF(位测试)
TEQ R0, R1 @ 更新标志位:R0 ^ R1(等值测试)
MUL R0, R1, R2 @ R0 = R1 * R2
MLA R0, R1, R2, R3 @ R0 = R1 * R2 + R3(乘加)
LDR R0, [R1] @ R0 = *R1
LDR R0, [R1, #4] @ R0 = *(R1+4)
LDR R0, [R1, #4]! @ 先更新R1=R1+4,再R0=*R1(前索引)
LDR R0, [R1], #4 @ 先R0=*R1,再更新R1=R1+4(后索引)
LDR R0, =0x0209C000 @ 伪指令:加载32位地址
STR R0, [R1] @ *R1 = R0
STR R0, [R1, #4] @ *(R1+4) = R0
LDRB R0, [R1] @ R0 = *(uint8_t*)R1(字节加载)
STRB R0, [R1] @ *(uint8_t*)R1 = R0(字节存储)
LDRH R0, [R1] @ R0 = *(uint16_t*)R1(半字加载)
STRH R0, [R1] @ *(uint16_t*)R1 = R0(半字存储)
LDR/STR 默认按字(32位)读写,加后缀 B 按字节(LDRB/STRB)、加后缀 H 按半字(LDRH/STRH)读写(原书 7.2.2 节)。
LDM R0, {R1, R2, R3} @ R1=*R0, R2=*(R0+4), R3=*(R0+8)
STM R0, {R1, R2, R3} @ *R0=R1, *(R0+4)=R2, *(R0+8)=R3
LDM R0!, {R1-R3} @ 先加载,然后R0=R0+12
PUSH {R0-R3, LR} @ 压栈(满递减FD):STMDB SP!, {R0-R3, LR}
POP {R0-R3, PC} @ 出栈:LDMIA SP!, {R0-R3, PC}
原书第 7.2.3 节指出,PUSH/POP 的另一种等价写法是 STMFD SP! 和 LDMFD SP!:
STMFD SP!, {R0~R3, R12} @ R0~R3,R12 入栈
STMFD SP!, {LR} @ LR 入栈
LDMFD SP!, {LR} @ 先恢复 LR
LDMFD SP!, {R0~R3, R12} @ 再恢复 R0~R3,R12
其中 STM = 多存储(Store Multiple),LDM = 多加载(Load Multiple),FD = Full Descending(满递减)。ATPCS 规定 ARM 使用 FD 类型堆栈:SP 指向最后一个入栈的数据,堆栈由高地址向低地址增长。STM/LDM 的寄存器列表中编号小的对应低地址,编号高的对应高地址。
原书堆栈示例:设当前 SP = 0x80000000,向下增长,执行 PUSH {R0~R3, R12} 后:
地址 内容
0x80000000 (原栈底,SP 初始处)
0x7FFFFFFC R12
0x7FFFFFF8 R3
0x7FFFFFF4 R2
0x7FFFFFF0 R1
0x7FFFFFEC R0 ← SP = 0x7FFFFFEC
再执行 PUSH {LR} 后 SP = 0x7FFFFFE8,LR 存放于 0x7FFFFFE8。出栈时按 POP {LR}、POP {R0~R3,R12} 顺序从栈顶(SP 当前指向处)地址依次减小提取数据。
说明:
PUSH/POP是STMFD SP!/LDMFD SP!的别名,二者编码相同;STMFD与STMDB亦为同一指令的两种写法。
⚠️ 来源说明:本小节不属于原书第七章内容,为扩展知识;原书只讲解了 STM/LDM 与 STMFD/LDMFD。
| 后缀 | 模式 | 用途 |
|---|---|---|
| IA | 后递增 | 连续内存读取 |
| IB | 前递增 | 连续内存写入 |
| DA | 后递减 | 逆序内存读取 |
| DB | 前递减 | 堆栈压入 |
堆栈类型后缀(FD/FA/ED/EA)用于描述堆栈模型,其中 FD(满递减)为 ARM 默认,对应
STMFD/LDMFD;不要把 FD 与 IA/IB/DA/DB 混为同类后缀。x86 使用的是空递减(ED)模型。
B loop @ 无条件跳转到loop(跳转范围 ±2KB 时可改用 B.W 32位版本)
B.W loop @ 32位版本跳转指令,跳转范围更大
BX LR @ 间接跳转,跳转到 Rm 中保存的地址,并切换指令集
BL function @ 跳转并保存返回地址到LR(用于函数调用)
BLX function @ 结合BX和BL:跳转到Rm指定地址、保存返回地址、可切换指令集
原书第 7.2.4 节跳转指令表:
| 指令 | 描述 |
|---|---|
B <label> |
跳转到 label;若跳转范围超过 ±2KB,可用 B.W <label> 使用 32 位版本 |
BX <Rm> |
间接跳转,跳转到 Rm 中保存的地址处,并且切换指令集 |
BL <label> |
跳转到标号地址,并将返回地址保存在 LR 中 |
BLX <Rm> |
结合 BX 和 BL 的特点,跳转到 Rm 指定地址、保存返回地址、切换指令集 |
原书示例:汇编初始化 C 运行环境后跳转到 main(B 指令,不返回):
_start:
ldr sp, =0X80200000 @ 设置栈指针
b main @ 跳转到 main 函数
汇编调用 C 函数(需要返回,用 BL):
push {r0, r1} @ 保存 r0,r1
cps #0x13 @ 进入 SVC 模式,允许其他中断再次进入
bl system_irqhandler @ 调用 C 语言中断处理函数
cps #0x12 @ 进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0X10] @ 中断执行完成,写 EOIR
| 条件码 | 助记符 | 含义 | 标志位 |
|---|---|---|---|
| 0000 | EQ | 相等 | Z=1 |
| 0001 | NE | 不相等 | Z=0 |
| 0010 | CS/HS | 无符号大于等于 | C=1 |
| 0011 | CC/LO | 无符号小于 | C=0 |
| 0100 | MI | 负数 | N=1 |
| 0101 | PL | 非负数 | N=0 |
| 0110 | VS | 溢出 | V=1 |
| 0111 | VC | 未溢出 | V=0 |
| 1000 | HI | 无符号大于 | C=1且Z=0 |
| 1001 | LS | 无符号小于等于 | C=0或Z=1 |
| 1010 | GE | 有符号大于等于 | N=V |
| 1011 | LT | 有符号小于 | N≠V |
| 1100 | GT | 有符号大于 | Z=0且N=V |
| 1101 | LE | 有符号小于等于 | Z=1或N≠V |
| 1110 | AL | 总是执行 | - |
CMP R0, #0
BNE not_zero
MOV R0, #1 @ R0=0时执行
not_zero:
@ ARM特有:条件执行
CMP R0, #10
MOVEQ R1, #0 @ R0==10时执行
MOVNE R1, #1 @ R0!=10时执行
原书第 7.1 节列出的常用伪操作:
| 伪操作 | 说明 |
|---|---|
.byte |
定义单字节数据,如 .byte 0x12 |
.short |
定义双字节数据,如 .short 0x1234 |
.long |
定义 4 字节数据,如 .long 0x12345678 |
.equ |
赋值语句,格式 .equ 变量名, 表达式,如 .equ num, 0x12 |
.align |
数据字节对齐,如 .align 4 表示 4 字节对齐 |
.end |
表示源文件结束 |
.global |
定义一个全局符号,格式 .global symbol,如 .global _start |
.byte 0x12 @ 定义8位数据
.short 0x1234 @ 定义16位数据
.long 0x12345678 @ 定义32位数据(等价于 .word)
.equ num, 0x12 @ 定义常量
扩展(GNU as 支持但原书未列):
.word、.hword、.asciz、.set、.weak、.balign、.macro等均非原书第 7.1 节所列内容。更多伪操作可参考《ARM Cortex-A(armV7)编程手册 V4.0》第 57 页。
.section .testsection @ 用 .section 自定义一个段
.text @ 代码段
.data @ 已初始化数据段
.bss @ 未初始化数据段
.rodata @ 只读数据段
.global _start @ 全局符号(类似C的extern)
.weak weak_func @ 弱符号(可被覆盖,扩展)
段名说明:
.text表示代码段,.data初始化的数据段,.bss未初始化的数据段,.rodata只读数据段。汇编程序的默认入口标号是_start,也可以在链接脚本中用ENTRY指定其他入口点。
.align 4 @ 4字节对齐(原书列举)
.balign 4 @ 4字节对齐(填充NOP,扩展)
.macro SET_GDIR reg, bit
LDR \reg, =0X0209C004
LDR r1, =\bit
STR r1, [\reg]
.endm
SET_GDIR r0, 0x8 @ 使用宏
GNU 汇编适用于所有架构,并非 ARM 独享。每行一条语句,每条语句由三个可选部分组成:
label:instruction @ comment
/* ... */。comment:注释内容。
add:
MOVS R0, #0X12 @设置 R0=0X12
注意! ARM 中的指令、伪指令、伪操作、寄存器名等可以全部大写,也可以全部小写,但是不能大小写混用。
GNU 汇编也支持函数,格式为函数名 + 函数体 + 返回语句,返回语句不是必须的:
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
其中 bx 就是返回指令。
@ 文件头注释
.syntax unified @ 统一语法(ARM/Thumb通用)
.cpu cortex-a7 @ 指定CPU
.text @ 代码段
.align 2 @ 4字节对齐
.global _start @ 入口点声明
_start: @ 程序入口
@ 代码...
@ 单行注释(推荐)
// C风格单行注释(也支持)
/* 多行注释 C风格 */
loop: @ 标签定义
B loop @ 跳转到标签
function: @ 函数标签
PUSH {LR} @ 保存返回地址
POP {PC} @ 返回
MOV R0, #255 @ 十进制
MOV R0, #0xFF @ 十六进制
MOV R0, #0b11111111 @ 二进制
MOV R0, #0377 @ 八进制
MOV R0, #'A' @ 字符常量
📖 来源说明:本节示例来自原书第八章 汇编 LED 灯实验与第十三章 BSP 工程管理实验(
imx6ul.lds链接脚本),不属于第七章内容,此处作为第七章汇编指令的综合应用。
@ LED灯汇编驱动 - IMX6ULL(原书第 8.3 节 led.s)
@ 功能:点亮GPIO1_IO03连接的LED
@ 文件:led.s
.global _start
_start:
@ ========================================
@ 步骤1:使能GPIO1时钟
@ ========================================
LDR R0, =0X020C4068 @ CCGR0寄存器地址
LDR R1, =0XFFFFFFFF @ 所有时钟使能
STR R1, [R0] @ 写入CCGR0
LDR R0, =0X020C406C @ CCGR1寄存器地址
STR R1, [R0] @ 写入CCGR1
LDR R0, =0X020C4070 @ CCGR2寄存器地址
STR R1, [R0] @ 写入CCGR2
LDR R0, =0X020C4074 @ CCGR3寄存器地址
STR R1, [R0] @ 写入CCGR3
LDR R0, =0X020C4078 @ CCGR4寄存器地址
STR R1, [R0] @ 写入CCGR4
LDR R0, =0X020C407C @ CCGR5寄存器地址
STR R1, [R0] @ 写入CCGR5
LDR R0, =0X020C4080 @ CCGR6寄存器地址
STR R1, [R0] @ 写入CCGR6
@ ========================================
@ 步骤2:设置GPIO1_IO03复用为GPIO1_IO03
@ ========================================
LDR R0, =0X020E0068 @ SW_MUX_GPIO1_IO03_BASE
LDR R1, =0X5 @ MUX_MODE = 5 (GPIO模式)
STR R1, [R0] @ 写入复用寄存器
@ ========================================
@ 步骤3:设置GPIO1_IO03的IO属性
@ ========================================
@ bit [16]: 0 HYS关闭
@ bit [15:14]: 00 默认下拉(PUS)
@ bit [13]: 0 keeper功能
@ bit [12]: 1 pull/keeper使能
@ bit [11]: 0 关闭开路输出
@ bit [7:6]: 10 速度100Mhz
@ bit [5:3]: 110 R0/6驱动能力
@ bit [0]: 0 低转换率
LDR R0, =0X020E02F4 @ SW_PAD_GPIO1_IO03_BASE
LDR R1, =0X10B0 @ IO属性值
STR R1, [R0] @ 写入属性寄存器
@ ========================================
@ 步骤4:设置GPIO1_IO03为输出
@ ========================================
LDR R0, =0X0209C004 @ GPIO1_GDIR寄存器
LDR R1, =0X0000008 @ bit3=1, 输出模式
STR R1, [R0] @ 写入方向寄存器
@ ========================================
@ 步骤5:点亮LED0 - 设置GPIO1_IO03输出低电平
@ ========================================
LDR R0, =0X0209C000 @ GPIO1_DR寄存器
LDR R1, =0 @ 输出低电平
STR R1, [R0] @ 写入数据寄存器
loop:
B loop @ 死循环,保持LED亮
| 行号 | 指令 | 说明 |
|---|---|---|
| 1 | LDR R0, =0X020C4068 |
伪指令,将CCGR0地址加载到R0 |
| 2 | LDR R1, =0XFFFFFFFF |
伪指令,将0xFFFFFFFF加载到R1 |
| 3 | STR R1, [R0] |
将R1的值存储到R0指向的地址 |
| 4 | LDR R0, =0X020E0068 |
加载GPIO复用寄存器地址 |
| 5 | LDR R1, =0X5 |
GPIO模式值5 |
| 6 | STR R1, [R0] |
配置GPIO复用功能 |
| 7 | LDR R0, =0X020E02F4 |
加载IO属性寄存器地址 |
| 8 | LDR R1, =0X10B0 |
IO属性配置值 |
| 9 | STR R1, [R0] |
配置IO属性 |
| 10 | LDR R0, =0X0209C004 |
加载GPIO方向寄存器地址 |
| 11 | LDR R1, =0X00000008 |
设置bit3为输出 |
| 12 | STR R1, [R0] |
配置GPIO为输出模式 |
| 13 | LDR R0, =0X0209C000 |
加载GPIO数据寄存器地址 |
| 14 | LDR R1, =0 |
输出低电平值 |
| 15 | STR R1, [R0] |
输出低电平,点亮LED |
| 16 | B loop |
无限循环,保持状态 |
/* 链接脚本 - imx6ul.lds */
SECTIONS {
. = 0X87800000; /* 链接起始地址(DDR 中,与 U-Boot 一致) */
.text : {
obj/start.o /* 必须把 start.o 放在最前面,保证 _start 位于 0x87800000 */
*(.text) /* 其余代码段 */
}
.rodata ALIGN(4) : { *(.rodata*) } /* 只读数据段 */
.data ALIGN(4) : { *(.data) } /* 已初始化数据段 */
__bss_start = .; /* bss 段起始符号 */
.bss ALIGN(4) : { *(.bss) *(COMMON) }/* 未初始化数据段 + 通用符号 */
__bss_end = .; /* bss 段结束符号 */
}
关键符号 _start / __bss_start / __bss_end:
_start:.global 声明的全局入口标号,中断向量表就放在它前面,位于链接起始地址 0x87800000。__bss_start / __bss_end:链接器生成的 bss 段首尾地址符号,C 代码进入前由汇编循环用 0 清零这段区域,保证未初始化全局变量初值为 0。.text 段必须把 obj/start.o 放在最前,否则 _start 不会落在 0x87800000。原书编译命令链(第八章 8.4.1 节):
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objdump -D led.elf > led.dis
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
| 特性 | IMX6ULL | STM32F4 | RK3568 |
|---|---|---|---|
| 内核 | Cortex-A7 | Cortex-M4 | Cortex-A55 |
| 汇编语法 | GNU汇编 | ARM汇编 | GNU汇编 |
| 启动文件 | start.S | startup_stm32f4xx.s | start.S |
| 时钟使能 | 手动写CCGR寄存器 | HAL_RCC_GPIOx_CLK_ENABLE() | 手动写CRU寄存器 |
| GPIO配置 | 手动写IOMUXC+GPIO寄存器 | HAL_GPIO_Init() | 手动写PMU+GPIO寄存器 |
| 延迟实现 | 简单循环延迟 | HAL_Delay() | 简单循环延迟 |
| 代码效率 | 中等 | 高 | 高 |
| 开发难度 | 高 | 中 | 高 |
@ IMX6ULL (GNU汇编)
.syntax unified
.cpu cortex-a7
ldr r0, =0x0209C000
str r1, [r0]
@ STM32 (ARM汇编)
AREA |.text|, CODE, READONLY
THUMB
LDR R0, =0x40020000
STR R1, [R0]
@ RK3568 (GNU汇编)
.syntax unified
.cpu cortex-a55
ldr x0, =0xFF720000
str w1, [x0]
| 寄存器 | IMX6ULL | STM32F4 | RK3568 |
|---|---|---|---|
| GPIO数据 | GPIOx_DR | GPIOx_ODR | GPIOx_DR |
| GPIO方向 | GPIOx_GDIR | GPIOx_MODER | GPIOx_DDR |
| GPIO输入 | GPIOx_DR | GPIOx_IDR | GPIOx_EXT_PORT |
| 时钟控制 | CCM_CCGRx | RCC_AHB1ENR | CRU_CLKGATE_CON |
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。
答案:
LDR指令:真正的加载指令,从内存中读取数据
LDR R0, [R1] @ 从R1指向的内存地址加载数据
LDR伪指令:用于加载32位常量或地址
LDR R0, =0x12345678 @ 加载32位立即数
LDR R0, =_start @ 加载标号地址
区别:
答案:
STMDB SP!, {reg_list}示例:
PUSH {R0-R3, LR} @ 等同于 STMDB SP!, {R0-R3, LR}
STMDB SP!, {R0-R3, LR} @ 直接使用STM指令
堆栈模型:
答案:
示例:
@ 传统方式
CMP R0, #0
BEQ zero_case
MOV R1, #1
B end
zero_case:
MOV R1, #0
end:
@ 条件执行(更简洁)
CMP R0, #0
MOVEQ R1, #0
MOVNE R1, #1
答案:
示例:
@ 位置相关代码(不推荐)
LDR R0, =_start @ 加载绝对地址
@ 位置无关代码(推荐)
ADRL R0, _start @ 加载相对地址
LDR R0, [PC, #offset] @ PC相对加载
应用场景:
答案:
函数实现:
@ 函数定义
my_function:
PUSH {LR} @ 保存返回地址
PUSH {R4-R7} @ 保存 callee-saved 寄存器
@ 函数体
MOV R0, #1 @ 返回值
POP {R4-R7} @ 恢复寄存器
POP {PC} @ 返回
@ 函数调用
BL my_function @ 调用函数
MOV R1, R0 @ 使用返回值
调用约定:
相关知识: [[01-Cortex-A7架构详解]] | [[03-时钟与电源管理]] | [[04-GPIO与中断系统]]
内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第七章 ARM汇编基础(7.1 GNU 汇编语法、7.2.1~7.2.6 常用汇编指令);第七章综合示例来自第八章、第十三章。 参考来源: 正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 最后更新: 2026-09-17