Protecting Critical Sections in Cortex-M Bare Metal Environments

Protecting Critical Sections in Cortex-M Bare Metal Environments

Today I will share with youthree implementations of critical section protection in Cortex-M bare metal environments.
If you have worked with embedded systems and RTOS, you must be familiar with the function codes OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL(). In RTOS, there are often multiple tasks (processes), and in some cases, certain special operations (such as Flash erase under XIP, low-power mode switching) cannot be interrupted at will, or some shared data areas cannot be accessed out of order (Task A is reading while Task B wants to write). In such cases, critical section protection strategies are needed.
The so-called critical section protection strategy simply means that multiple tasks must mutually exclude access to hardware or software critical resources in the system. In an RTOS environment, there are existing critical section protection interface functions, but there is also a need for such protection in bare metal systems. In bare metal systems, critical section protection is mainly related to global interrupt control.
In a previous article “General Triple Interrupt Control Design in Embedded MCUs”, I introduced the third level of interrupt control, which is global interrupt control. Today, I will introduce three critical section protection methods starting from this global interrupt control:

1. Critical Section Protection Test Scenarios

There are basically two scenarios for critical section protection testing. The first scenario involves multiple tasks that are unrelated and do not nest, as shown in the code below. Task1 and Task2 are protected in sequence, so the critical section protection functions enter_critical() and exit_critical() are always executed in strict pairs:

void critical_section_test(void)
{
    // Enter critical section
    enter_critical();
    // Perform protected task 1
    do_task1();
    // Exit critical section
    exit_critical();

    // Enter critical section
    enter_critical();
    // Perform protected task 2, unrelated to task 1
    do_task2();
    // Exit critical section
    exit_critical();
}
The second scenario involves multiple tasks that may be related and may have nesting situations, as shown in the code below. Task2 is a sub-task of Task1, and in this case, you will find that enter_critical() is executed twice before exit_critical() is executed twice.

It is important to note that although sub-task Task3 inside Task1 is not actively protected like sub-task Task2, since the main task Task1 is overall protected, sub-task Task3 should also be protected.

void do_task1(void)
{
    // Enter critical section
    enter_critical();
    // Perform protected task 2, a sub-task of task 1
    do_task2();
    // Exit critical section
    exit_critical(); 

    // Perform task 3
    do_task3();
}

void critical_section_test(void)
{
    // Enter critical section
    enter_critical();
    // Perform protected task 1
    do_task1();
    // Exit critical section
    exit_critical();
}

2. Three Implementations of Critical Section Protection

Now that the critical section protection test scenarios are clear, let’s move on to the implementation of the critical section protection functions enter_critical() and exit_critical():

2.1 Basic Method

The first method is a very basic approach, which directly encapsulates the system global interrupt control functions __disable_irq() and __enable_irq(). In the previous test scenarios, this implementation can effectively handle non-nested task protection, but fails for mutually nested task protection. In the previous test code, Task3 should also be protected, but it is not because the exit_critical() immediately opens the global interrupt after Task2.

void enter_critical(void)
{
    // Disable global interrupts
    __disable_irq();
}

void exit_critical(void)
{
    // Enable global interrupts
    __enable_irq();
}

2.2 Improved Method

Can the basic method be improved? Certainly, we just need to add a global variable s_lockObject to keep track of the number of times the critical section has been entered, as shown in the following code. Each time enter_critical() is called, it will disable global interrupts (ensuring that the critical section is protected) and increment the count, while exit_critical() will only enable global interrupts when the current count is 1 (i.e., when it is not a nested critical section protection situation), otherwise, it will simply decrement the count. The improved implementation can clearly protect Task3 in the previous test code.

static uint32_t s_lockObject;

void init_critical(void)
{
    __disable_irq();
    // Reset counter
    s_lockObject = 0;
    __enable_irq();
}

void enter_critical(void)
{
    // Disable global interrupts
    __disable_irq();
    // Increment counter
    ++s_lockObject;
}

void exit_critical(void)
{
    if (s_lockObject <= 1)
    {
        // Only enable global interrupts when the counter is not greater than 1, and reset the counter
        s_lockObject = 0;
        __enable_irq();
    }
    else
    {
        // If the counter is greater than 1, simply decrement the counter
        --s_lockObject;
    }
}

2.3 Ultimate Method

Although the improved method solves the problem of nested critical section task protection, it adds a global variable and an initialization function, making the implementation less elegant. Additionally, global variables in embedded systems are easily tampered with, posing certain risks. Is there a better implementation?

Of course, there is. This can be achieved using the special mask register PRIMASK of the Cortex-M processor core. Below are the definitions for the PRIMASK register bits (taken from the ARMv7-M manual). Only the lowest bit PM is effective; when PRIMASK[PM] is 1, global interrupts are disabled (raising the execution priority to 0x0/0x80); when PRIMASK[PM] is 0, global interrupts are enabled (no effect on execution priority).

Protecting Critical Sections in Cortex-M Bare Metal Environments

Now you should understand that the functions __disable_irq() and __enable_irq() actually operate on the PRIMASK register. Since the PRIMASK register also retains the status of the global interrupt switch, we can use the PRIMASK value to replace the global variable s_lockObject in the improved method. The code implementation is as follows:

uint32_t enter_critical(void)
{
    // Save current PRIMASK value
    uint32_t regPrimask = __get_PRIMASK();
    // Disable global interrupts (actually set PRIMASK to 1)
    __disable_irq();

    return regPrimask;
}

void exit_critical(uint32_t primask)
{
    // Restore PRIMASK
    __set_PRIMASK(primask);
}
Since the prototypes of enter_critical() and exit_critical() have changed, the usage should also be adjusted accordingly:
void critical_section_test(void)
{
    // Enter critical section
    uint32_t primask = enter_critical();
    // Perform protected task
    do_task();
    // Exit critical section
    exit_critical(primask);

    // ...
}

Appendix: PRIMASK Register Setting Functions in Various IDEs

//////////////////////////////////////////////////////
// Implementation in IAR environment (see cmsis_iccarm.h file)
#define __set_PRIMASK(VALUE)        (__arm_wsr("PRIMASK", (VALUE)))
#define __get_PRIMASK()             (__arm_rsr("PRIMASK"))

//////////////////////////////////////////////////////
// Implementation in Keil environment (see cmsis_armclang.h file)
__STATIC_FORCEINLINE void __set_PRIMASK(uint32_t priMask)
{
  __ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");
}

__STATIC_FORCEINLINE uint32_t __get_PRIMASK(void)
{
  uint32_t result;

  __ASM volatile ("MRS %0, primask" : "=r" (result) );
  return(result);
}
Thus, the three implementations of critical section protection in Cortex-M bare metal environments have been introduced. Thank you for reading.

This article is sourced from the internet and aims to share knowledge for free. All copyrights belong to the original author. If there are any copyright issues regarding this work, please contact me for deletion.

Protecting Critical Sections in Cortex-M Bare Metal Environments

Leave a Comment

×