Hello! Welcome to your next lesson in the "Interrupts and Exception Handling" module.
In our last session, we detailed the entire lifecycle of an Interrupt Service Routine (ISR), from the hardware trigger to context restoration. A key takeaway was that an interrupt can pause your main application code at any instruction. While powerful, this preemption creates a significant challenge: what happens when an ISR and the code it interrupted try to modify the same data? This leads to a classic and dangerous problem in embedded systems called a race condition.
Today, we'll tackle this head-on. This lesson directly addresses the learning outcome: Implement critical sections by enabling and disabling interrupts using PRIMASK or BASEPRI registers. You will learn how to protect shared data and create sections of code that are "indivisible" or atomic, a fundamental skill for writing robust low-level drivers and working with any real-time operating system.
1. The Problem: Race Conditions and Non-Atomic Operations
In programming, an atomic operation is one that is guaranteed to complete without being interrupted. A single assembly instruction, like LDR R0, [R1], is atomic. However, a single line of C code often is not.
Consider this seemingly simple C statement:
uint16_t g_counter = 0;
// in main loop or a task
g_counter++;
A C compiler might translate g_counter++ into three distinct assembly instructions for a 32-bit ARM processor:
- LDR (Load): Load the current value of
g_counterfrom memory into a core register. - ADD (Add): Increment the value in the register.
- STR (Store): Store the new value from the register back into the memory location of
g_counter.
Now, imagine an ISR also increments this counter:void Timer_ISR(void) { g_counter++; }
What happens if the timer interrupt occurs right after the main loop has executed the LDR but before it has executed the STR?
- Main Code:
LDRloads the value ofg_counter(e.g., 100) into a register (R0). - ---> INTERRUPT OCCURS <--- The processor saves context and jumps to
Timer_ISR. - ISR: The ISR executes its own
LDR(loads 100),ADD(increments to 101), andSTR(stores 101 back tog_counter).g_counteris now 101. - ---> ISR RETURNS <--- The processor restores context and resumes the main code.
- Main Code: It continues from where it left off. It performs the
ADDon its register R0 (which still holds the old value, 100), making it 101. - Main Code: It performs the
STR, storing 101 back tog_counter.
The counter was incremented twice, but its final value is 101, not 102. An update has been lost. This is a classic race condition. To prevent this, the LDR-ADD-STR sequence must be made atomic. This is achieved by creating a critical section.
2. The Solution: Critical Sections via Interrupt Masking
A critical section is a segment of code that must be executed without being preempted to ensure data integrity. The most direct way to enforce this is to temporarily disable interrupts before entering the critical section and re-enable them immediately after exiting.
This ensures that once your code begins the LDR-ADD-STR sequence, no ISR can interrupt it, guaranteeing the operation's atomicity relative to the interrupt sources.
The ARM Cortex-M architecture provides two primary mechanisms for this: the PRIMASK register and the BASEPRI register.
3. The "Sledgehammer": Global Interrupt Disable with PRIMASK
The simplest approach is to disable all configurable interrupts globally. This is done using the PRIMASK register. It contains a single bit, the I bit, which acts as a master switch.
PRIMASK[0] = 1: All interrupts with configurable priority are disabled (masked).PRIMASK[0] = 0: Interrupts are enabled.
While you can modify this register directly, it's standard practice to use assembly instructions or CMSIS core functions designed for this purpose.
ARM Cortex-M, Interrupts and FreeRTOS: Part 1
The article 'ARM Cortex-M, Interrupts and FreeRTOS: Part 1' by Erich Styger provides an excellent explanation of these registers. Let's start with the section on PRIMASK.
Please read the subsection titled 'PRIMASK'. It's short and explains the register, the assembly instructions cpsid i and cpsie i, and the equivalent CMSIS functions.
As the article shows, you have two main ways to control PRIMASK in your code:
1. Inline Assembly:
__asm volatile ("cpsid i"); // disable interrupts (set PRIMASK)
// ... critical section ...
__asm volatile ("cpsie i"); // enable interrupts (clear PRIMASK)
2. CMSIS Functions (Recommended): The Cortex Microcontroller Software Interface Standard (CMSIS) provides a portable hardware abstraction layer. These functions are the standard, toolchain-independent way to control the core.
#include "core_cm4.h" // Or your specific core header
__disable_irq(); // Sets PRIMASK bit
// ... critical section ...
__enable_irq(); // Clears PRIMASK bit
Our previous corrupted counter example can now be fixed:
void safely_increment_counter() {
__disable_irq();
g_counter++;
__enable_irq();
}
Drawback: Using PRIMASK is a blunt instrument. It blocks every interrupt, including high-priority, time-sensitive ones like a motor control timer or emergency stop signal. This increases the interrupt latency for all sources. For this reason, critical sections protected this way must be kept as short as possible.
4. The "Scalpel": Priority-Based Masking with BASEPRI
For more sophisticated control, ARM Cortex-M3, M4, and M7 cores (but not M0/M0+) offer the BASEPRI register. This allows you to disable interrupts only up to a certain priority level, leaving higher-priority interrupts unaffected.
BASEPRIis set to a priority value (e.g., 5).- Any interrupt with a priority numerically greater than or equal to
BASEPRI's value is blocked. (Remember, on ARM, a higher number means lower urgency). - Interrupts with a priority numerically less than
BASEPRI's value (i.e., higher urgency) can still preempt the code.

ARM Cortex-M, Interrupts and FreeRTOS: Part 1
Let's return to the same article to learn about this more refined tool.
Please read the subsection titled 'BASEPRI'. Focus on understanding how it works as a mask, the examples provided, and the CMSIS function used to control it.
The key CMSIS function for this is __set_BASEPRI(). Unlike the simple enable/disable for PRIMASK, using BASEPRI requires a careful save-and-restore sequence to avoid unintended side effects.
#include "core_cm4.h"
#define UART_INTERRUPT_PRIORITY 5 // Example priority
void safely_access_uart_buffer() {
uint32_t prev_basepri;
// 1. Save current BASEPRI value
prev_basepri = __get_BASEPRI();
// 2. Set BASEPRI to block UART interrupt and lower-priority interrupts
// Note: Priority values must be shifted to align with the register's format.
__set_BASEPRI(UART_INTERRUPT_PRIORITY << (8 - __NVIC_PRIO_BITS));
// ... critical section accessing a buffer shared with the UART ISR ...
// 3. Restore the original BASEPRI value
__set_BASEPRI(prev_basepri);
}
Note: BASEPRI cannot be used to disable interrupts with the highest priority (priority 0). For that, you must still use PRIMASK.
This selective masking is a powerful feature for real-time systems. It allows you to protect a resource from a low-priority logging interrupt without delaying a critical, high-priority motor control interrupt.
Test your understanding!
You are developing a system with two ISRs:
CAN_RX_IRQHandlerwith priority 4, which reads messages into a buffer.Motor_Emergency_Stop_IRQHandlerwith priority 1, which immediately halts a motor.
Your main task needs to parse the data in the CAN buffer. You need to protect this access from the CAN_RX_IRQHandler to prevent data corruption. Which method should you use, __disable_irq() or __set_BASEPRI(), and why?
Show answer
You should use __set_BASEPRI() configured to a priority of 4.
Reasoning:
- Setting
BASEPRIto 4 will block theCAN_RX_IRQHandler(priority 4) and any other interrupts with lower or equal urgency (priorities 4, 5, 6...). - Crucially, it will not block the
Motor_Emergency_Stop_IRQHandler, which has a higher urgency (priority 1). The emergency stop remains responsive. - If you used
__disable_irq(), you would block all interrupts, including the emergency stop. This would introduce unacceptable latency for a safety-critical function.
5. Critical Sections in a FreeRTOS Environment
Given your background and the job description, understanding how this low-level hardware is used by an RTOS like FreeRTOS is essential. An RTOS must protect its own internal data structures (like task lists and scheduler states) using critical sections. FreeRTOS provides its own API for this, which intelligently uses PRIMASK or BASEPRI under the hood.
ARM Cortex-M, Interrupts, and FreeRTOS
The same article by Erich Styger is a go-to resource for this topic. It masterfully connects the hardware registers to the FreeRTOS implementation.
Please read the following three sections: 'FreeRTOS port critical sections': Understand how FreeRTOS uses portDISABLE_INTERRUPTS and how its implementation differs between Cortex-M0 and M4/M7 cores. 'Task Critical Sections': See how the user-facing macros taskENTER_CRITICAL() and taskEXIT_CRITICAL() are built upon these port-level functions. 'configMAX_SYSCALL_INTERRUPT_PRIORITY': This is the most important part. Focus on how this configuration constant allows FreeRTOS to use BASEPRI to create a partition between high-priority, non-blocked interrupts and lower-priority interrupts that can interact with the RTOS.
Here's the practical summary for your interview preparation:
- User API: You use
taskENTER_CRITICAL()andtaskEXIT_CRITICAL()to protect your own application's shared data. - Core-Specific Implementation:
- On Cortex-M0/M0+, which lack
BASEPRI, FreeRTOS has no choice but to use__disable_irq(). All interrupts are blocked during kernel critical sections. - On Cortex-M3/M4/M7, FreeRTOS uses
__set_BASEPRI().
- On Cortex-M0/M0+, which lack
- The
configMAX_SYSCALL_INTERRUPT_PRIORITYSetting:- This crucial setting in
FreeRTOSConfig.hdefines the priority value that FreeRTOS writes intoBASEPRIwhen it enters a critical section. - This creates a powerful interrupt partitioning model:
- ISRs with priority below
configMAX_SYSCALL_INTERRUPT_PRIORITY(higher numeric value): These ISRs are allowed to call FreeRTOS API functions (e.g.,xQueueSendFromISR). However, they will be blocked by the RTOS kernel's critical sections. - ISRs with priority above
configMAX_SYSCALL_INTERRUPT_PRIORITY(lower numeric value): These are "real-time" ISRs. They are never blocked by the RTOS kernel, ensuring the lowest possible latency. The trade-off is that they cannot call any FreeRTOS API functions.
- ISRs with priority below
- This crucial setting in
This design is a key reason why FreeRTOS on Cortex-M4/M7 can provide excellent real-time responsiveness while still offering a rich set of OS features.
Interview Preparation
-
Question: "What is a race condition? Give me an embedded systems example."
- Your Answer: "A race condition occurs when the outcome of a system depends on the unpredictable timing of concurrent operations. A classic example is a 16-bit or 32-bit global variable, like a counter, that is incremented in both a main application task and an ISR. Since the increment operation (
variable++) is not atomic at the machine level—it's a load, modify, store sequence—an interrupt occurring between the load and store can cause one of the increments to be lost."
- Your Answer: "A race condition occurs when the outcome of a system depends on the unpredictable timing of concurrent operations. A classic example is a 16-bit or 32-bit global variable, like a counter, that is incremented in both a main application task and an ISR. Since the increment operation (
-
Question: "How would you prevent that race condition? What tools does the ARM Cortex-M core provide?"
- Your Answer: "By implementing a critical section, which ensures the non-atomic operation completes without being interrupted. The Cortex-M core provides two main registers for this. The first is
PRIMASK, which acts as a global switch to disable all configurable interrupts. The second, on M3/M4/M7 cores, isBASEPRI, which allows for more granular control by disabling interrupts only up to a specific priority level."
- Your Answer: "By implementing a critical section, which ensures the non-atomic operation completes without being interrupted. The Cortex-M core provides two main registers for this. The first is
-
Question: "What are the trade-offs between using
PRIMASKandBASEPRI?"- Your Answer: "
PRIMASKis simple but acts like a sledgehammer, disabling all interrupts and potentially increasing the latency for high-priority, real-time events. It's suitable for very short critical sections or on Cortex-M0 cores that lackBASEPRI.BASEPRIis a scalpel, allowing you to block lower-priority interrupts (like a UART) while leaving high-priority ones (like a safety-critical ADC) enabled. This minimizes interrupt latency but requires more careful management of interrupt priorities."
- Your Answer: "
-
Question (Advanced): "How does an RTOS like FreeRTOS handle critical sections on a Cortex-M4, and what does
configMAX_SYSCALL_INTERRUPT_PRIORITYdo?"- Your Answer: "FreeRTOS on a Cortex-M4 cleverly uses the
BASEPRIregister. When the kernel enters a critical section, it doesn't disable all interrupts. Instead, it writes the value ofconfigMAX_SYSCALL_INTERRUPT_PRIORITYtoBASEPRI. This creates a priority ceiling. Any ISR with a priority numerically lower than this value (higher urgency) is not affected by the RTOS kernel at all, guaranteeing low latency. ISRs with a priority at or below this ceiling are temporarily blocked but are permitted to call RTOS API functions, like those ending in...FromISR."
- Your Answer: "FreeRTOS on a Cortex-M4 cleverly uses the
Conclusion
Today you've learned one of the most fundamental concepts in concurrent embedded programming: protecting shared resources. You now understand how a simple C statement can lead to data corruption and how to use the core's hardware features to prevent it.
Key Takeaways:
- Non-atomic operations on shared data can lead to race conditions.
- Critical sections are code segments that must execute without preemption to ensure atomicity.
PRIMASKprovides a global interrupt disable, which is simple but impacts latency for all interrupts.BASEPRI(on Cortex-M3+) provides a priority-based interrupt mask, allowing high-priority ISRs to remain responsive.- RTOSs like FreeRTOS build on these hardware features to provide safe and efficient multitasking, using
BASEPRIto partition interrupts into real-time and non-real-time groups.
Next Lesson Preview:
We've repeatedly mentioned the term "interrupt latency." Disabling interrupts, even for a short time, increases it. In our next lesson, we will formally define interrupt latency, break down all the hardware and software factors that contribute to it, and explore advanced ARM core features like tail-chaining and late-arriving interrupts that are specifically designed to minimize this latency and optimize interrupt handling performance.