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();
}
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
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).

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);
}
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);
}
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.