\n\n是否有 \xe2\x80\x9cstandard\xe2\x80\x9d 或惯用的处理方法,以便读取器函数不会获得半更新的值?
\n
您需要做的是使用我所说的“原子访问防护”或“中断防护”。这是我感兴趣的一个领域,我花了很多时间来学习和使用各种类型的微控制器。
\n@chux - 恢复莫妮卡,是正确的,但这里有一些我想补充说明的内容:
\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// 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)\ndoAtomicRead()
::确保原子读取而不关闭中断!如上所示,使用原子访问防护的另一种方法是重复读取变量,直到它不再更改,这表明在仅读取变量的某些字节后,变量在读取过程中未更新。
\n请注意,这适用于任何大小的内存块。uint64_t
下面的示例中使用的类型甚至可以是数十struct my_struct
或数百字节。它不限于任何尺寸。doAtomicRead()
仍然有效。
这是这种方法。@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()
又是相同的函数,但这次有大量的解释性注释。我还展示了一个注释掉的细微变化,这在某些情况下可能会有所帮助,如评论中所述。
/// @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
仅读取一次,可以优化上述内容,以便每次迭代仅获得一次新读数,而不是两次,如下所示:
[这是我最喜欢的版本:]
\nuint64_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()
:
// 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 位值在内存中已经处于撕裂写入状态,则读取器可能会错误地将其视为有效。
\nSeqLock没有这个限制,因此对于多核情况很有用。但是,如果您不需要它(例如:您有一个单核微控制器),它的效率可能会较低,并且该doAtomicRead()
技巧效果很好。
对于单调递增计数器的特殊边缘情况(不适用于可以用任何值更新的变量,例如存储传感器读数的变量!),正如 Brendan 在这里建议的那样,您只需要重新读取最重要的值64 位值的一半并检查它是否没有改变。因此,为了(可能)稍微提高上述函数的效率doAtomicRead()
,请对其进行更新。唯一可能的撕裂(除非您错过 2^32 计数)是当低半部分换行且高半部分递增时。这就像检查整个事情,但重试的频率会更低。
doAtomicRead()
在 FreeRTOS 中使用volatile
封装在模块中的共享全局变量,因此它们感觉不像全局变量。这是一种“共享内存”多线程模型。这些变量不需要是原子的。为所有变量提供了 setter 和 getter。只有这一项任务可以写入变量。它必须比读者任务具有更高的优先级才能正常工作。即:通过成为比消费者更高优先级的任务,它模拟处于受保护的 ISR 中,并且能够相对于读者/消费者以原子方式写入。doAtomicRead()
功能,如上所述。请注意,这适用于任何大小的内存块。我的示例中使用的类型uint64_t
甚至可能是数十或数百字节的结构。它不限于任何尺寸。我的c/containers_ring_buffer_FIFO_GREAT.c演示来自我的eRCaGuy_hello_world存储库。此代码示例的描述来自我在此文件顶部的注释:
\n\n\n用 C 语言(也在 C++ 中运行)演示基本、高效的无锁SPSC(单生产者单消费者)环形缓冲区 FIFO\n队列。
\n该队列仅在 SPSC 上下文中无锁工作,例如在 ISR 需要将数据发送到主循环的裸机微控制器上。
\n
[Peter Cordes 关于SeqLock(“序列锁”)模式的回答]用 32 位原子实现 64 位原子计数器
\n[我的答案] C++ 递减单字节(易失性)数组的元素不是原子的!为什么?(另外:如何在 Atmel AVR mcus/Arduino 中强制原子性)
\n我对哪些 Arduinos 支持 ATOMIC_BLOCK? 的长而详细的回答 和:
\nATOMIC_BLOCK(ATOMIC_RESTORESTATE)\n{\n my_var_copy = my_var;\n}\n
Run Code Online (Sandbox Code Playgroud)\n[我的问答] STM32 微控制器上哪些变量类型/大小是原子的?
\n在STM32 mcus上禁用中断的技术:https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
\n在 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)
从 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)
归档时间: |
|
查看次数: |
1484 次 |
最近记录: |