如何在 ARM Cortex A9 上实现临界区

电器工程 C 嵌入式 中断
2022-01-20 17:18:33

我正在将一些遗留代码从 ARM926 内核移植到 CortexA9。此代码是裸机,不包括操作系统或标准库,全是自定义的。我遇到的故障似乎与应该通过代码的关键部分防止的竞争条件有关。

我想要一些关于我的方法的反馈,看看我的关键部分是否可能没有为这个 CPU 正确实现。我正在使用 GCC。我怀疑有一些微妙的错误。

此外,是否有一个开源库具有这些类型的 ARM 原语(甚至是一个很好的轻量级自旋锁/信号量库)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

代码使用如下:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

“关键”的想法是允许嵌套临界区,这些临界区用于函数的开头和结尾以创建可重入函数。

谢谢!

4个回答

在没有操作系统的情况下处理关键部分最困难的部分实际上并不是创建互斥锁,而是弄清楚如果代码想要使用当前不可用的资源会发生什么。load-exclusive 和 conditional-store-exclusive 指令使得创建一个“swap”函数变得相当容易,给定一个指向整数的指针,它将原子地存储一个新值,但返回指向的整数包含的内容:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

给定一个像上面这样的函数,一个人可以很容易地通过类似的东西进入一个互斥锁

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

在没有操作系统的情况下,主要困难通常在于“无法获得互斥锁”代码。如果在受互斥体保护的资源繁忙时发生中断,则可能需要让中断处理代码设置一个标志并保存一些信息以指示它想要做什么,然后让任何类似 main 的代码获取互斥锁在要释放互斥锁时检查,以查看在持有互斥锁时中断是否想做某事,如果是,则代表中断执行操作。

尽管通过简单地禁用中断(实际上,禁用中断可以消除​​对任何其他类型的互斥体的需要)可以避免中断想要使用互斥保护资源的问题,但通常最好避免禁用中断超过必要的时间。

一个有用的折衷办法是使用如上所述的标志,但要释放互斥锁禁用中断的主线代码并在这样做之前检查上述标志(释放互斥锁后重新启用中断)。这种方法不需要让中断禁用很长时间,但可以防止如果主线代码在释放互斥锁后测试中断标志,则在它看到标志的时间和它看到标志的时间之间存在危险。作用于它,它可能会被其他获取和释放互斥体并作用于中断标志的代码抢占;如果主线代码在释放互斥锁后没有测试中断标志,

在任何情况下,最重要的是有一种方法,当资源不可用时,尝试使用互斥保护资源的代码将有一种方法,一旦资源被释放,就可以重复其尝试。

这是一种处理关键部分的笨拙方式;禁用中断。如果您的系统有/处理数据故障,它可能无法正常工作。它还会增加中断延迟。Linux irqflags.h有一些处理这个的宏指令可能有用cpsiecpsid但是,它们不保存状态并且不允许嵌套。 cps不使用寄存器。

对于Cortex-A系列,ldrex/strex它们效率更高,可以为临界区形成互斥体,或者它们可以与无锁算法一起使用以摆脱临界区。

从某种意义上说,这ldrex/strex似乎是一个 ARMv5 swp然而,它们在实践中实现起来要复杂得多。您需要一个工作缓存,并且需要在缓存中的目标内存ldrex/strexARM 文档ldrex/strex相当模糊,因为他们希望机制在非 Cortex-A CPU 上工作。然而,对于 Cortex-A,保持本地 CPU 缓存与其他 CPU 同步的机制与用于实现ldrex/strex指令的机制相同。对于 Cortex-A 系列,保留粒度ldrex/strex保留内存的大小)与高速缓存行相同;如果您打算修改多个值,例如双向链表,您还需要将内存与缓存行对齐。

我怀疑有一些微妙的错误。

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

您需要确保序列永远不会被抢占否则,您可能会得到两个启用中断的关键变量,并且锁定释放将不正确。您可以将swp指令与密钥内存一起使用以确保 ARMv5 上的一致性,但该指令在 Cortex-A 上已被弃用,ldrex/strex因为它更适用于多 CPU 系统。

所有这些都取决于您的系统有什么样的调度。听起来你只有主线和中断。您经常需要关键部分原语与调度程序有一些挂钩,具体取决于您希望关键部分使用的级别(系统/用户空间/等)。

此外,是否有一个开源库具有这些类型的 ARM 原语(甚至是一个很好的轻量级自旋锁/信号量库)?

这很难以可移植的方式编写。即,此类库可能存在于某些版本的 ARM CPU 和特定操作系统中。

我看到这些关键部分存在几个潜在问题。所有这些都有一些警告和解决方案,但作为总结:

  • 出于优化或随机其他原因,没有什么可以阻止编译器在这些宏之间移动代码。
  • 它们保存和恢复编译器期望内联汇编保持不变的处理器状态的某些部分(除非另有说明)。
  • 没有什么可以阻止在序列中间发生中断并在读取和写入之间更改状态。

首先,您肯定需要一些编译器内存屏障GCC 将这些实现为clobbers基本上,这是一种告诉编译器“不,你不能在这段内联汇编中移动内存访问,因为它可能会影响内存访问的结果”的一种方式。具体来说,在 begin 和 end 宏上都需要"memory""cc"clobbers。这些也将防止其他事物(如函数调用)相对于内联程序集被重新排序,因为编译器知道它们可能具有内存访问权限。我已经看到用于 ARM 的 GCC 在带有"memory"clobbers 的内联汇编中的条件代码寄存器中保持状态,所以你肯定需要"cc"clobber。

其次,这些关键部分的保存和恢复不仅仅是中断是否启用。具体来说,他们正在保存和恢复大部分CPSR(当前程序状态寄存器)(链接适用于 Cortex-R4,因为我找不到 A9 的漂亮图表,但它应该是相同的)。实际可以修改哪些状态有一些微妙的限制,但在这里它是不必要的。

除其他外,这包括条件代码(其中cmp存储了类似指令的结果,以便后续条件指令可以对结果进行操作)。编译器肯定会对此感到困惑。"cc"使用上面提到的clobber很容易解决这个问题。但是,这会使代码每次都失败,因此听起来不像您看到的问题。虽然有点像定时炸弹,因为修改随机其他代码可能会导致编译器做一些不同的事情,这将被打破。

这还将尝试保存/恢复用于实现 Thumb 条件执行的 IT 位。请注意,如果您从不执行 Thumb 代码,这无关紧要。我从来没有弄清楚 GCC 的内联汇编是如何处理 IT 位的,除了得出的结论之外,这意味着编译器决不能将内联汇编放在 IT 块中,并且总是期望汇编在 IT 块之外结束。我从未见过 GCC 生成违反这些假设的代码,而且我已经做了一些相当复杂的内联汇编并进行了大量优化,所以我有理由相信它们是成立的。这意味着它可能实际上不会尝试更改 IT 位,在这种情况下一切都很好。尝试修改这些位被归类为“架构不可预测”,所以它可以做各种坏事,但可能根本不会做任何事情。

最后一类将被保存/恢复的位(除了实际禁用中断的位)是模式位。这些可能不会改变,因此可能无关紧要,但是如果您有任何故意更改模式的代码,这些中断部分可能会导致问题。在特权模式和用户模式之间切换是我期望的唯一情况。

第三,没有什么能阻止中断在MRSMSRin之间改变 CPSR 的其他部分ARM_INT_LOCK任何此类更改都可能被覆盖。在大多数合理的系统中,异步中断不会改变它们被中断的代码的状态(包括 CPSR)。如果他们这样做了,就很难推断代码会做什么。但是,这是可能的(在我看来,更改 FIQ 禁用位最有可能),因此您应该考虑您的系统是否这样做。

以下是我将如何以解决我指出的所有潜在问题的方式实现这些:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

确保使用编译,-mcpu=cortex-a9因为至少某些 GCC 版本(如我的)默认使用不支持cpsiecpsid.

我使用了 inands而不是andinARM_INT_LOCK所以如果在 Thumb 代码中使用它是一个 16 位指令。无论如何,"cc"clobber 都是必要的,所以它严格来说是性能/代码大小的好处。

01本地标签,供参考。

这些应该以与您的版本相同的方式使用。ARM_INT_LOCK和你原来的一样快/小。不幸的是,我无法想出一种方法来ARM_INT_UNLOCK在几乎没有指示的任何地方安全地进行操作。

如果您的系统对何时禁用 IRQ 和 FIQ 有限制,这可以简化。例如,如果它们总是一起禁用,您可以像这样组合成一个cbz+ cpsie if

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

或者,如果您根本不关心 FIQ,那么它类似于完全放弃启用/禁用它们。

如果您知道在锁定和解锁之间没有其他任何东西会更改 CPSR 中的任何其他状态位,那么您也可以使用 continue 与您的原始代码非常相似的东西,除了两者都使用"memory""cc"clobbersARM_INT_LOCKARM_INT_UNLOCK