Here’s the scenario: you’ve got a microcontroller-based synthesizer. The synthesizer has a lot to do. It needs to check inputs like potentiometers, sliders, and keys. It needs to indicate its state to the user with LEDs and displays. And most importantly, it needs to generate noise. The standard method of handling all these tasks is to order them in a loop and handle them one at a time ad infinitum. But, not all of these tasks are as important as the other tasks. How can they be sorted and how can the system ensure that the most important tasks are handled before tasks of lesser importance? Let’s dig in. The most important task a synthesizer handles is generating audio. In a digital synthesizer, the audio generation is a strictly timed process; perfection is required. If the timing is not exact, the audio will contain anomalous artifacts. If the timing is simply inconsistent, it will be noisy, either like a haze has been placed over it or with added static white noise. If it’s consistently or repetitively off-timed, the frequency of the inconsistency will be audibly layered on top of the desired sound. The synthesizer must output samples consistently at the exact appropriate time, not allowing the handling of other tasks to interfere. This means the next sample must be calculated and auxiliary tasks must be processed before the time to output an audio sample arrives, ensuring that the system is totally idle and waiting for the right time.
There are a number of problems with this standard approach. One, this is a heavily manual design process. Assuring the accuracy of the timing throughout the development process, as new features are added, can become a headache. Two, some processor time is guaranteed to be wasted. Not all tasks take the same amount of time to run; some less, some more. When short tasks are handled, the processor will be idle for potentially useful periods of time. If a task grows to the point that it can no longer be safely completed within the time between samples, i.e. the sample period, the timing of the audio generation may be compromised. Neither problem is desirable. As they say, there must be a better way. And there is. It called an RTOS.
What is an RTOS?
An RTOS is a Real-Time Operating System. It’s “Real-Time” because it’s primary concern is that events happen when they should, not in clock cycle, microprocessor-time terms, but in Real Time. Instead of manually managing the task timing, the RTOS handles running the tasks at a designated time and frequency. Tasks in an RTOS environment can be assigned priorities, ensuring that higher priority tasks have bulletproof timing. Any task of higher priority can preempt any lower priority task. Once the higher priority task runs, the system returns to the lower priority task.
For a synthesizer, this works great. Inputs can be checked and processed and the interface generated with relative freedom while still guaranteeing that the audio stream is extremely consistent and artifact free.
Let’s look at what makes up an RTOS a little closer. I’m familiar with FreeRTOS, which is what I’ve been using on my latest projects. As it’s name suggests, it’s free. There are many others. FreeRTOS just happened to be the one that worked well for me. So, my examples will be based on what I’ve developed using FreeRTOS. If you’d like to use my FreeRTOS files, you can download them at the bottom of the page.
There’s quite a bit going on behind the scenes with an RTOS, but, you don’t have to pay too much attention to the man behind the curtain. You also don’t have to deeply modify code to convert it to run in an RTOS environment. You do have to write it in an RTOS specific way, and that is to divide it into tasks and register them with the RTOS. Software is traditionally divided into tasks anyway, so the transition is fairly easy. Let’s take an example.
Suppose we need to blink an LED at a constant rate. If this task were written in standard C, in an oversimplified fashion, the function might look something like this:
static uint8_t u8ledIO;
/*Toggle the I/O pin*/
u8ledIO ^= 0x01;
This function would be called in an endless loop with some timing associated. In the simplest situation, the microcontroller wouldn’t have anything else to do during the time in between toggling the LED and a timer would be used to stall the processor until the correct time arrives.
wait(1);//wait one second
That’s pretty simple and it’s not much more complicated to configure a timer with an interrupt that sets a flag whenever necessary to avoid the problem of idling the processor. The timing would suffer a little if the processor was still busy with other tasks too long after a flag was set, but it could suffice, particularly in this situation.
if(u8LEDBlinkFlag == TRUE)
u8LEDBlinkFlag = FALSE;
//do other stuff…
This method is fairly common, particularly in applications where the timing is not critical. And even if it were, the required code could be run inside an interrupt handler directly. The timing of that function would be pretty accurate, although the length of time to enter an interrupt does vary. An RTOS offers a simple solution to this problem. Let’s examine what an RTOS version of the LED blink code would look like.
void vTaskBlinky(void *pvParameters)
vTaskDelay(1000);//time to delay in milliseconds
It looks suspiciously similar to the while loop because it’s just as simple, yet it’s vastly superior. With this simple routine, the RTOS inserts this routine into the schedule with the specified delay and goes about it’s business running other things. Note how the code is contained inside an endless loop inside the task. In an RTOS environment, all the tasks contain infinite loops. Also, note that the delay is specified in millisecond increments. These increments are called ticks, or time slices, whose length is specified rather simply in a configuration file. A useful length of time in a synthesizer is the sample rate, which gets to the point of having accurate audio output timing. A tick rate of the length of the time between audio samples synchronizes the process, but we also have to be certain that the synthesis routine runs predictably at exactly that time. The mechanism for certainty is priority.
Each task in an RTOS system is given a priority. Any task with a higher priority preempts any task of a lower priority. The priority is assigned when the task is registered with the system. It can also be changed later and there are advanced mechanisms which allow a tasks priority to temporarily change. A little more about that later, but for now lets look at how a task’s priority is set, at least in FreeRTOS.
(const signed char *)”LED Handler”,/*Handle*/
There are a few parameters being passed to the xTaskCreate function that I haven’t mentioned, and won’t dig into, as they access more advanced RTOS features, but there you can see the priority being set. In FreeRTOS, a lower number equates to higher priority. This task has a priority of 3 and this system has a maximum priority of 5, a variable set in the configuration file. So this task preempts tasks of priority 4 and 5 when it needs to run and can be preempted by tasks of 0,1, and 2 priority when they need to run. When this task or any other is preempted, the RTOS saves the task’s state, stops processing it, moves to the higher priority task, and later will return to this task at exactly the point it left off.
One important fact to note is that priority zero is reserved for the idle task. The idle task is run whenever no other task needs to be run. Although we’re trying to avoid idle time with the RTOS, the idle task must run occasionally to clean up memory usage. You can, although it’s not wise, assign a task to priority 0 along with the idle task. It just can’t be a total processor hog, not allowing the Idle Task to get it’s job done.
If you’re a sharp embedded thinker, you may see that not everything is totally rosy in RTOSland. Despite the serious benefits of an RTOS, there are some complications. You could imagine a situation where a task of lower priority is preempted by a task of higher priority right in the middle of calculating some critical value. Then, not knowing any better, the higher priority task may try to access the result of the lower priority task’s operation before it’s actually calculated. This is a problem of data integrity which could compromise the entire system. Fear not though, the good RTOS people have foreseen this problem and have provided a solution.
A mutex, short for mutual exclusion, allows the lower priority task to block the higher task’s access to the critical data until it has finished its calculation. Typically, the mutex conveys a mechanism of priority inheritance, wherein the lower priority task inherits the higher priority tasks priority level until the mutex is released by the formerly lower priority task. Basically, a mutex can only be “held” by one task at a time. The lower priority task “takes” the mutex preventing anyone else from taking it. When the lower priority task is finished with the data, it “gives” the mutex, thereby allowing the mutex to be taken by another task.
The implementation of this type of data control can be one of the more difficult aspects of dealing with an RTOS for a novice. In one instance, I had a task which updated LEDs based on a menu system. The LEDs were connected to shift registers via a SPI bus. The LED task was low priority because I didn’t need to update the LEDs all that often and when I did, it wasn’t critical that it happened in nanoseconds. The SPI task is high priority because it’s transmitting in the MHz frequency range and I couldn’t allow for pauses or interruptions in the transmission. So, the LED task was writing variables indicating LED state which the SPI task would send to the shift registers. Unfortunately, the SPI task occasionally picked up a value which was an intermediate number in a calculation. This was visible on the LEDs with occasional short flickers. So, I created a mutex which prevented this corruption and all was well.
There a number of other critical features of RTOSes which make them worth using. We already discussed how events could be scheduled to run based on time, but they can also be scheduled based on the availability of some data or input. Microcontrollers can spend a lot of time just checking if some event has occurred or if some data has been received. With an RTOS, much like waiting for a time, the microcontroller can freeze a task until some input or data needs a response. The device for implementing this feature in an RTOS is called a queue. A queue is a bank of memory which can be filled and emptied by tasks. One task or interrupt can send information to another task. This allows for communication between tasks and allows the tasks to use data when it’s appropriate.
A semaphore is a feature which allows tasks to be synchronized. It is taken and given like a mutex, but does not involve protecting data. One task can inform another that it’s time to run and vice versa, a task can wait to run until another has. I have used this feature in a number of ways, but a good example is how I synchronize my sequencer. If I have eight tracks, I’ll have eight semaphores and one synchronization routine. The central routine notifies each track that its time to start back at the beginning. Here’s what the code for the synchronization routine looks like:
if(++u8sixteenthNoteCount == NUMBER_OF_SIXTEENTH_NOTES_PER_LOOP)
u8sixteenthNoteCount = 0;
u8currentStep = 0;
Pretty simple, n’est-ce pas? The synchronization routine is the highest priority and the tracking routines are below it, a necessary arrangement because they may need to interrupt one of the tracking routines to inform it that it’s time to begin again. Here’s a puzzle for you: Imagine what happens if a high priority task is waiting for a semaphore from a lower priority task. I’ll leave you to think about that one.
Summing it up
An RTOS is a truly great way to organize code, particularly in situations where timing is of the essence. There are many places to learn more and many RTOSes to use. As with anything in electronics, the best way to learn is to start doing. So get going!
Here is a package of files that I’ve successfully used with STM32 microcontrollers: