首页 > 解决方案 > 裸机可移植库中的 C99“原子”加载

问题描述

我正在为裸机嵌入式应用程序开发一个便携式库。

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

我试图确保负载的一致性(即我没有读取垃圾,因为负载被中断并且值发生了变化)而不诉诸于禁用中断。只要读取的值正确,读取计数器后值是否改变都没有关系。这行得通吗?

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

标签: catomicc99isr

解决方案


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

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

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

  • Zilog ZNEO 是具有 32 位寄存器的 16 位平台,对寄存器的读-修改-写访问是原子的,但内存读-修改-写通常通过寄存器进行往返,并且需要 3 条指令 - 因此预先添加 RMW操作asm("\tATM")

  • Zilog Z80 和 eZ80 需要将代码包装在asm("\tDI")and中asm("\tEI"),尽管这仅在知道代码运行时始终启用中断时才有效。如果它们可能没有被启用,那么就会出现问题,因为 Z80 不允许读取IFF1中断启用触发器的状态。因此,您需要在某处保存其状态的“影子”,并使用该值有条件地启用中断。不幸的是,eZ80 没有提供允许访问的中断控制器寄存器IEF1(eZ80 使用IEFn命名法而不是IFFn)——因此这种架构疏忽从古老的 Z80 延续到了“现代”。

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

例如,您可以提供默认为空的宏MYLIB_BEGIN_ATOMIC(vector)MYLIB_END_ATOMIC(vector)用于包装需要针对给定中断向量(或例如-1,如果针对所有中断向量)进行原子访问的代码。自然地,替换MYLIB_为特定于您的库的“命名空间”前缀。

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


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


推荐阅读