So far in the RTOS Revealed series, we have largely discussed the functionality of a kernel in terms of running tasks and communication between them. In this article, we will expand on that and consider what else a kernel can do, which is largely manifest in a number of other API calls that may be available. We will also take a look at what lies beyond a kernel: What makes a kernel become an operating system?
Beyond task scheduling and communication, an RTOS will undoubtedly include functionality – API calls – to control tasks in a selection of ways. We can look at some of the possibilities.
Task Creation and Deletion – In a “dynamic” RTOS, there will be calls to enable the creation of tasks (and other RTOS objects), as and when they are required. Such calls include a wide range of parameters which define the task; for example: code entry point, stack size, and priority. A corresponding task deletion API call enables resources to be release after the task is no longer required.
In a “static” RTOS, the defining parameters of a task are set up in some kind of configuration file at build time.
Task Suspend and Resume – As we have seen, most RTOSes have the concept of a “suspended” state for tasks. This may be entered in a variety of ways. One is for an explicit call to a Task Suspend API function. This may be called by the task itself or by another task. A corresponding Resume Task call enables the task to be a candidate for scheduling again.
Task Sleep – In a real-time system, the control of time is obviously a requirement and can take many forms. A simple one is the ability for a task to “sleep” – i.e. the task is suspended for a specific period of time. When that time has elapsed, the task is “woken” and is a candidate for scheduling again. An API call will generally be available for this purpose. Of course, this functionality is dependent upon the availability of a real-time clock.
Relinquish – When using a Round Robin scheduler, a task may wish to give up control of the processor to the next task in the chain. A Task Relinquish API will be available for this purpose. The task is not suspended – it is available for scheduling when its turn comes around again. With a time-sliced scheduler, it may equally be desirable for a task to relinquish the remainder of its time slot, if it has no further useful work to do immediately. A task relinquish has no logical meaning with a Run to Completion or Priority scheduler.
Task Termination – As we saw in a previous article, in addition to being Ready or Suspended, an RTOS may support other task states. A task may be “Finished”, which means its main function has simply exited – no special API call is required. A task may be “Terminated”, which means that it is not available for scheduling and must be reset in order to be available to run again – see Task Reset below. This requires a specific API call. The availability of these additional task states, the terminology used, and their precise definitions will vary widely from one RTOS to another.
Task Reset – Many RTOSes offer a Task Reset API call, which enables a task to be returned to its start-up condition. It may be in a Suspended state and require a Task Resume in order to become a scheduling candidate.
Task Priority etc. – In a dynamic RTOS, API calls are likely to be available to adjust several task characteristics at runtime. Examples include priority and time-slice duration.
There will more information about task control in a future article, when we look at its implementation.
An RTOS will provide a range of API calls to provide tasks with information about the system, including:
Task information – how many tasks are in the system and details of their configuration and current status.
Information about other kernel objects – how many of each type of object is in the system and object-specific configuration and status information. For example:
- What is the current capacity of a queue to accept more messages?
- How many tasks are suspended on a specific mailbox?
RTOS version information. It is common for an API call to be available that returns such data.
In many applications, it is useful for a program to be able to dynamically grab some memory when it is required, and release it again when it is no longer needed. Embedded software is no exception. However, the conventional approaches are prone to problems, which in desktop applications are unlikely or just inconvenient, but may be disastrous in an embedded system. There are ways to provide this kind of facility, even in a static RTOS.
Problems with malloc() and free()
In a desktop C program, a function can call malloc() , specifying how much memory is required and receive back a pointer to the storage area. Having made use of the memory, it may be relinquished with a call to free() . The memory is allocated from an area called the “heap”. The problem with this approach is that with an uncoordinated sequence of calls to these functions, the heap may easily become fragmented and memory allocations will then fail, even if enough memory is available, because contiguous areas are not large enough. In some systems (like Java and Visual Basic), elaborate “garbage collection” schemes are employed to effect defragmentation. The problems with these schemes are that it can result in very significant, unpredictable runtime delays and the need to use indirect pointers (which is not the way that C works). If malloc() and free() were implemented in a reentrant way (they usually are not) and used by RTOS tasks, the fragmentation would occur very rapidly and system failure would be almost inevitable.
In C++, there are operators, new and delete , which perform broadly the same functions as malloc() and free() . They also suffer from the same limitations and problems.
A solution to providing dynamically available memory for a real-time system, in a reliable and predictable fashion, is to use a memory block-based approach. Such blocks are commonly called “partitions”; partitions may be allocated from a “partition pool”.
A partition pool contains a certain number of blocks, each of the same size. The number and size of the blocks in a partition are determined when the partition pool is created. This may be dynamically, if the RTOS permits, or statically at build time. Typically, an application may include several partition pools offering blocks of various sizes.
If a task requires some memory, it makes an API call requesting a block from a specific pool. If that call is successful, the task will receive a pointer to the allocated block. If the call fails, because no partitions are currently available in the specified pool, the task may receive an error response. Alternatively, the task may be blocked (suspended) until another task frees a block in the partition.
It is usual for a task to simply pass on the pointer to the memory block to any code that needs to utilize it. This results in a problem when the block is no longer needed. If the code only has a pointer to the block, how can it tell the RTOS, in an API call, from which partition pool it wishes to free memory? The answer is that most RTOSes maintain extra data within the allocated block (typically a negative offset from the pointer) that provides the required information. Thus, the API call to free a block only requires its address.
There will be more information about memory partitions in a future article, which describes their implementation in Nucleus SE.
It is self-evident that functionality concerned with the use and control of time are likely to be available in a real-time OS. Exactly what is on offer will vary from one RTOS to another, but we can look at some common facilities. In all cases, a real-time clock is a given for any of these services to function.
System Time – A simple system time, or “tick clock”, is nearly always available. This is simply a counter (generally 32 bit), which is incremented by the real-time clock interrupt service routine, and may be set and read via API calls.
Service Call Timeouts – It is common for an RTOS to allow blocking API calls – i.e. the calling task is suspended (blocked) until the requested service can be provided. Normally that blocking is indefinite, but some RTOSes offer a timeout, whereby the call returns when the timeout expires, if the service continues to be unavailable. API call timeouts are not supported in all RTOSes.
Task Sleep – Tasks normally have an option to suspend themselves for a fixed time period. This was discussed earlier under Task Control .
Application Timers – To enable application tasks to perform timing functions, most RTOSes offer timer objects. These are independent timers, updated by the real-time clock ISR, which may be controlled by API calls. Such calls configure, control and monitor the timer operation. Typically, they may be set for one-shot or automatic restart operation. It is also common for an expiration routine to be supported – a function which is executed each time the timer completes a cycle. There will be more information about application timers in a future article, which describes their implementation.
Interrupts, Drivers and Input/Output
The extent to which various RTOSes are concerned with interrupts and input/output is very variable. Likewise, some RTOSes have a very clearly defined structure for device drivers, which may be quite complex and be an issue when selecting a specific product.
Interrupts are an issue for an RTOS for two reasons:
- Without some safeguards, an interrupt service routine (ISR) will “steal” CPU time, thus compromising the real-time behavior of the RTOS.
- If an ISR makes API calls which affect the task scheduling situation, this must be controlled and the RTOS must have the opportunity to run its scheduling algorithm. An example of such an API call would be one that wakes up a task of higher priority than the one that was running when the interrupt occurred.
Some RTOSes take complete control of all interrupts. A series of API calls are available to allow application ISRs to be “registered”. This approach enables the scheduler to determine exactly when interrupts are allowed and facilitates the use of the majority of API calls from within an ISR.
Nucleus RTOS, for example, has a “low level” and “high level” ISR concept, which enables reliable interrupt control without introducing unnecessary overheads (i.e. increased interrupt latency).
Other RTOSes may take a somewhat “hands off” approach to interrupts, leaving it to the applications programmer to ensure that ISRs are well-behaved. Typically, an additional ISR prefix and suffix are provided to secure API calls made therein.
Nucleus SE takes quite a light-weight approach to interrupt handling, which will be described in a future article.
Most RTOSes define some kind of device driver structure. The details vary from one RTOS to another, but a driver generally consists of two interacting components: some inline code (API calls) and an ISR. Typically other API calls will be available to control and register drivers.
The majority of RTOSes currently on the market are not concerned with higher-level input/output, but some do define stream I/O, which is basically just establishing a connection between appropriate device drivers and standard C functions like printf() .
Historically, RTOSes would often support a “console” – a user interface to the RTOS via a serial line. This was largely for diagnostic and debug purposes. The use of modern, RTOS-aware debuggers has mostly removed the need for such facilities.
RTOSes are usually required to minimize their memory footprint and offer greatest performance. Thus, integrity checking is not a high priority. Using modern, RTOS-aware debug technology, much checking can be done outside of the RTOS itself, but some facilities are also commonly implemented.
API Call Parameter Checks
The parameters to RTOS API calls may be numerous and complex. Hence, errors are always a possibility. Many RTOSes offer runtime parameter checking, returning an error code if a parameter is invalid. Since this involves extra code and runtime performance is adversely affected by such checks, parameter checking is generally a build or configuration option.
For most types of scheduler (other than Run to Completion), each task has its own stack, the size of which is most commonly set on an individual basis. In some RTOSes there is a separate stack for the kernel; in others, the task stack is “borrowed” during an API call. The integrity of stacks is clearly important to the overall reliability of the system. So, it is common for RTOSes to provide some provision for runtime integrity checking. There are a couple of possibilities:
- There may be an API call that returns the amount of stack space remaining for the current or specified task.
- Stacks may be configured with “guard words” at their base. These are set to a unique (normally odd, non-zero) value and checked for over-writing from time to time.
Although not generally supported directly by an RTOS, an application task may be dedicated to verifying the integrity of the whole system. Such a task may be responsible for the servicing of a hardware watchdog. The task may receive periodic input (e.g. a signal setting) from each of the critical tasks. Only when it has confirmed that all tasks have “checked in” does it service the watchdog and prevent a system reset.
An RTOS is rather more than just a kernel, which is where we have focused attention thus far. This is definitely an area where a desktop operating system differs widely from an embedded RTOS. On the desktop, all the additional components will normally be present and installed – all desktop machines have a graphical user interface and very few are not networked in some way, for example. A desktop machine has no real resource limitations; memory, hard drive space and CPU power are available in abundance. In the resource-limited world of embedded systems, such additional components as graphics, networking and a file system may be required, but they must be options and each should be scalable to minimize their memory footprint.
Networking and Connectivity
Most embedded systems are “connected” in some way. It is, therefore, unsurprising that there is a lot of interest in embedded networking solutions and many products on the market.
TCP/IP – The standard protocol for the Internet is TCP/IP, which results in its very wide use, and makes it the obvious choice for many other applications. Typically, TCP/IP is used over Ethernet (IEEE802.3), which traditionally performs at 10Mbps; nowadays 100Mbps is quite common and 1Gbps is coming along. It is also possible to use TCP/IP over other media. For example, PPP (Point to Point Protocol) is an implementation of TCP/IP over a serial line, which has been adapted in recent years for broadband Internet connections.
The version of the fundamental IP protocol, used until recently has been v4 (IPv4). However, this version is becoming outdated, as it is running out of address capacity. The solution is IPv6, which increases address capacity drastically and includes improved security and maintenance facilities. IPv6 is widely available and is demanded for equipment in most countries and for military applications everywhere.
An alternative, related protocol, User Datagram Protocol (UDP), is useful when maximum performance is required. UDP does not offer the reliability and ordering of TCP, but is light-weight and efficient.
USB – Universal Serial Bus is very widely used for devices that require connection to desktop computers. It offers a very easy to use, “plug & play” interface, which is the visible face of some quite complex software technology inside. An embedded device, that is to be connected to a PC, needs to be implemented as a USB “function”, which requires a specific set of software components. If a device is intended to control other USB interfaced equipment (like a PC normally does), it will require a USB “host” software stack. A variant of USB – USB On-The-Go – enables a device to swap between being a host and a function dynamically.
IEEE1394 – Another serial interface standard, which is used particular when large volumes of data need to be transferred between devices quickly (e.g. video data) is IEEE1394 – also known as FireWire and i.Link.
Wireless – The convenience and popularity of various wireless technologies among consumers has led to the demand for such capabilities in embedded devices. Wi-Fi (the IEEE802.11 series) is true networking, allowing both peer to peer and infrastructure topologies over a reasonable range. There is increasing interest in the security of data in such networks, which must be addressed in the software. Other radio technologies – notably Bluetooth and ZigBee – provide short-range, point to point wireless connectivity.
Since networking is very widely demanded, there are many vendors offering solutions. A problem for purchasers is verifying the quality of available products. Unlike an RTOS kernel, fully testing the functionality and performance of a protocol stack is a non-trivial process. Fortunately, validation suites are available – albeit at considerable cost – and the potential buyer can easily ask a vendor which suite(s) they are utilizing.
It is becoming increasingly common for embedded devices to be fitted with a graphical display of some type. This may be a very small, simple, monochromatic LCD (in non-smart cell phone, entry-level MP3 player, intruder alarm system, etc.). At the other end of the scale, a set-top box may have a high-resolution HDTV screen to play with. Such displays require software support, which is fully integrated into the RTOS kernel.
Since it is so common for a graphic display to be accompanied by input devices, their support is generally included in a graphics package. Possibilities include pointing devices (like a mouse), touch screens, keypads and full keyboards.
Graphics may be used in various ways. It may be simply a means to display information – the hazard/information (“matrix”) displays used of freeways is a good example. Or a display might be part of a graphic user interface (GUI) with menus, windows, icons, and the like. In each case, quite different software is required and a graphics package option provided with an RTOS should enable the required flexibility without imposing a disproportionate memory footprint.
When an embedded application needs to store and process considerable quantities of data, it is clearly sensible to organize that data into some kinds of file system. This may be in (RAM) memory, in built-in flash memory, on a flash drive, a conventional hard drive or an optical disk (CD-ROM or DVD-ROM). Again, this capability needs fully RTOS-integrated software support. A file system must be carefully designed to handle the reentrancy requirements of a multi-tasking system.
Adherence to standards is particularly important with file systems. For example, using an MS-DOS compatible disk format enables the developer to leverage a well-proven design and offers complete data exchangeability with desktop systems.
Colin Walls has over thirty years experience in the electronics industry, largely dedicated to embedded software. A frequent presenter at conferences and seminars and author of numerous technical articles and two books on embedded software, Colin is an embedded software technologist with Mentor Embedded [the Mentor Graphics Embedded Software Division], and is based in the UK. His regular blog is located at: http://blogs.mentor.com/colinwalls. He may be reached by email at firstname.lastname@example.org