This video will explain what is a race condition, what may go wrong if a race condition occurs,
and how to prevent race conditions.
Let me use a simple example to explain what a race condition is.
In this example, the software configures the SysTick timer to generate an interrupt every
half a second.
The SysTick interrupt handler toggles the output of pin PB2.
After setting up SysTick and initializing GPIO, the main program runs a dead loop.
See the previous tutorials for how to set up the system timer, and how to initialize
GPIO as outputs.
Build this program.
Download the program to the board.
Push the reset button on the board and start to run the program.
Let's look at the output signal on PB2.
The output signal is a square wave with a frequency of 1 Hz and a duty of 50%.
This is exactly what we expect because the SysTick timer toggles the output PB2 every
half a second.
Now let's make a minor change to the main function.
Instead of running a dead loop sat the end, the main function repeatedly toggles a different
GPIO pin in the same port, let's say, pin PB3.
Build the program.
Download the program.
Reset and run.
Now, let'ss look at the output.
The output of PB2 has been changed.
And it is not we have expected.
Both the signal period and the duty of the output signal has been changed.
So, what has happened?
A race condition has taken place.
In this example, we have two tasks, which toggles two different outputs concurrently.
The SysTick interrupt handler periodically toggles the output PB2, and the main function
repeatedly toggles the output PB3.
Let's do a quick review on how software toggles a GPIO output.
In a C program, we can toggle an output by performing an exclusive-OR operation with
the bit mask.
This simple C statement is not an atomic operation, which means it cannot complete in a single step.
It takes multiple instructions to complete.
If we translate it into assembly, it involves at least three operations: load, modify, and
store.
First, load the entire current ODR value, then, flip the target bit, and finally, store
the entire updated value back to the ODR register.
Similarly, to toggle the output PB3, we need to perform a load-modify-store sequence of
operations.
The execution of the SysTick interrupt handler and the main function are interleaved over
time.
The following is one possible interleaving.
Suppose the output of both pins is zero at the beginning.
A SysTick interrupt is generated.
The processor stops the execution of the main function and runs the SysTick interrupt handler.
The interrupt handler toggles PB2.
After the interrupt exits, the main program resumes from where it was stopped, and starts
the load-modify-store sequence of operations to toggle PB3.
At the end, both PB2 and PB3 are successfully toggled.
However, things may go terribly wrong.
The following is one example.
If the SysTick interrupt arrives before the main function complete the load-modify-store
sequence, the processor executes the SysTick interrupt handler in response to the interrupt.
After the interrupt exits, PB2 has been successfully toggled.
However, the old output of pin PB2 is also stored in the register r3.
After the interrupt handler completes, the main function resumes the execution.
When the main function executes the store instruction, the old output of PB2 is mistakenly
written to the ODR register.
As result, the output of PB2 is changed back to 0.
At the end, the output of PB2 fails to be toggled.
A race condition is defined as the anomalous behavior of software systems, in which the
result depends on the uncontrollable interleaving of concurrent tasks.
When two or more tasks manipulate a shared variable concurrently, the result of the execution
depends on the particular order in which the manipulation takes place.
A race condition is an error behavior.
A race condition is analog to a horse race.
Two or more computation tasks are in a race.
The result is uncontrollable, depending on who wins.
The fundamental reason behind a race condition is that modifying a variable is not an atomic
operation.
Put a different way, the sequence of load-modify-store operations are not an indivisible operation.
How to prevent a race condition from happening?
The key idea is to ensure that only one task enters a critical section.
A critical section is defined as a section of code, in which a task accesses shared variables.
First, let's take a look at a specific solution available for GPIO.
On STM32, each GPIO port has a BSRR register, which stands for Bit Set Reset Register.
Writing a bit to 0 in BSRR has no impact on the corresponding output.
However, writing a bit to 1 in BSRR can set or reset the corresponding output.
Therefore, the BSRR register allows software to set or reset a group of pins atomically,
while the state of the other pins are preserved.
This shows the implementation of the SysTick interrupt handler and the main function.
This implementation avoids the race condition shown previously.
Read the STM32L4 reference manual to find out more information on BSRR.
Here, we give a generic solution, which avoids a race condition.
We define two functions: enter_ Critical_Section(), and leave_Critical_Section().
Both are inline assembly functions.
The first function disables all maskable interrupts.
On Cortex-M processors, some interrupts, such as bus fault, are not maskable.
The second function enables all maskable interrupts.
These are the new implementation of the interrupt handler and the main function.
In the main function, before updating the shared variable ODR, it disables all maskable
interrupts so that the SysTick interrupt handler cannot enter the critical section.
It enables the interrupts after accessing the shared variable.
Similarly, in the SysTick interrupt handler, it also calls this pair of functions to ensure
that the critical section is executed in a mutually exclusive way.
In this simple example, the SysTick interrupt handler does not have to call this pair of
functions.
However, in a more complex system, calling these two functions to guard the critical
section is necessary.
For example, another interrupt with a higher urgency may preempt the current interrupt
and modify shared variables.
A better generic solution is to program the interrupt mask register BASEPRI to disable
non-urgent interrupts instead of all interrupts.
The function of enter_Critical_Section() takes a none-zero argument, which is called priority.
It programs the BASEPRI register to disable all interrupts, which have a priority value
larger than the input argument.
Note that, on Cortex-M, a higher priority value represents a lower urgency.
In the function of leave_Critical_Section(), clearing the BSEPRI register to 0 enables
all interrupts.
In this example, the main function sets the priority value of the SysTick interrupt to 15.
The function of enter_Critical_Section() disables all interrupts with an interrupt value larger
than 6.
Therefore, if the main function enters the critical section, the SysTick interrupts are
disabled until the main function leaves the critical section.
Accordingly, accesses to the ODR registers are mutually exclusive.
However, all interrupts with a priority value less than or equal to 6 are not disabled.
Please visit the book website for tutorials, and project templates.