时间轴

2025-09-28

init

ARM 常用寄存器

ARM寄存器

指令分类

  1. 内存加载和存储指令
  2. 多字节内存加载和存储
  3. 算数和移位指令
  4. 移位操作
  5. 位操作指令
  6. 条件操作
  7. 跳转指令
  8. 独占访存指令
  9. 内存屏障指令
  10. 异常处理指令
  11. 系统寄存器访问指令

加载与存储指令

  1. LDR
  2. STR

练习 1:ldr 指令

练习1: ldr指令

练习1代码

注意 lsl #3 中间不要有分号

当扩展为 lsl 时,amount 只能为#0 或#3(ARM 文档定义)

练习 2:ldr 前变基模式和后变基模式

练习2

练习2代码

练习 3:str 的前变基和后变基模式

练习3

练习3调试

ldr 标签(literal)

读取PC+label的值

练习 4:ldr 伪指令

练习4: ldr伪指令

练习4代码

  • 指令:每一条指令都对应一种 CPU 操作
  • 伪指令:对编译器发出的命令,它是在对源程序汇编期间由汇编程序处理的操作,它们可以完成如处理处理器选择,定义程序模式,定义数据,分配存储区,指示程序结束等功能,总之,可以分解为几条指令的集合
  • ldr 指令既可以是大范围的地址读取伪指令,也可以是内存访问指令,而当它的第二个参数前面有“=”时,表示伪指令,否则表示内存访问指令
  • ldr 伪指令没有立即数访问限制

ldr x6, MY_LABEL 是内存访问指令

ldr x7, =MY_LABEL 是 ldr 伪指令

练习 5:实现 memcpy 汇编

练习5

练习5代码

练习5代码及调试

可以看到 0xffffffff 没有复制过去,这是因为起始地址没有四字节对齐

练习5代码

对齐起始地址

mov 指令

  1. 16 位立即数

mov指令

ldp 和 stp

A32 指令集中提供LDMSTM来实现多字节内存加载和存储,到了 A64 指令集,不再提供 LDM 和 STM 指令,而是采用 LDP 和 STP 指令

  • ldp 和 stp 可以一条指令加载和存储 16 个字节

LDP和STP

练习 6: memset

练习6

练习6代码

练习 7:坑

  1. 加载一个很大的数值到通用寄存器,例如 0xffff_0000_ffff_ffff

  2. 加载一个寄存器的值,例如 sctrl_el1 寄存器

SCTLR_EL1

练习7 坑2

3.

练习7:坑3

4.

练习7:坑4

5.

练习7:坑5

练习7:坑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 默认小端序(低地址存低字节)。
  • 还有一类原子加载/存储指令LDAXRSTLXR等)用于锁操作

算数与移位指令

pstate 处理器状态中四个条件操作码 NZCV

NZCV条件标志域

add 加法指令

普通的加法指令 add
  • 使用寄存器的加法
  • 使用立即数的加法
  • 使用移位操作的加法
adds 指令-影响条件标志位(进位)

主要影响 C 标志位(无符号溢出)

sub 减法指令

普通的减法指令
subs 指令-影响条件标志位(C 标志位)

adc 指令(带进位的加法指令)

ADC指令

sbc 指令(带进位的减法指令)

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 条件标志位

练习1:adds和cmp指令的C条件标志位

练习1代码

名称 含义
31 N 结果为负数
30 Z 结果为零
29 C 无符号比较无借位(Carry)
28 V 有符号溢出

练习1调试

练习 2 cmp 和 sbc 指令

练习2 cmp和sbc指令

练习2代码

移位操作

  • lsl 逻辑左移
  • lsr 逻辑右移
  • asr 算数右移
  • ror 循环右移

移位操作

按位与操作

  • and 与操作
  • ands 带进位的与操作,影响 Z 标志位

按位或操作

  • orr 或操作
  • eor 异或操作

异或

按位清除

  • bic 位清零指令

不常见

练习 3:测试 ands 指令以及 Z 标志位

练习3:ands指令以及Z标志位

练习3代码

位段插入操作

位段(bitfield)

位段插入操作

bfi 位段插入指令

bitfield insert

位段插入指令

  • Xd:目标寄存器(Destination)

  • Xn:源寄存器(Source)

  • lsbLeast Significant Bit,即 目标寄存器 Xd 中要开始插入的最低位位置索引(从 0 开始计数)。

  • width:要插入的位宽(bit 数)。

bfi位段插入指令

ubfx 无符号数的位段提取指令
sbfx 有符号数的位段提取指令

sbfx有符号数的位段提取指令

注意是从 0 开始计数

练习 4:测试 bitfield 指令

练习4: 测试bitfield指令

练习4代码及调试

练习4调试

乘法和除法指令

乘法:

指令 功能说明
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 指令来读取寄存器

练习5

注意 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 往这个寄存器写入通常会触发异常,不能随意修改。

练习5代码及调试

零计数指令 clz

clz: 计算最高为 1 的比特位前面有几个 0

CLZ指令

比较指令

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 指令以及条件操作后缀

实验1:cmp/cmn指令以及条件操作后缀

练习1代码及调试

练习1代码

NZCV 寄存器结构图

NZCV寄存器结构图

1
2
3
4
┌───┬───┬───┬───┐
NZCV
└───┴───┴───┴───┘
31 30 29 28 ← 在 PSTATENZCV 系统寄存器的位位置

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

条件选择指令 csel

cset

条件选择指令 cset

csinc

Conditional Select Increment

条件选择指令 csinc

练习 2:条件选择指令

练习2 条件选择指令

练习2 代码及其调试

跳转指令

b 无条件跳转

跳转指令,无条件的跳转指令,不返回

跳转范围:PC+/- 128MB

b.cnd 有条件跳转指令

有条件的跳转指令,不返回

cnd 为条件操作后缀

跳转范围:PC+/- 1MB

bx 跳转到寄存器指定地址处

跳转到寄存器指定的地址处,不返回

带返回地址(PC+4 => x30),适用于 call 子函数

返回地址保存到 x30 中,保存的是父函数的 PC+4

跳转范围:PC+/- 128MB

跳转到寄存器指定的地址处,可以返回

返回地址保存到 x30 中,保存的是父函数的 PC+4

返回指令

ret

从子函数中返回,通常是 x30 里保存了返回地址

eret

从当前的异常模式返回,通常可以实现模式的切换,例如 EL1 切换到 EL0

它从当前的异常模式中返回,它会从 SPSR 中恢复 PSTATE,从 ELR 中获取跳转地址,并返回到该地址

练习 3: 为啥 ret 之后就跑飞了

练习3: 为什么ret后就跑飞了

练习3代码

陷阱与坑

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

adr指令

  • adrp 指令,加载 PC 相对地址的 label 的地址,它只加载 label 所属的 4KB 对齐的地址,范围为+/- 4GB

adrp指令

adrp指令图解

练习 1: 测试 adrp 和 ldr 指令

练习1: 测试adrp和ldr指令

练习1代码

练习1调试

练习1调试

练习1调试

adrp 和 ldr 究竟有什么不同

陷阱与坑:

adrp和ldr究竟有什么不同

  • ldr 伪指令:加载的绝对地址
  • adrp 指令:加载的 PC 相对地址

  • 当链接地址 = 运行地址时

ldr 伪指令 加载的地址 = adrp 指令加载的地址

  • 当链接地址!=运行地址时

ldr 伪指令地址:加载的时链接地址(或称为虚拟地址)

adrp 指令:加载的时当前运行地址的 PC 值+label 的 offset,即 label 在当前运行时的地址(或者称为物理内存)

练习 2:adrp 和 ldr 指令的陷阱

练习2: adrp和ldr指令的陷阱

内存独占加载和存储指令

  • ldxr 指令

内存独占加载指令,从内存中以独占 exclusive 的方式加载内存的地址到通用寄存器

  • stxr 指令

内存独占存储指令

  1. ldrx 是加载内存,不过它会通过独占监视器来监视这个内存的访问,监视器会把这个内存地址标记为独占访问,保证其独占的方式来访问

内存独占加载指令 ldrx

  1. stxr 是有条件的存储内存,刚才 ldxr 标记的内存地址被独占的方式存储了,注意第一个寄存器为 w0

内存独占存储指令 stxr

内存独占加载和存储指令原理

  • ldxr 指令和 stxr 指令通常需要配对使用
  • Linux 内核常常用来实现 atomic 的访问,例如 atomic_write(),atomic_set_bit()
  • spinlock 机制可以简单地使用 ldxr 和 stxr 指令来实现

练习 3:ldxr 和 stxr 指令的使用

练习3: ldxr和stxr指令的使用

练习3代码

异常处理指令

指令 名称 描述
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 清除指令流水线和缓存,强制重新取指 特殊 改变系统状态寄存器后,强制刷新指令
  1. DMB

作用: 保证内存访问的先后顺序,但不阻塞执行。
例子:

1
2
3
str x0, [x1]     // 写数据
dmb sy // 确保写操作完成
ldr x2, [x3] // 再读其他数据
  1. DSB

作用: 必须等待之前所有内存操作完成,再继续执行后续指令。
例子:

1
2
3
str x0, [x1]     // 写数据
dsb sy // 等待写入完成
isb // 保证后面执行的是最新的代码
  1. ISB

作用: 清除 CPU 指令预取缓存,通常在修改系统控制寄存器后用。
例子:

1
2
msr sctlr_el1, x0  // 修改系统控制寄存器
isb // 强制刷新指令流

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 指令加载宏

坑1: ldr指令加载宏

ldr指令加载宏问题调试

ARM文档解释

在裸机编程中,没有 enable MMU,因此内存属性一律被视为 Device memory

ldr 指令访问 8 个字节,如果地址不是 8 个字节对齐,那么会触发 alignment 异常,也就是 Data abort

解决办法:

1
2
3
ldr x6, MY_LABEL
// 修改
ldr w6, MY_LABEL

坑 2: ldr 指令加载字符串

坑2: ldr指令加载字符串

这也是 alignment 导致的,因为不能保证 string1 的起始地址是 8 个字节对齐

解决办法:让 string1 按 8 个字节对齐

坑2的解决办法

对齐访问总结

  • 对于 normal memory,支持不对齐访问。(需要 enable MMU,并设置内存属性为 normal)
  • 可以单独设置不对齐访问时触发异常(设置 SCTLR_Elx.A)
  • 对于 Device memory,不对齐访问会触发 Data Abort 异常
    • 没有 enable MMU 的系统,例如我们的实验代访问 DDR 编程访问 device memory
  • 指令预取,需要 4 个字节对齐,否则会触发异常
  1. Normal memory 支持不对齐访问
  • 条件
    • 启用了 MMU(Memory Management Unit);
    • 并且该内存区域的属性被设置为 Normal memory
  • 结果
    • 支持对字(4 字节)、半字(2 字节)、字节(1 字节)的不对齐访问;
    • 可实现跨边界访问,不触发异常;
  1. 配置不对齐访问异常(SCTLR_ELx.A 位)
  • SCTLR_EL1.A 控制是否允许 不对齐访问(Alignment Check):
    • A = 0(默认):允许不对齐访问(如果 memory 类型允许);
    • A = 1强制对齐访问,一旦发生不对齐访问,则触发 Alignment fault 异常(Data Abort)
  1. Device memory 不支持不对齐访问
  • 特点
    • 无论 MMU 是否启用,Device memory 类型都不允许不对齐访问
  • 后果
    • 一旦访问未对齐的地址(如访问 0x1003 的 32-bit 数据),将触发 Data Abort 异常
  • 应用示例
    • 实验环境中直接映射访问 DDR、外设寄存器 等,通常被定义为 Device memory 类型;
    • 访问这些区域时必须对齐,如访问 32-bit 数据必须 4 字节对齐;
  1. 指令预取要求
  • 指令取址 必须是 4 字节对齐(即地址最低两位必须为 0);
  • 否则会触发 Instruction Abort 异常
  • 原因:ARMv8 指令以 4 字节为基本单位存储和取指,无法从非对齐位置解码执行指令。

坑 3:存储指令数据大小

使用 str 指令来设置寄存器,一定要注意寄存器的位宽,否则会出现死机问题

树莓派 4b 上的寄存器位宽都是 32bit 的,也就是 4 个字节

坑3: 存储指令数据大小

坑 4:ldxr 的大坑

坑4:ldxr的大坑

ldxr 指令的使用会有很多限制

  1. 首先确保访问的内存是 normal memory 并且是 sharable

ldxr指令要求内存是normal memory并且是sharable的

  1. 如果访问 device memory,例如 MMU 没有打开的情况,那就需要 CPU IP 核心支持以独占的方式访问 device memory,这个需要查询具体 CPU IP 手册的描述

例如: <\>第 6.45 章节

在Cortex-A72没有打开MMU时以独占方式访问device memory会出错

解决方法:

填充好页表,使能 MMU 和 cache,才能使用 ldxr 和 stxr

在汇编中实现串口打印功能

大作业: 在汇编中实现串口打印功能

大作业预期结果

🧱 1. GPIO 初始化(引脚复用配置)

1
2
3
4
5
6
7
ldr x1, =GPFSEL1
ldr w0, [x1]
and w0, w0, #0xffff8fff // 清除 GPIO14 功能选择位(bit 12-14)
orr w0, w0, #0x4000 // 设置 GPIO14 为 ALT0 (UART0_TXD)
and w0, w0, #0xfffc7fff // 清除 GPIO15 功能选择位(bit 15-17)
orr w0, w0, #0x20000 // 设置 GPIO15 为 ALT0 (UART0_RXD)
str w0, [x1]

✅ 作用:将 GPIO14 和 GPIO15 设置为 UART 功能(ALT0 模式)。
🧱 2. 禁用上拉/下拉(仅 Pi 3B 配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifdef CONFIG_BOARD_PI3B
ldr x1, =GPPUD
str wzr,[x1]

// delay 150 cycles
mov x0, #150
1:
sub x0, x0, #1
cmp x0, #0
bne 1b

ldr x1, =GPPUDCLK0
ldr w2, #0xc000 // 对 GPIO14 和 GPIO15 生效
str w2, [x1]

// delay again
mov x0, #150
2:
sub x0, x0, #1
cmp x0, #0
bne 2b

ldr x1, =GPPUDCLK0
str wzr, [x1]
isb
#endif

✅ 对 GPIO14/15 禁用上拉/下拉,符合 BCM2837 的初始化流程。
🧱 3. UART 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 禁用 UART
ldr x1, =U_CR_REG
str wzr, [x1]

// 设置波特率
ldr x1, =U_IBRD_REG
mov w2, #26 // Integer Baud Rate Divisor
str w2, [x1]

ldr x1, =U_FBRD_REG
mov w2, #3 // Fractional Baud Rate Divisor
str w2, [x1]

// 设置数据格式
ldr x1, =U_LCRH_REG
mov w2, #0x70 // FIFO enable + 8-bit word length
str w2, [x1]

// 禁用中断
ldr x1, =U_IMSC_REG
str wzr, [x1]

// 使能 UART (TX/RX/UART enable)
ldr x1, =U_CR_REG
mov w2, #0x301 // UARTEN | TXE | RXE
str w2, [x1]
isb

✅ 正确配置波特率和数据格式(8N1, FIFO),最后使能 UART。

你使用的是:

波特率 = UARTCLK / (16 * (IBRD + FBRD/64))
假设 UARTCLK = 48 MHz,则波特率约为 115200。

🧱 4. 单个字符输出函数 put_uart

1
2
3
4
5
6
7
8
9
10
11
put_uart:
ldr x1, =U_FR_REG
1:
ldr w2, [x1]
and w2, w2, #0x20 // 检查 TXFF (Transmit FIFO full)
cmp w2, #0
b.ne 1b // 如果 FIFO 满,则等待

ldr x1, =U_DATA_REG
str w0, [x1] // 写入字符
ret

✅ 会等待 FIFO 可用后发送字符。

🧱 5. 字符串输出函数 put_string_uart

1
2
3
4
5
6
7
8
9
10
11
put_string_uart:
mov x4, x0 // x0: 指向字符串
mov x6, x30 // 保存返回地址
1:
ldrb w0, [x4] // 读取一个字节(字符)
bl put_uart
add x4, x4, 1
cmp w0, #0
bne 1b
mov x30, x6
ret

✅ 输入:x0 = 字符串地址,将其打印出来直到遇到 NULL(0x00)。