“Multitasking has entered the mainstream of embedded systems design”
Andrew Davis, Embedded Systems and Multitasking Software, 1998
Using multi-tasking in embedded systems is a must today, given the advantages of separating overall control and each task activity. Affordable, powerful hardware and off-the-shelf real-time operating systems are the enabling factors that made this the norm today.
While a newly designed multi-tasking application has a clean and comprehensive design, that design will degrade with changes over time. If fixing an issue is assigned to a new programmer, or even someone previously familiar with the application, understanding the as-built behavior can become a challenge. Quite a number of the steps in (re)learning the software are the same as for sequential programs. But a number of key analyses are specific to multi-tasking applications.
A task, identified by a root function in C/C++, is executed over and over. It is essentially like a loop that is coded around the function call. While understanding what happens in the first round of execution might be easy, understanding the behavior of subsequent executions gets more complicated because now the state of all task relevant non-local variables is as they were at the end of the previous execution.
A good number of tasks are there to get data from sensors or send data to controllers. For those, it is key that the data is for the current iteration and not (partially) from older iterations. Here, tracking the flow of the non-local variables through tasks is critical in distinguishing correct from incorrect use (more about this “out-of-step” or “z-variable” flaw in a later article).
Of course, the analysis gets more complicated because a task cannot be considered by itself. Each task interacts with other tasks and the global state of the application through shared variables, synchronization including transfer of control, and events or messages.
The global state of the application is the set of global variables that are used by more than one task. The usage pattern or variable flow between the tasks for these global variables is another key component of understanding the application. Figure 1 shows a sample of that abstraction of the application behavior.
The simplest multi-tasking designs have each task finish before the next starts, perhaps with the exception of interrupts that get handled by their routine if they are not disabled. However, newer systems use explicit synchronization commands that can also transfer control from one task to another through semaphores, events or messages. For these, it is important to get an abstract view of the task interdependence through these synchronization commands. Such as view can be seen in Figure 2, which shows Wait and Post operations for events.
In a single processor system, the operating system will sequentialize the tasks; however, there can still be interrupts occurring at any time causing a different task to execute. In multi-processor systems, it is a given that any mix of tasks can be running concurrently. Trying to consider all of this will be too much for a programmer; fortunately it is sufficient to understand what happens with global variables and in the synchronization commands.
Synchronization commands, such as disabling and enabling interrupts or requesting and releasing semaphores, define a critical region. Identifying where these critical regions exist in the tasks, seeing how far they stretch, and confirming that they are properly balanced are important steps in exploration of the application.
Understanding multi-tasking applications requires a number of abstractions of the source code. Together, they help the programmer form a deeper understanding of the system so that continuous enhancements and fixes keep the application design intact.