C++递减单字节(易失性)数组的元素不是原子!为什么?(还有:如何在Atmel AVR mcus/Arduino中强制原

C++ decrementing an element of a single-byte (volatile) array is not atomic! WHY? (Also: how do I force atomicity in Atmel AVR mcus/Arduino)

本文关键字:Atmel 还有 mcus Arduino AVR 字节 易失性 单字节 数组 C++ 元素      更新时间:2023-10-16

我只是损失了几天,实际上,大约25个小时的工作,因为我试图在一些我不知道的简单事情上调试我的代码。

事实证明,在AVR ATmega328 8位微控制器(Arduino)上,在C++中递减单字节数组的元素不是原子操作,需要原子访问保护(即关闭中断)。为什么会这样???此外,确保Atmel AVR微控制器上变量的原子访问的所有C技术是什么?

以下是我所做的事情的简化版本:

//global vars:
const uint8_t NUM_INPUT_PORTS = 3;
volatile uint8_t numElementsInBuf[NUM_INPUT_PORTS];
ISR(PCINT0_vect) //external pin change interrupt service routine on input port 0
{
//do stuff here
for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
numElementsInBuf[i]++;
}
loop()
{
for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
{
//do stuff here
numElementsInBuf[i]--; //<--THIS CAUSES ERRORS!!!!! THE COUNTER GETS CORRUPTED.
}
}

这是一个很好的循环版本:

loop()
{
for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
{
//do stuff here
noInterrupts(); //globally disable interrupts 
numElementsInBuf[i]--; //now it's ok...30 hrs of debugging....
interrupts(); //globally re-enable interrupts 
}
}

注意";原子访问防护";,即:在递减之前禁用中断,然后在递减之后重新启用中断。

由于我在这里处理的是单个字节,我不知道我需要原子访问保护。为什么我需要它们来处理这个案子?这是典型的行为吗?我知道如果这是一个2字节值的数组,我会需要它们,但为什么要1字节值呢????通常情况下,对于1字节的值,这里不需要原子访问保护


更新:读取";原子接入";此处的部分:http://www.gammon.com.au/interrupts.这是一个很好的来源


相关(STM32 MCU的答案):

因此,我们知道读取或写入AVR 8位MCU上的任何单字节变量都是原子操作,但STM32 32位MCU呢哪些变量在STM32上具有自动原子读写功能答案就在这里:哪些变量类型/大小在STM32微控制器上是原子的?。

ATmega328数据表表明:

ALU支持寄存器之间或常量与寄存器之间的算术和逻辑运算

它没有提到ALU能够直接在内存位置上操作。因此,为了减少一个值,这意味着处理器必须执行几个操作:

  • 将值加载到寄存器中
  • 递减寄存器
  • 将价值存储回

因此,减量操作不是原子操作,除非您做了一些特殊的事情使其成为原子操作,例如禁用中断。对于更新内存,这种读/修改/写要求可能更常见。

如何使操作成为原子操作的细节取决于平台。较新版本的C和C++标准明确支持原子操作;我不知道ATmega的工具链是否支持这些更新的标准。

我对Arduino和中断了解不多,所以我可能不会在这里回答您的特定问题,但在多线程环境中,使用--++进行递减和递增从来都不是原子。此外,volatile在C++中也不是一般意义上的atomic(证明)。虽然我知道volatile在您对微控制器进行编程时是有意义的,但我怀疑我的答案可能不适用于您的情况。

如果用三个单独的volatile uint8_t替换一个volatile uint8_t阵列,它是否有效?

2023年5月10日更新:问题中的问题与我7年前在2016年编写的第一个环形缓冲区实现有关。我最后写了一个非常好的环形缓冲区实现,当在任何支持C11或C++11原子类型的系统上使用时,它都是无锁的。这是我写过的最好的实现,也是我见过的最好的。它解决了其他实现的许多问题。完整的详细信息在文件的顶部。它同时在CC++中运行。您可以在这里看到完整的实现:我的eRCaGuy_hello_world repo中的containers_ring_buffer_FIFO_GREAT.c。


好的;为什么递增/递减单字节变量不是原子变量"这里的Ishamael和这里的Michael Burr回答得很好。

本质上,在8位AVR mcu上,8位读取是原子的,8位写入是原子的增量和减量从来都不是原子,在这个体系结构上的多字节读写也不是原子!

既然我得到了--递减和++递增操作从来都不是原子操作,即使是在字节值上执行(请参阅上面的答案和此处的Nick Gammon链接),我想确保我如何在Atmel AVR微控制器上强制原子性的后续问题也得到了回答,因此这个问题成为了一个很好的资源。

以下是我所知道的在Atmel AVR微控制器中强制原子化的所有技术,如Arduino:

  1. 选项1(首选方法):

    uint8_t SREG_bak = SREG; // save global interrupt state
    noInterrupts();          // disable interrupts (for Arduino only; this is 
    // an alias of AVR's "cli()")
    // your atomic variable-access code goes here
    SREG = SREG_bak;         // restore interrupt state
    
  2. 选项2(不太安全,不推荐使用的方法,因为如果您在ISR内部调用的代码块或库中意外使用这种方法,可能会导致您无意中启用嵌套中断):

    Arduino在CCD_ 10中提供的宏;arduino1.8.13/hardware/arduino/avr/cores/arduino/arduino.h";,例如:

    noInterrupts();  // disable interrupts (Arduino only; this is an alias to 
    // AVR's "cli()")
    // your atomic variable-access code goes here
    interrupts();    // enable interrupts (Arduino only; this is an alias to 
    // AVR's "sei()")
    

    备选方案2:

    AVRlibc宏直接转换为AVRcli汇编指令。这些宏在CCD_ 12中定义为"0";arduino-1.8.13/hardware/tools/avr/avr/include/avr/interrup.h";,例如:

    cli();  // clear (disable) the interrupts flag; `noInterrupts()` is simply 
    // a macro to this macro
    // your atomic variable-access code goes here
    sei();  // set (enable) the interrupts flag; `interrupts()` is simply a 
    // macro to this macro
    
  3. 选项3[BEST](与选项1基本相同;只是使用avr-libc库中的宏,当然在大括号中应用变量范围)

    AVRlibc在CCD_;arduino-1.8.13/hardware/tools/avr/avr/include/util/atomic.h";,例如

    #include <util/atomic.h> // (place at the top of your code)
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
    // your atomic variable-access code goes here
    }
    

    这些宏依赖于gcc扩展__cleanup__属性(请参见此处:https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html,并在页面上搜索";"清除"),其运行";当变量超出范围时运行函数";。本质上,这允许您在C.中创建对象或变量析构函数(类似C++的概念)

    参见:

    1. 关于ATOMIC_BLOCK()宏的官方AVRlibc文档:http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html.
    2. gcccleanup属性文档:https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html
    3. 我非常彻底的回答是:哪些Arduinos支持ATOMIC_BLOCK?。我涵盖:
      1. 哪些Arduino支持ATOMIC_BLOCK宏
      2. ATOMIC_BLOCK宏是如何使用gcc编译器在C中实现的,在哪里可以看到它们的源代码
      3. 如何在C++中实现Arduino中的ATOMIC_BLOCK功能(与avrlibc的gcc C版本相反)?-包括编写功能上类似于C++的CCD_ 19对象的版本

为什么不直接使用C11和C++11或更高版本提供的atomic_*类型

您可能知道C和C++在2011或更高版本中的原子类型。在这两种语言中,它们都有别名,如atomic_boolatomic_uint_fast32_t

  1. 在C中,atomic_uint_fast32_t_Atomic uint_fast32_t的别名。必须包含<stdatomic.h>头文件才能使用它们。
    1. 请参阅cpprreference社区wiki文档中的C:https://en.cppreference.com/w/c/thread#Atomic_operations
  2. 在C++中,atomic_uint_fast32_tstd::atomic<std::uint_fast32_t>的别名。必须包含<atomic>头文件才能使用它们。
    1. 请在此处查看有关C++的cppreference社区wiki文档:https://en.cppreference.com/w/cpp/atomic/atomic

但是,这些类型在8位Atmel/Micchip ATmega328 mcus上不可用!请参阅我在这个答案下面的评论。

我刚刚查了一下。在Arduino 1.8.13中,当我执行#include <stdatomic.h>atomic_uint_fast32_t i = 0;时,我得到:error: 'atomic_uint_fast32_t' does not name a type; did you mean 'uint_fast32_t'?这是针对ATmega328 mcu的。Arduino使用avr-g++用C++进行构建。因此,8位AVR gcc/g++工具链不支持原子类型。这可能是因为随着语言标准的发展,AVRlibc不再受到很好的支持,也不再得到很好的更新,尤其是因为我相信它是自愿的,而且在现代32位微控制器统治世界的时代,它是一个低级的8位微控制器。

另请参阅我的回答和@Michael Burr的回答下对此的评论讨论。

完整的示例用法:如何有效地、原子地读取共享的volatile变量

因此,我们必须使用如上所述的原子访问保护来强制原子性。在我们的8位AVR MCU中,这意味着关闭中断以防止中断,然后在中断完成后恢复中断状态。做到这一点的最佳方法通常是快速原子地复制出感兴趣的变量,然后在需要更多时间的计算中使用副本。要点如下:

#include <util/atomic.h>
// shared variable shared between your ISR and main loop; you must *manually*
// enforce atomicity on 8-bit AVR mcus!
volatile uint32_t shared_variable;
ISR(PCINT0_vect)
{
// interrupts are already off here, inside ISRs, by default
// do stuff to get a new value for the shared variable
// update the shared volatile variable
shared_variable = 789;
}
// process data from the ISR
void process_data_from_isr()
{
// our goal is to quickly atomically copy out volatile data then restore
// interrupts as soon as possible
uint32_t shared_variable_copy;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
// your atomic variable-access code goes here
//
// KEEP THIS SECTION AS SHORT AS POSSIBLE, TO MINIMIZE THE TIME YOU'VE
// DISABLED INTERRUPTS!
shared_variable_copy = shared_variable;
}
// Use the **copy** in any calculations, so that interrupts can be back ON
// during this time!
do_long_calculations(shared_variable_copy);
}
loop()
{
process_data_from_isr();
}
int main()
{
setup();
// infinite main loop
for (;;)
{
loop(); 
}
return 0;
}

相关:

  1. [My Q&A]哪些变量类型/大小在STM32微控制器上是原子的
  2. https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
  3. *****[我的答案]哪些Arduino支持ATOMIC_BLOCK?[我如何在C中用__attribute__((__cleanup__(func_to_call_when_x_exits_scope)))复制这个概念,在C++中用类构造函数和析构函数复制这个概念?]
  4. 有关如何在STM32微控制器中实现这一点,请参阅我的答案:在STM32微控制中禁用和重新启用中断以实现原子访问保护的各种方法有哪些