C99 "atomic" 在裸机可移植库中加载

C99 "atomic" load in baremetal portable library

我正在为裸机嵌入式应用程序开发一个可移植的库。

假设我有一个定时器 ISR,它递增一个计数器,并且在主循环中,这个计数器读取来自一个肯定不是原子负载。

我正在努力确保加载一致性(即我不会因为加载中断和值更改而读取垃圾)而不求助于禁用中断。只要读取的值是正确的,读取计数器后值是否改变都没有关系。这样做有用吗?

uint32_t read(volatile uint32_t *var){
    uint32_t value;
    do { value = *var; } while(value != *var);
    return value;
}

您是否运行正在任何 uint32_t 大于单个汇编指令字 read/write 大小的系统上?如果不是,内存的 IO 应该是单个指令,因此是原子的(假设总线也是字大小的......)当编译器将它分解成多个较小的 read/writes 时,你会遇到麻烦。否则,我总是不得不求助于 DI/EI。您可以让用户配置您的库,以便它具有原子指令或最小 32 位字大小是否可用的信息,以防止中断旋转。如果你有这些保证,就不需要验证码了。

虽然要回答这个问题,但在必须拆分 read/write 的系统上,您的代码并不安全。想象这样一种情况,您在 "do" 部分正确读取了您的值,但在 "while" 部分检查期间该值被拆分。此外,在极端情况下,这是一个无限循环。为了完全安全,您需要重试计数和错误条件来防止这种情况发生。循环案例肯定是极端的,但我想要它以防万一。这当然会使 运行 时间更长。

让我们展示一个失败案例作为示例 - 将在一次读取 8 位值的机器上使用 16 位数字,以便于理解:

  1. 从内存中读取的值 *var 是 0x1234
  2. 读取8位0x12
  3. *var 变为 0x5678
  4. 读取 8 位 0x78 - 现在的值为 0x1278(无效)
  5. *var 变为 0x1234
  6. 验证步骤读取 8 位 0x12
  7. *var 变为 0x5678
  8. 验证读取8位0x78

值确认正确 0x1278,但这是一个错误,因为 *var 只有 0x1234 和 0x5678。

另一个失败案例是当 *var 恰好以与您的代码 运行ning 相同的频率更改时,这可能会导致每次验证失败时的无限循环。或者即使它最终爆发了,这也将是一个很难跟踪的性能错误。

极不可能有任何一种可移植的解决方案来解决这个问题,尤其是因为许多纯 C 平台实际上是纯 C 并且使用一次性编译器,即没有主流和符合现代标准的东西gcc 或 clang。因此,如果您真正以根深蒂固的 C 为目标,那么它都是特定于平台的并且不可移植 - 以至于 "C99" 支持是失败的原因。对于可移植的 C 代码,您可以期待的最好的是 ANSI C 支持——指的是 ANSI 发布的第一个非草案 C 标准。不幸的是,这仍然是主要供应商逃避的共同点。我的意思是:Zilog 以某种方式侥幸逃脱,即使他们现在只是 Littelfuse 的一个部门,Littelfuse 以前是 Littelfuse 收购的 IXYS Semiconductor 的一个部门。

例如,这里有一些编译器,其中只有一种特定于平台的方式:

  • Zilog eZ8 使用 "recent" Zilog C 编译器(任何 20 岁或以下的都可以):8 位值读取-修改-写入是原子的。编译器生成字对齐字指令(如 LDWXINCWDECW)的 16 位操作也是原子操作。如果 read-modify-write 适合 3 条或更少的指令,您将在操作前添加 asm("\tATM");。否则,您需要禁用中断:asm("\tPUSHF\n\tDI");,然后重新启用它们:asm("\tPOPF");.

  • Zilog ZNEO 是一个 16 位平台,带有 32 位寄存器,对寄存器的读-修改-写访问是原子的,但是内存读-修改-写往返通过寄存器,通常, 并采用 3 条指令 - 因此在 R-M-W 操作前加上 asm("\tATM").

  • Zilog Z80 和 eZ80 需要将代码包装在 asm("\tDI")asm("\tEI") 中,尽管这仅在知道代码运行时始终启用中断时才有效。如果它们可能未启用,则存在问题,因为 Z80 不允许读取 IFF1 的状态 - 中断启用触发器。因此,您需要在某处保存其状态的 "shadow",并使用该值有条件地启用中断。不幸的是,eZ80 不提供允许访问 IEF1 的中断控制器寄存器(eZ80 使用 IEFn 命名法而不是 IFFn)——所以这种架构监督是从古老的 Z80 继承下来的到 "modern" 一个。

这些不一定是最流行的平台,而且许多人不会为 Zilog 编译器烦恼,因为它们的质量相当差(低到你真的不得不编写一个 eZ8-targeting 编译器*)。然而,这些奇怪的角落是纯​​ C 代码库的支柱,库代码别无选择,只能适应这一点,如果不是直接,那么至少通过提供可以用特定于平台的魔法重新定义的宏。

例如您可以提供默认为空的宏 MYLIB_BEGIN_ATOMIC(vector)MYLIB_END_ATOMIC(vector),它们将用于包装需要相对于给定中断向量进行原子访问的代码(或者例如 -1 如果相对于所有中断向量)。当然,将 MYLIB_ 替换为特定于您的图书馆的 "namespace" 前缀。

要在 "modern" Zilog 平台上启用特定于平台的优化,例如 ATM vs DI,可以向宏提供一个额外的参数来分隔假定的 "short" 编译器倾向于为更长的序列生成三指令序列的序列。这种微优化通常需要汇编输出审计(易于自动化)来验证指令序列长度的假设,但至少驱动决策的数据是可用的,用户可以选择使用它或忽略它.


*如果一些迷失的灵魂想知道任何接近奥术的东西。 eZ8 - 问吧。我对那个平台了解得太多了,细节如此血腥,以至于即使是现代好莱坞 CG 和 SFX 也很难在屏幕上重现体验的真实深度。我也可能是那里唯一的 运行 20MHz eZ8 部件偶尔以 48MHz 时钟运行 - 这肯定是多元宇宙允许的恶魔附身的标志。如果你认为这种堕落的行为被用于生产硬件是令人发指的——我同意你的看法。 las,商业案例就是商业案例,该死的物理定律。