ASOS: A new software development paradigm for the Internet of Things – Part 1: Basic building blocks
As the Internet grows, it has extended its reach to include home alarm systems, thermostats, toasters, light bulbs, and other devices that are not normally considered 'intelligent' to create the Internet of Things (IoT). Engineers grapple with creating sustainable electronics while adding ever more functionality to devices that previously never needed them. From an economic point of view, we need to keep these things affordable. From an eco-friendly point of view, we need to keep power consumption down.
One solution is software synthesis that can take software written at a high level of abstraction and create lower-level software code that is optimized for cost and power consumption. The SynthOS tool is one such example and it is now available completely free, for all uses, noncommercial and commercial.
In this article, I discuss the general concepts of RTOS synthesis as implemented in SynthOS, and I also give information about how to create C code at a high level for input to SynthOS. Synthesis is the process of taking a high-level description and turning it into a lower-level description that, in the case of software, can be compiled directly. By working at a higher level, the user does not need to be involved with implementation details.
In particular, this article deals with synthesis of a real-time operating system (RTOS) that is used to control multiple tasks running on an embedded system. We call the resulting synthesized RTOS an application specific operating system or ASOS. In particular, I want to focus on the low power optimization that can be achieved through RTOS synthesis.
Matching RTOSes to design needs
Before RTOS synthesis, an embedded system designer had two options when it comes to an RTOS–purchase an off-the-shelf system from an RTOS vendor or write a custom one. Writing an RTOS, even a simple scheduler, requires operating system expertise in such things as multitasking, mutexes, and interrupt handling.
The designer must be aware of conditions like race conditions, deadlock situations, and priority inversion. Writing and debugging an RTOS requires effort that could be better spent on the core technologies of the embedded system–the proprietary drivers for custom hardware and proprietary algorithms and application tasks running on top of the RTOS.
Using an off-the-shelf RTOS from an RTOS vendor has its advantages, but also its disadvantages. Advantages include the fact that the RTOS is tested and supported by the vendor. Disadvantages include the fact that the system is difficult to debug because much is hidden from the user. It is also requires a large memory since it needs to support all possible users with various kinds of applications, and so it incorporates features that each particular system may not be using.
Although the RTOS is optimized for the particular processor to which it is ported, that is often not the processor that is ideal for the application and is probably not the most low-power processor for the application. This is because RTOS vendors have an interest in porting their RTOS only to high-end processors so that they can charge more for these tools. RTOS vendors, other than the very smallest ones, do not port tools to simple low-power processors. Despite this, systems using 8-bit processors are far more abundant than those using 32-bit or 64-bit processors and the number of these small processor-based systems is growing fast, propelled by the IoT.
Another factor that discourages RTOS vendors from porting their RTOS to smaller processors is that these processors do not have the hardware necessary to assist with task management and task switching. In particular, off-the-shelf RTOSes need a memory manager to keep tasks from interfering with each other.
An off-the-shelf RTOS also relies on context switching hardware in the processor to allow the state of the machine to be swapped out and a new state swapped in during a task switch. Smaller processors do not incorporate these features.
So as an embedded developer you are caught in a dilemma: Writing your own RTOS is complex and time consuming while purchasing an off-the-shelf RTOS can require a processor that is more complex, costly, and power hungry than is otherwise necessary.
The RTOS synthesis solution
RTOS synthesis provides a good way to create an ASOS for these smaller processors even though they lack hardware support for an OS and have limited resources. RTOS synthesis can thought of like a compiler. In the old days, programmers needed to maintain stacks and heaps. Function calls required data to be pushed onto and off of the stack in the correct order or the system would break entirely. And forget about the complexity of trying to maintain a garbage collection mechanism, especially if it needed to be written in assembly language. These skills required a high level of expertise and experience.
Then high level programming languages like ALGOL, FORTRAN, BASIC, and C allowed programmers to simply call functions that returned values. Programmers no longer needed to understand the details of the mechanisms that transferred data between functions or how local data was created and destroyed. RTOS synthesis does the same thing for embedded systems programmers. They can write code at a higher level of abstraction without understanding the mechanisms for transferring data between tasks or how the task scheduler gives processing resources to one task without affecting the others.
To synthesize an embedded system, the programmer writes drivers and application tasks in a high-level language like C. Instead of complex system API calls, the programmer uses “primitives” that look like simple C function calls, as diagrammed on the left in Figure 1. The programmer then enters some system information into the RTOS synthesis tool by way of a text configuration file, shown at the bottom of Figure 1. Detailed information about the particulars of the primitives and system information used for RTOS synthesis are provided below in the following sections.
At the press of a button, the synthesis tool examines all of the source code. Based on the system requirements in the configuration file, the requirements of the application source code (including driver code), and the particular primitives used in the application tasks, the tool strips out the primitives in the source code and replaces them with source code routines to manipulate operating system data structures, set mutexes and semaphores, open and close mailboxes and message queues, and call operating system functions.
Next the synthesis tool writes the task management kernel of the RTOS, again in source code. The kernel includes only the functionality that is required by the applications. Data structures used for global variables, semaphores, mutexes, and task swap areas are defined to have the minimum sizes necessary to support the tasks. The output is low-level source code consisting of an ASOS and the modified application source code controlled by the ASOS, shown on the right of Figure 1.
SynthOS recognizes four types of tasks – call, init, ISR and loop - illustrated in Figure 2, that are each defined below. These task types are strongly tied to the SynthOS primitives and the project file that is described in Part 2 in this series.
Call task A Call task is one that is not executed unless it is specifically started by an executing task. A SynthOS_call() or SynthOS_start() primitive is used to start execution of the Call task.
Init task An Init task is executed once during the initialization of the software, and it can also be started by an executing task just like a Call task, in which case a SynthOS_call() or SynthOS_start() primitive is used to start execution of the Init task. Init tasks cannot contain SynthOS primitives.
ISR task An interrupt service routine (ISR) task is one that processes an interrupt. It only executes when a specific interrupt is received by the processor. It is important that ISR tasks be as short as possible. This is because it is typically necessary to turn off interrupts while processing an ISR task so that it is not interrupted by another ISR task. You do not want to turn off interrupts for very long, because you may miss or delay the processing of an important interrupt. An ISR task should simply set global variables and start a Call task that performs the ISR function by using a SynthOS_start() primitive. A SynthOS_call() primitive must not be used within an ISR task or the ISR task will not complete and the system will hang.
Unlike the other tasks, SynthOS has no particular knowledge of ISR tasks, and so SynthOS primitives cannot be used to start execution, wait for completion, or check the status of an ISR task.
Loop task A Loop task is executed by the task management code that is generated by SynthOS to schedule the task execution, using an algorithm defined by the scheduler that you have selected in the SynthOS project file.
Currently the algorithm can be specified in the project file as a round robin scheduler or a priority scheduler.
The round robin scheduler executes each Loop task in succession, letting each task run until it either becomes blocked waiting for an event or until it voluntarily relinquishes control of the processor by calling SynthOS_sleep(). Each task may be configured to execute on every pass of the scheduling loop, or it may be executed on every n number of passes. This frequency is set in the project file. A diagram of a round robin scheduler is shown in Figure 3. The synthesized code is represented by shaded areas while the user code is represented by unshaded areas.
The priority scheduler allows you to assign priorities to each Call task and Loop task; when the scheduler is ready to schedule another task, it will always select the task with highest priority that is ready to run (i.e., not blocked waiting for an event). If there are several task of highest priority ready to run, the scheduler will select the task that has been waiting the longest. Task priorities are set in the project file. A diagram of a priority scheduler is shown in Figure 4. The synthesized code is represented by shaded areas while the user code is represented by unshaded areas.