【问题标题】:C99 "atomic" load in baremetal portable library裸机可移植库中的 C99“原子”加载
【发布时间】:2020-06-16 15:46:07
【问题描述】:

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

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

我正在尝试确保负载一致性(即,我没有读取垃圾,因为负载被中断并且值发生了变化)而不使用禁用中断。只要读取的值正确,读取计数器后值是否改变都没有关系。这能解决问题吗?

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

【问题讨论】:

  • 为了原子性,最好有一个锁,例如一个全局标志,当设置时读取操作将循环直到未设置。锁由中断处理程序设置
  • @NikosM。这不符合图书馆的目的。基本上,我正在尝试在普通 C99 中实现条件变量。
  • 我认为这行不通。条件不是也加载值以检查它吗?意味着它会受到实际负载的任何问题的影响?
  • @ChrisRollins 这个想法是,如果我可以连续两次读取相同的值,那是因为加载没有中断(或者它被中断但它的值没有改变)
  • 基本上我认为尝试以完全可移植的方式做到这一点是没有希望的;人们总是可以想象一些奇怪的架构,它会失败。

标签: c atomic c99 isr


【解决方案1】:

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

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

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

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

  • Zilog Z80 和 eZ80 需要将代码包装在 asm("\tDI") 和 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 平台上启用特定于平台的优化,例如 ATM 与 DI,可以向宏提供一个附加参数,以分隔编译器易于生成的三个假定的“短”序列- 指令序列与更长的指令序列。这种微优化通常需要汇编输出审计(很容易自动化)来验证指令序列长度的假设,但至少驱动决策的数据是可用的,用户可以选择使用它还是忽略它.


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

【讨论】:

  • 感谢您的澄清。可移植是指可以由符合标准的编译器构建的库,而不是可以在每个平台上构建的库。我正在努力抽象出特定于平台的细节,因为我知道它们确实无法避免。我想我将原子实现为一组宏,并将实际实现留给客户端。
  • @AndréMedeiros 有道理 - 这是唯一的方法 :(
【解决方案2】:

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

不过,要回答这个问题,在必须拆分读/写的系统上,您的代码是不安全的。想象一下,您在“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 恰好以与您的代码运行相同的频率发生变化,这可能导致每次验证失败时出现无限循环。或者即使它最终确实爆发了,这将是一个非常难以跟踪的性能错误。

【讨论】:

  • 我看不出你提到的问题。如果在do 部分中正确读取了该值,则不会发生任何不好的事情。如果while 中的测试无论出于何种原因返回true,那么您返回一个假设是正确的值。如果它返回 false,无论出于何种原因,您只需再试一次,不会造成任何伤害。
  • 如果 ISR 真的只是将 *var 增加 1,那么从某种意义上说,结果 0x1278 并没有真正错误,因为 *var 在执行我们的 @ 期间的某个时刻确实等于 0x1278 987654326@ 功能。我试着想出一个不是这种情况的例子,但我做不到。
  • 这正是我无法弄清楚的极端案例类型,这绝对是合理的。计时器只是一个例子,但这种情况实际上发生在我的库中的其他地方,其中顺序递增的假设可能不正确。谢谢。
猜你喜欢
  • 2010-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-18
相关资源
最近更新 更多