
SnipOS Overview:
What is SnipOS?
SnipOS is a lightweight multitasking real-time operating system designed especially for instrumentation and communications applications. It is built from code modules derived from the SNIPPETS project archives. As such, it has a proven track record in multiple applications on a variety of target platforms.
What makes SnipOS different from other RTOS’s?
SnipOS is a cooperative multitasker, not a preemptive multitasker. This has several implications:
Pro:
Since it doesn’t require a scheduler, its run time overhead may be significantly reduced over an equivalent preemptive multitasker. When it’s idle it can put the processor into a designated low power mode and sleep until needed without being awakened by periodic interrupts needed to run a scheduler.
It can be written entirely in C or EC++, obviating the need for most assembly code. This also makes it quite portable. At this writing, it has been successfully used on systems based on 8-, 16-, and 32-bit processors - usually without modification. All that has needed to be changed are ISR’s, which can often be written in C.
Cooperative multitaskers do not require functions (other than ISR’s) to be reentrant.
Cooperative multitaskers generally have fewer (or no) concerns about atomic operations since each task completes before relinquishing control. (Mutex’s may still be required for ISR’s.)
Con:
It is not a general-purpose OS for use in system with numerous asynchronous events. However, it is well suited to systems with limited and well-defined functionality.
It is not well suited for systems with a multitude of tasks, or which require a plethora of task priorities.
How is SnipOS implemented?
SnipOS is a Finite State Machine Operating System (FSMOS). It is designed to manage up to six tasks by default (although this may be changed depending on performance requirements and/or processor bandwidth). Each task in an FSMOS is a finite state machine (FSM). Each task/FSM under SnipOS is defined by a comprehensive state table. Each entry in the table represents a specific event occurring in a defined state.
The tasks in an FSMOS may be rigorously specified in ITU standard Specification and Description Language (SDL). Indeed, FSMOS’s must, by their very nature, be rigorously designed. Hacking code without a coherent overall design simply isn’t an option. Enforcing this level of design discipline typically results in more reliable software. At the very least, it enhances maintainability.
What facilities does SnipOS provide?
Counters: Up/down, recycling, alarming
Timers: Generate events with/without offset and/or automatic reload
Mutex’s: Single bit flags mapped to a control structure
Priorities: Blocked, normal (run once each pass), high (run multiple each pass), low (run every Nth pass)
Event queues: Normal and deferred
Buffer management: Create, release, search
Simple filters: Arithmetic mean, running average, or “Olympic” (discard high and low, average remainder)
Comprehensive queue management: 8→64-bit data queues, event queues, plus native pointer queues
How do I develop a SnipOS application?
SnipOS comes with its own MAIN.C module which should rarely, if ever, need to be modified. It also comes with six TASK_n.C files for all six standard tasks, each with a corresponding TASK_n.H file. The six standard tasks may be defined in any way you need for your system. Six tasks were chosen in order to support, e.g., a main task, a utility task, and two full duplex communications tasks.
Additional tasks may be added by using the existing tasks as templates. Notes:
The maximum number of allowable tasks is defined by the macro TASKS_MAX in SNIPOS.H. This macro is used to size the task descriptor array.
Each task is activated by calling its initialization function in SnipOS’s main.c file. A call to each task’s FSM engine is also included in SnipOS’s main.c file. To use fewer tasks, simply comment out the corresponding initialization function and the call to its FSM engine. With those removed, you may then remove any unused .C and .H files from your project or make files.
Although written in portable standard C, the individual task modules hide most of their details in static variables and functions. The only published interface to each are its initialization and FSM engine functions, as previously described. Each task/FSM has its own static state table and event/message queues. Each task’s event and message queues have a public interface to allow tasks to interact.
Tightly coupled to the tasks but global in scope are the event definitions and buffer management. Events drive state table transitions and the actions of each task. As such, they must be global so that other tasks and ISR’s may place events into a task’s event queue. However, each set of event definitions is specific to a particular task, since true global event definitions would require a proliferation of cases within each task’s state table.
Buffers may be passed in association with events, but buffer management is generalized and available to all tasks in the system. During the system design, required buffer sizes are defined, then the buffer pools initialized. Although C and C++ support dynamic memory allocation, its use is generally a bad idea in a real-time system. 1 Aside from inefficiency, the time to allocate and deallocate memory makes dynamic memory management non-deterministic. As much of a problem as this is in a preemptive multitasking RTOS, it’s a death sentence for a cooperatively multitasking RTOS such as SnipOS. SnipOS therefore treats buffer pool management in a manner analogous with how standard C treats files. A SoSBUFFER type is defined and all access to the buffer pool is via pointers to this type. Buffer pools are initialized via calls to SosInitBuf(). Buffers are obtained by calls to SosGetBuf() and freed by calls to SosFreeBuf(). The system defaults to handle up to 8 buffer pools of different sizes, although this may be increased by changing the value of a macro in SNIPOS.H. Since these buffers use explicit calls to obtain one for use and to subsequently free it, they are known as persistent buffers.
SnipOS also supports circular buffers. As with persistent buffers, circular buffer pools are merely arrays of blocks of memory referenced using pointers. The difference is that circular buffers need not be freed after use. After all available circular buffers have been used in the pool, the next call to obtain one simply reuses the oldest buffer in the pool. Since all SnipOS buffer pools can be used for data of any width, circular buffers may be created for circular buffers of only one data element, e.g. a circular array of single bytes or words.
1 SnipOS does make limited use of dynamic memory allocation internally, using it for small static memory structures that are allocated once and never freed.
Development Example:
As an example, consider the development of an industrial instrument. In order to implement this, we need to acquire sample data at a specific sample rate. After acquiring samples, we need to assemble the data into raw data sets. Once the raw data sets are complete, we need to process the data sets into reduced data sets suited for human readability. This instrument will also have some method for communicating the reduced data sets back to a central location. This may be via TCP/IP, radio modem, or some serial communications channel (e.g. RS-485). It may also have some facility for local communications with a PC or other portable host, e.g. USB. Finally, there will be housekeeping tasks, e.g. verifying data integrity using CRC or maintaining a watchdog timer.
Step 1 – Define the tasks
In the case of the instrument described above, as in most cases, the predefined tasks will be sufficient to do everything the instrument needs to do. In this case we have our main task that acquires and processes data, two full duplex communications channels, each with transmit and receive tasks, and a maintenance task.
Step 2 – Define the states
In the case of the main task, these will include:
An idle state. Each task/FSM needs a “do nothing” state, always state number zero. Sometimes, this task will have a specific non-trivial purpose, e.g. battery charging.
States to acquire and process data. In the example, these will include:
A state to acquire data
A state to build the raw data sets
A state to process the raw data
A state to publish the processed data. In this case, publishing may mean running an LCD or other display, building data packets for transmission, etc.
Note that although the main task could be written as a continuous process with conditional code to handle the functional beaks between actions, it is broken into smaller tasks. This is necessary in a cooperative multitasker such as SnipOS since each task/FSM must run to completion before relinquishing control. However, this also results in smaller, less complex code modules which will, in turn, be more reliable and maintainable.
Step 3 – Define the ISR’s
In the example, these will handle timers and communications interrupts. The ISR’s, in turn, communicate with the tasks by adding events (often with buffers of message data) to the tasks’ event queues.
Step 4 – Define the events for each state
The events may be generated within the ISR’s or within the tasks themselves. All event definitions for every task will include a NoOp event, always defined as zero. In this example, the events may signal that a data set is complete or processing is complete. If you have any state module which requires significant time to complete, you will want to subdivide it into stages for the same reasons noted in step 2 above. As each step is completed, it will enqueue a system event to trigger further processing.
In the communications task(s), events will be generated in accordance with the specific communications protocol being implemented.
Step 5 – Write the state tables
Each task/FSM will have its own state table. This is a two dimensional array where the first dimension is the state and the second is the event. Not all events will have defined actions in a particular state. The actions for such states will tell the task/FSM to do nothing. Where a state has a defined response to a specific event, the table entry will consist of a pointer to a function to execute and the state to be in upon exit.
The specified state may be the same as the current state, or it may trigger a sate change. When the task/FSM executes a state change, it executes exit code for the state being left and entry code the new state. The entry and exit functions are defined in two arrays of function pointers with one function for each state. If no entry or exit function is used, the corresponding table entry will be NULL.
At times, you may need to implement an immediate state change within a code module, overriding the exit state in the state table. For this, there is a force_state() function in UTILS.C.
Step 6 – Size the event queues
Each task/FSM has two queues for pending events. The normal event queue is where events are posted to be processed. There is also a high priority event queue. Originally this was used for deferred events from the event queue which cannot be processed currently for some reason. It was therefore the primary mechanism for requeuing events. Since each task/FSM will attempt to process high priority events before processing normal events, normal events may be given precedence by enqueuing them in the high priority queue.
Step 7 – Define the buffers
Since the example uses two communications channels, it will need message/packet buffers sized for each. You will need to define two arrays of static buffers. Each array will have corresponding macros defining the array’s buffer size and array length.
During initialization, these arrays will be placed under control of the buffer manager. In addition to initialization, the buffer manager provides functions to get a new buffer and to free a non-circular buffer which is no longer needed (as previously noted, circular buffers are never freed). Depending on the communications protocol, you may also need additional queues to hold transmitted messages until receipt of an ACK in case retransmission is required. These queues will be pointer queues pointing to the buffers. The process of defining new queues is similar to that of defining new buffers.
Step 8 – Write the code
The final step is to write the functional code. In the example, this will include entry and exit functions for each state in each task/FSM, code for each entry in each state table, and ISR’s.
Congratulations - you’ve written your first SnipOS project!
Compared to FSMs built using switch() statements, table-based FSMs take longer to write initially, but are much easier to maintain. Need to add functionality? Simply insert new states and associated actions into the table(s). In addition, the execution speed of table-based FSMs is deterministic while FSMs using switch() statements will degrade depending on the length of the switch() statement and the order of the individual states within the switch() statement.
The Future: SnipOS is a work in progress. Although its components have been used on a number of projects in the past, SnipOS as a pre-packaged distribution is new. As this is being written, SnipOS is being used in three new systems and what I learn from each is being incorporated into its design.
FAQ:
| Q: | SnipOS only allows six tasks. Why is this number fixed? | |
| A: | First of all, SnipOS will allow more than six tasks. It is delivered with
six tasks configured, but adding tasks is quite simple. To add a task, simply change the value of the
TASKS_MAX
macro in snipos.h.
After that, you can use any of the existing task_N.c
and associated task_n.h
files as templates for your new task. Finally, each new task will
require the addition of two lines of code to main().
Actually, the original design of SnipOS supported an unlimited number of user-defined tasks. It included a template file for building new tasks and a RegisterTask() function to install them into the system. However, it created problems:
|
|
|
|
||
| Q: | OK, but why set the number of tasks at six? | A: | The short answer is experience. SnipOS evolved as an embedded FSMOS for use in microcontroller-based instruments. Cooperative multitaskers typically don’t scale gracefully, so you don’t want to allow too many tasks in such an environment. Based on experience, such a system may typically need to support two communications protocols and some sort of background maintenance task. Allowing one task each for the transmit and receive services of two protocols, six tasks provided for all the services such an OS may need and is easily within the processor bandwidth of most microcontrollers which comprise SnipOS’s target platforms. |
|
|
||
| Q: | Why does SnipOS use so many queues? | |
| A: | This gets back to the issue of efficiency. The queue management code in
SnipOS has evolved from a package I originally wrote for SNIPPETS
back in the late 80’s. These queue functions have proved to run
rapidly and reliably under a wide variety of conditions, with
performance that is not only deterministic but actually constant
regardless of the size of the queue or the number of items enqueued.
In looking at the various ways to implement the buffer management code, queues were a natural fit with definite performance advantages over other design options (e.g. linked lists - again). The queue package was also (obviously) instrumental from the outset in the design of the SnipOS event queues. | |