Concurrency

With a few exceptions, MCUs are single-core systems. Multitasking is not something that is generally done; instead, there's a single thread of execution with timers and interrupts adding asynchronous methods of operation.

Atomic operations are generally supported by compilers and AVR is no exception. The need for atomic blocks of instructions can be seen in the following cases. Keep in mind that while a few exceptions exist (MOVW to copy a register pair and indirect addressing via X, Y, Z pointers), instructions on an 8 bit architecture generally only affect 8 bit values.

  • A 16 bit variable is byte-wise read in the main function and updated in an ISR.
  • A 32 bit variable is read, modified and subsequently stored back in either main function or ISR while the other routine could try to access it.
  • The execution of a block of code is time-critical (bitbanging I/O, disabling JTAG).

A basic example for the first case is given in the AVR libc documentation:

#include <cinttypes> 
#include <avr/interrupt.h> 
#include <avr/io.h> 
#include <util/atomic.h> 
 
volatile uint16_t ctr; 
 
ISR(TIMER1_OVF_vect) { 
   ctr--; 
} 
 
int main() { 
         ctr = 0x200; 
         start_timer(); 
         sei(); 
         uint16_t ctr_copy; 
         do { 
               ATOMIC_BLOCK(ATOMIC_FORCEON) 
               { 
                     ctr_copy = ctr; 
               } 
         } 
         while (ctr_copy != 0); 
 
         return 0; 
} 

In this code, a 16-bit integer is being changed in the interrupt handler, while the main routine is copying its value into a local variable. We call sei() (SEt global Interrupt flag) to ensure that the interrupt register is in a known state. The volatile keyword hints to the compiler that this variable and how it's accessed should not be optimized in any way.

Because we included the AVR atomic header, we can use the ATOMIC_BLOCK macro, along with the ATOMIC_FORCEON macro. What this does is create a code section that is guaranteed to be executed atomically, without any interference from interrupt handlers and the like. The parameter we pass to ATOMIC_BLOCK forces the global interrupt status flag into an enabled state.

Since we set this flag to the same state before we started the atomic block, we do not need to save the previous value of this flag, which saves resources.

As noted earlier, MCUs tend to be single-core systems, with limited multitasking and multithreading capabilities. For proper multithreading and multitasking, one would need to do context switches, whereby not only the stack pointer of the running task is saved, but also the state of all registers and related.

This means that while it would be possible to run multiple threads and tasks on a single MCU, in the case of 8-bit MCUs such as the AVR and PIC (8-bit range), the effort would most likely not be worth it, and would require a significant amount of labor.

On more powerful MCUs (like the ESP8255 and ARM Cortex-M), one could run real-time OSes (RTOSes), which implement exactly such context switching, without having to do all of the heavy lifting. We will look at RTOSes later in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.35.58