Introduction to the RT Kernel

The main ChibiOS RTOS kernel is called RT, the name ChibiOS/RT refers to the RT RTOS component of ChibiOS. The RT kernel is vaguely inspired by Posix, most of the Posix threading mechanisms and API is present in ChibiOS even if there is not a strict compatibility at API level, there is an equivalence at functionality level. The RT kernel is designed around few simple concepts:

  1. In must be fast, execution efficiency is the main requirement.
  2. The code must be optimized for size unless this conflicts with point 1.
  3. The kernel must offer a complete set of RTOS features unless this conflicts with requirement 1.
  4. It must be intrinsically safe. Primitives with dangerous corner cases are simply not allowed. The rule is to guide the user to choose a safe approach if possible.
  5. The code base must be elegant, consistent, readable and rules-driven.
  6. This may be subjective but working with the code must be a pleasant experience.

Coding Conventions

A very important aspect are the coding conventions consistently adopted in the ChibiOS project.

Code Style

The ChibiOS/RT code style is the standard K&R style with a few changes:

  • Tabs are forbidden.
  • Non UTF-8 characters are forbidden.
  • C++ style comments are forbidden.
  • Indentation is 2 spaces.
  • Multiple empty lines are forbidden.
  • Insertion of empty lines is regulated.
  • The code is written in “code blocks”, blocks are composed by:
    • An empty line.
    • A comment describing the block. The comment can be on a single or multiple lines.
    • One or more code lines.
    • Comments on the same line of statements are allowed but not encouraged.
  • Files are all derived from templates.
  • No spaces at the end of lines.

This is a small subset, a more detailed specification for the coding style is available in the web documentation.

Naming Conventions

There is a strong focus on naming conventions in ChibiOS, understanding the naming rules allows to understand the purpose and the context of a code object by simply looking at the name.

API Functions

The name of a function meant to be an API is always composed as follow:

ch<subsystem><verb>[<extra>][Timeout][<I|S|X>]()

Where:

  • ch. Identifies an ChibiOS/RT API.
  • <subsystem>. Identifies the subsystem of the kernel where this function belongs. This part also identifies the object type on which the function operates. For example “Sem” indicates that the function operates on a semaphore_t object which is passed by pointer as first parameter.
  • <verb>. The action performed by this function.
  • <extra>. The extra part of the name or other context information.
  • [Timeout]. If the function is able to stop the execution of the invoking thread and has a time-out capability.
  • <I|S|X>. Optional function class attributes. This attribute, if present, strictly defines the system state compatible with the API function. The “API Classes” section will describe the relationship between function classes and the system state.

For example:

chSemWaitTimeoutS(&sem, 1000);

This is a ChibiOS/RT function “ch” belonging to the semaphores subsystem “Sem”, it performs a Wait operation with Timeout capability. The class of function is “S” which requires the function to be called from thread context within a critical section. You can see that there is a lot of information encoded into an apparently banal function name. A special case of API functions are object initializers which have always the form:

ch<subsystem>ObjectInit()

Note that subsystems in ChibiOS/RT are associated to object types, for example the “Sem” subsystem is about semaphore_t objects. By writing:

chSemObjectInit(&sem, 1)

We are performing an initialization on a semaphore_t object. Initializers are unique functions because can be called from any context even before the kernel initialization. This mean that you can initialize RT global objects before RT is activated by calling chSysInit(). Internal Functions

Non-static functions that are not meant to be API functions must be written entirely in lower case and prefixed by an underscore.

Variables, Fields and Structures

Names must be fully written in lower case, underscore is used as separator.

Types

Simple or structured type follow the same convention, the symbol must be written all in lower case, underscore is used as separator and an “_t” is added at the end. examples:

thread_t, semaphore_t.

Macros

Macros are written fully in upper case. A “CH_” prefix is encouraged but not enforced. A common prefix for grouped macros is mandatory. Examples:

CH_IRQ_EPILOGUE(), THD_FUNCTION().

Architecture

The ChibiOS/RT kernel is internally divided in well defined modules, much care has been taken to keep the subsystems well isolated, this allows to enable or disable each subsystem from the configuration file. Disabling unused subsystems allows for strong savings in code and/or data space.

The kernel is structured as follow:

All kernel services are build around the central scheduler module. The scheduler exports a low level API that allows to build virtually any kind of synchronization primitive, other modules are built using the scheduler services and have no interactions. There are few exceptions:

  • Mailboxes and Binary Semaphores are built using Counter Semaphores (note, both modules are in OSLIB, not strictly par of the RT kernel).
  • Condition Variables are built to work with Mutexes in order to form Monitor constructs.

Other fundamental modules present in the kernel are the Threading module, the Virtual Timers module and the Memory Management optional modules that will be described separately in next chapters.

System States

One important concept in ChibiOS/RT are the so called System States, the global behavior of the system is regulated by an high level state machine:

The states have a strong meaning and must be understood in order to utilize the RTOS correctly:

  • Init. This state represents the execution of code before the RTOS is initialized using chSysInit(). The only kind of OS functions that can be called in the “Init” state are object initializers.
  • Thread. This state represents the RTOS executing one of its threads. Normal APIs can be called in this state.
  • Suspended. In this state all the OS-IRQs are disabled but Fast-IRQs are still served.
  • Disabled. In this state all interrupt sources are disabled.
  • S-Locked. This is the state of critical sections in thread context.
  • I-Locked. This is the state of critical sections in ISR context.
  • IRQ WAIT. When the system has no threads ready for execution it enters a state where it just waits for interrupts. This state can be mapped on a low power state of the host architecture.
  • ISR. This state represents the execution of ISR code.

The states transitions are regulated by physical events like IRQs and call of system APIs. The states marked as double circles are the states where the RTOS APIs can be invoked. Note that the context switch operation is always performed synchronously within a critical section, the switch protocol is: enter critical section, perform switch, leave critical section. The system is never stalled within a critical section. System states can also be seen as abstracted HW states, some architectures may not support some of the states, for example “fast interrupts” and the related functionalities are stubbed.

There are some additional states not directly related to the RTOS activity but still important from the system point of view:

  • Fast ISR. This state represents the execution of ISR code of a fast IRQ source.
  • NMI. This state represents the execution of ISR code of a non-maskable interrupt source.

Invoking RTOS APIs is never allowed in the above states.

API Classes

As mentioned before the ChibiOS/RT APIs are grouped in several classes characterized by a suffix letter. The function class defines the System State where the function can be safely used. Unlike other RTOSes there is not a “Compatibility matrix” for system functions. The functions class defines their behaviour and the compatible context for APIs.

Normal Functions

Normal functions have no suffix and can only be invoked from the “Thread” state unless the documentation of the function states further restrictions or defines a special context.

S-Class Functions

Functions with suffix “S” can only be invoked from the “S-Locked” state, this means that this class of functions are meant to be called in a thread-level critical section. This class of functions can reschedule internally if required.

I-Class Functions

Functions with suffix “I” can be called either in the “I-Locked” state and in the “S-Locked” state. Both ISR-level and thread-level critical sections are compatible with this class. Note that this class of functions never reschedule internally, if called from “S-Locked” state an explicit reschedule must be performed, for example by calling chSchRescheduleS(), before leaving the critical section. Failing to perform a required reschedule is caught by an assertion so the condition is easily detectable during development.

X-Class Functions

This class of functions has no special requirements and can be called from any context where API functions can be called: “Thread”, “S-Locked” and “I-Locked”.

Special Functions

Special functions have no specific suffix but have special execution requirements specified in their documentation.

Object Initializers

This kind of functions are meant for objects initialization and can be used in any context, even before the kernel is initialized. Initialized objects themselves are “passive” until referred by some other function. Note that most kernel objects have also static initializers, macros that allocate objects and initialize them using a static variable initialization.

Thread Working Areas

In ChibiOS/RT threads occupy a single, contiguous region of memory called Thread Working Area. Working areas can be statically or dynamically allocated and are always aligned using the same alignment required for stack pointers, for example, for ARM Cortex-M devices an alignment of 8 bytes is enforced in order to abide to EABI specification. The thread data structures are laid in memory as follow:

The various regions need an explanation:

  • thread_t. It is the structure containing all the fixed thread data. It is the most important object type handled by the kernel. Many API functions take pointers to thread_t as parameters but there must be no assumption that the structure is allocated at the beginning of the working area, this is an implementation detail.
  • Thread Stack. It is the stack area used by the thread, all threads run on private stacks.
  • port_extctx. It is the ISR-pushed stack frame, it is a port-dependent structure.
  • PORT_INT_REQUIRED_STACK. It is a fixed-size memory area required for interrupts servicing. The size of this area is dependent on the underlying CPU architecture and IRQ requirements.
  • port_intctx. This is the context switch context, it is a port-dependent structure.
  • Stack Guard Page. In some ports a guard page is placed below the Stack Base address. This area is made not accessible using HW mechanisms and catches stack overflows.

Note that port_extctx and PORT_INT_REQUIRED_STACK are only pushed in the stack when an IRQ is being serviced. The structure port_intctx is pushed when the thread is switched out and becomes not running. From the application perspective the internal structure of the working area is not relevant, port-related parts do not impact portability.

Declaring Working Areas

Working areas are declared using a special macro:

THD_WORKING_AREA(wa_name, 128);

The above declarations statically allocates a working area with a 128 bytes stack space. Note that a working area cannot be declared using simply an array because the macro takes care of several things:

  • It considers the size of the various regions to be allocated in the working area, the specified size is the size for just the stack space.
  • It enforces the required alignments and any other architecture-dependent restriction.

This means that the final size of the working are is greater than the specified 128 bytes but portability of the declaration is ensured.

Thread States

During their life cycle threads go through several states, the state machine is regulated by the API and events in the kernel:

Note that in ChibiOS/RT there are multiple sleeping states that are indicated on the diagram as a single state. Each synchronization object has its own sleeping states, this is done in order to understand on which kind of object the thread is sleeping on.

Thread Functions

In ChibiOS/RT threads code is exactly equivalent to a C function with a special declaration. Multiple threads, each one with its working area, can concurrently run the same function. One thing must be very clear, each thread has its own stack, this means that:

  • Automatic C variables are always thread-private unless passed by reference to other threads.
  • Static C variables declared inside the function body are private to the function but shared among all threads running that function.
  • Static C variables declared outside the function body are private to the C module but accessible by threads accessing the module functions.
  • Global C variables are always potentially accessible by all threads in the system.

Every variable falls necessarily in one of the above cases.

Declaring Thread Functions

This is a typical thread declaration:

/* Static variables shared among all threads.*/
uint32_t counter;
static unsigned abc[16];
 
/* Working area for the thread.*/
static THD_WORKING_AREA(waCounter, 128);
 
/* Thread function.*/
static THD_FUNCTION(Counter, arg) {
  static uint32_t cows = 0; /* Thread shared variable.*/
  bool condition = true;    /* Thread private variable.*/
 
  while (condition) {
    /* Thread code.*/
    cows++;
    condition = (bool)(cows < 100);
 
    /* Small interval.*/
    chThdSleepMilliseconds(100);
  }
 
  chThdExit((msg_t)cows);
}

The thread counts 100 cows spacing them by 100mS then returns the number of cows, the returned value can optionally be retrieved by another thread. Note that the variable cows is static, if there are more than one thread counting cows then there would be an atomicity problem because the variable is shared among all threads.

Note that thread take a parameter of type void * passed by the spawning thread and returns void. The thread parameter is private of the thread instance so, if multiple threads are running on the same function, each one could have received a different parameter.