Table of Contents
RT Threading
The threading module is responsible for operations related to static threads. One important concept is the current thread, some functions inherently operate or the thread executing the function. The services of the threading module are:
- Declaration.
- Life Cycle.
- Delays.
- Threads References.
- Threads Queues.
- Thread Time.
- Priority Management.
- Round Robin.
This chapter will only address static threads, the optional dynamic allocation of threads will be described in the “RT Dynamic Threading” chapter.
Threads Declaration API
Threads and threads working areas are declared using macros that hide then inner implementation details:
THD_WORKING_AREA() | Statically allocates a working are for a thread. |
THD_FUNCTION() | Declares a thread function hiding eventual compiler-specific keywords. |
Threads Management API
The threads management API takes care of starting, terminating and synchronizing with threads. |
chThdGetSelfX() | Returns a pointer to the current thread. |
chThdCreateStatic() | Creates and starts a static thread. |
chThdCreateI() | Creates a thread without starting it. |
chThdStart() | Starts a thread previously created using chThdCreateI() . |
chThdStartI() | Starts a thread previously created using chThdCreateI() . |
chThdExit() | Terminates the current thread returning a message. |
chThdExitS() | Terminates the current thread returning a message. |
chThdWait() | Waits for the specified thread to terminate then returns its exit message. The thread can be again created if necessary. |
chThdTerminate() | Sets the termination flag in the destination thread. The thread is not deleted but just asked to exit. The termination is cooperative. |
chThdShouldTerminateX() | Returns true if the current thread has the termination flag set. |
chThdTerminatedX() | Returns true if the specified thread is terminated. |
Delays API
One common problem is to insert delays into the execution of threads. ChibiOS/RT provides several solutions for this problem. Thread delays are characterized by:
- The achievable resolution depends on the system tick frequency, if the frequency is 1000Hz then the delays resolution is 1mS.
- The time spent into a delays is used to run other threads, there is not busy waiting involved.
chThdSleep() | Inserts a delay specified as number of system ticks, the delay is approximated to the next tick boundary. |
chThdSleepSeconds() | Inserts a delays specified in seconds, the delay is approximated to the next tick boundary. |
chThdSleepMilliseconds() | Inserts a delays specified in milliseconds. Note that the real resolution depends on system tick, the delay is approximated to the next tick boundary. |
chThdSleepMicroseconds() | Inserts a delays specified in microseconds. Note that the real resolution depends on system tick, the delay is approximated to the next tick boundary. |
chThdSleepUntil() | Sleeps until the system time counter reaches the specified value. |
chThdSleepUntilWindowed() | Special case of chThdSleepUntil() where a time window is specified. |
Threads References API
One common need is to have a thread suspended and waiting for a synchronous event. ChibiOS/RT offers a low level, lightweight mechanism called Thread References. Basically a thread reference is a pointer that can be NULL
or point to a waiting thread.
There are two possible operations:
- Suspend, makes a
NULL
reference point to a thread. - Resume, wakes up a waiting thread resetting the reference to
NULL
again.
The thread reference variable type is thread_reference_t
. Note that, only one thread can suspend on a reference, trying to suspend on a non-NULL
reference is an error caught by an assertion.
One interesting additional feature is that it is possible to pass a message between the entity calling resume and the thread waiting into suspend.
chThdSuspendS() | Suspends the invoking thread on a reference variable. |
chThdSuspendTimeoutS() | Suspends the invoking thread on a reference variable with a timeout specification. |
chThdResume() | Resumes a suspended thread. |
chThdResumeI() | Resumes a suspended thread (I-Class variant). |
chThdResumeS() | Resumes a suspended thread (S-Class variant). |
Threads Queues API
Thread queues are a special kind of FIFO object, the following operations are defined:
- Enqueue, a thread enqueues itself and goes to sleep.
- Dequeue Next, wakes up the next thread in the queue if any.
- Dequeue All, wakes up all threads in the queue.
Dequeue operations are also possible from ISR context. Enqueue operations can have an optional timeout specification. The threads queue variable has type threads_queue_t
. Note that a message can be passed from the thread calling dequeue and the thread(s) exiting from enqueue.
chThdQueueObjectInit() | Initializes a thread queue object. |
chThdQueueIsEmptyI() | Returns true if the queue is empty. |
chThdEnqueueTimeoutS() | Enqueues the calling thread in the queue. |
chThdDoDequeueNextI() | Dequeues the next thread in the queue, the queue is assumed to contain at least one element. |
chThdDequeueNextI() | Dequeues the next thread in the queue, if any. |
chThdDequeueAllI() | Dequeues all thread in the queue, if any. |
Priority Management API
In ChibiOS/RT it is possible for a thread to change its own priority, this is normally not required but there are use cases.
chThdGetPriorityX() | Returns the priority of the current thread. |
chThdSetPriority() | Changes the thread priority level, returns the old priority. |
Round Robin API
Round robin scheduling is only used when there are several CPU-intensive threads placed at the same priority level. Round robin scheduling can work in two distinct ways:
- Preemptive Round Robin. This mode is activated by setting
CH_CFG_TIME_QUANTUM
to a value greater than zero. In this mode the thread using the CPU is preempted by its peers after its time slot has been used. - Cooperative Round Robin. This mode is activated by setting
CH_CFG_TIME_QUANTUM
to zero. In this mode the switch between threads at the same priority level is always cooperative. Cooperative mode is preferable because the kernel becomes slightly more efficient because it does not have to handle time slots.
chThdYield() | The current thread relinquishes its time slice to the next thread in the round robin chain. This function has not effect if the current thread is the only one at the current priority level. |
Examples
Static Thread Declaration
Declaring a thread and its working area.
/* MyThread Working Area.*/ static THD_WORKING_AREA(waMyThread, 128); /* MyThread function.*/ static THD_FUNCTION(MyThread, arg) { /* Thread body code.*/ ...; }
Spawning and Resynchronizing
A LED blinks while the system is busy with an operation. A thread is spawned in order to blink the LED and it is terminated when the operation is finished, the main thread resynchronizes with the result returned by the spawned thread.
/* * Blinker thread. */ static THD_WORKING_AREA(waBlinker, 128); static THD_FUNCTION(Blinker, arg) { unsigned i = 0; while (!chThdShouldTerminateX()) { /* Toggling a LED while the main thread is busy.*/ toggle_led(); /* Delay of 250 milliseconds.*/ chThdSleepMilliseconds(250); /* Counting the number of blinks.*/ i++; } /* Returning the number of iterations.*/ chThdExit((msg_t)i); } /* * Main application. */ int main(void) { /* * System initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ chSysInit(); /* Main code.*/ ...; /* The blinker thread is spawned.*/ thread_t *tp = chThdCreateStatic(waBlinker, sizeof(waBlinker), NORMALPRIO + 1, Blinker, NULL); /* Performing operations while the LED is blinking.*/ ...; /* Stopping the blinker thread and retrieving the number of times the LED toggled.*/ chThdTerminate(tp); msg_t n = chThdWait(tp); /* Continuing.*/ ...; }
Fixed Intervals #1
This example shows a thread blinking a LED at fixed intervals of one second.
static THD_WORKING_AREA(waBlinker, 128); static THD_FUNCTION(Blinker, arg) { while (true) { LED_on(); chThdSleepMilliseconds(500); LED_off(); chThdSleepMilliseconds(500); } }
Note that in the above example there is the strong assumption that the functions LED_on()
and LED_off()
are executed in a finite time which must be less than the tick interval (let's say 1mS). If the function execution time exceeds the tick interval then an error accumulates after each cycle and the blinker period is no more exactly one second.
Fixed Intervals #2
This solution does not suffer of the limitation described in the previous example however it is a bit more complex to understand.
static THD_WORKING_AREA(waBlinker, 128); static THD_FUNCTION(Blinker, arg) { systime_t time = chVTGetSystemTime(); // Current system time. while (TRUE) { time = chTimeAddX(time, TIME_MS2ST(500)); LED_on(); chThdSleepUntil(time); time = chTimeAddX(time, TIME_MS2ST(500)); LED_off(); chThdSleepUntil(time); } }
In this example the execution time of LED_on()
and LED_off()
no more matters as long it is less than 500mS, a much larger margin than the 1mS of the previous example.
In case the deadline is exceeded than the thread would go sleeping for a very long time because it would have to wait for the system time counter to wrap back to zero and reach the specified time value. Considering that the counter is usually 32 bits wide means that the thread would be woken after a very long time, it would be virtually locked.
This method is valid as long we know that the execution time between the calls to chThdSleepUntil()
is strictly less than the required interval. This method is valid for hard realtime systems where deadlines are supposed to never be violated.
Fixed Intervals #3
The following method is tolerant to occasional violation of the calculated deadlines, it is suited to soft realtime use cases because the interval can occasionally exceed the designed interval but it is able to recover.
static THD_WORKING_AREA(waBlinker, 128); static THD_FUNCTION(Blinker, arg) { systime_t prev = chVTGetSystemTime(); // Current system time. while (true) { LED_on(); prev = chThdSleepUntilWindowed(prev, chTimeAddX(prev, TIME_MS2ST(500))); LED_off(); prev = chThdSleepUntilWindowed(prev, chTimeAddX(prev, TIME_MS2ST(500))); } }
Thread Serving an IRQ
This is a common use case, we need a worker thread to be woken when a specific IRQ is triggered.
/* * The reference variable must be initially set to NULL. */ thread_reference_t uart_thread_ref = NULL; /* * ISR serving the UART RX FIFO interrupt. */ CH_IRQ_HANDLER(UART_RX_IRQ) { CH_IRQ_PROLOGUE(); /* If there is data available in the UART RX FIFO.*/ if ((UART->SR & UART_SR_DATA_AVAILABLE) != 0) { /* Entering I-Locked state and resuming the thread, if suspended.*/ chSysLockFromISR(); chThdResumeI(&uart_thread_ref, MSG_OK); chSysUnlockFromISR(); /* Resetting interrupt source.*/ UART->SR &= ~UART_SR_DATA_AVAILABLE; } CH_IRQ_EPILOGUE(); } /* * The thread operates within a critical zone except when: * 1) Data is being processed. * 2) Thread is waiting for data. * This is done in order to check for data availability atomically * considering that there is an ISR involved. The duration of the * critical zone is very short. */ static THD_WORKING_AREA(waUartReceiveThread, 128); static THD_FUNCTION(UartReceiveThread, arg) { chSysLock(); while (true) { /* Makes sure to empty the RX FIFO before suspending.*/ while ((UART->SR & UART_SR_RXFIFO_CNT) > 0) { /* After exiting the critical zone more IRQs may occur but the thread is not suspended so the resume operation would do nothing because the uart_thread_ref reference is NULL.*/ chSysUnlock(); process_data(UART->DR); chSysLock(); } /* The FIFO is now empty, waiting for more data or an interruption in the RX data stream of at least 500mS.*/ msg_t msg = chThdSuspendTimeoutS(&uart_thread_ref, TIME_MS2ST(500)); if (msg == MSG_TIMEOUT) handle_comm_timeout(); } }
Processing Frames
Lets assume to have some kind of networking peripheral, frames are received and are processed by one or more threads. The network interface can go down and the even must be notified to the processing threads.
/* * ISR serving the network interrupt. */ CH_IRQ_HANDLER(RX_FRAME_IRQ) { CH_IRQ_PROLOGUE(); /* If the network is down all the waiting threads are notified.*/ if ((NET->SR & NET_SR_ERROR) != 0 { /* Entering I-Locked state and waking up all threads with a special message.*/ chSysLockFromISR(); chThdDequeueAllI(&rx_frames_queue, MSG_RESET); chSysUnlockFromISR(); /* Resetting interrupt source.*/ NET->SR &= ~NET_SR_ERROR; } /* If there is a frame available in the network interface.*/ if ((NET->SR & NET_SR_FRAME_AVAILABLE) != 0) { /* Entering I-Locked state and waking up one thread, if available, the frame is lost if there are no available threads.*/ chSysLockFromISR(); /* Getting the pointer to the frame buffer and passing it as message.*/ void *frame_ptr = net_get_frame_buffer(); if (chThdQueueIsEmptyI(&rx_frames_queue) { /* Frame dropped, no thread ready to serve it right now.*/ process_dropped_frame(frame_ptr); net_return_frame_buffer(frame_ptr); } else chThdDequeueNextI(&rx_frames_queue, (msg_t)frame_ptr); chSysUnlockFromISR(); /* Resetting interrupt source.*/ NET->SR &= ~NET_SR_FRAME_AVAILABLE; } CH_IRQ_EPILOGUE(); } /* * Frame processing thread, there can be more than one thread executing this code. * Note, 4 working areas are allocated, one for each thread. */ static THD_WORKING_AREA(waProcessFrameThread1, 128); static THD_WORKING_AREA(waProcessFrameThread2, 128); static THD_WORKING_AREA(waProcessFrameThread3, 128); static THD_WORKING_AREA(waProcessFrameThread4, 128); static THD_FUNCTION(ProcessFrameThread, arg) { while (true) { /* Waiting for a network event.*/ chSysLock(); msg_t msg = chThdEnqueueTimeoutS(&rx_frames_queue, TIME_INFINITE); chSysUnlock(); /* Processing the event.*/ if (msg == MSG_RESET) { /* Handling network failure.*/ process_failure(); } else { /* Process incoming frame.*/ process_frame((void *)msg); net_return_frame_buffer((void *)msg); } } } /* * Initialization. */ void main(void) { /* * System initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ chSysInit(); /* Starting the frames processor threads.*/ chThdCreateStatic(waProcessFrameThread1, sizeof(waProcessFrameThread1), NORMALPRIO + 1, ProcessFrameThread, NULL); chThdCreateStatic(waProcessFrameThread2, sizeof(waProcessFrameThread2), NORMALPRIO + 1, ProcessFrameThread, NULL); chThdCreateStatic(waProcessFrameThread3, sizeof(waProcessFrameThread3), NORMALPRIO + 1, ProcessFrameThread, NULL); chThdCreateStatic(waProcessFrameThread4, sizeof(waProcessFrameThread4), NORMALPRIO + 1, ProcessFrameThread, NULL); /* Networking subsystem initialization, processing starts.*/ net_init(); /* Continuing.*/ ...; }
Priority Ceiling
This example implements mutual exclusion on a shared resource using a priority ceiling mechanism, this is commonly found in RTOSes implementing the OSEK specification. The assumption is that the shared resource is given a priority higher than all threads trying to access it.
#define MY_RESOURCE_PRIORITY (NORMALPRIO + 10) void access_resource(void) { prio_t oldprio; /* Checking for priority violations using an assertion.*/ chDbgAssert(chThdGetPriorityX() < MY_RESOURCE_PRIORITY, "resource priority violation"); /* Escalating priority in order to access the resource safely.*/ oldprio = chThdSetPriority(MY_RESOURCE_PRIORITY); /* Safely accessing the resource while running at its priority level.*/ ...; /* Priority return.*/ chThdSetPriority(oldprio); }
Cooperative Round Robin
Several threads execute CPU-intensive code and voluntary relinquish the CPU to their peers after a while. Note that CPU-intensive threads are usually placed at the bottom of the priorities scale.
/* * Processing threads. */ static THD_WORKING_AREA(waProcessThread1, 128); static THD_WORKING_AREA(waProcessThread2, 128); static THD_WORKING_AREA(waProcessThread3, 128); static THD_WORKING_AREA(waProcessThread4, 128); static THD_FUNCTION(ProcessThread, arg) { while (true) { /* Doing some CPU-heavy processing.*/ ...; /* Voluntarily passing control to the next peer.*/ chThdYield(); } } /* * Initialization. */ void main(void) { /* * System initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ chSysInit(); /* Starting the frames processor threads.*/ chThdCreateStatic(waProcessThread1, sizeof(waProcessThread1), NORMALPRIO-10, ProcessThread, NULL); chThdCreateStatic(waProcessThread2, sizeof(waProcessThread2), NORMALPRIO-10, ProcessThread, NULL); chThdCreateStatic(waProcessThread3, sizeof(waProcessThread3), NORMALPRIO-10, ProcessThread, NULL); chThdCreateStatic(waProcessThread4, sizeof(waProcessThread4), NORMALPRIO-10, ProcessThread, NULL); /* Continuing on top of the round robin bunch.*/ ...; }