This entry is part 1 of 3 in the series RTOS Alternatives.

As microcontrollers increase in speed and capacity, it’s becoming much more tempting to simply fit a full Real-Time Operating System (RTOS) into a design, without considering other options. This is a shame, because over time a number of alternatives have been developed that can be simple and effective. In this series, I will be discussing some of these, starting with the very simplest — the super loop.
Super Loops
Without an operating system to fall back on, the application is responsible for executing any tasks that must run. The easiest way to do this is just to keep calling task functions repeatedly in an infinite (super) loop:
1 2 3 4 5 6 7 | int main(void) { while (1) { Task_1(); Task_2(); } } |
Note that we need an infinite loop in the main function because there is no OS to return to.
The solution above has the advantage of utter simplicity; it will run the two tasks, one after the other, as fast as they can be run. The disadvantages are that there will be no pre-emption and no accuracy to the timing of the tasks.
Timing can be an important issue for many tasks, such as debouncing a button press, sampling data from an ADC, or executing a control algorithm. These tasks are usually required to run with a fixed period, which we might be able to accomplish in a super loop. Suppose we want to run the tasks above every ten milliseconds, with five milliseconds between them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void Delay(uint32_t milliseconds) { // Setup a hardware timer for the given time // Loop until the delay has been reached. } int main(void) { while (1) { Task_1(); Delay(5); Task_2(); Delay(5); } } |
This might work as described, but only if the two tasks are extremely short. The problem here is the delay can only begin after the associated task has finished. If the tasks each had a constant Worst-Case Execution Time (WCET), then we could simply reduce the delay values appropriately, but this is exceptionally rare and not very practical (any change to the tasks or optimisation settings could invalidate the delay). The problem is shown here, where we assume that both tasks take around two milliseconds to complete:

Clearly we don’t want the task’s period to be tied to its execution time. If the problem is that the delay only starts after the task finishes, can we find a way to start it before the task runs? A sandwich delay does precisely that.
Sandwich Delays
The goal of a sandwich delay is to achieve a delay that is constant from the beginning of the task. The delay function used previously started a timer and looped until the desired time had passed; this time, we can simply try starting the timer before the task runs:
1 2 3 4 5 6 7 8 9 10 11 12 | int main(void) { while (1) { Start_Timer(5); Task_1(); Wait_For_Timer(); Start_Timer(5); Task_2(); Wait_For_Timer(); } } |
It should be obvious where the name comes from — the task is sandwiched by the two delay calls. The timing behaviour is also greatly improved, now providing the ten millisecond periods that we were looking for:

We have the periodic behaviour that we were looking for, but what about the lack of pre-emption? I will address this point at length later in the series, but for now consider what happens if some of our tasks are very long and others are very frequent. In the example above, what if we also needed a task run every millisecond at a higher priority (e.g. to acquire a sample from an ADC)?
A simple sandwich delay cannot provide this behaviour. In fact, we have no support for higher priorities at all; this is where foreground/background scheduling comes into play.
Foreground/Background Scheduling
Foreground/background scheduling is simply the practice of using interrupt handlers to carry out the most important work in your system. Tasks run from the super loop in the main function become the background tasks, while the interrupt handlers become the foreground (higher priority) tasks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Run by hardware, every millisecond void Timer_ISR(void) { sample_buffer[index++] = ADC_Get_Data(); } void main(void) { Init_ISR(); while (1) { Start_Timer(5); Background_Task_1(); Wait_For_Timer(); Start_Timer(5); Background_Task_2(); Wait_For_Timer(); } } |
In the above example, we still have both of our tasks — which now run in the ‘background’. In the foreground we have added a new task that retrieves a sample from an ADC, which should run every millisecond. Because the new task is actually triggered directly in an interrupt handler, it has the ability to pre-empt any background task:

So now we have two priorities and some pre-emption going on. It’s possible to have more than two priorities if you have other interrupts and associated handlers; many systems can do nesting and prioritisation of interrupt handlers. That doesn’t make it a good idea! Having many different interrupt sources just doesn’t scale well — in my experience, predictability drops off rapidly with each additional source beyond the basic system tick timer.

What we really want is a way to get prioritisation and accurate timing for background tasks as well, ideally without sacrificing predictability, simplicity, performance or memory usage in the process. We can do this — easily — with a simple scheduler, which is the topic of the next article in this series.
Tags: C++, Embedded Systems, RTOS, Software