读取由 ISR 更新的 64 位变量

Mar*_*ryK 5 c embedded atomic interrupt

我没有找到太多关于非原子操作的材料。

\n

假设我有一个 32 位处理器,并且我想在 64 位变量中保留微秒计数。中断将每微秒更新一次变量。调度程序是非抢占式的。将有一个函数用于清除变量,另一个函数用于读取变量。由于它是 32 位处理器,因此访问将是非原子的。是否有 \xe2\x80\x9cstandard\xe2\x80\x9d 或惯用的处理方法,以便读取器函数不会获得半更新的值?

\n

Gab*_*les 5

\n

是否有 \xe2\x80\x9cstandard\xe2\x80\x9d 或惯用的处理方法,以便读取器函数不会获得半更新的值?

\n
\n

您需要做的是使用我所说的“原子访问防护”“中断防护”。这是我感兴趣的一个领域,我花了很多时间来学习和使用各种类型的微控制器。

\n

@chux - 恢复莫妮卡,是正确的,但这里有一些我想补充说明的内容:

\n

读取易失性变量,请进行复制以便快速读取:

\n

通过快速复制变量,然后在计算中使用该副本,最大限度地减少中断关闭的时间:

\n
// ==========\n// Do this:\n// ==========\n\n// global volatile variables for use in ISRs\nvolatile uint64_t u1;\nvolatile uint64_t u2;\nvolatile uint64_t u3;\n\nint main()\n{\n    // main loop\n    while (true)\n    {\n        uint64_t u1_copy;\n        uint64_t u2_copy;\n        uint64_t u3_copy;\n\n        // use atomic access guards to copy out the volatile variables\n        // 1. Save the current interrupt state\n        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;\n        // 2. Turn interrupts off\n        interrupts_off();\n        // copy your volatile variables out\n        u1_copy = u1;\n        u2_copy = u2;\n        u3_copy = u3;\n        // 3. Restore the interrupt state to what it was before disabling it.\n        // This leaves interrupts disabled if they were previously disabled\n        // (ex: inside an ISR where interrupts get disabled by default as it\n        // enters--not all ISRs are this way, but many are, depending on your\n        // device), and it re-enables interrupts if they were previously\n        // enabled. Restoring interrupt state rather than enabling interrupts\n        // is the right way to do it, and it enables this atomic access guard\n        // style to be used both inside inside **and** outside ISRs.\n        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;\n\n        // Now use your copied variables in any calculations\n    }\n}\n\n// ==========\n// NOT this!\n// ==========\n\nvolatile uint64_t u1;\nvolatile uint64_t u2;\nvolatile uint64_t u3;\n\nint main()\n{\n    // main loop\n    while (true)\n    {\n        // 1. Save the current interrupt state\n        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;\n        // 2. Turn interrupts off\n        interrupts_off();\n\n        // Now use your volatile variables in any long calculations\n        // - This is not as good as using copies! This would leave interrupts\n        //   off for an unnecessarily long time, introducing a ton of jitter\n        //   into your measurements and code.\n\n        // 3. Restore the interrupt state to what it was before disabling it.\n        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;\n\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

写入易失性变量,请快速写入:

\n

通过在更新易失性变量时快速禁用中断,最大限度地减少中断关闭的时间:

\n
// global volatile variables for use in ISRs\nvolatile uint64_t u1;\nvolatile uint64_t u2;\nvolatile uint64_t u3;\n\nint main()\n{\n    // main loop\n    while (true)\n    {\n        // Do calculations here, **outside** the atomic access interrupt guards\n\n        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;\n        interrupts_off();\n        // quickly update your variables and exit the guards\n        u1 = 1234;\n        u2 = 2345;\n        u3 = 3456;\n        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

替代方案:通过重复读取循环进行无锁原子读取doAtomicRead()::确保原子读取而不关闭中断!

\n

如上所示,使用原子访问防护的另一种方法是重复读取变量,直到它不再更改,这表明在仅读取变量的某些字节后,变量在读取过程中未更新。

\n

请注意,这适用于任何大小的内存块。uint64_t下面的示例中使用的类型甚至可以是数十struct my_struct或数百字节。它不限于任何尺寸。doAtomicRead()仍然有效。

\n

这是这种方法。@Brendan 和 @chux-ReinstateMonica 以及我在@chux-ReinstateMonica 的回答下讨论了一些想法。

\n
#include <stdint.h>  // UINT64_MAX\n\n#define MAX_NUM_ATOMIC_READ_ATTEMPTS 3\n\n// errors\n#define ATOMIC_READ_FAILED (UINT64_MAX)\n\n/// @brief          Use a repeat-read loop to do atomic-access reads of a \n///     volatile variable, rather than using atomic access guards which\n///     disable interrupts.\nuint64_t doAtomicRead(const volatile uint64_t* val)\n{\n    uint64_t val_copy;\n    uint64_t val_copy_atomic = ATOMIC_READ_FAILED;\n    \n    for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)\n    {\n        val_copy = *val; \n        if (val_copy == *val)\n        {\n            val_copy_atomic = val_copy;\n            break;\n        }\n    }\n\n    return val_copy_atomic;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果您想更深入地了解,这里doAtomicRead()又是相同的函数,但这次有大量的解释性注释。我还展示了一个注释掉的细微变化,这在某些情况下可能会有所帮助,如评论中所述。

\n
/// @brief          Use a repeat-read loop to do atomic-access reads of a \n///     volatile variable, rather than using atomic access guards which\n///     disable interrupts.\n///\n/// @param[in]      val             Ptr to a volatile variable which is updated\n///                                 by an ISR and needs to be read atomically.\n/// @return         A copy of an atomic read of the passed-in variable, \n///     if successful, or sentinel value ATOMIC_READ_FAILED if the max number\n///     of attempts to do the atomic read was exceeded.\nuint64_t doAtomicRead(const volatile uint64_t* val)\n{\n    uint64_t val_copy;\n    uint64_t val_copy_atomic = ATOMIC_READ_FAILED;\n    \n    // In case we get interrupted during this code block, and `val` gets updated\n    // in that interrupt\'s ISR, try `MAX_NUM_ATOMIC_READ_ATTEMPTS` times to get\n    // an atomic read of `val`.\n    for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)\n    {\n        val_copy = *val; \n\n        // An interrupt could have fired mid-read while doing the **non-atomic**\n        // read above, updating the 64-bit value in the ISR and resulting in\n        // 32-bits of the old value in the 64-bit variable being wrong now\n        // (since the whole 64-bit value has just been updated with a new\n        // value), so verify the read above with a new read again.\n        // \n        // Caveat: \n        //\n        // Note that this method is **not _always_** foolproof, as technically\n        // the interrupt could fire off and run again during this 2nd read,\n        // causing a very rare edge-case where the exact same incorrect value\n        // gets read again, resulting in a false positive where it assigns an\n        // erroneous value to `val_copy_atomic`! HOWEVER, that is for **you or\n        // I** to design and decide as the architect. \n        //\n        // Is it _possible_ for the ISR to really fire off again immediately\n        // after returning? Or, would that never happen because we are\n        // guaranteed some minimum time gap between interrupts? If the former,\n        // you should read the variable again a 3rd or 4th time by uncommenting\n        // the extra code block below in order to check for consistency and\n        // minimize the chance of an erroneous `val_copy_atomic` value. If the\n        // latter, however, and you know the ISR won\'t fire off again for at\n        // least some minimum time value which is large enough for this 2nd\n        // read to occur **first**, **before** the ISR gets run for the 2nd\n        // time, then you can safely say that this 2nd read is sufficient, and\n        // you are done.\n        if (val_copy == *val)\n        {\n            val_copy_atomic = val_copy;\n            break;\n        }\n\n        // Optionally delete the "if" statement just above and do this instead.\n        // Refer to the long "caveat" note above to see if this might be\n        // necessary. It is only necessary if your ISR might fire back-to-back\n        // with essentially zero time delay between each interrupt.\n        // for (size_t j = 0; j < 4; j++)\n        // {\n        //     if (val_copy == *val)\n        //     {\n        //         val_copy_atomic = val_copy;\n        //         break;\n        //     }\n        // }\n    }\n\n    return val_copy_atomic;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

通过在循环开始之前添加一个额外的读取,并在循环中*val 仅读取一次,可以优化上述内容,以便每次迭代仅获得一次新读数,而不是两次,如下所示:

\n

[这是我最喜欢的版本:]

\n
uint64_t doAtomicRead(const volatile uint64_t* val)\n{\n    uint64_t val_copy_new;\n    uint64_t val_copy_old = *val;\n    uint64_t val_copy_atomic = ATOMIC_READ_FAILED;\n    \n    for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)\n    {\n        val_copy_new = *val; \n        if (val_copy_new == val_copy_old)\n        {\n            // no change in the new reading, so we can assume the read was not \n            // interrupted during the first reading\n            val_copy_atomic = val_copy_new;\n            break;\n        }\n        // update the old reading, to compare it with the new reading in the\n        // next iteration\n        val_copy_old = val_copy_new;  \n    }\n\n    return val_copy_atomic;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

一般用法 示例doAtomicRead()

\n
// global volatile variable shared between ISRs and main code\nvolatile uint64_t u1;\n\n// Inside your function: "atomically" read and copy the volatile variable\nuint64_t u1_copy = doAtomicRead(&u1);\nif (u1_copy == ATOMIC_READ_FAILED)\n{\n    printf("Failed to atomically read variable `u1`.\\n");\n\n    // Now do whatever is appropriate for error handling; examples: \n    goto done;\n    // OR:\n    return;\n    // etc.\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这要求写入器对于任何读取器都是原子的,例如,在单个写入器写入此变量的情况下,这是正确的。例如,此写入可能发生在 ISR 内部。我们仅检测到撕裂的读取(由于读取器被中断)并重试。如果当该读取器运行时 64 位值在内存中已经处于撕裂写入状态,则读取器可能会错误地将其视为有效。

\n

SeqLock没有这个限制,因此对于多核情况很有用但是,如果您不需要它(例如:您有一个单核微控制器),它的效率可能会较低,并且该doAtomicRead()技巧效果很好。

\n

对于单调递增计数器的特殊边缘情况(不适用于可以用任何值更新的变量,例如存储传感器读数的变量!),正如 Brendan 在这里建议的那样,您只需要重新读取最重要的值64 位值的一半并检查它是否没有改变。因此,为了(可能)稍微提高上述函数的效率doAtomicRead(),请对其进行更新。唯一可能的撕裂(除非您错过 2^32 计数)是当低半部分换行且高半部分递增时。这就像检查整个事情,但重试的频率会更低。

\n

doAtomicRead()在 FreeRTOS 中使用

\n
    \n
  1. 单个 FreeRTOS 任务从所有传感器收集传感器数据(通过 SPI、I2C、串行 UART、ADC 读数等)。它写入volatile封装在模块中的共享全局变量,因此它们感觉不像全局变量。这是一种“共享内存”多线程模型。这些变量不需要是原子的。为所有变量提供了 setter 和 getter。只有这一项任务可以写入变量。它必须比读者任务具有更高的优先级才能正常工作。即:通过成为比消费者更高优先级的任务,它模拟处于受保护的 ISR 中,并且能够相对于读者/消费者以原子方式写入
  2. \n
  3. 较低优先级的读取器任务可以读取数据。他们使用我的无锁doAtomicRead()功能,如上所述。请注意,这适用于任何大小的内存块。我的示例中使用的类型uint64_t甚至可能是数十或数百字节的结构。它不限于任何尺寸。
  4. \n
  5. 就是这样!无论如何,不​​需要信号量、锁或互斥体。一项任务“产生”数据。所有其他任务都会消耗它,无锁。这是单一生产者、多消费者模型。
  6. \n
  7. 它会丢弃数据,这对于大多数传感器数据来说是可以的。换句话说,生产者总是用最新数据覆盖值,而消费者只获取最新数据。如果需要执行任何移动平均值或过滤器而不丢失过滤器输入上的数据,则应由生产者任务在将数据存储到共享内存以供消费者使用之前执行此过滤。
  8. \n
\n

进一步讨论原子访问防护禁用中断等主题。

\n
    \n
  1. 我的c/containers_ring_buffer_FIFO_GREAT.c演示来自我的eRCaGuy_hello_world存储库。此代码示例的描述来自我在此文件顶部的注释:

    \n
    \n

    用 C 语言(也在 C++ 中运行)演示基本、高效的无锁SPSC(单生产者单消费者)环形缓冲区 FIFO\n队列。

    \n

    该队列仅在 SPSC 上下文中无锁工作,例如在 ISR 需要将数据发送到主循环的裸机微控制器上。

    \n
    \n
  2. \n
  3. [Peter Cordes 关于SeqLock(“序列锁”)模式的回答]用 32 位原子实现 64 位原子计数器

    \n
  4. \n
  5. [我的答案] C++ 递减单字节(易失性)数组的元素不是原子的!为什么?(另外:如何在 Atmel AVR mcus/Arduino 中强制原子性)

    \n
  6. \n
  7. 哪些 Arduinos 支持 ATOMIC_BLOCK? 的长而详细的回答 和:

    \n
      \n
    1. ATOMIC_BLOCK 宏是如何使用 gcc 编译器在 C 中实现的,在哪里可以看到它们的源代码?以及
    2. \n
    3. 如何用 C++(而不是 avrlibc\ 的 gcc C 版本)在 Arduino 中实现 ATOMIC_BLOCK 功能?
    4. \n
    5. 我详细解释了这个非常聪明的原子访问保护宏如何通过 gcc 扩展在 C 中工作,以及如何轻松地在 C++ 中实现它:\n
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE)\n{\n    my_var_copy = my_var;\n}\n
      Run Code Online (Sandbox Code Playgroud)\n
    6. \n
    \n
  8. \n
  9. [我的问答] STM32 微控制器上哪些变量类型/大小是原子的?

    \n
      \n
    1. 并非所有变量都需要原子访问保护来进行简单的读取和写入(对于增量/减量,它们总是这样做!--请参阅上面列表中的第一个链接!),因为某些变量对于给定的体系结构自然具有原子读取和写入。\n
        \n
      1. 对于 8 位 AVR 微控制器(如 Arduino Uno 上的 ATmega328):8 位变量具有天然的原子读写能力
      2. \n
      3. 对于 32 位 STM32 微控制器,所有 32 位及以下的非结构(简单)类型都具有天然的原子读写功能有关详细信息以及源文档和证明,请参阅我上面的答案。
      4. \n
      \n
    2. \n
    \n
  10. \n
  11. 在STM32 mcus上禁用中断的技术:https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/

    \n
  12. \n
  13. [我的答案]全局易失性变量未在 ISR 中更新:如何使用原子访问防护在 Arduino 中识别和修复竞争条件:

    \n
  14. \n
  15. [我的回答]在 STM32 微控制器中禁用和重新启用中断以实现原子访问防护的各种方法有哪些?

    \n
  16. \n
  17. https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

    \n
  18. \n
\n


chu*_*ica 3

在 ISR 内,通常会阻止后续中断(除非优先级更高,但通常不会触及那里的计数),因此很简单count_of_microseconds++;

在 ISR 之外,要访问(读或写),count_of_microseconds您需要中断保护或原子访问。

atomic不可用时*1但解释控制可用时:

volatile uint64_t count_of_microseconds;
...
saved_interrupt_state();
disable_interrupts();
uint64_t my_count64 = count_of_microseconds;
restore_interrupt_state();
// now use my_count64 
Run Code Online (Sandbox Code Playgroud)

否则使用

atomic_ullong count_of_microseconds;
...
unsigned long long my_count64 = count_of_microseconds;
// now use my_count64 
Run Code Online (Sandbox Code Playgroud)

请参阅如何在 C 中使用原子变量?

从 C89 开始,volatile与 一起使用count_of_microseconds


[更新]

无论非 ISR 代码中使用哪种方法(此答案或其他)来读/写计数器,我建议将读/写代码包含在辅助函数中以隔离这组关键操作。


*1 <stdatomic.h>自 C11 起可用__STDC_NO_ATOMICS__ 且未定义。

#if __STDC_VERSION__ >= 201112
#ifndef __STDC_NO_ATOMICS__
#include <stdatomic.h>
#endif
#endif
Run Code Online (Sandbox Code Playgroud)

  • @Brendan 这个想法的一个简单变体是阅读该项目两次。如果同样的话我们就完成了。否则读第三。如果第二个和第三个相同,我们就完成了,否则程序错误。我对无限循环持怀疑态度。我宁愿避免访问争用,因此提出了中断禁用的想法。 (3认同)
  • 还有第三种选择 - 一个循环读取高半部分,读取低半部分,然后再次读取高半部分;直到上半部分连续两次相同 - 例如,可能像 `new_high = counter[1]; 做{旧的高=新的高; 低=计数器[0];new_high = 计数器[1]; } while (old_high != new_high);`. (2认同)
  • @Brendan [循环](/sf/ask/5013687661/#comment126587664_71625379) 和 [3 个读取] (/sf/ask/5013687661/#comment126587730_71625379) 当对象缺少“易失性”时可能会失败。如果没有这个,对象的重新读取可能会被优化。 (2认同)