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.
SynthOS primitives are placed in the codeto perform task management. For example, a SynthOS primitive is used inone task to start execution of another task. SynthOS primitives looklike C language function calls and they are used in the code similar toOS APIs for a traditional RTOS. The next sections explain the syntax foreach of the following SynthOS primitives: SynthOS_call, SynthOS_check, SynthOS_sleep, SynthOS_start and SynthOS_wait .
Notethat SynthOS primitives can be inferred by SynthOS during synthesis. Inthose cases, the primitives are not needed but may be used for claritywithin the code.
SynthOS_call This is a blockingprimitive, which means that execution of the current task is suspendeduntil some later time when a condition is met. In this case, thecondition is the completion of the called task.
SynthOS_call(taskname(a, b, c, …));
retval = SynthOS_call(taskname(a, b, c, …));
retval = taskname(a, b, c, …);
This statement is used to begin execution of another task (taskname in the example) while suspending execution of the current task untilthe called task has completed. In the first example, the called taskdoes not return a value. In the second example, the called task returns avalue.
In the alternative syntax, a SynthOS_call is inferred by SynthOS. The user has specified that task taskname is a Call Task in the SynthOS project file. Because the task returns avalue, the current task cannot continue until the called task, taskname , has completed and returned a value.
Notethat if several tasks call the same Call task, the call requests areserialized in the order in which they occur; only one instance of agiven Call task may execute at a time.
Executionof the called task is begun and execution of the current task issuspended until the called task has completed. To begin execution of atask without suspending the current task, use the SynthOS_start statement.
SynthOS_call can only be used to start a task that has been designated as a Call Task or an Init Task.
SynthOS_call can only be placed in the highest level of a task. It cannot be placedwithin a function or subroutine that is called from a task.
The parameters passed to a task cannot be data structures.
SynthOS_check This is a non-blocking primitive, which means that execution of thecurrent task continues after the SynthOS primitive is executed.
Ifseveral different tasks call the same Call task, this primitive willreturn true if any instance of that Call task is scheduled forexecution. If you would like to know if a specific instance of a Calltask is executing, it is recommended that you invoke that Call task onlyfrom one other task.
flag = SynthOS_check(taskname);
This statement checks whether a Call task is currently executing.
SynthOS_check returns true if the task is executing, false if all instances of the task have completed execution.
SynthOS_check can only check a task that has been designated as a Call Task
SynthOS_check can be placed anywhere in the source code.
SynthOS_sleep This is a blocking primitive, which means that execution of the current task is suspended until the scheduler reschedules it.
Thisstatement is used to give control back to the OS to resume execution ata later time as determined by the OS. It may be thought of as avoluntary “preemption point” that allows the scheduler to reschedule ahigher priority task that is ready to run. Inserting SynthOS_sleep(); primitives in long sections of code can increase the responsiveness of your system.
Executionof the current task is paused. At some later point, the operatingsystem will restart execution of that task at the point after the SynthOS_sleep(); primitive. When this resumption occurs is determined by the operatingsystem according to its scheduling algorithm and the priority of othertasks waiting to execute.
SynthOS_sleep(); can only be placed in the highest level of a task. It cannot be placedwithin a function or subroutine that is called from a task.
SynthOS_start This primitive is a non-blocking primitive, which means that executionof the current task continues after the SynthOS primitive is executed.
SynthOS_start(taskname(a, b, c, …));
taskname(a, b, c, …);
Thisstatement is used to begin execution of another task (taskname in theexample) without suspending execution of the current task.
In the alternative example, a SynthOS_start is inferred by SynthOS. The user has specified that task taskname is aCall Task in the SynthOS project file. Because the task does not return avalue, the current task continues without suspension.
Executionof the called task is begun and execution of the current task continuesconcurrently. To begin execution of a task while suspending the currenttask, use the SynthOS_call statement.
SynthOS_start can only start a task that has been designated as a Call Task or Init Task
SynthOS_start can be placed anywhere in the source code.
The parameters passed to a task cannot be data structures.
SynthOS_wait This is a blocking primitive, which means that execution of the currenttask is suspended until some later time when a condition is met. Inthis example, the task continues execution when taskname is idle orcondition is true, depending on which version of the primitive is used.
Thisprimitive suspends execution of the current task and waits for anothertask to finish executing or for a condition to be true. The conditioncan be any legal C expression using constants and global variables suchas (x == i*j + 5) .
SynthOS_wait(taskname) Execution of the calling task is suspended until the specific Call taskhas completed. Note that this statement does not begin execution oftaskname. To begin execution of taskname and then wait for it to finishexecuting, use the SynthOS_call primitive. If several different tasks call the same Call task, thisprimitive will cause the caller to block until there are no longer anyinstances of that Call task scheduled for execution. If you would liketo block until a specific instance of a Call task completes, it isrecommended that you invoke that Call task from only one place in yourapplication.
SynthOS_wait(condition) Execution of the calling task is suspended until the specifiedcondition is true. Remember that because multiple tasks are runningconcurrently, one event in one task may cause the condition to becometrue, which should cause the calling task to continue executing, butbefore the calling task can execute the next line of its code, anothertask may cause the condition to become false.
SynthOS_wait can only wait on a task that has been designated as a Call Task
SynthOS_wait can only be placed in the highest level of a task. It cannot be placedwithin a function or subroutine that is called from a task.
Bob Zeidman is the president and founder of Zeidman Technologies that develops software tools for hardware/software codesign. He is the author of the books Designing with FPGAs and CPLDs , Verilog Designer's Library , Introduction to Verilog , and Just Enough Electronics to Impress Your Friends and Colleagues . Bob holds an MSEE degree from Stanford and a BSEE and BA in physics from Cornell. His e-mail address is .