rt-kernel is a secure, small, efficient and reliable embedded real-time operating system (embedded RTOS) designed to meet even the hardest real-time demands. It is well-suited for automotive powertrain control applications, real-time audio and video streaming applications and fits most other embedded applications as well. RTOS kernel sizes depend on the processor being used and the feature set. A full-featured RTOS kernel can be as small as 13 kB (ARM, Thumb instruction set) and the footprint can be reduced further to a minimum of 6 kB, if not all RTOS kernel services are required.
RTOS ports are available for many processor families, including ARM, PowerPC and Blackfin®. A port consists of an architecture layer for the processor, and a Board Support Package, or BSP, for the peripherals.
The services provided by the embedded RTOS kernel can be divided into the following areas:
Multitasking is a way of letting several different execution units, or tasks, share a single processor so that all tasks can be said to run in parallel. In reality the RTOS scheduler chooses which task it should run according to the scheduling policy, but because tasks can be swapped in and out of the processor at a high rate, the illusion of parallelism occurs.
Time-triggered tasks are tasks that are activated according to cyclic schedules defined by the application. Time-triggered tasks have priority over normal tasks. The schedule defines a set of time-triggered tasks, along with activation points and deadlines for each task. The RTOS monitors time-triggered tasks for missed deadlines.
Semaphores, mutexes and flags provide synchronisation between tasks, while mailboxes and signals also provide a way of exchanging data.
The rt-kernel embedded RTOS performs error detection when running in the RTOS kernel. Unlike many traditional embedded RTOS, rt-kernel services do not return error codes. Instead, a common error handling routine is invoked whenever the RTOS kernel detects an error condition. This simplifies application code as the tedious and error-prone checking of return values is eliminated.
Interrupt services are for the most part provided by the architecture layers of each processor. The RTOS provides a unified mechanism for enabling, disabling and attaching to interrupts, thereby making it possible to reuse drivers regardless of the system they were originally written for.
An optional event logging mechanism is provided. All application interactions with the RTOS kernel can be logged. A host tool is used to present the collected data. This is normally used during debugging when it can be a great help to visualise the behaviour of the real-time system.
The RTOS provides the functionality needed for creating a dynamic real-time system. Tasks and all kernel objects can be created and destroyed at run-time. This provides a lot of flexibility for many types of applications. A server could for instance create a new task to handle each new session. When the session finishes, the task and all its resources can be returned to the system.
The drawback to a completely dynamic system is the memory fragmentation problem. The memory is said to be fragmented when a request to allocate memory fails because there is no contiguous memory area large enough to satisfy the request, even though there is enough free memory in the memory heap.
The dynamic memory allocation algorithm used by the rt-kernel RTOS is not particularly prone to memory fragmentation, but for some applications it can nevertheless be an issue. For this type of application the RTOS also provides a static memory heap.
An application using the static memory heap must create all resources (tasks, semaphores, etc) when the system starts up. No resources should be dynamically created when the system is running. Likewise, no resources should be destroyed. The static memory heap will not reuse any memory that is returned to it. Fragmentation will not be an issue because once the system is running, no extra memory will be allocated from the heap.
The heap can be further subdivided into pools suitable for allocation of fixed size messages. This allows dynamic allocation of data for signals and mailboxes even in an otherwise completely static systems. A default pool for signals is allocated when the system boots. See Memory Pools for more information.
Kernel resources can also be declared statically. For instance a task can be declared and initialised as follows:
An rt-kernel application consists of a set of tasks. A task is essentially a piece of code that is swapped in and out of the processor as dictated by the RTOS kernel scheduling policy. A task is always in one of the following states:
|RUNNING||The task is the highest priority task and is using the processor.|
|READY||The task is ready to run, but is not currently the highest priority task.|
|WAITING||The task is waiting for an event. Also known as being blocked.|
|DEAD||The task has finished.|
The typical life cycle of a task is shown in the figure. The task is created and becomes READY. When it is the highest priority task in the READY state, it is swapped in and becomes RUNNING. When it waits for an event (also known as blocking), it enters the WAITING state and is swapped out of the processor. In this state it does not consume any processor resources. When the event occurs the task becomes READY (also known as unblocking), but is not swapped in until it is again the highest priority task in the READY state.
Eventually the task may finish, at which point it enters the DEAD state. The RTOS will then remove the task from the system.
Most tasks execute in a loop. A typical pattern is shown below.
Note that the task will alternate between the WAITING and RUNNING states, and may in fact not be running very often (the rate depends on how often the semaphore is signalled). This is an important aspect of real-time tasks; they must not run for too long, as this would prevent lower-priority tasks from running (also known as starving the lower priority tasks).
See Tasks for a detailed description of all RTOS kernel functions related to tasks.
Every task has a stack associated with it. The stack is allocated from the memory heap when the task is created, and is used to store data local to the task.
It is important that the stack is large enough to hold all the data that the task needs to store. Careful analysis is needed to determine the size of the stack because the amount needed varies during execution of the task (it will increase with the number of subroutines the task has called).
The RTOS is able to detect some stack overflows. Specifically, it will detect if the stack has overflown when a RTOS kernel service is called that causes the task to be swapped out. The RTOS does not detect temporary overflows between such events. Note that the task is swapped out whenever an interrupt (e.g. the system tick) occurs. The stack will be checked for overflow at that time.
The RTOS stores the task state on the stack. The stack must always have space for the task state. The stack does not need to accommodate nested interrupts, as the state for these are stored on a separate interrupt stack.
The RTOS creates two tasks when the system is started. The first task it creates is the idle task. This is the task that is swapped in when no other task is ready to run. The idle task must be the lowest priority task in the system. Priority 0 is reserved for the idle task.
The idle function is defined as a weak symbol. You may override it by declaring a function with the same name in your own program. Note however that the function you define must never block. Also pay special attention to the stack usage of the idle function which must never exceed the value defined by the configuration macro CFG_IDLE_STACK_SIZE.
The second task that is created is the reaper task. The purpose of this task is to reclaim the resources allocated by tasks that have finished. When a task enters the DEAD state it will automatically signal the reaper task which will return the resources allocated when the task was started. The reaper task has priority 1 which is the second lowest priority in the system. Application tasks can also use priority level 1 but like all tasks they must enter the WAITING state periodically so the reaper task is allowed to run.
Note that only the resources allocated by the RTOS are returned. This consists of the stack space for the task, and the management area used internally in the RTOS kernel. All other resources allocated by the task itself must be returned before the task finishes, or a memory leak will occur.
The idle task is necessary in all applications. The RTOS kernel must occupy the processor with something if no other task is ready to run at the moment. The reaper task is not strictly necessary in a static system where no resources are ever reclaimed. However, the reaper and idle tasks do not occupy the processor at all if they have no reason to be activated.
You can remove the reaper task from the system by setting the configuration macro CFG_REAPER_STACK_SIZE to zero.
Time-triggered tasks are defined statically according to a set of schedules provided by the user. It is not possible to create time-triggered tasks dynamically at run-time. However, it is possible to change schedules during run-time.
Time-triggered tasks have priority over normal tasks and interrupts. They are started at the times given by the schedule. The schedule also defines a stopping point, or a deadline, by which time the task must have finished executing. The RTOS will call an error routine if a task misses its deadline.
The schedule also defines a set of interrupts that are managed by the time-triggered subsystem. These time-triggered interrupts have priority over time-triggered tasks. Each time-triggered interrupt is allowed to fire only once after it has been activated, to stop a misfiring interrupt from damaging the performance of the system. It is then disabled until the next cycle or until it is explicitly re-enabled later in the schedule.
The figure below shows the different processing levels of the RTOS when the time-triggered subsystem is being used.
Conceptually, time-triggered tasks execute at priority level
The schedules are defined in a text-file such as the one shown below.
The example file would result in two schedules, named sched1 and sched2. The first schedule consists of two tasks (task1 and task2), and two interrupts (IRQ_1 and IRQ_2). task1 starts at tick 0, and must have finished by tick 11. task2 starts at tick 3 and will preempt task1, if it is still running. task2 must have finished by tick
The schedule is written in YAML syntax. YAML is a machine markup language much like XML, but unlike XML it is easily readable by humans. Read more about the syntax at the YAML homepage http://www.yaml.org
Lists are denoted by a dash. Each item of the list is preceded by the dash. All items at the same indentation level belong to the same list. TABS are not permitted for indentation, use spaces only. Key and value pairs are separated by a colon.
The schedule is normally saved in file with a .tt suffix. rt-collab Workbench recognises this file type and when building it will invoke the tt_parse tool. The tool produces a C file with the data needed for the RTOS to run the schedule. The C file is then compiled and linked with the application.
The file contains five main sections:
The includes section is a list of any header files to be included in the C file. The header files would typically contain definitions of any macros used in the schedule.
The stack_size attribute specifies the size of the stack used for time-triggered tasks. All time-triggered tasks share the same stack.
The interrupts section is a list of all time-triggered interrupts. The interrupt definition consists of an interrupt vector, an interrupt service routine (ISR) to handle the interrupt, and an argument to be passed to the ISR.
The tasks section is a list of all time-triggered tasks. The task definition consists of a name, an entry point function, and an argument to be passed to the entry point function.
The schedules section is a list of schedules. All times in the schedules are given in ticks relative to start of the schedule. The schedule definition consists of a name for the schedule and the period of the schedule. This is followed by a list of the events that make up the schedule.
There are two types of events: tasks, and interrupts. A task event consists of a starting and stopping point for the task. In each run of the schedule, the task will be started at the tick given by the starting point of the event. It must have finished executing when the stopping point occurs, or it will have missed its deadline. The task can be restarted once the deadline has passed if needed.
An interrupt event consists of an enabling point for the time-triggered interrupt. Time-triggered interrupts are disabled at the start of the schedule. They must be explicitly enabled if they are to be used during the schedule. An interrupt can be enabled more than once during the run of the schedule.
A timer is a function that is called by the RTOS kernel at intervals defined by the application. The timer function can be called periodically or one time only. The timer can be thought of as a simplified task that may be invoked periodically.
The RTOS will call the timer function from interrupt context. This means that timers are subject to the same restrictions as interrupt service routines. In particular, the timer function must not call any function that may block, such as sem_wait(), mtx_lock(), and others. See Interrupt Services for further details on interrupt services.
Timers offer a simple, low-overhead mechanism for periodic execution compared to tasks. However, a timer is more restricted than a task. Timers and tasks can work in unison; a timer can for instance perform some initial work and then signal a task to perform the remainder of the work from task context instead.
Semaphores are used for synchronisation in real-time systems. A semaphore is essentially a counter with atomic updates. The value of the counter determines if the semaphore is available. In order to proceed a task using the semaphore must first read, then write the counter. The RTOS guarantees that access to the counter is atomic.
A semaphore can be used to guard access to shared resources. The semaphore is initialised to the number of resources it protects. This type of semaphore is called a counting semaphore. A task trying to take the semaphore will be blocked if the value of the counter is less than 1, indicating that there are no free resources, otherwise it will decrease the counter and proceed. When it has finished with the resource it signals the semaphore, and in doing so it increases the value of the counter and unblocks the first task that may have been blocked on the semaphore.
A semaphore that can only have the values 1 and 0 is a binary semaphore and can be used to implement mutual exclusion, however mutexes are optimised for that type of operation and should be used instead.
A semaphore with an initial value of 0 can be used for synchronisation. A typical pattern is to signal a task from an interrupt service routine:
The function sem_wait() is used to wait on the semaphore. The task will continue to run if the semaphore is available, otherwise the task will be blocked until the task becomes available.
The function sem_signal() is used to signal the semaphore, which will unblock the first task that may have been blocked on the semaphore.
See Semaphores for a detailed description of all RTOS kernel functions related to semaphores.
Mutexes are binary semaphores optimised for mutual exclusion. They are typically used to guard a critical region in an application against simultaneous execution by multiple tasks.
Mutexes are recursive. This allows a task to lock a mutex more than once.
The example shows why recursiveness is important. myTask calls two functions that operate on a list. Access to the list is guarded with a mutex because it is in an inconsistent state while the functions are modifying it.
When myTask calls bar, the mutex is locked and no other task can access the list. The bar function performs some operation on the list and then calls foo, perhaps to avoid code duplication. The foo function can be called directly and must therefore also lock the mutex while it operates on the list. myTask has already locked the mutex when it first called bar, so execution continues. When foo finishes, it unlocks the mutex. However, the RTOS keeps track of the number of times the mutex has been locked and will not unlock it at this stage. If it did, myTask would no longer be guaranteed exclusive access to the list when it returns to the bar function. The mutex will not be unlocked until the bar function exits.
The figure shows three tasks. tLow is a low-priority task, tMedium is a medium-priority task, and tHigh is a high-priority task. All three tasks are ready to run. tHigh has the highest priority and is currently swapped in.
tHigh then tries to lock a mutex that is already locked by tLow. Because the mutex is already locked, tHigh enters the WAITING state and is swapped out. The highest priority task ready to run is swapped in - in this case that is tMedium.
The priority inheritance protocol implemented by rt-kernel mutexes solves this problem. When tHigh tries to take the mutex already locked by tLow, the RTOS will temporarily raise the priority of tLow to that of tHigh. tHigh is then swapped out and tLow is swapped in, because it now has a higher priority than tMedium. tLow continues to run until it unlocks the mutex, at which point the RTOS will restore the priority of tLow to its original level. tHigh is now able to lock the mutex and continue execution.
The function mtx_lock() is used to lock a mutex. The task will continue to run if the mutex is available, otherwise the task will be blocked until the mutex becomes available.
The function mtx_unlock() is used to unlock the mutex, which will unblock the first task that may have been blocked on the mutex.
See Mutexes for a detailed description of all RTOS kernel functions related to mutexes.
Flags are used to synchronise a task to external events. Unlike semaphores, it is possible to wait for many events at the same time. A flag object is usually made up of 32 individual flags and the application can choose to wait for any combination of the flags to occur. (The number of flags in a flag object is equal to the number of bits in an integer for the current architecture. The size of an int is 32 bits on most architectures).
The task in the example above waits for two flags. When either one, or possibly both flags become set, the task will be unblocked and will perform the action corresponding to the flag.
The example also illustrates that the flags remain set until explicitly cleared. In the example above both flags could become set at the same time. In that case, the task would first perform the FOO action, then again wait for BAR or FOO. Since BAR was already set the function would return immediately and perform the BAR action.
The function flags_wait_any() is used to wait for any of the individual flags to be set.
The function flags_wait_all() is used to wait for all individual flags to be set.
The function flags_clr() is normally used to clear one or more flags so they can be set again.
The function flags_set() is used to set one or more flags. The task that was waiting for the flags to become set will become READY.
See Flags for a detailed description of all RTOS kernel functions related to flags.
Mailboxes are RTOS kernel objects that can hold messages to be delivered between tasks. Mailboxes have a finite size. The size is configured when the mailbox is created. A task that tries to post a message to a mailbox that is full will be blocked. A task that tries to fetch from a mailbox that is empty will also be blocked.
A mailbox can hold any type of message. The message is just a pointer to a data structure. All tasks that access the mailbox must agree on the representation of the data. The RTOS transfers the value of the pointer between the posting and fetching tasks. The message itself is not copied. The posting task must not use the message after posting it to the mailbox. The fetching task should free the message if it was dynamically allocated.
The example illustrates a task that is used to print to the console. The task will fetch and print to the console any message that is posted to the console mailbox. Other tasks call the function consolePrint to print to the console. This would ensure that all messages printed to the console appear in order. If tasks printed to the console directly, the messages would appear garbled because a lower priority task might be preempted by a higher priority task.
The example is not perfect however, as the calling tasks must be careful not to change the memory area pointed to by msg until it has been printed by the console task. A complete solution would either buffer the data, or implement some form of synchronisation to block the calling task until the message was safely printed.
The function mbox_post() is used to post a message to the mailbox.
The function mbox_fetch() is used to fetch a message from the mailbox.
See Mailboxes for a detailed description of all RTOS kernel functions related to mailboxes.
Signals are messages that can be sent directly from task to task. Unlike mailboxes there is no need to provide a RTOS kernel object to hold undelivered messages.
Signals can represent any kind of data structure. Each type of signal is associated with a number. The number is chosen by the application when the signal is created. The number should be unique, so that no two types of signals share the same number. When a task receives a signal it can decide what course of action to take based on the number identifying the signal type.
Signals can be filtered. A task can choose to receive only certain types of signals. Signals that are sent to the task while the filter is being used will be kept in a queue, and can be received later. This mechanism can for instance be used in a subroutine to only deal with the types of signals that are of interest for the subroutine. Signals that were delivered while in the subroutine can be received by the main task when execution returns from the subroutine.
The type of the signal must be defined so that the first member of the signal data structure is the signal number. This is the only information about the signal that is of interest to the RTOS. The number will be used to match against the filter if one has been applied by the receiving task. The signal number 0 is used to terminate filter lists and is therefore reserved.
When working with signals is it convenient to define all signal numbers and types in a common header file that is shared by all modules in the application (e.g. signals.h).
The following example illustrates a simple client/server application. The server provides two functions: it can add two numbers and return the result, and it can be sent an exit request which will terminate the task.
A convenience function can be provided to encapsulate the add function in a subroutine. The calling task does not need to know that the result is being provided by a separate task. The subroutine is blocking; it will not return until the server task has computed the result of the operation.
As the example shows, signals are dynamically allocated each time they are used, by calling sig_create(). There is an ownership associated with the signal. The signal is owned by the task that created it, until that task sends the signal to another task. The signal must not be modified after it has been sent. The task that receives the signal becomes the new owner, and is responsible for releasing the allocated resource by calling sig_destroy().
Note that in the example, the SIG_ADD signal is reused to send the result of the operation back to the caller. The signal is therefore destroyed by the caller. The SIG_EXIT signal is not reused and is destroyed by the server task.
Unlike many traditional embedded RTOS, rt-kernel services do not return error codes. Errors that are detected by the RTOS kernel are with few exceptions fatal errors. There is very little the application can do to handle the error gracefully. In a production system, the only possible course of action is often to reset the system and start over.
When the RTOS detects an error, it calls a common error handler. The default error handler will halt the system in a busy loop. This is normally used during development, when a debugger is used to load and run code. If an error occurs, the debugger will be in the busy loop, the error code can be inspected, and the debugger backtrace function can be used to find out exactly where in the application the RTOS detected the error.
Alternatively, the application can install its own error handler. The error handler can attempt to handle the error. For instance, if an out of memory error was detected, the application could attempt to free memory if it is known that some memory area can be safely deallocated. If there is no safe way to handle the error, the application should reset the system.
The approach taken by the rt-kernel RTOS also has the beneficial side effect that the application does not have to check return values from RTOS services. This is a tedious and error-prone procedure that can lead to errors going undetected.
The rt-kernel embedded RTOS supports nested interrupts, i.e. interrupts of higher priority can preempt lower priority interrupts. A dedicated interrupt stack is used to store the state of nested interrupts.
The application should call int_connect() to install the interrupt service routine. The RTOS will store the address of the ISR in an internal table. When the interrupt occurs, the RTOS kernel will first swap out the currently running task, then call the ISR.
Lower level interrupts are disabled while the ISR is running. To maintain a low interrupt latency for the system, it is important that all interrupts are handled as quickly as possible. A common design pattern for complex peripherals is to let the ISR clear the interrupt source, then notify a task that handles the higher level processing of the interrupt.
ISRs can call all RTOS services that do not block. They must not call any RTOS service that can block. In other words, it is safe to call sem_signal() to unblock a task waiting on the semaphore. However, the ISR can not call sem_wait().
The following example illustrates how to install an ISR. See Semaphores for an example of how an ISR and a task can cooperate to handle an interrupt.
The function int_connect() is used to install the interrupt service routine. It takes an extra argument that will be passed to the ISR when it is invoked. The argument is usually used to pass a pointer to a driver state structure. By encapsulating the driver state in a structure, it becomes possible to reuse the driver code for more than one instance of the driver.
The rt-kernel embedded RTOS supports dynamic memory allocation from a heap. The standard C memory allocation functions malloc() and free() are supported. The malloc functions executes with interrupts locked and are therefore thread-safe. The heap is created when the system boots and will fill the available RAM.
RTOS kernel objects are allocated from the heap when they are created. See Memory Allocation Strategies for a discussion on how to create a static system.
The RTOS memory pools support allocation of fixed size messages. They are primarily intended for allocating signal and mailbox payloads, but can be used to allocate any object. The sig_create() call allocates signals from the default signal pool, which is created when the system boots.
A memory pool can be used to allocated messages of up to 8 user-definable sizes. The buffer that is returned will be of the smallest available size that will hold the requested number of bytes.
All RTOS kernel events can be logged. An event is in this context defined as any interaction with the RTOS kernel. Examples of events are tasks being swapped in and out, calling RTOS kernel services, interrupts occurring, etc. User-defined events can also be logged.
Event logging is useful as a complement to traditional debugging tools. The event log offers insight into how the system behaves over a period of time, which traditional debuggers can not do. It should be noted that there is an overhead associated with collecting the log data. This may cause systems running under tight margins to behave differently when the events are being logged.
Event logging can be completely disabled by setting the size of the event log buffer to zero. Event logging is always disabled initially, and must be enabled by calling log_enable().
Note that currently the rt-kernel RTOS is only shipped with event logging compiled in. This increases the size of the the RTOS libraries. Contact rt-labs if you have no need for event logging and prefer the space savings instead.
The Board Support Package is responsible for configuring the board and initialising the RTOS. The BSP will normally contain an assembly file that sets up the board so that the RTOS can run, a timer driver, and a driver for the interrupt controller.
The boot sequence is the time from power-on until the RTOS starts executing the first task. The following events take place.
The target's reset vector should be mapped so that it starts to execute the function _start in the assembler file crt0.S. This function is responsible for setting up the embedded target to a point where it can execute C code. At a minimum, this consists of:
Depending on the target it may be necessary to perform additional hardware initialisation. However, in general it is preferable to defer as much initialisation as possible to the bsp init functions, where it can be implemented in C.
The RTOS kernel __init function initialises the the RTOS internal structures, and then calls the bsp_early_init function.
The bsp_early_init function should configure the hardware resources of the target. Typically this involves configuring PLLs, MMUs, interrupt controllers, and so on. Finally, the function must also initialise the heap according to the configuration file config.h.
Next, the idle and reaper kernel tasks are created by the __init function. Multitasking is enabled and the main task is swapped in with interrupts enabled. The entry point of the main task is always the __main function which immediately calls the bsp_late_init function.
The bsp_late_init function should initialise all remaining drivers, starting with the tick timer. At this point, all the RTOS functionality is available. When the drivers have been initialised, execution continues with the startup function which starts the services that were enabled in config.h.
Finally, the main function is called. The main function is the start of the user application and should be defined as a parameter-less function returning int.
The function should return an int to stop the compiler from issuing a warning. The RTOS does not use the return value.
The RTOS can be configured through the file config.h in the BSP. This file contains macros for all configurable parameters.
|CFG_STARTUP_INIT||Enable the startup routine|
|CFG_STATS_INIT||Start the statistics service|
|CFG_TTOS_INIT||Start the time-triggered scheduler|
|CFG_IRQ_STACK_SIZE||Configure the size of the IRQ stack|
|CFG_STACK_ERR_LIMIT||Configure the max stack usage allowed [percent]|
|CFG_MAIN_STACK_SIZE||Configure the size of the main task stack|
|CFG_MAIN_PRIORITY||Configure the priority of the main task|
|CFG_IDLE_STACK_SIZE||Configure the size of the idle task stack. This is normally set to a reasonable value. You will only need to adjust this if you override the idle function|
|CFG_REAPER_STACK_SIZE||Configure the size of the reaper task stack. You may set this to zero to remove the reaper task. This will save some memory in a completely static system|
|CFG_HEAP_TYPE_type||Configure the type of heap, CFG_HEAP_TYPE_STATIC or CFG_HEAP_TYPE_DYNAMIC|
|CFG_SIG_POOL_SIZE||Configure the size of signal pool|
|CFG_SIG_POOL_BLOCKS||Configure the available block sizes in the signal pool|
|CFG_EVENT_LOG_SIZE||Configure the size of the event log|
|CFG_TICKS_PER_SECOND||Configure the number of ticks per second|
Depending on the BSP, there may be additional configuration parameters.
The BSP must be rebuilt after changing the configuration parameters. To rebuild, run make in the top-level bsp directory.