2

Is there a definitive list of which Arduino architectures support the ATOMIC_BLOCK() macro, and a list of #defines for each architecture?

I've tried searching through the Arduino cores on GitHub to see where ATOMIC_BLOCK is used. If it's not used in a core, I've assumed it's not supported. I generated this list based upon that assumption, but how accurate is it?

ArduinoCore         Supported       Archived    #define
-------------------------------------------------------
  samd
  avr                  Yes                      __AVR__
  arc32
  sam                  Yes
  primo                               Yes
  megaavr              Yes
  API
  nRF528x-mbedos
  mbed

I would like to write some code, something like this (edited in response to comments):

#define ATOMIC_BLOCK_SUPPORTED (__AVR__ || __MEGAAVR__ || __SAM__)
.
.
.
#if ATOMIC_BLOCK_SUPPORTED
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
#endif

        // Do not disturb.

#if ATOMIC_BLOCK_SUPPORTED
    }
#endif

For architectures that don't support ATOMIC_BLOCK, I would like to write my own code to save, disable and restore the global interrupt flag, something like this.

    asm
    {
        // Save interrupt flag.
    }
    // Clear interrupt flag.
    // Do not disturb.
    // Restore interrupt flag.

Potential Solution?

#ifdef __AVR__
#include <util/atomic.h>
#elif defined(__ARCH2__)
#define ATOMIC_BLOCK(/* for architecture 2 */)
#elif defined(__ARCH3__)
#define ATOMIC_BLOCK(/* for architecture 3 */)
// etc
#endif
.
.
.
#ifdef ATOMIC_BLOCK
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
#endif

        // Do not disturb.

#ifdef ATOMIC_BLOCK
    }
#endif

Is there a definitive list of code that saves, disables and restores the global interrupt flag for every Arduino architecture that could be dropped into each #elif branch?

10
  • 1
    Have you tried #ifdef ATOMIC_BLOCK? Commented Jul 23, 2020 at 19:36
  • 1
    Or even: #ifndef ATOMIC_BLOCK /* define your own ATOMIC_BLOCK here */ #endif Commented Jul 23, 2020 at 19:36
  • 1
    Have you found any one that doesn't support it? Commented Jul 23, 2020 at 19:37
  • 1
    @user253751, thanks for your suggestions. I think most architectures don't support ATOMIC_BLOCK. I've added a potential solution. Do you know of any code that saves, disables and restores the global interrupt flag each Arduino architecture?
    – tim
    Commented Aug 14, 2020 at 20:39
  • 2
    Note that, on AVR, ATOMIC_BLOCK is not provided by the Arduino core, but by the avr-libc. Commented Aug 15, 2020 at 12:02

1 Answer 1

6

This answer covers: 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?.

Which Arduino's support the ATOMIC_BLOCK macros?

Which Arduinos support ATOMIC_BLOCK?

Only AVR (ATmel AVR architecture) microcontrollers support the ATOMIC_BLOCK macros, because these macros are part of the avr-libc library, which, you guessed it!, supports only AVR microcontrollers.

AVR-based Arduinos include those based on the ATmega328 microcontroller (Arduino Uno, Nano, Mini, etc.), ATmega2560 (Arduino Mega 2560), ATmega32U4 (Arduino Leonardo, Pro Micro), etc.

How are the ATOMIC_BLOCK macros implemented in C with the gcc compiler, and where can I see their source code?

avr-libc is available for download here: https://www.nongnu.org/avr-libc/ --> "source code and documentation" link under the "Downloads" section. Ex: here is the tarball for v 2.0.0: http://download.savannah.gnu.org/releases/avr-libc/avr-libc-2.0.0.tar.bz2.

The entire source code for the ATOMIC_BLOCK macros is contained in avr-libc-2.0.0/include/util/atomic.h.

ATOMIC_BLOCK is an ingenious macro written in C and relying upon the gcc "cleanup" attribute C extension which can call a function you define when a variable goes out of scope. Essentially, this is identical to a C++ class destructor which gets called when an instance of a class in C++ goes out of scope. The gcc "cleanup" attribute extension is a way to get C++-like behavior in C.

Here is the official gcc documentation: https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html. It states:

cleanup (cleanup_function)

The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

If -fexceptions is enabled, then cleanup_function is run during the stack unwinding that happens during the processing of the exception. Note that the cleanup attribute does not allow the exception to be caught, only to perform an action. It is undefined what happens if cleanup_function does not return normally.

Its format is like this:

int x __attribute__((__cleanup__(func_to_call_when_x_exits_scope))) = 0;

You use it like this:
RUN A REAL EXAMPLE OF THIS CODE IN C ONLINE HERE.

static __inline__ void cleanup_my_byte(uint8_t *my_byte_p)
{
    Serial.print("my_byte is going out of scope! Its value is ");
    Serial.println(*my_byte_p);
}

void setup()
{
    uint8_t my_byte __attribute__((__cleanup__(cleanup_my_byte))) = 7;
} // The following function call occurs automatically here as `my_byte` 
  // exits its scope!:
  //    `cleanup_my_byte(&my_byte);`

Studying the avr-libc-2.0.0/include/util/atomic.h file referenced above, you can see how the ATOMIC_RESTORESTATE macro is implemented, using this attribute. I've put just the relevant pieces from that file here:

#include <avr/io.h>
#include <avr/interrupt.h>

#define ATOMIC_BLOCK(type) for ( type, __ToDo = __iCliRetVal(); \
                           __ToDo ; __ToDo = 0 )

static __inline__ uint8_t __iCliRetVal(void)
{
    cli();
    return 1;
}

#define ATOMIC_RESTORESTATE uint8_t sreg_save \
    __attribute__((__cleanup__(__iRestore))) = SREG

static __inline__ void __iRestore(const  uint8_t *__s)
{
    SREG = *__s;
    __asm__ volatile ("" ::: "memory");
}

Note that cli() clears (turns off) interrupts, and sei() sets (turns on) interrupts.

So, you can see that ATOMIC_RESTORE_STATE gets replaced by uint8_t sreg_save __attribute__((__cleanup__(__iRestore))) = SREG, which backs up the global interrupt state from the AVR SREG 8-bit register into sreg_save, then calls this function call when sreg_save exits its scope: __iRestore(&sreg_save);. That function call to __iRestore() then sets the SREG register back to where it was, restoring the interrupt state (turning it ON if it was ON before, or OFF if it was OFF before).

The official documentation for ATOMIC_BLOCK is here: https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html. An example usage is this:

ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
    my_var_copy = my_var;
}

The ATOMIC_RESTORESTATE part backs up the SREG register and sets up the call for __iRestore(&sreg_save); to occur when sreg_save exits this block scope, as we've already discussed, and the ATOMIC_BLOCK part gets converted into this: for ( ATOMIC_RESTORESTATE, __ToDo = __iCliRetVal(); __ToDo ; __ToDo = 0 ). This means that the entire block is now an ingenious for loop which runs only once! Prior to entering the for loop block, __iCliRetVal() is called once, clearing (turning OFF) interrupts, and __ToDo is set to 1, or true, so the for loop runs at least once, and then at the end of the first run it is set to 0, or false, to stop the for loop after one single run! As we've already gone over, the sreg_save variable now goes out-of-scope at the closing brace and the call to __iRestore(&sreg_save); is made, via the gcc extension, to restore the interrupt state! Ingenious!

How could you implement the ATOMIC_BLOCK functionality in Arduino in C++ (as opposed to avrlibc's gcc C version)?

In C++ (Arduino is C++, so we don't have to be limited to just C plus gcc extensions, as was the constraint in the avrlibc ATOMIC_BLOCK macros), you can get a similar effect using destructors in structs or classes (or, just use the avrlibc pattern shown above if you like, as that gcc C code is also perfectly valid gcc C++ code!).

Option 1: example to create a usage style similar to C++'s std::lock_guard

This, for example, would implement a form of ATOMIC_BLOCK(ATOMIC_FORCEON) {}, using a simple class in C++:

It relies on Arduino's built-in functions interrupts() (same as avrlibc's sei() function) and noInterrupts() (same as avrlibc's cli() function). I wish that Arduino also had built-in saveInterrupts() and restoreInterrupts() functions, but unfortunatley they don't. Someone will have to go file a feature request and request them, or implement them themselves and do a Pull Request (PR) on GitHub to try to get them back into the main Arduino branches.

RUN A REAL EXAMPLE OF THIS CODE IN C++17 ONLINE HERE.

#define ATOMIC_BLOCK_FORCEON AtomicBlockForceOn atomicBlockForceOn_

class AtomicBlockForceOn
{
public:
    // Constructor: called when the object is created
    inline AtomicBlockForceOn()
    {
        noInterrupts(); // turn interrupts OFF
    }

    // Destructor: called when the object is destroyed (ex: goes
    // out-of-scope)
    inline ~AtomicBlockForceOn()
    {
        interrupts(); // turn interrupts ON
    }
};

That's it! Now use it like this:

uint16_t my_var_copy;
{
    ATOMIC_BLOCK_FORCEON;
    // now do whatever you want here, with interrupts disabled; ex:
    my_var_copy = my_var;
} // the `atomicBlockForceOn_` object's destructor is called here 
  // and interrupts are automatically turned back ON!

Pretty cool!

This, I imagine, is probably exactly how C++'s std::lock_guard is implemented! Its usage is very similar:

void safe_increment()
{
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
 
    // g_i_mutex is automatically released when lock
    // goes out of scope
}

Option 2: example to create a usage style similar to avrlibc's ATOMIC_BLOCK macros

To obtain a usage style of ATOMIC_BLOCK(ATOMIC_FORCEON) { } in C++ one can definitely use the gcc C style which avrlibc uses, since that is also valid C++, but let's continue with the C++ class style shown just above instead, with just a few tweaks. We will add a couple more helper macros and a couple more functions to the class to make this work. Here it is, with a usage example shown at the end which is identical to how avrlibc's ATOMIC_BLOCK can be used! I think this is pretty cool too.

RUN A REAL EXAMPLE OF THIS CODE IN C++17 ONLINE HERE.

#define ATOMIC_BLOCK(type) for(type; type##_OBJECT_NAME.run(); \
    type##_OBJECT_NAME.stop())
#define ATOMIC_FORCEON_OBJECT_NAME atomicBlockForceOn_
#define ATOMIC_FORCEON AtomicBlockForceOn ATOMIC_FORCEON_OBJECT_NAME

class AtomicBlockForceOn
{
public:
    // Constructor: called when the object is created
    inline AtomicBlockForceOn()
    {
        noInterrupts(); // turn interrupts OFF
    }

    // Destructor: called when the object is destroyed (ex: goes
    // out-of-scope)
    inline ~AtomicBlockForceOn()
    {
        interrupts(); // turn interrupts ON
    }
    
    // Can we run? Returns true to run the `for` loop or 
    // `false` to stop it.
    inline bool run()
    {
        return run_now;
    }
    
    // Tell the `for` loop to stop
    inline void stop()
    {
        run_now = false;
    }
    
private:
    bool run_now = true;
};

Use it like avrlibc's ATOMIC_BLOCK() { } macro!

Usage example:

uint16_t my_var_copy;
ATOMIC_BLOCK(ATOMIC_FORCEON)
{
    // now do whatever you want here, with interrupts disabled; ex:
    my_var_copy = my_var;
} // the `ATOMIC_FORCEON_OBJECT_NAME` (`atomicBlockForceOn_`) object's
  // destructor is called here and interrupts are automatically 
  // turned back ON!

Further reading:

  1. Nick Gammon's excellent "Interrupts" page, which is how I first got started learning about interrupts in 2012~2014 or so: https://gammon.com.au/interrupts.
  2. Other answer's I've done about interrupts, locks/guards, and things:
    1. global volatile variable not being updated in ISR
    2. My own race condition problem: https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why
    3. My own race condition solution, demonstrating 3 (or 4, depending on how you look at it) ways to enforce "atomic access guards" on AVR-based 8-bit Arduinos: https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why/39693278#39693278
  3. What's that weird type##_OBJECT_NAME thing I did in the macro anyway? Read more about macro token concatenation using the ## preprocessor operator in the official gcc C++ user manual here: https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html. Note: this also works in C of course.
  4. gcc inline or __inline__, and static inline or static __inline__ rules: https://gcc.gnu.org/onlinedocs/gcc/Inline.html
3
  • 1
    Thanks. That's a great answer. I'd previously read some of your Further Reading (points 1 & 2i-iii) while researching the issue. I was about to collate it, but you've saved me the job by creating an excellent reference post. The macro with the for loop and Option 1 & 2 with the class are what I was looking at implementing, so thanks for a clear explanation of them. Now need to figure out what to put in the macro __iRestore and the class constructure/destructor to save/restore the global interrupt flag for each architecture.
    – tim
    Commented Aug 22, 2020 at 15:29
  • 1
    Correct. You have to figure out how to disable/enable interrupts now for each architecture, then put the appropriate code into the constructor/destructor. Here's how to do it on STM32, for instance, which is used in the Arduino "professional" line such as the $103 Portenta H7, based on the STM32H7 microcontroller: prim = __get_PRIMASK();, __disable_irq();, if (!prim) { __enable_irq(); }. That's using the low-level CMSIS libraries. Commented Aug 22, 2020 at 21:04
  • 1
    You can also use a half dozen other techniques, including the STM32 core libraries and the FreeRTOS libraries (if using the FreeRTOS real time operating system). I have personal notes on it all that I need to publish on my website someday. 32-bit microcontrollers are beasts. I have no idea how to do it on Atmel/Microchip 32-bit SAM or SAMD microcontrollers, such as the Zero or Due, but there are ways for all chips, you just have to figure them out. Commented Aug 22, 2020 at 21:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.