C++ decrementing an element of a single-byte (vola

2019-05-22 14:33发布

问题:

I just lost days, literally, ~25 hrs of work, due to trying to debug my code over something simple that I didn't know.

It turns out decrementing an element of a single-byte array in C++, on an AVR ATmega328 8-bit microcontroller (Arduino) is not an atomic operation, and requires atomic access guards (namely, turning off interrupts). Why is this??? Also, what are all of the C techniques to ensure atomic access to variables on an Atmel AVR microcontroller?

Here's a dumbed down version of what I did:

//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.
  }
}

Here's the version of loop that's fine:

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 
  }
}

Notice the "atomic access guards", ie: disabling interrupts before decrementing, then re-enabling them after.

Since I was dealing with a single byte here, I didn't know I'd need atomic access guards. Why do I need them for this case? Is this typical behavior? I know I'd need them if this was an array of 2-byte values, but why for 1-byte values???? Normally for 1-byte values atomic access guards are not required here...


Update: read the "Atomic access" section here: http://www.gammon.com.au/interrupts. This is a great source.


Related (answer for STM32 mcus):

So we know that reading from or writing to any single-byte variable on AVR 8-bit mcus is an atomic operation, but what about STM32 32-bit mcus? Which variables have automatic atomic reads and writes on STM32? The answer is here: Which variable types/sizes are atomic on STM32 microcontrollers?.

回答1:

The ATmega328 data sheet indicates that:

The ALU supports arithmetic and logic operations between registers or between a constant and a register

It doesn't mention the ALU being able to operate directly on memory locations. So in order to decrement a value, this means that the processor must perform several operations:

  • load the value into a register
  • decrement the register
  • store the value back

Therefore the decrement operation is not atomic unless you do something special to make it atomic, such as disable interrupts. This kind of read/modify/write requirement is probably more common than not for updating memory.

The details of how an operation can be made atomic are platform dependent. Newer versions of the C and C++ standards have explicit support for atomic operations; I have no idea if a toolchain for the ATmega supports these newer standards.



回答2:

I don't know much about Arduino and interrupts, so I might not answer your particular question here, but in multithreaded environment decrementing and incrementing using -- and ++ is never atomic. Moreover, volatile also does not mean atomic in C++ in general (proof). Though I know that volatile is meaningful when you program microcontrollers, so I suspect that my answer might not apply to your case.

Does it work if you replace an array of volatile uint8_ts with three separate volatile uint8_ts?



回答3:

Ok, the answer to "Why is incrementing/decrementing a single byte variable NOT atomic?" is answered very well here by Ishamael here, and Michael Burr here.

Now that I got my answer that -- decrement and ++ increment operations are never atomic, even when done on byte values (see answers above and Nick Gammon's link here), I'd like to ensure the follow-up question of how do I force atomicity on Atmel AVR microcontrollers is also answered so this question becomes somewhat of a resource:

Here are all techniques I am aware of to force atomicity in Atmel AVR microcontrollers, such as Arduino:

1) Option 1 (the preferred method):

uint8_t SREG_bak = SREG; //save global interrupt state
noInterrupts(); //disable interrupts (for Arduino only; this is an alias of AVR's "cli()")
//atomic variable-access code here
SREG = SREG_bak; //restore interrupt state

2) Option 2 (the less-safe, not recommended method, since it can cause you to inadvertently enable nested interrupts if you accidentally use this approach in a code block or library which gets called inside an ISR):

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

Alternative option 2:

cli(); //clear (disable) interrupts flag; noInterrupts() is simply a macro for this
//atomic variable-access code here
sei(); //set (enable) interrupts flag; interrupts() is simply a macro for this

3) Option 3 (essentially the same as option 1; just using a macro held in an avr-libc library instead, and with variable scope applied within the braces of course)
source: http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html

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