2

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?.

Community
  • 1
  • 1
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • 3
    Why? Because the standard says so. The myth that single ints or bytes are accessed atomically automatically comes from intel processors being very forgiving. C++ is not. – nwp Apr 03 '16 at 05:42
  • can you point me to a source? I'll search around too. – Gabriel Staples Apr 03 '16 at 05:43
  • On an 8-bit AVR-based microcontroller, including Arduino, reading and writing single-byte variables (declared `bool`, `boolean`, `byte`, `char`, `uint8_t`, `int8_t`, etc) is always atomic, though incrementing or decrementing them, I just learned, is not atomic. See here, under the "Atomic access" section: http://www.gammon.com.au/interrupts – Gabriel Staples Apr 03 '16 at 05:48
  • 1
    Increment/decrement operations on many (most?) platforms are not atomic because they rely on distinct read/modify/write primitives. – Michael Burr Apr 03 '16 at 05:54
  • 2
    In short, `volatile` in C or C++ has nothing to do with atomicitily, synchronization between threads, or anything like this. People keep thinking it does, but it does not. This is different from at least Java, where `volatile` *does* have read/write atomicity and thread synchronization guarantees (but *still* no atomicity guarantees for increment/decrement etc operations). `volatile` in C/C++ is an optimization disabling keyword, nothing more. – hyde Apr 03 '16 at 06:23
  • 2
    @hyde, I realize my title may make people think that *I think* that `volatile` guarantees atomicity, but I know it does not. I am using `volatile` simply to prevent optimizations which would otherwise make the compiler think it already knows the variable state without re-reading it from the register, when it was meanwhile changed in an interrupt. – Gabriel Staples Apr 03 '16 at 13:30
  • My question wasn't how to make something atomic, it was why is something (incrementing a byte) *not* atomic when I thought it was, so my "answer" on *how* to make something atomic isn't an answer to my question at all. I just included that as extra info since *how* to make something atomic is going to be the first logical question of anyone reading this to learn the *why* incrementing a byte is not atomic. I've already marked Ishamael's answer as the correct one. – Gabriel Staples Sep 11 '16 at 21:26
  • Please remove `Solved` from your title and post the solution you found as a proper answer. – Jongware Sep 23 '16 at 22:11
  • done; please upvote my answer if so inclined. – Gabriel Staples Sep 26 '16 at 00:51
  • 1
    Related: [Can num++ be atomic for 'int num'?](https://stackoverflow.com/q/39393850) - AVR is a load/store architecture, no memory-destination `sub` or `add`, so `arr[i]--` will necessary be separate load/ALU/store instructions, so interrupts can happen between them. – Peter Cordes May 10 '23 at 17:45

3 Answers3

7

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.

Michael Burr
  • 333,147
  • 50
  • 533
  • 760
  • Thanks for the help to understand *why* incrementing/decrementing a single byte is not an atomic operation. To any readers who now want to know *how* to enforce atomic access to variables on an Atmel AVR microcontroller, I've added 3 ways to do this at the bottom of my answer, since I figured this *how-to* information is the answer to the first logical question to follow my *why* question. – Gabriel Staples Sep 11 '16 at 21:32
  • Michael, unfortunately, I could use your help again: https://stackoverflow.com/questions/52784613/which-variable-types-sizes-are-atomic-on-stm32-microcontrollers – Gabriel Staples Oct 12 '18 at 18:42
  • `I have no idea if a toolchain for the ATmega supports these newer standards.` I just checked. In Arduino 1.8.13, when I do `#include ` and then `atomic_uint_fast32_t i = 0;`, I get: `error: 'atomic_uint_fast32_t' does not name a type; did you mean 'uint_fast32_t'?` Note: for anyone wondering, here is the cppreference community wiki documentation for atomic types [in C](https://en.cppreference.com/w/c/thread#Atomic_operations) and [in C++](https://en.cppreference.com/w/cpp/atomic/atomic). This is for the ATmega328 mcu. Arduino was building with C++: `avr-g++`. – Gabriel Staples May 10 '23 at 16:57
  • So, the 8-bit AVR gcc/g++ toolchain doesn't support atomic types. Instead, we can enforce atomicity using atomic access guards [as described in my answer here](https://stackoverflow.com/a/39693278/4561887). – Gabriel Staples May 10 '23 at 17:01
  • Note: `arduino-1.8.13/hardware/tools/avr/bin/avr-g++ --version` shows `avr-g++ (GCC) 7.3.0`. I just installed the latest Arduino 2.1.0 and got the same error too. Checking its g++ version, I see it is the same: `~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++ --version` --> output: `avr-g++ (GCC) 7.3.0` – Gabriel Staples May 10 '23 at 18:41
  • @GabrielStaples: Why are you using C `stdatomic.h` in a C++ program? ISO C++ doesn't support that until C++23 (https://en.cppreference.com/w/cpp/header/stdatomic.h). Normally in C++ you'd use C++11 ``. (Which also defines types like `atomic_uint_fast32_t`, or at least `std::atomic_uint_fast32_t`. https://en.cppreference.com/w/cpp/atomic/atomic) – Peter Cordes May 10 '23 at 18:48
  • @PeterCordes, when I use `#include `, the compiler gets past that line. When I use `#include `, it stops immediately with: `/home/gabriel/Arduino/temp/blink_w_atomic_types/blink_w_atomic_types.ino:34:10: fatal error: atomic: No such file or directory #include `. In case you want to give it a shot, you can download the latest Arduino IDE here: https://www.arduino.cc/en/software – Gabriel Staples May 10 '23 at 18:51
  • I [opened an issue](https://github.com/arduino/toolchain-avr/issues/92). – Gabriel Staples May 10 '23 at 18:55
5

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?

Ishamael
  • 12,583
  • 4
  • 34
  • 52
  • that answers it: treat this like a multithreaded environment here. incrementing and decrementing is never atomic. that's what I didn't know. On an Arduino, int is 2-bytes, so no, no operation on a 2 or more-byte variable in Arduino is atomic, and that's correct, volatile doesn't make it atomic...that's what noInterrupts() and interrupts() are for...to force atomicity. – Gabriel Staples Apr 03 '16 at 05:35
  • do you have any sources that talk about `--` and `++` not being atomic operations in general in C++? – Gabriel Staples Apr 03 '16 at 05:39
  • I meant `uint8_t`s, not `int`s. Quick googling didn't find any reliable sources, so I don't have links handy, but in general if you want atomic increments you should either use `std::atomic`, use locks, or use corresponding platform-dependent functions, such as InterlockedIncrement on windows. – Ishamael Apr 03 '16 at 05:43
  • 2
    @GabrielStaples the reason it isn't atomic is because the assembly code generated for an increment ends up being multiple instructions. executing multiple instructions without a locking mechanism is inherently not thread safe. – OpenUserX03 Apr 03 '16 at 05:50
  • Thanks for the help to understand *why* incrementing/decrementing a single byte is not an atomic operation. To any readers who now want to know *how* to enforce atomic access to variables on an Atmel AVR microcontroller, I've added 3 ways to do this at the bottom of my answer, since I figured this *how-to* information is the answer to the first logical question to follow my *why* question. – Gabriel Staples Sep 11 '16 at 21:32
2

Update 10 May 2023: the problem in the question was related to my first ever ring buffer implementation I wrote 7 years ago in 2016. I finally wrote a really good ring buffer implementation that is lock-free when used on any system which supports C11 or C++11 atomic types. It is the best implementation I've ever written, and also the best I've ever seen. It solves a lot of the problems of other implementations. Full details are in the top of the file. It runs in both C and C++. You can see the full implementation here: containers_ring_buffer_FIFO_GREAT.c in my eRCaGuy_hello_world repo.


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.

Essentially, on an 8-bit AVR mcu, 8-bit reads are atomic, and 8-bit writes are atomic, and that's it! Increment and decrement are never atomic, nor are multi-byte reads and writes on this architecture!

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 a good 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()")
    // your atomic variable-access code goes 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):

    Macros offered by Arduino in Arduino.h at "arduino-1.8.13/hardware/arduino/avr/cores/arduino/Arduino.h", for instance:

    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()")
    

    Alternative option 2:

    AVRlibc Macros directly to the AVR cli assembly instruction. These macros are defined in interrupt.h at "arduino-1.8.13/hardware/tools/avr/avr/include/avr/interrupt.h", for instance:

    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. Option 3 [BEST] (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)

    Super fancy macros offered by AVRlibc in atomic.h at "arduino-1.8.13/hardware/tools/avr/avr/include/util/atomic.h", for example.

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

    These macros rely on the gcc extension __cleanup__ attribute (see here: https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html, and search the page for "cleanup"), which runs "runs a function when the variable goes out of scope". Essentially, this allows you to create object or variable destructors (a C++-like concept) in C.

    See:

    1. The official AVRlibc documentation on the ATOMIC_BLOCK() macro: http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html.
    2. gcc cleanup attribute documentation: https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html
    3. My very thorough answer where I go into this a lot: Which Arduinos support ATOMIC_BLOCK?. I cover:
      1. Which Arduino's support the ATOMIC_BLOCK macros?
      2. How are the ATOMIC_BLOCK macros implemented in C with the gcc compiler, and where can I see their source code?
      3. How could you implement the ATOMIC_BLOCK functionality in Arduino in C++ (as opposed to avrlibc's gcc C version)? - including writing a version functionally similar to C++'s std::lock_guard object.

Why not just use the atomic_* types offered by C11 and C++11 or later?

You may be aware of the atomic types in C and C++ as of their 2011 versions or later. In both languages, you have aliases to them like atomic_bool and atomic_uint_fast32_t.

  1. In C, atomic_uint_fast32_t is an alias to _Atomic uint_fast32_t. You must include the <stdatomic.h> header file to use them.
    1. See the cppreference community wiki documentation on this for C here: https://en.cppreference.com/w/c/thread#Atomic_operations
  2. In C++, atomic_uint_fast32_t is an alias to std::atomic<std::uint_fast32_t>. You must include the <atomic> header file to use them.
    1. See the cppreference community wiki documentation on this for C++ here: https://en.cppreference.com/w/cpp/atomic/atomic

However, these types are not available on 8-bit Atmel/Microchip ATmega328 mcus! See my comments below this answer.

I just checked. In Arduino 1.8.13, when I do #include <stdatomic.h> and then atomic_uint_fast32_t i = 0;, I get: error: 'atomic_uint_fast32_t' does not name a type; did you mean 'uint_fast32_t'? This is for the ATmega328 mcu. Arduino was building with C++ using avr-g++. So, the 8-bit AVR gcc/g++ toolchain does not yet support atomic types. It's probably because AVRlibc isn't well supported nor well-updated anymore as the language standards progress, especially since it's on a voluntary basis, I believe, and is a lowly 8-bit microcontroller in the days of modern 32-bit microcontrollers ruling the world.

See also the comment discussion about this under my answer and @Michael Burr's answer.

Full example usage: how to efficiently, atomically, read shared volatile variables

So, instead, we must enforce atomicity using atomic access guards as described above. In our case on 8-bit AVR mcus, that means turning off interrupts to prevent being interrupted, then restoring the interrupt state when done. The best way to do this is usually to quickly atomically copy out your variable of interest, then use your copy in calculations which take more time. Here's the gist of it:

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

Related:

  1. [My Q&A] Which variable types/sizes are atomic on STM32 microcontrollers?
  2. https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
  3. ***** [My answer] Which Arduinos support ATOMIC_BLOCK? [and how can I duplicate this concept in C with __attribute__((__cleanup__(func_to_call_when_x_exits_scope))) and in C++ with class constructors and destructors?]
  4. For how to do this in STM32 microcontrollers instead, see my answer here: What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • https://godbolt.org/z/z1a386vc1 shows AVR GCC is able to compile a function that increments an `_Atomic unsigned char` in C. But it does `rjmp __atomic_fetch_add_1`, not inlining the operation, so you'd have to link with libatomic.a https://godbolt.org/z/aM7bhscoY shows Godbolt's AVR G++ install missing ``, but Godbolt has that problem for some other architectures as well, I think just an install problem. https://godbolt.org/z/9PTbr7hK4 shows GNU C `__atomic_fetch_add(ptr, 1, __ATOMIC_SEQ_CST)` compiling in C++ mode, same as the `_Atomic` keyword in C. – Peter Cordes May 10 '23 at 18:00
  • @PeterCordes, interesting. I'd have to experiment with newer versions of Arduino too. I'll leave it alone for now though. – Gabriel Staples May 10 '23 at 18:07
  • @PeterCordes, I just tried the latest version of the Arduino IDE: 2.1.0. No change. See [my new comments under Michael Burr's answer](https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why/36382183#comment134415315_36382183). That's too bad. – Gabriel Staples May 10 '23 at 18:42