Atomic operations, where a single instruction executes in an uninterruptible sequence, are essential for many embedded systems and microcontroller applications. The ARM Cortex M3 processor provides atomic read-modify-write instructions for 32-bit and 16-bit data types. However, single bit access presents challenges for atomicity. This article examines techniques for achieving atomic single bit writes on the Cortex M3.
The Need for Atomic Single Bit Writes
Single bit variables are commonly used for flags and status registers in embedded software. Setting or clearing bits in a control register often enables or disables critical hardware functions. These bit operations must execute atomically, without interruption, to avoid race conditions between interrupt handlers and main program flow.
For example, the main program may test a “transmit enable” bit, see it is clear, then get interrupted before setting it. The interrupt could then test the bit, also see it clear, and start transmitting. This leads to two simultaneous transaction attempts. Atomic test-and-set prevents such problems.
Cortex M3 Limitations for Single Bit Writes
The ARM Cortex M3 provides atomic read-modify-write instructions like LDREX/STREX for 32-bit data types, and LDREXH/STREXH for 16-bit data. However these operate on entire 32-bit or 16-bit words. Single bit atomicity is not directly supported by the architecture.
Single bit variables are typically implemented as bitfields within 32-bit CPU registers. A read-modify-write sequence on the entire 32-bit register is not atomic for the single bit. The processor may interrupt between reading and writing the register, allowing the bit to be changed by an interrupt handler.
Approach 1: Disable Interrupts
The simplest method is to disable interrupts before accessing the bit variable, and re-enable interrupts after. This prevents any interrupt handler code from running during the bit manipulation. For example: // Globally disable interrupts __disable_irq(); // Atomically clear bit 0 of control register CONTROL &= ~(1 << 0); // Re-enable interrupts __enable_irq();
Disabling interrupts is very heavy handed though, and can negatively impact real-time performance. Lengthy critical sections with interrupts disabled can lead to deadlock or data loss scenarios.
Approach 2: Use LDREX/STREX
We can leverage the Cortex M3’s native atomic primitives, LDREX and STREX, to implement single bit atomic writes. The process is:
- Use LDREX to load the 32-bit register containing the bit variable into a temporary variable.
- Manipulate the desired bit in the temporary variable.
- Use STREX to attempt to write the temporary variable back to the original register.
- Check if STREX succeeded, and loop back to LDREX again if not.
For example: uint32_t temp; do { temp = LDREX(CONTROL); temp |= (1 << 0); // Atomic bit set } while (STREX(temp, CONTROL));
The LDREX/STREX commands form an atomic read-modify-write sequence. The processor guarantees the CONTROL register value loaded by LDREX will not be modified before the STREX. The do-while loop handles any simultaneous access by retrying if STREX fails.
This method can be abstracted into a function for setting/clearing single bits: void atomic_bit_set(volatile uint32_t *reg, uint8_t bit) { uint32_t temp; do { temp = LDREX(reg); temp |= (1 << bit); } while (STREX(temp, reg)); }
The limitation of this technique is that interrupts are disabled for the duration of the LDREX/STREX loop. A series of repeated atomic accesses could delay interrupt handling. So care should be taken to minimize use of this method.
Approach 3: Use Locking Primitives
We can implement mutex locks or semaphores in Cortex M3 to provide synchronization for non-atomic code sections. A typical pattern is: // Globally define a mutex mutex_t bit_lock; // Lock mutex before bit manipulation mutex_lock(&bit_lock); CONTROL |= (1 << 0); // Set bit // Unlock mutex after mutex_unlock(&bit_lock);
If interrupts are also disabled during mutex lock/unlock, then this constructs a critical section preventing simultaneous access. The mutex ensures only one processor or context manipulates the bit at once.
Locking adds significant overhead however. A mutex implementation may disable interrupts internally, impacting real-time performance. Common problems like priority inversion must also be considered with locking.
Approach 4: Use Atomic Peripheral Access
For register bits controlling peripheral devices like GPIO, timers, communication buses etc., an alternative is leveraging the peripheral’s own atomic access mechanisms.
For example, the Cortex M3 SysTick timer has an atomic SET_ENABLE register to start/stop counting. The GPIO block has atomic BIT_SET and BIT_CLEAR registers for individual pin manipulation. Using theseatomic peripheral registers prevents external modification of bits during the atomic operation.
This keeps the processor interruptible, avoiding critical sections. But it depends on the peripheral having atomic access support, and is not generally applicable.
Conclusion
For single bit variables not mapped to a peripheral, using LDREX/STREX atomic primitives provides the best balance of robustness and real-time performance on Cortex M3. Lengthy critical sections should be avoided if possible. Atomic peripheral registers can also be leveraged when applicable.
Proper use of atomic access techniques is essential for robust embedded software on Cortex M3 and other ARM processors. Atomicity prevents race conditions and provides deterministic behavior for critical flag variables.
For additional information on implementing atomic operations in ARM Cortex-M3, consult…