在嵌入式系统开发中,内存访问是最基础也是最重要的操作之一。ARM架构提供了多种存储指令来满足不同场景下的数据存储需求,其中STRD和STREX系列指令因其特殊的设计,在双字存储和原子操作领域扮演着关键角色。
提示:ARMv7架构开始引入的LPAE(Large Physical Address Extension)特性会影响这些指令的内存访问行为,特别是在处理64位数据时。
STRD指令的语法格式如下:
asm复制STRD{cond} Rt, Rt2, [Rn {, #+/-imm}]
STRD{cond} Rt, Rt2, [Rn], #+/-imm
STRD{cond} Rt, Rt2, [Rn, #+/-imm]!
指令编码分为T1(Thumb)和A1(ARM)两种格式。T1编码使用12位立即数(范围0-1020,4字节对齐),而A1编码使用8位立即数(范围0-255)。在二进制编码中,关键控制位包括:
STRD支持三种经典寻址方式:
asm复制STRD R0, R1, [R2, #0x10] ; 地址=R2+0x10,R2不变
asm复制STRD R0, R1, [R2, #0x10]! ; 地址=R2+0x10,R2=R2+0x10
asm复制STRD R0, R1, [R2], #0x10 ; 地址=R2,R2=R2+0x10
指令执行流程如下:
需要注意的限制条件:
STREX系列包含多个变体,适用于不同数据宽度:
| 指令 | 数据宽度 | 版本要求 | 典型应用场景 |
|---|---|---|---|
| STREX | 32位 | ARMv6+ | 普通共享变量 |
| STREXB | 8位 | ARMv6K+ | 字节标志位 |
| STREXH | 16位 | ARMv6K+ | 短整型计数器 |
| STREXD | 64位 | ARMv6K+ | 双精度浮点数或指针 |
STREX指令的执行包含关键三步:
以STREXD为例的典型使用模式:
asm复制try:
LDREXD R0, R1, [R2] ; 加载并获取独占权
... ; 修改数据
STREXD R3, R0, R1, [R2]; 尝试存储
CMP R3, #0 ; 检查是否成功
BNE try ; 失败则重试
STREX指令可能触发两种异常情况:
特殊处理规则:
基于STREX的典型自旋锁实现:
asm复制lock:
MOV R0, #1 ; 锁值=1(锁定状态)
STREX R1, R0, [R2] ; 尝试获取锁
CMP R1, #0 ; 检查是否成功
BNE lock ; 失败则重试
DMB ; 内存屏障保证顺序
unlock:
DMB ; 确保所有操作完成
MOV R0, #0 ; 锁值=0(解锁状态)
STR R0, [R2] ; 普通存储即可
使用LDREX/STREX实现的多生产者队列:
asm复制enqueue:
LDREX R3, [R1] ; 加载尾指针
ADD R4, R3, #1 ; 计算新位置
STREX R5, R4, [R1] ; 尝试更新
CMP R5, #0
BNE enqueue ; 冲突则重试
STR R0, [R3] ; 存储数据
DMB ; 保证可见性
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| STREX总是返回1 | 未配对的LDREX | 检查前置加载指令 |
| 随机数据异常 | 未处理对齐 | 确保数据按宽度对齐 |
| 死锁 | 中断中使用了STREX | 禁用中断或使用其他同步机制 |
| 性能下降 | 缓存抖动 | 调整数据结构布局减少冲突 |
从ARMv6到ARMv7的关键改进:
向后兼容注意事项:
在实际开发中,我经常遇到需要平衡性能和正确性的场景。一个典型的经验是:在高度竞争的环境中,基于STREX的自旋锁可能不如使用调度器支持的互斥体高效。此时可以考虑混合策略——先尝试有限次数的原子操作,失败后再转入内核辅助的同步机制。