Cortex-M3 设计用于重度低延迟和低抖动多任务处理,即它的中断控制器与内核协作,以保证从中断触发到中断处理的周期数。ldrex/strex 被实现为一种与所有这些协作的方式(我的意思是中断屏蔽和其他细节,例如通过位带别名设置原子位),否则,单核、非 MMU、非缓存 µC用处不大。如果它没有实现它,低优先级任务将不得不持有一个锁,这可能会产生小的优先级反转,具有硬实时系统无法应对的延迟和抖动,至少不会在失败的 ldrex/strex 所具有的“重试”语义所允许的量级。
附带说明一下,严格来说,就时序和抖动而言,Cortex-M0 具有更传统的中断时序配置文件(即当中断到达时它不会中止内核上的指令),受到更多抖动和延迟的影响. 在这个问题上(再次,严格计时),它与旧模型(即 arm7tdmi)更具可比性,后者也缺乏原子加载/修改/存储以及原子增量和减量以及其他低延迟协作指令,需要中断禁用/更频繁地启用。
我在 Cortex-M3 中使用这样的东西:
#define unlikely(x) __builtin_expect((long)(x),0)
static inline int atomic_LL(volatile void *addr) {
int dest;
__asm__ __volatile__("ldrex %0, [%1]" : "=r" (dest) : "r" (addr));
return dest;
}
static inline int atomic_SC(volatile void *addr, int32_t value) {
int dest;
__asm__ __volatile__("strex %0, %2, [%1]" :
"=&r" (dest) : "r" (addr), "r" (value) : "memory");
return dest;
}
/**
* atomic Compare And Swap
* @param addr Address
* @param expected Expected value in *addr
* @param store Value to be stored, if (*addr == expected).
* @return 0 ok, 1 failure.
*/
static inline int atomic_CAS(volatile void *addr, int32_t expected,
int32_t store) {
int ret;
do {
if (unlikely(atomic_LL(addr) != expected))
return 1;
} while (unlikely((ret = atomic_SC(addr, store))));
return ret;
}
换句话说,它将 ldrex/strex 带入众所周知的 Linked Load and Store Conditional,并且它还实现了比较和交换语义。
如果您的代码仅使用比较和交换就可以正常工作,您可以像这样为 cortex-m0 实现它:
static inline int atomic_CAS(volatile void *addr, int32_t expected,
int32_t store) {
int ret = 1;
__interrupt_disable();
if (*(volatile uint32_t *)addr) == expected) {
*addr = store;
ret = 0;
}
__interrupt_enable();
return ret;
}
这是最常用的模式,因为某些架构最初只有它(想到 x86)。
从我的立场来看,用 CAS 模拟 LL/SC 模式似乎很难看。特别是当 SC 除了 LL 之外还有多条指令时,虽然很常见,但 ARM 并不特别推荐在 Cortex-M3 的情况下使用它,因为任何中断都会使 strex 失败,如果你开始在 ldrex 之间花费太长时间/strex 您的代码将花费大量时间在循环中重试 strex,这可能被解释为滥用模式并违背其自身目的。
至于您的附带问题,在 cortex-m3 的情况下, strex 在寄存器中返回,因为语义已经由更高级别的体系结构定义(strex/ldrex 存在于定义 armv7-m 之前实现的多核臂中,并且之后,缓存控制器实际检查 ldrex/strex 地址,即 strex 仅在缓存控制器无法证明加载/存储所触及的数据线未修改时才会失败)。
如果我推测,我会说原始设计具有这种语义,因为在早期,这种原子是在库中设计的:你会在汇编器中实现的函数中返回成功/失败,这需要尊重 ABI他们中的大多数(我所知道的)使用寄存器或堆栈,而不是标志来返回值。
此外,编译器在使用寄存器着色方面比在其他指令使用它时破坏标志更好,即考虑一个生成标志的复杂操作,在它的中间你有一个 ldrex/strex 序列,以及随后的操作需要标志:编译器必须将标志移动到寄存器,无论如何都需要额外的指令。