Semaphore and Mutex on ARM Cortex-M
Once you go deep in microcontroller low-level stuff, concurrent tasks make you need mutex and semaphores for multi-thread synchronization or preventing race conditions. Here is how to implement mutual exclusion and semaphores on ARM Cortex-M3/M4 in C/C++ since it is not available on bare-metal 😆
Exclusive Access
Synchronization between different threads or cores have always been a complex task since it requires special logic. Imagine you have a shared variable between the main thread and interrupt service routine. Let’s say your main thread writes the inverse of bit2 to bit0 and the ISR always toggles bit1. You will end up with corrupted data sooner or later. Consider the following non-atomic read-modify-write operation. The final value is expected to be 00000011 but it becomes 00000001 without intention.
The only solution to this is exclusive memory access. In other words, only one thread should have access to the shared memory at a time, until it completes its RMW operation (or the write operation has to be aborted if a higher priority thread got access before it finishes the cycle). That’s shown below.
However, this solution is only possible with hardware support and this is available on some architectures.
Cortex-M Solution
On ARMv7-M and ARMv7E-M architectures (Cortex-M3/M4/M7) there is exclusive monitor mechanism that exactly adresses RMW problems. It provides LDREX, STREX and CLREX instructions. Considering the illustration above, the read access to the shared memory should be via LDREX and after modification the write-back should be via STREX.
By executing LDREX the exclusive monitor flag is set by the CPU. Then it is checked by the CPU when executing STREX and no write takes place if the flag is cleared. The flag is cleared when an exception is entered, an exception is exited and STREX or CLREX is executed. For further details read here.
Implementation
The code (C++11) is on GitHub, an example project on STM32 Discovery is here and the details are below.
Exclusive Read-Modify-Write
The following C code shows the implementation of pseudo-atomic read-modify-write on Arm Cortex-M.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
bool ReadModifyWrite_add(unsigned char * memory, unsigned char addition){ do { // Read the current value. unsigned char val = __LDREXB(memory); // Try to write. if (0 == __STREXB((val + addition), memory)) { // Data memory barrier instruction. __DMB(); // Written, return success. return true; } } while (true); // Failure. return false; } |
The code basically uses exclusive monitor instructions to add a number to a shared memory byte. It tries to write a new value to the memory and on success STREXB returns zero. Otherwise the same operation is tried again in a loop. If a higher priority thread or interrupt occurs between the lines 4 and 6, the exclusive write will return non-zero and will not change the memory at all.
However, cortex-m-semaphore-mutex project is generalized for more possibilities other than addition.
1 2 3 4 5 |
bool MemoryEx::ReadModifyWrite_if(uint32_t &memory, MemoryModifier modifier, TimeoutProvider timeout){ //... // Do the trick. //... } |
It calls a user-provided modifier function of type std::function<bool (uint32_t&)> with the current memory value. If the modifier modifies the value and returns true, it updates the memory. In addition, a user-provided timeout function std::function<bool ()> is called to break the loop after a certain time.
Testing the Exclusive Monitor
The following code can be used to test the exclusive operation. Enable i.e. SysTick interrupt and put a breakpoint on the line 27 and observe a hit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
bool MemoryEx::ReadModifyWrite_if(uint32_t &memory, MemoryModifier modifier, TimeoutProvider timeout){ uint32_t memory_val; do { // Read the current value. memory_val = __LDREXW(&memory); // Call the modifier. if (modifier(memory_val)) { // Data memory barrier instruction. __DMB(); // Enter randomly. if(std::rand() & 0x00000008){ // Wait for interrupt. __WFI(); } // Try to write. if (0 == __STREXW(memory_val, &memory)) { // Data memory barrier instruction. __DMB(); // Written, return success. return true; }else{ // Put a breakpoint below. __NOP(); } } } // Check for timeout. while (!timeout()); // Failure. return false; } |
This happens because the exclusive monitor flag is cleared by the ISR (no need to call CLREX explicitly).
Mutex and Semaphore (Binary/Counting)
On top of this concept, I provide mutex and semaphore classes. Mutex can be locked and unlocked to protect shared resources i.e. serial port. While threadA has the lock threadB waits until it is unlocked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Create a mutex. memory_exclusive::Mutex mutex; //... void threadA(){ // Lock mutex. if(mutex.Lock(mutex.TryAlways)){ //... // Use the shared resource. //... // Unlock mutex. mutex.Unlock(); } } //... void threadB(){ // Lock mutex. if(mutex.Lock(mutex.TryAlways)){ //... // Use the shared resource. //... // Unlock mutex. mutex.Unlock(); } } |
Semaphore can be used for signaling purposes between different threads or deferred interrupt servicing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Create a semaphore. memory_exclusive::Semaphore semaphore; //... void interrupt_routine(){ // Give semaphore. semaphore.Give(semaphore.TryOnce) // Also possible: semaphore.GiveCounting(...); } //... void main_thread(){ // Loop. while(true){ // Take semaphore. if(semaphore.Take(semaphore.TryAlways)){ //... // Do the job. //... } } } |
Happy coding 🤗