The standard technique to enforce atomic access to volatile variables shared with ISRs, via "atomic access guards" or "interrupt guards", in particular when running a bare metal, single-threaded cooperative multi-tasking application with no operating system, is as follows:
// 1. save interrupt state
// 2. disable only the interrupts necessary
// You get atomic access to volatile variables shared with ISRs here,
// since ISRs are the only other "context" or running "thread" which
// might attempt to modify a shared memory block or variable.
// 3. restore interrupt state
See also where I describe this in detail here, including best practices (keep interrupts off for short period of time) and how to do atomic reads withOUT disabling interrupts first, via my doAtomicRead() repeat-read-loop function: Reading a 64 bit variable that is updated by an ISR.
I have previously documented how to do this for AVR microcontrollers/Arduino: How do I force atomicity in Atmel AVR mcus/Arduino?.
But, how do I do this for STM32 microcontrollers? I know there are a lot of ways.
Please cover the following techniques:
- Via ARM-core CMSIS:
- for global interrupts
- for specific IRQs (Interrupt Requests)
- Via STM32 HAL (Hardware Abstraction Layer)
- Via FreeRTOS
This answer is related, but insufficient: How can I re-enable the stm32f103's external interrupt after I disable it?
Update 10 May 2023: one of my primary motivating factors in learning this stuff was related to my first ever ring buffer implementation I wrote 7 years ago in 2016, leading to this debugging problem where I lost 25 hours of debugging work in 2 days. 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.
Multiple ways to enable/disable interrupts in STM32 mcus
...to enable atomic access (critical section) guards:
1. Via ARM-core CMSIS:
1.A. For global interrupts
For the definition of these functions, see:
To save and restore the interrupt state, use
__get_PRIMASK(), like this:When dealing with global interrupts, this is the best way for bare-metal, non-FreeRTOS code!
I think this technique is also cross-compatible with ALL ARM-core mcus, not just STM32.
I first learned this technique from Tilen Majerle, here: https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/. His work and contributions to clear up this super-obfuscated stuff are infinitely valuable and appreciated!
His example:
1.B. For specific IRQs (Interrupt Requests)
It is best to avoid disabling global interrupts, if possible, and disable only the fewest number of specific interrupts possible to achieve atomicity for your specific code. So, using these functions allows you to enable or disable only the specific interrupts you need to!
Enable or disable specific types of interrupts:
NVIC stands for "Nested Vector Interrupt Controller". Nested interrupts (meaning: a higher-priority interrupt can still fire within an ISR) are enabled by default on STM32 microcontrollers. Each interrupt type has a priority assigned to it, with lower numbers being higher priority, and higher-priority interrupts are able to fire while an ISR is being processed for a lower-priority interrupt. See here for a little more information on the STM32 NVIC: https://stm32f4-discovery.net/2014/05/stm32f4-stm32f429-nvic-or-nested-vector-interrupt-controller/.
Contrast this to AVR microcontrollers (ex: ATMega328 / Arduino Uno), which do not have priority-based interrupts, so by default, when any ISR is being processed, all interrupts (ie: global interrupts) are automatically disabled as the program enters the ISR. Note that even on AVR mcus, however, you can still manually enable nested interrupts / ISRs if you like by manually re-enabling global interrupts inside your ISR, via a call to
interrupts()on Arduino orsei()(set interrupts) on raw AVR.Each ARM-core microcontroller manufacturer, I believe, including STM32 types, must define and create its own list of
IRQn_Typeinterrupt request types, so see below for the STM32 details on their specific interrupt types defined for each mcu.2. Via STM32 HAL (Hardware Abstraction Layer) libraries
Enable or disable specific types of interrupts:
See, for example: "stm/stm32f2xx/st_hal_v1.1.3/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_cortex.c/.h" - definitions for those functions above are in those files. See them online:
Here are the definitions of
HAL_NVIC_EnableIRQ()andHAL_NVIC_DisableIRQ(). Notice that they just check to ensure yourIRQnis valid, then they pass the input argument on to the ARM-core CMSISNVIC_EnableIRQ()andNVIC_DisableIRQ()functions above!:For
IRQn_Types: see the appropriate definition file for your specific board! These are board-specific definitions, for your board from your manufacturer. Here are all of the boards in the STM32 F2xx line, for instance: https://github.com/STMicroelectronics/STM32CubeF2/tree/master/Drivers/CMSIS/Device/ST/STM32F2xx/Include. Let's look at thestm32f217xx.hfile specifically:From this file, we can see the
typedef enumdefinition for theIRQn_Type, which is the "STM32F2XX Interrupt Number Definition". Here is what it looks like:2.A. Example usage using STM32 HAL:
To get exclusive access (to ensure strings are atomically printed, for instance) to the
USART1for printing debug chars via a HAL-based blocking (polled) mode (ie: viaHAL_UART_Transmit()), you need to disable all interrupts forUSART1_IRQnby doing the following. (This guarantees you get atomic access to this device):3. Via FreeRTOS:
The FreeRTOS atomic-access-guard / interrupt-related functions are listed under the "Modules" section of the Kernel Control API here: Kernel Control:
3.A. Higher-level macros:
portDISABLE_INTERRUPTS()anyway, which is the port implementation of the lower-leveltaskDISABLE_INTERRUPTS(), shown below.3.B. Lower-level macros:
These do NOT support nested calls!
Official documentation on them is on the main "Kernel Control" page:
Notes and limiatations:
taskDISABLE_INTERRUPTS()at the link above states:taskENABLE_INTERRUPTS()at the link above states:taskDISABLE_INTERRUPTS()is demonstrated as the technique used to panic inside an example macro definition forconfigASSERT().taskDISABLE_INTERRUPTS()might be preferred overtaskENTER_CRITICAL()because no amount of callingtaskEXIT_CRITICAL()from another thread will re-enable interrupts oncetaskDISABLE_INTERRUPTS()has been called [I think!?]--rather, one would have to explicitly (and accidentally) calltaskENABLE_INTERRUPTS()(ex: from another thread) to re-enable interrupts oncetaskDISABLE_INTERRUPTS()has been called. In other words, using the low-leveltaskDISABLE_INTERRUPTS()call is appropriate here because it will truly cause the system to sit in a loop, as desired, whereastaskENTER_CRITICAL()would not.3.C. Mutexes and other OS (Operating System)-enabled synchronization primitives
Beyond the examples above, you can also use FreeRTOS queues (which are thread-safe, unlike all containers in the C++ std library), mutexes, semaphores, task notifications, and other synchronization primitives, where able and where appropriate, to protect certain data which is shared between FreeRTOS tasks (threads), assuming you are running FreeRTOS.
See the list of these tools here: https://www.freertos.org/a00106.html, and in the left-hand navigation menus once you click on that link.
4. TODO: mutex primitives: raw, OS-free spin locks via atomic
set_and_test()(read, modify, write) instructionsAdd an atomic
test_and_set()(set_and_test()orread_modify_write()really makes more sense as a function name for this, I think) demo using ARM-core CMSIS functions, or assembly, or whatever means necessary, to demonstrate writing a spin lock in STM32. I don't know how to do this yet so it will require finding the right function or operation to use. See here: https://en.wikipedia.org/wiki/Test-and-set#Pseudo-C_implementation_of_a_spin_lock:Here is a spin lock implementation I did in C11 using
_Atomictypes. It should work just fine for STM32 as well, and probably compiles to use the underlying exclusiveSTREX/LDREXoperations to store (write) and read (load), but I'd have to check that by looking at the assembly. Additionally, this implementation would need to be modified to add safety anti-deadlock mechanisms such as automatic deferral, timeout, and retry, to prevent deadlock. See my notes here: Add basic mutex (lock) and spin lock implementations in C11, C++11, AVR, and STM325. See also:
doAtomicRead()func which ensures atomic access withOUT turning interrupts offFor Microchip PIC32 microcontrollers
See the References section of my question here: How to properly count timer overflows to convert a 32-bit high-resolution timer into a 64-bit high-resolution timer: