硬件开发(5)—ARM汇编
1.arm汇编基础
1.伪操作与代码结构
汇编伪操作并非处理器指令,而是用于指导汇编器组织代码段、指定指令集等,是构建启动代码的 “骨架”。启动代码的标准开头由以下伪操作组成:
area reset, code, readonly ;定义代码段,命名为reset
code32 ;表示后续指令使用 32位的 ARM 指令集
entry ;程序入口...
(编写核心代码)...
end ;结束
2.指令
1.mov
- 立即数赋值:
MOV{S}<c> <Rd>, #<const>
(Rd
:目标寄存器,#<const>
:立即数) - 寄存器赋值:
MOV{S}<c> <Rd>, <Rm>
(Rm
:源寄存器)
移位类型 | 格式示例 | 功能说明 |
---|---|---|
逻辑左移(LSL) | MOV R0, R1, LSL #3 | R1 的值左移 3 位(乘 8),结果赋给 R0 |
逻辑右移(LSR) | MOV R0, R1, LSR #2 | R1 的值右移 2 位(除 4,无符号) |
算术右移(ASR) | MOV R0, R1, ASR #2 | R1 的值右移 2 位(除 4,有符号,保留符号位) |
循环右移(ROR) | MOV R0, R1, ROR #4 | R1 的值循环右移 4 位,低位补到高位 |
2.add、sub:加法与减法
- 立即数作为第二操作数: ADD{S}<c> <Rd>, <Rn>, #<const>
- 寄存器作为第二操作数寄存器: ADD{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
- 寄存器作为第二操作数移位量: ADD{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
- 立即数作为第二操作数: SUB{S}<c> <Rd>, <Rn>, #<const>
- 寄存器作为第二操作数寄存器: SUB{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
- 寄存器作为第二操作数移位量: SUB{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
- mov r0, #0xFF
- mov r1, #0xF0
- mov r2, #1
- sub r6, r0, #0xF0
- sub r7, r0, r1
- sub r7, r0, r1, lsl #1
- sub r8, r0, r1, lsl r2
立即数:
准确的说这里所指的是12位立即数imm12。先说怎么判断某数是不是12位立即数,12位立即数的条件是:
把某个数展开成2进制,该数必须存在一种循环右移(偶数位),使得移位后高24位全0,低8位即为有效imm8(最高位1与最低位1如果大于8,则不可能是立即数)
3.ldr:加载指令
LDR<c> <Rt>, <label> (非立即数使用)
4.sdr:存放指令
STR<c> <Rt>, <label> (将Rt
寄存器的值存储到label
对应内存地址)
5..bic(bit clear):指定位置清0
- BIC{S}<c> <Rd>, <Rn>, #<const>
- BIC{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
- BIC{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
6.orr(or):指定位置1
- ORR{S}<c> <Rd>, <Rn>, #<const>
- ORR{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
- ORR{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
7.MVN:按位取反移动指令
- MVN{S}<c> <Rd>, #<const>
- MVN{S}<c> <Rd>, <Rm>{, <shift>}
- MVN{S}<c> <Rd>, <Rm>, <type> <Rs>
取反赋给目标源
8.条件判断标志NZCV
CPSR寄存器中条件判断标志位
- N: 符号标志位:上条指令执行结果最高位bit31为1,则 N = 1, 当结果作为有符号解释时为负值;
- Z: 零值标志位:上条指令执行结果为0(即bit0 - bit31 均为0),则 Z = 1;
- C: 进位标志位:进行无符号解读,如果在加法过程中进位或者减法时没有借位,则为 C = 1,否则 C = 0
- V: 溢出标志位:进行有符号解读,是否发生溢出 -2^31 - 2^31-1(两个正数加得负数,两个负数加得正数)
条件码:eq ge gt le lt al(无条件执行)
9.cmp(compare):比较指令
- CMP<c> <Rn>, #<const>
- CMP<c> <Rn>, <Rm>{, <shift>}
- CMP<c> <Rn>, <Rm>, <type> <Rs>
示例:比较三个数大小
10.b bl bx :跳转指令
指令 | 格式 | 核心作用 | 应用场景 |
---|---|---|---|
B | B<c> <label> | 无条件 / 条件跳转,不保存返回地址 | 循环跳转 |
BL | BL<c> <label> | 跳转前将当前PC (下一条指令地址)存入LR ,支持返回 | 调用汇编函数(如初始化函数) |
BX | BX <Rm> | 从Rm 指定的地址跳转,支持 ARM/Thumb 指令集切换 | 函数返回(BX LR ,将LR 的值赋给PC ) |
3.基础运用
1.循环
循环三要素:
- 循环结束条件
- 推动循环趋向终结的语句
- 循环的循环体
1.do...while(先执行再判断)
mov r0, #0
mov r1, #0
loop
add r1, r1, r0
add r0, r0, #1
cmp r0, #100
ble loop
2.while、for(先判断再执行)
mov r0, #0
mov r1, #0
loop
cmp r0, #100
bgt finish
add r1, r1, r0
add r0, r0, #1
b loop
finish
b finish
2.函数定义及其调用
1.基本形式
b main
func
mov r0, #1
mov r1, #2
add r3, r0, r1
bx lr
main
mov r0, #100
mov r1, #200
bl func
mov r3, #300
2.栈类型
类型 | 定义 |
---|---|
满减栈(FD) | 栈指针指向 “最后一个已压栈的数据”,压栈时 SP 先减 4,再存数据 |
满增栈(FA) | 栈指针指向 “最后一个已压栈的数据”,压栈时先存数据,再 SP 加 4 |
空减栈(ED) | 栈指针指向 “下一个待压栈的空地址”,压栈时先存数据,再 SP 减 4 |
空增栈(EA) | 栈指针指向 “下一个待压栈的空地址”,压栈时 SP 先加 4,再存数据 |
注:arm默认为满减栈
1.栈指针(SP)初始化
压栈前:ldr sp, =0x40001000 ;初始为空,sp指向栈顶
2.保护现场(压栈):STMFD 指令
STMFD <Rn>{!}, <registers>
; <Rn>:栈指针寄存器(通常是SP)
; {!}:压栈后自动更新SP(SP -= 4×寄存器个数)
; <registers>:需压栈的寄存器列表(如r0-r12, lr)
e.g.stmfd sp!, {r0-r12, lr}
3.恢复现场(弹栈):LDMFD 指令
LDMFD <Rn>{!}, <registers>
; <Rn>:栈指针寄存器(通常是SP)
; {!}:弹栈后自动更新SP(SP += 4×寄存器个数)
; <registers>:需恢复的寄存器列表(与压栈顺序一致)
e.g.ldmfd sp!, {r0-r12, lr}
3.在汇编中调用c语言函数
步骤 1:创建 C 语言文件(main.c)
// main.c
extern void c_add(void);
// C函数:计算5个参数的和(r0-r3传前4个,第5个通过栈传递)
int c_add(int a, int b, int c, int d, int e)
{
return a + b + c + d + e;
}
步骤 2:汇编中声明导入 C 函数
步骤3:保护现场 bl函数调用 恢复现场
步骤4:栈对齐伪指令:preserve8 用于确保函数调用时栈指针保持 8 字节对齐
步骤 5:工程配置(Keil 环境)
删除冲突文件:删除工程中默认的
startup_xxx.s
(如startup_stm32f103.s
)和.sct
(分散加载文件);添加自定义文件:将
start.s
和main.c
添加到工程;配置 ROM/RAM:
点击 “魔术棒”→Target
→设置ROM1
:Start=0x0
,Size=0x2000
(ROM 起始地址和大小,需匹配芯片);
设置IRAM1
:Start=0x40000000
,Size=0x1000
(RAM 地址,确保 SP 初始化地址在范围内);链接配置:
点击 “魔术棒”→Linker
→勾选Use Memory Layout from Target Dialog
(使用 Target 配置的内存布局,无需分散加载文件);调试配置:
点击 “魔术棒”→Debug
→选择Use Simulator
(使用模拟器)→取消Run to main
(确保从汇编启动入口开始执行)。
步骤6:函数传参
- 前 4 个参数:通过
r0-r3
寄存器传递(r0传第 1 个,r1
传第 2 个,以此类推); - 第 5 个及以后参数:通过栈传递(参数压栈顺序与函数声明顺序一致);
- 返回值:通过
r0
寄存器返回(32 位值),64 位值通过r0-r1
返回
stmfd sp!, {r0-r12, lr}
mov r0, #1
mov r1, #2
mov r2, #3
mov r3, #4
mov r4, #5
stmfd sp!, {r4}
bl c_add
ldmfd sp!, {r4}
ldmfd sp!, {r0-r12, lr}
4.在c语言中调用汇编
步骤 1:汇编中定义并导出函数(export func1)
步骤 2:C 中声明并调用汇编函数
5.ARM 工作模式切换:修改 CPSR 寄存器
start_hander
ldr sp, =0x40001000 ; 初始化SVC模式的栈指针
mrs r0, cpsr ; 读取当前程序状态寄存器(CPSR)到r0
bic r0, r0, #(0x1F << 0) ; 清除CPSR的M域(最低5位,控制工作模式)
bic r0, r0, #(1 << 7) ; 清除CPSR的I位(bit7),允许IRQ中断
orr r0, r0, #(0x10 << 0) ; 设置M域为User模式(0x10 = 0b10000)
msr cpsr_c, r0 ; 将修改后的值写入CPSR的控制位段,完成模式切换
ldr sp, =0x40001000 ; 加载User模式栈的初始地址
sub sp, sp, #1024 ; 调整栈指针,预留1024字节栈空间(满减栈)
6.异常向量表
1.异常向量表
; 异常向量表:CPU发生异常时的入口地址表,每个入口占4字节
ldr pc, =start_hander ; 0x00000000:复位异常(CPU复位后第一个执行的入口)
ldr pc, =undefine_hander ; 0x00000004:未定义指令异常
ldr pc, =software_hander ; 0x00000008:软件中断异常(SWI/SVC)
ldr pc, =prefetch_hander ; 0x0000000C:预取指异常(指令读取错误)
ldr pc, =data_hander ; 0x00000010:数据异常(数据访问错误)
nop ; 0x00000014:预留位置(保证地址对齐)
ldr pc, =irq_hander ; 0x00000018:IRQ中断(外部中断)
ldr pc, =fiq_hander ; 0x0000001C:FIQ中断(快速中断)
当异常发生时,CPU 会自动跳转到对应地址执行处理函数
2. 异常处理函数
; 未定义指令异常处理(死循环,防止程序跑飞)
undefine_hander b undefine_hander ;
软中断异常处理
software_hander
stmfd sp!, {r0-r12, lr} ;保护现场
bl software_vector ;调用具体的中断处理逻辑
ldmfd sp!, {r0-r12, pc}^ ;恢复现场 + 异常返回,此时使用pc是为了恢复中断前状态并把之前保存的lr的值写给pc
3. 软中断触发函数
export asm_swi_fun ; 导出符号,供C语言调用
asm_swi_fun
swi #7 ; 触发软件中断,#7是中断编号(用于区分不同服务)
bx lr ; 函数返回(中断处理完成后继续执行)
4.启动初始化
start_hander ; 初始化SVC模式栈指针(特权模式栈)
ldr sp, =0x40001000 ; 切换到User模式(应用程序模式)并允许IRQ中断
mrs r0, cpsr ; 读取当前程序状态寄存器(CPSR)
bic r0, r0, #(0x1F << 0) ; 清除低5位(工作模式位)
bic r0, r0, #(1 << 7) ; 清除I位(允许IRQ中断)
orr r0, r0, #(0x10 << 0) ; 设置为User模式(0x10=0b10000)
msr cpsr_c, r0 ; 写入CPSR,完成模式切换 ; 初始化User模式栈(应用程序栈,大小1024字节)
ldr sp, =0x40001000
sub sp, sp, #1024 ; 满减栈:栈顶地址 = 0x40001000 - 1024
b main ; 跳转到C语言main函数
5.主函数
int main(void)
{
asm_swi_fun(); // 调用汇编函数,触发SWI #7软件中断
while(1); // 死循环,防止程序退出
}