ARM 汇编
时间轴
2025-09-28
init
ARM 常用寄存器
指令分类:
- 内存加载和存储指令
- 多字节内存加载和存储
- 算数和移位指令
- 移位操作
- 位操作指令
- 条件操作
- 跳转指令
- 独占访存指令
- 内存屏障指令
- 异常处理指令
- 系统寄存器访问指令
加载与存储指令
- LDR
- STR
练习 1:ldr 指令
注意 lsl #3 中间不要有分号
当扩展为 lsl 时,amount 只能为#0 或#3(ARM 文档定义)
练习 2:ldr 前变基模式和后变基模式
练习 3:str 的前变基和后变基模式
ldr 标签(literal)
读取PC+label的值
练习 4:ldr 伪指令
- 指令:每一条指令都对应一种 CPU 操作
- 伪指令:对编译器发出的命令,它是在对源程序汇编期间由汇编程序处理的操作,它们可以完成如处理处理器选择,定义程序模式,定义数据,分配存储区,指示程序结束等功能,总之,可以分解为几条指令的集合
- ldr 指令既可以是大范围的地址读取伪指令,也可以是内存访问指令,而当它的第二个参数前面有“=”时,表示伪指令,否则表示内存访问指令
- ldr 伪指令没有立即数访问限制
ldr x6, MY_LABEL 是内存访问指令
ldr x7, =MY_LABEL 是 ldr 伪指令
练习 5:实现 memcpy 汇编
可以看到 0xffffffff 没有复制过去,这是因为起始地址没有四字节对齐
mov 指令
- 16 位立即数
ldp 和 stp
A32 指令集中提供LDM和STM来实现多字节内存加载和存储,到了 A64 指令集,不再提供 LDM 和 STM 指令,而是采用 LDP 和 STP 指令
- ldp 和 stp 可以一条指令加载和存储 16 个字节
练习 6: memset
练习 7:坑
加载一个很大的数值到通用寄存器,例如 0xffff_0000_ffff_ffff
加载一个寄存器的值,例如 sctrl_el1 寄存器
3.
4.
5.
注意使用 ldr 时,可能产生 literal pool, 最好用 adrp 指令
存储指令变种
指令 | 含义(全称) | 访问大小 | 符号扩展 |
---|---|---|---|
LDR | Load Register | 32 位(W 寄存器)或 64 位(X 寄存器) | 无符号 |
LDRSW | Load Register Signed Word | 32 位 → 符号扩展到 64 位 | 有符号 |
LDRB | Load Register Byte | 8 位 → 零扩展到 32/64 位 | 无符号 |
LDRSB | Load Register Signed Byte | 8 位 → 符号扩展到 32/64 位 | 有符号 |
LDRH | Load Register Halfword | 16 位 → 零扩展到 32/64 位 | 无符号 |
LDRSH | Load Register Signed Halfword | 16 位 → 符号扩展到 32/64 位 | 有符号 |
LDRQ | Load Register Quadword (NEON/SIMD 寄存器) | 128 位(V 寄存器) | 无符号 |
STR | Store Register | 32 位或 64 位 | 无符号 |
STRB | Store Register Byte | 8 位 | 无符号 |
STRH | Store Register Halfword | 16 位 | 无符号 |
补充说明
- 无符号加载 (LDR, LDRB, LDRH):高位部分填充
0
。 - 有符号加载 (LDRSB, LDRSH, LDRSW):会进行符号扩展(最高位是 1 则扩展为 1)。
- 如果访问和存储 4 个字节或 8 个字节都是用 ldr 和 str,只不过目标寄存器使用 wn 或者 xn
STR
系列指令不存在有符号版本,因为存储时“原封不动”写入内存,不会进行符号扩展、零扩展或任何填充操作,但要注意寄存器位宽,如 xn 和 wn 时不同的。- AArch64 默认小端序(低地址存低字节)。
- 还有一类原子加载/存储指令(
LDAXR
、STLXR
等)用于锁操作
算数与移位指令
pstate 处理器状态中四个条件操作码 NZCV
add 加法指令
普通的加法指令 add
- 使用寄存器的加法
- 使用立即数的加法
- 使用移位操作的加法
adds 指令-影响条件标志位(进位)
主要影响 C 标志位(无符号溢出)
sub 减法指令
普通的减法指令
subs 指令-影响条件标志位(C 标志位)
adc 指令(带进位的加法指令)
sbc 指令(带进位的减法指令)
Wd = Wn - Wm-1+ C
cmp 比较指令
比较两个数的大小,内部使用 subs 指令来实现,影响 C 标志位
等同于
1 | SUBS XZR, <Xn>, #<imm> |
当 x1 和 x2 都为无符号数时
1 | cmp x1, x2 |
x1-x2 = x1+ ~x2+1(发生溢出)
当 x1>= x2 时,C=1
当 x1<x2 时 C=0
练习 1 adds 和 cmp 指令的 C 条件标志位
位 | 名称 | 含义 |
---|---|---|
31 | N | 结果为负数 |
30 | Z | 结果为零 |
29 | C | 无符号比较无借位(Carry) |
28 | V | 有符号溢出 |
练习 2 cmp 和 sbc 指令
移位操作
- lsl 逻辑左移
- lsr 逻辑右移
- asr 算数右移
- ror 循环右移
按位与操作
- and 与操作
- ands 带进位的与操作,影响 Z 标志位
按位或操作
- orr 或操作
- eor 异或操作
按位清除
- bic 位清零指令
不常见
练习 3:测试 ands 指令以及 Z 标志位
位段插入操作
位段(bitfield)
bfi 位段插入指令
bitfield insert
Xd:目标寄存器(Destination)
Xn:源寄存器(Source)
lsb:Least Significant Bit,即 目标寄存器 Xd 中要开始插入的最低位位置索引(从 0 开始计数)。
width:要插入的位宽(bit 数)。
ubfx 无符号数的位段提取指令
sbfx 有符号数的位段提取指令
注意是从 0 开始计数
练习 4:测试 bitfield 指令
乘法和除法指令
乘法:
指令 | 功能说明 |
---|---|
MADD | Multiply-Add:Xd = (Xn * Xm) + Xa |
MNEG | Multiply-Negate:Xd = -(Xn * Xm) |
MSUB | Multiply-Subtract:Xd = Xa - (Xn * Xm) |
MUL | Multiply:Xd = Xn * Xm (低 64 位结果) |
SMADDL | Signed Multiply-Add Long:32 位*32 位 → 64 位,加上加数 |
SMNEGL | Signed Multiply-Negate Long:32 位*32 位 → 64 位,取负 |
SMSUBL | Signed Multiply-Subtract Long:32 位*32 位 → 64 位,执行减法 |
SMULH | Signed Multiply returning High half:取 128 位结果的高 64 位(有符号) |
SMULL | Signed Multiply Long:32 位*32 位 → 64 位 |
UMADDL | Unsigned Multiply-Add Long:无符号版本 |
UMNEGL | Unsigned Multiply-Negate Long:无符号版本 |
UMSUBL | Unsigned Multiply-Subtract Long:无符号版本 |
UMULH | Unsigned Multiply returning High half:取高 64 位(无符号) |
UMULL | Unsigned Multiply Long:无符号 32 位*32 位 → 64 位 |
除法:
指令 | 功能说明 |
---|---|
SDIV | Signed Divide:有符号除法 |
UDIV | Unsigned Divide:无符号除法 |
比较与跳转指令
练习 5:使用 bitfield 指令来读取寄存器
注意 ubfx 这类指令只能对普通寄存器的值进行位域提取,不能直接提取系统寄存器,因此需要先用 msr 把系统寄存器存入通用寄存器
读取系统寄存器(move register from system)
1 | mrs x1, ID_AA64ISAR0_EL1 |
写入系统寄存器(move system from register)
1 | msr ID_AA64ISAR0_EL1, x1 |
⚠️ 但注意:很多 CPU 特性寄存器(如
ID_AA64ISAR0_EL1
)是 只读的,所以MSR
往这个寄存器写入通常会触发异常,不能随意修改。
零计数指令 clz
clz: 计算最高为 1 的比特位前面有几个 0
比较指令
cmp
比较两个数
cmp x1, x2
x1-x2
cmn
负向比较
cmn x1, x2
x1+x2
条件操作后缀
条件后缀 | 含义 | 标志 | 条件码 | 缩写说明 |
---|---|---|---|---|
EQ | 相等 | Z=1 | 0b0000 | Equal |
NE | 不相等 | Z=0 | 0b0001 | Not Equal |
CS/HS | 无符号数大于或等于 | C=1 | 0b0010 | Carry Set / Higher or Same |
CC/LO | 无符号数小于 | C=0 | 0b0011 | Carry Clear / Lower |
MI | 负数 | N=1 | 0b0100 | Minus |
PL | 正数或零 | N=0 | 0b0101 | Plus |
VS | 溢出 | V=1 | 0b0110 | Overflow Set |
VC | 未溢出 | V=0 | 0b0111 | Overflow Clear |
HI | 无符号数大于 | (C=1) && (Z=0) | 0b1000 | Higher |
LS | 无符号数小于或等于 | (C=0) ∨ (Z=1) | 0b1001 | Lower or Same |
GE | 带符号数大于或等于 | N = V | 0b1010 | Greater or Equal |
LT | 带符号数小于 | N ≠ V | 0b1011 | Less Than |
GT | 带符号数大于 | (Z=0) && (N=V) | 0b1100 | Greater Than |
LE | 带符号数小于或等于 | (Z=1) ∨ (N≠V) | 0b1101 | Less or Equal |
AL | 无条件执行 | — | 0b1110 | Always |
NV | 无条件执行 | — | 0b1111 | Never (保留,一般不用) |
练习 1:cmp/cmn 指令以及条件操作后缀
NZCV 寄存器结构图
1 | ┌───┬───┬───┬───┐ |
N (Negative flag)
- 结果为负数(有符号数最高位为 1) → N=1
- 否则 N=0
Z (Zero flag)
- 结果等于 0 → Z=1
- 否则 Z=0
C (Carry flag)
- 加法:有进位 → C=1
- 减法:无借位(即操作数 1 ≥ 操作数 2) → C=1
- 否则 C=0
V (Overflow flag)
- 有符号加/减法溢出(结果超出可表示范围) → V=1
- 否则 V=0
条件选择指令
搭配 cmp 指令来使用
csel
cset
csinc
Conditional Select Increment
练习 2:条件选择指令
跳转指令
b 无条件跳转
跳转指令,无条件的跳转指令,不返回
跳转范围:PC+/- 128MB
b.cnd 有条件跳转指令
有条件的跳转指令,不返回
cnd 为条件操作后缀
跳转范围:PC+/- 1MB
bx 跳转到寄存器指定地址处
跳转到寄存器指定的地址处,不返回
bl (Branch with Link)带返回地址的跳转
带返回地址(PC+4 => x30),适用于 call 子函数
返回地址保存到 x30 中,保存的是父函数的 PC+4
跳转范围:PC+/- 128MB
blx (Branch with Link to Register)
跳转到寄存器指定的地址处,可以返回
返回地址保存到 x30 中,保存的是父函数的 PC+4
返回指令
ret
从子函数中返回,通常是 x30 里保存了返回地址
eret
从当前的异常模式返回,通常可以实现模式的切换,例如 EL1 切换到 EL0
它从当前的异常模式中返回,它会从 SPSR 中恢复 PSTATE,从 ELR 中获取跳转地址,并返回到该地址
练习 3: 为啥 ret 之后就跑飞了
陷阱与坑
bl 指令:用于 call 子函数,它把返回地址写入到 x30 寄存器,返回地址为 PC+4
在一个函数里调用 bl 来 call 子函数,有可能会把父函数的 lr 寄存器给冲走,然后父函数的 ret 返回就跑飞了
简单来讲就是子函数更改了 x30 寄存器的值,返回时没有恢复
比较并跳转指令
cbz cbnz tbz tbnz
指令 | 缩写含义 | 作用描述 |
---|---|---|
CBZ | Compare and Branch if Zero | 检查指定寄存器的值是否为零,若为零则跳转到目标地址,跳转范围+/- 1MB。 |
CBNZ | Compare and Branch if Non-Zero | 检查指定寄存器的值是否非零,若非零则跳转到目标地址,跳转范围+/- 1MB。 |
TBZ | Test Bit and Branch if Zero | 检查指定寄存器的某一位(bit)是否为 0,若该位为 0 则跳转到目标地址,跳转范围+/- 32KB。 |
TBNZ | Test Bit and Branch if Non-Zero | 检查指定寄存器的某一位(bit)是否为 1,若该位为 1 则跳转到目标地址,跳转范围+/- 32KB。 |
其他重要的指令
PC 相对地址加载指令
- adr 指令,加载 PC 相对地址的 label 的地址,范围为+/- 1MB
- adrp 指令,加载 PC 相对地址的 label 的地址,它只加载 label 所属的 4KB 对齐的地址,范围为+/- 4GB
练习 1: 测试 adrp 和 ldr 指令
adrp 和 ldr 究竟有什么不同
陷阱与坑:
- ldr 伪指令:加载的绝对地址
adrp 指令:加载的 PC 相对地址
当链接地址 = 运行地址时
ldr 伪指令 加载的地址 = adrp 指令加载的地址
- 当链接地址!=运行地址时
ldr 伪指令地址:加载的时链接地址(或称为虚拟地址)
adrp 指令:加载的时当前运行地址的 PC 值+label 的 offset,即 label 在当前运行时的地址(或者称为物理内存)
练习 2:adrp 和 ldr 指令的陷阱
内存独占加载和存储指令
- ldxr 指令
内存独占加载指令,从内存中以独占 exclusive 的方式加载内存的地址到通用寄存器
- stxr 指令
内存独占存储指令
- ldrx 是加载内存,不过它会通过独占监视器来监视这个内存的访问,监视器会把这个内存地址标记为独占访问,保证其独占的方式来访问
- stxr 是有条件的存储内存,刚才 ldxr 标记的内存地址被独占的方式存储了,注意第一个寄存器为 w0
- ldxr 指令和 stxr 指令通常需要配对使用
- Linux 内核常常用来实现 atomic 的访问,例如 atomic_write(),atomic_set_bit()
- spinlock 机制可以简单地使用 ldxr 和 stxr 指令来实现
练习 3:ldxr 和 stxr 指令的使用
异常处理指令
指令 | 名称 | 描述 |
---|---|---|
SVC #imm | 系统调用指令(Supervisor Call) | 应用程序通过 SVC 指令从用户态跳转到内核态操作系统,通常进入 EL1 异常级别,用于触发系统调用 |
HVC #imm | 虚拟化系统调用指令(Hypervisor Call) | 主机操作系统通过 HVC 指令从 EL1 进入 EL2,调用虚拟化管理程序(Hypervisor),用于虚拟化支持场景 |
SMC #imm | 安全监控系统调用指令(Secure Monitor Call) | 主机操作系统或监控程序通过 SMC 指令从普通世界(Non-secure World)进入安全世界(Secure World),通常触发 EL3 异常,用于 TrustZone 安全机制 |
系统寄存器访问指令
指令 | 描述 |
---|---|
MRS | 读取系统寄存器的值到通用寄存器(Move Register from System) |
MSR | 将通用寄存器的值写入系统寄存器(Move Register to System) |
内存屏障指令
指令 | 全称 | 作用 | 强度 | 示例用途 |
---|---|---|---|---|
DMB | Data Memory Barrier | 保证内存访问指令的执行顺序(如写后读不会乱序) | 中等 | 多核间共享数据通信 |
DSB | Data Synchronization Barrier | 等待所有内存访问完成后再执行后续指令 | 强 | 外设寄存器读写前后,缓存同步 |
ISB | Instruction Synchronization Barrier | 清除指令流水线和缓存,强制重新取指 | 特殊 | 改变系统状态寄存器后,强制刷新指令 |
- DMB
作用: 保证内存访问的先后顺序,但不阻塞执行。
例子:
1 | str x0, [x1] // 写数据 |
- DSB
作用: 必须等待之前所有内存操作完成,再继续执行后续指令。
例子:
1 | str x0, [x1] // 写数据 |
- ISB
作用: 清除 CPU 指令预取缓存,通常在修改系统控制寄存器后用。
例子:
1 | msr sctlr_el1, x0 // 修改系统控制寄存器 |
DMB/DSB 指令参数详解(内存屏障的粒度与作用范围)
参数 | 访问顺序控制 | 共享范围 | 说明 |
---|---|---|---|
SY |
读/写 | 全系统共享 | 最强,所有处理器和设备可见,常用于设备驱动或关键同步 |
ST |
写 | 全系统共享 | 只对写操作建立顺序 |
LD |
读 | 全系统共享 | 只对读操作建立顺序 |
ISH |
读/写 | Inner Shareable(内部共享) | 多核共享同一个 L2 cache 时使用 |
ISHST |
写 | Inner 共享 | 内部共享域的写操作屏障 |
ISHLD |
读 | Inner 共享 | 内部共享域的读操作屏障 |
NSH |
读/写 | Non-shareable(不共享) | 本核私有内存使用 |
NSHST |
写 | 不共享 | 本核私有写屏障 |
NSHLD |
读 | 不共享 | 本核私有读屏障 |
OSH |
读/写 | Outer Shareable(外部共享) | 多个 L2 之间共享时使用 |
OSHST |
写 | Outer 共享 | 外部共享写屏障 |
OSHLT |
读 | Outer 共享 | 外部共享读屏障 |
Overview 总结
- 运行在 aarch64 execution state 的指令集,64 是指它的运行环境,而不是指令长度
- 指令长度是 32bit,而不是 64bit
- 31 个通用寄存器,xn 为 64bit,wn 为 32bit
- 零寄存器:xzr, wzr
- pc 不是通用寄存器,通常不能直接返回
- x30 用作链接寄存器 lr,用于函数返回
- ELR_ELx 用于从异常返回
- 每个异常等级都有自己的栈 SP,例如 SP_EL0, SP_EL1
SP 不是通用寄存器
SIMD 和浮点运算的寄存器
Qn(126bit,16 字节),Dn(64bit),Sn(32bit), Hn(16bit),Bn(Bn)
PSTATE
(Processor State Register)不是单独存在的一个物理寄存器,而是多个位段组成的处理器状态集合。PSTATE
的值控制 CPU 的当前状态,包括异常屏蔽、当前异常级别、条件码等。
字段 | 含义说明 |
---|---|
NZCV |
条件码标志位(ALU Flags)用于条件跳转等:N(负号)、Z(零)、C(进位)、V(溢出) |
Q |
溢出标志位位,仅 AArch32 用某些 SIMD 操作溢出时设置 |
DAIF |
异常屏蔽位:D:调试异常屏蔽A:SError 屏蔽I:IRQ 中断屏蔽F:FIQ 中断屏蔽 |
SPSel |
SP 寄存器选择(仅 AArch64)控制是使用 SP_EL0 还是 SP_ELx |
CurrentEL |
当前异常级别(EL0/EL1/EL2/EL3)影响特权级访问和执行 |
E |
字节序选择(AArch32)控制数据访问是小端或大端 |
IL |
非法指令标志设为 1 时,所有指令都会被当作 UNDEFINED 执行,调试用 |
SS |
单步执行标志(Software Stepping)与调试器联合使用,每次执行一条指令后触发异常 |
- 加载和存储指令
指令 | 类型 | 数据大小 | 是否符号扩展 | 存储/加载目标 |
---|---|---|---|---|
LDR |
普通加载 | 依寄存器 | 否 | 任意位宽 |
LDRSW |
加载有符号 word | 32-bit | ✔(符号扩展至 64-bit) | Xn only |
LDRB |
加载字节 | 8-bit | 否 | Wn/Xn |
LDRSB |
加载符号字节 | 8-bit | ✔ | Wn 或 Xn |
LDRH |
加载半字 | 16-bit | 否 | Wn/Xn |
LDRSH |
加载符号半字 | 16-bit | ✔ | Wn 或 Xn |
STRB |
存储字节 | 8-bit | N/A | — |
STRH |
存储半字 | 16-bit | N/A | — |
指令后缀含义
后缀 | 含义 |
---|---|
B |
Byte(8-bit) |
H |
Halfword(16-bit) |
W |
Word(32-bit) |
S |
Sign-extended(符号扩展) |
无后缀 | 默认按目标寄存器位数加载或存储 |
- 多字节加载和存储指令
- A64 指令集已经把 ldm,stm,push,pop 指令删掉
- ldp 和 stp 来实现多字节加载和存储指令
与 PC 相关的指令
ldr x0, =label
(伪指令)
- 含义:加载
label
的地址(链接地址)到x0
。 - 实现方式:汇编器会在
.rodata
区域生成一个常量表,ldr
实际上是从常量表中加载地址。 - 适用场景:任何位置都能使用,但依赖链接时地址信息,在支持重定位的系统中要谨慎。
ldr x0, label
- 含义:将
label
地址处的值加载到x0
中。 - 注意:这里的
label
是个地址,指向的数据会被加载到寄存器中(不是加载地址本身)。 - 区别于上面:这个不是加载地址,而是通过地址去读取数据。
adr x0, .
- 含义:把当前 PC 的值加载到
x0
中。 - 常用场景:定位当前代码位置(如实现 Position-Independent Code)。
adrp x0, label
- 含义:将
label
所在 page 的页首地址(4KB 对齐)加载到x0
中。 - 用途:
- 多用于 位置无关代码(PIC)。
- 搭配
add
实现完整地址加载:
标志位 | 含义 | 常见触发情况 |
---|---|---|
N |
Negative(负号)标志位:结果为负时置位 | 如 subs x0, x1, x2 得到负数 |
Z |
Zero(零)标志位:结果为零时置位 | 如 subs x0, x1, x1 ,结果为 0 |
C |
Carry(进位)标志位:无符号加法进位/无符号减法借位 | 如 adds , subs , adc , sbc |
V |
Overflow(溢出)标志位:有符号数加减运算时溢出 | 如 adds , subs 溢出 |
推荐查阅文档
arm8.6 Chapter C3 A64 Instruction Set Overview
QEMU 能跑但板子不能跑的陷阱与坑
坑 1:ldr 指令加载宏
在裸机编程中,没有 enable MMU,因此内存属性一律被视为 Device memory
ldr 指令访问 8 个字节,如果地址不是 8 个字节对齐,那么会触发 alignment 异常,也就是 Data abort
解决办法:
1 | ldr x6, MY_LABEL |
坑 2: ldr 指令加载字符串
这也是 alignment 导致的,因为不能保证 string1 的起始地址是 8 个字节对齐
解决办法:让 string1 按 8 个字节对齐
对齐访问总结
- 对于 normal memory,支持不对齐访问。(需要 enable MMU,并设置内存属性为 normal)
- 可以单独设置不对齐访问时触发异常(设置 SCTLR_Elx.A)
- 对于 Device memory,不对齐访问会触发 Data Abort 异常
- 没有 enable MMU 的系统,例如我们的实验代访问 DDR 编程访问 device memory
- 指令预取,需要 4 个字节对齐,否则会触发异常
- Normal memory 支持不对齐访问
- 条件:
- 启用了 MMU(Memory Management Unit);
- 并且该内存区域的属性被设置为 Normal memory;
- 结果:
- 支持对字(4 字节)、半字(2 字节)、字节(1 字节)的不对齐访问;
- 可实现跨边界访问,不触发异常;
- 配置不对齐访问异常(SCTLR_ELx.A 位)
- SCTLR_EL1.A 控制是否允许 不对齐访问(Alignment Check):
A = 0
(默认):允许不对齐访问(如果 memory 类型允许);A = 1
:强制对齐访问,一旦发生不对齐访问,则触发 Alignment fault 异常(Data Abort);
- Device memory 不支持不对齐访问
- 特点:
- 无论 MMU 是否启用,Device memory 类型都不允许不对齐访问;
- 后果:
- 一旦访问未对齐的地址(如访问
0x1003
的 32-bit 数据),将触发 Data Abort 异常;
- 一旦访问未对齐的地址(如访问
- 应用示例:
- 实验环境中直接映射访问 DDR、外设寄存器 等,通常被定义为 Device memory 类型;
- 访问这些区域时必须对齐,如访问 32-bit 数据必须 4 字节对齐;
- 指令预取要求
- 指令取址 必须是 4 字节对齐(即地址最低两位必须为 0);
- 否则会触发 Instruction Abort 异常;
- 原因:ARMv8 指令以 4 字节为基本单位存储和取指,无法从非对齐位置解码执行指令。
坑 3:存储指令数据大小
使用 str 指令来设置寄存器,一定要注意寄存器的位宽,否则会出现死机问题
树莓派 4b 上的寄存器位宽都是 32bit 的,也就是 4 个字节
坑 4:ldxr 的大坑
ldxr 指令的使用会有很多限制
- 首先确保访问的内存是 normal memory 并且是 sharable
- 如果访问 device memory,例如 MMU 没有打开的情况,那就需要 CPU IP 核心支持以独占的方式访问 device memory,这个需要查询具体 CPU IP 手册的描述
例如: <\
解决方法:
填充好页表,使能 MMU 和 cache,才能使用 ldxr 和 stxr
在汇编中实现串口打印功能
🧱 1. GPIO 初始化(引脚复用配置)
1 | ldr x1, =GPFSEL1 |
✅ 作用:将 GPIO14 和 GPIO15 设置为 UART 功能(ALT0 模式)。
🧱 2. 禁用上拉/下拉(仅 Pi 3B 配置)
1 | #ifdef CONFIG_BOARD_PI3B |
✅ 对 GPIO14/15 禁用上拉/下拉,符合 BCM2837 的初始化流程。
🧱 3. UART 初始化
1 | // 禁用 UART |
✅ 正确配置波特率和数据格式(8N1, FIFO),最后使能 UART。
你使用的是:
波特率 = UARTCLK / (16 * (IBRD + FBRD/64))
假设 UARTCLK = 48 MHz,则波特率约为 115200。
🧱 4. 单个字符输出函数 put_uart
1 | put_uart: |
✅ 会等待 FIFO 可用后发送字符。
🧱 5. 字符串输出函数 put_string_uart
1 | put_string_uart: |
✅ 输入:x0 = 字符串地址,将其打印出来直到遇到 NULL(0x00)。