
Mastering External Interrupts with ChibiOS PAL: From Polling to Events
Introduction
Embedded system applications are becoming increasingly complex, and as a result, developers are constantly looking for new ways to make their firmware nimbler, simpler and more efficient. One of the most critical aspects of programming embedded systems is the use of interrupts to handle external events instead of polling. Interrupts are an essential feature of any microcontroller, and they allow the system to respond immediately to a change in the environment.
In this article, we will explore how to use ChibiOS PAL to master external interrupts. Specifically, we will focus on digital inputs and explain how to do polling and detect changes in the status of a GPIO line using APIs such as palReadLine
. We will also examine the limitations of polling and how catching interrupts via hardware can overcome these limitations.
We will introduce the palEvent
API, which allows to efficiently handle external interrupts and implement event-wait and event-callback paradigms. The article will provide plenty of examples and comparison between polling and interrupts. By the end of this article, readers will have a solid understanding of how to use ChibiOS PAL to handle external interrupts in their embedded system applications.
The concept of polling
In our previous article, Mastering GPIOs with ChibiOS PAL: a practical guide, we demonstrated how to use the palReadPad
and palReadLine
APIs to read the status of a digital input line. These APIs provide a return value of either PAL_HIGH
or PAL_LOW
, representing the state of the line at the exact moment the API is executed. This API can be used in conjunction with a thread loop to perform a periodic check of the state of the line as shown in the following example.
while(true) { if(palReadPad(GPIOA, 3) == PAL_HIGH) { /* The line is high. */ } else { /* The line is low. */ } chThdSleepMilliseconds(40); }
This loop polls the status of PA3 continuously every 40ms and depending on the status will enter the if block or the else block. However, what is happening on PA3 is outside of our control, and any changes to the line will occur asynchronously to our thread, as illustrated in the following picture.

This is a clear example of Polling i.e. a technique used in computer programming and electronics to continuously check the status of a device or resource.
Limitation of polling
If our application demands to check this line with a specific time cadency, polling cannot be avoided. But if we are more interested in the change of status of PA3 it is worth noticing that if the line remains in a logic state for less than the polling time (i.e. 40 ms), there is a possibility that we may not detect it. This issue can be resolved by increasing the polling frequency (e.g. reducing the sleep time of the thread). However, this approach can significantly impact the CPU usage, as each time the sleep period elapses, an interrupt is triggered, and ChibiOS must reschedule and switch between threads (context switch).
Let us say that PA3 is normally in the High logical state and whenever our event of interest happens the line gets pulled low for a variable amount of time. As we are interested in the detection of the state transition we may decide to revise our code example and implement something different. The following code implements the detection of the falling edge (transition from High to Low) on the line PA3.
while (true) { if (palReadPad(GPIOA, 3) == PAL_HIGH) { /* If we reach this point the line is in the reset state (High). */ while (palReadPad(GPIOA, 3) != PAL_LOW) { /* The execution will be confined to this internal loop until the line does not get pulled low. */ chThdSleepMilliseconds(40); } /* We reach this point only when a transition high to low happened. */ /* HERE GOES OUR ACTION. */ } chThdSleepMilliseconds(10); }
The edge detection is achieved through cascading conditional checks. At the beginning of the thread loop, if the line PA3 is high, the code enters the if
block and the inner while
loop within a few instructions. In comparison to our millisecond sleep, this process is nearly instantaneous, taking only a few CPU instructions and hence hundreds of nanoseconds. Once the inner loop is entered, the thread checks the line’s status every 40ms and remains in the inner loop until a falling edge transition occurs. When this transition occurs, the condition of the inner loop is invalidated, and we exit the inner loop. At this point, we can execute the associated action for a falling edge, and the external loop restarts. It’s worth noting that until the line stays low, we will not enter the first if
block and a much faster polling frequency (10ms) occurs. This ensures that we do not execute the action twice if the line stays low for a prolonged period. The following figure provides a visual example of the code just explained

The image demonstrates how many context switches are necessary to detect only three edges, highlighting the inefficiency of polling. Additionally, if an event of short duration occurs while the thread is sleeping (e.g., the line goes low for 2 milliseconds), it may not be detected. The shorter the event duration, the higher the likelihood of missing it. To address this issue, one solution is to increase the thread frequency, which comes at the cost of higher CPU utilization.
Handling interrupts with PAL Events: an efficient alternative to polling
Modern microcontrollers provide the ability to capture external interrupts from GPIOs, which enables the detection of falling or rising edges in hardware and associating these events with an Interrupt Service Routine (ISR). ChibiOS PAL provides an API called PAL Events, which simplifies the usage of this feature. This API supports two programming paradigms:
- Event-Callback, which enables the execution of a callback function whenever an event is detected.
- Wait for Event, which allows a thread to wait for an event to occur and synchronize threads.
To use this API, it is necessary to enable at least one of the following switches in halconf.h
.
/*===========================================================================*/ /* PAL driver related settings. */ /*===========================================================================*/ /** * @brief Enables synchronous APIs. * @note Disabling this option saves both code and data space. */ #if !defined(PAL_USE_CALLBACKS) || defined(__DOXYGEN__) #define PAL_USE_CALLBACKS TRUE #endif /** * @brief Enables synchronous APIs. * @note Disabling this option saves both code and data space. */ #if !defined(PAL_USE_WAIT) || defined(__DOXYGEN__) #define PAL_USE_WAIT TRUE #endif
The switch’s name suggests that PAL_USE_CALLBACKS
enables the API related to the Event Callback paradigm, while PAL_USE_WAIT
enables the API related to the Wait for Event paradigm.
Enabling and disabling events
The first set of APIs that we want to introduce allows enabling the event detection:
/** * @brief Pad event enable. * @note Programming an unknown or unsupported mode is silently ignored. * * @param[in] port port identifier * @param[in] pad pad number within the port * @param[in] mode pad event mode * * @api */ #define palEnablePadEvent(port, pad, mode) \ do { \ osalSysLock(); \ palEnablePadEventI(port, pad, mode); \ osalSysUnlock(); \ } while (false)
The API requires the Port and Pad to listen for an event to happen and the mode that specifies if we are listening for a rising edge, a falling edge or both.
/** * @name PAL event modes * @{ */ #define PAL_EVENT_MODE_EDGES_MASK 3U /**< @brief Mask of edges field. */ #define PAL_EVENT_MODE_DISABLED 0U /**< @brief Channel disabled. */ #define PAL_EVENT_MODE_RISING_EDGE 1U /**< @brief Rising edge callback. */ #define PAL_EVENT_MODE_FALLING_EDGE 2U /**< @brief Falling edge callback. */ #define PAL_EVENT_MODE_BOTH_EDGES 3U /**< @brief Both edges callback. */ /** @} */
So if we are looking for once again the falling edge on PA3 we may want to write something like this:
/* Enabling the Event Listening on PA3 for a Falling Edge. */ palEnablePadEvent(GPIOA, 3, PAL_EVENT_MODE_FALLING_EDGE);
The API comes also in a variant where you can use the Line identifier instead of Port and Pad.
/** * @brief Line event enable. * @note Programming an unknown or unsupported mode is silently ignored. * * @param[in] line line identifier * @param[in] mode line event mode * * @api */ #define palEnableLineEvent(line, mode) \ do { \ osalSysLock(); \ palEnableLineEventI(line, mode); \ osalSysUnlock(); \ } while (false)
Note that this API comes also in the I-Class variant. In ChibiOS I-Class functions are safe to be executed in critical sections such as an Interrupt Service Routine (more about the topic on this page of the ChibiOS book).
/** * @brief Pad event enable. * @note Programming an unknown or unsupported mode is silently ignored. * * @param[in] port port identifier * @param[in] pad pad number within the port * @param[in] mode pad event mode * * @iclass */ #define palEnablePadEventI(port, pad, mode) \ pal_lld_enablepadevent(port, pad, mode) /** * @brief Line event enable. * @note Programming an unknown or unsupported mode is silently ignored. * * @param[in] line line identifier * @param[in] mode line event mode * * @iclass */ #define palEnableLineEventI(line, mode) \ pal_lld_enablelineevent(line, mode)
It is important to notice that is not possible to call twice the palEnablePadEvent or palEnableLineEvent on the same line. PAL offers an API to disable events listening. This API is very similar to the previous obviously and doesn’t accept any mode.
/** * @brief Pad event disable. * @details This function also disables previously programmed event callbacks. * * @param[in] port port identifier * @param[in] pad pad number within the port * * @api */ #define palDisablePadEvent(port, pad) \ do { \ osalSysLock(); \ palDisablePadEventI(port, pad); \ osalSysUnlock(); \ } while (false) /** * @brief Line event disable. * @details This function also disables previously programmed event callbacks. * * @param[in] line line identifier * * @api */ #define palDisableLineEvent(line) \ do { \ osalSysLock(); \ palDisableLineEventI(line); \ osalSysUnlock(); \ } while (false)
The same APIs come in the I-Class variant. Before moving forward a note about the STM32 implementation.
On STM32 microcontrollers, there are a limited number of channels available for external interrupts on GPIOs. Specifically, there are only 16 channels, each of which can be configured to listen to only one GPIO port. For example, Channel 0 can be configured to listen to PA0 or PB0 or PC0, and so on, while Channel 5 can be used for one of the GPIOs between PA5, PB5, PC5, and so on. Consequently, it is not possible to enable events on GPIOs with the same pad number simultaneously (e.g. both PA0 and PB0). Therefore, if multiple external interrupt sources are needed, GPIOs with different pad numbers must be used.
Associating a Callback with an event: the Event-Callback paradigm
Once an event is enabled, it is possible to associate a Callback to it with another API.
#if (PAL_USE_CALLBACKS == TRUE) || defined(__DOXYGEN__) /** * @brief Associates a callback to a pad. * * @param[in] port port identifier * @param[in] pad pad number within the port * @param[in] cb event callback function * @param[in] arg callback argument * * @api */ #define palSetPadCallback(port, pad, cb, arg) \ do { \ osalSysLock(); \ palSetPadCallbackI(port, pad, cb, arg); \ osalSysUnlock(); \ } while (false) /** * @brief Associates a callback to a line. * * @param[in] line line identifier * @param[in] cb event callback function * @param[in] arg callback argument * * @api */ #define palSetLineCallback(line, cb, arg) \ do { \ osalSysLock(); \ palSetLineCallbackI(line, cb, arg); \ osalSysUnlock(); \ } while (false) #endif /* PAL_USE_CALLBACKS == TRUE */
In this case, the callback will be executed every time that the event occurs. The following block of code shows an example where a callback is associated with a falling edge on the line PA3.
#include "ch.h" #include "hal.h" #define MY_LINE PAL_LINE(GPIOA, 3U) /* Callback associated to the event. */ static void my_callback(void *arg) { (void)arg; /* HERE GOES OUR ACTION. */ } /* Application entry point. */ int main(void) { /* ChibiOS/HAL and ChibiOS/RT initialization. */ halInit(); chSysInit(); /* Configuring the Line as Input Pull Up.*/ palSetLineMode(MY_LINE, PAL_MODE_INPUT_PULLUP); /* Enabling the event on the Line for a Falling edge. */ palEnableLineEvent(MY_LINE, PAL_EVENT_MODE_FALLING_EDGE); /* Associating a callback to the Line. */ palSetLineCallback(MY_LINE, my_callback, NULL); /* main() thread loop. */ while (true) { palToggleLine(LINE_LED_GREEN); chThdSleepMilliseconds(500); } }
It is worth noticing that the function my_callback
will be executed by an ISR of the external interrupt. Therefore, the code is executed in the ISR context and no blocking function is allowed (e.g. a chSleep
function) and any function of ChibiOS called in here needs to be I-Class or X-Class.
Using PAL events to synchronize threads: the Wait for Event paradigm
The next API that we are going to introduce allows synchronizing threads with an event.
/** * @brief Waits for an edge on the specified port/pad. * * @param[in] port port identifier * @param[in] pad pad number within the port * @param[in] timeout the number of ticks before the operation timeouts, * the following special values are allowed: * - @a TIME_IMMEDIATE immediate timeout. * - @a TIME_INFINITE no timeout. * . * @returns The operation state. * @retval MSG_OK if an edge has been detected. * @retval MSG_TIMEOUT if a timeout occurred before an edge cound be detected. * @retval MSG_RESET if the event has been disabled while the thread was * waiting for an edge. * * @api */ msg_t palWaitPadTimeout(ioportid_t port, iopadid_t pad, sysinterval_t timeout); /** * @brief Waits for an edge on the specified line. * * @param[in] line line identifier * @param[in] timeout operation timeout * @returns The operation state. * @retval MSG_OK if an edge has been detected. * @retval MSG_TIMEOUT if a timeout occurred before an edge cound be detected. * @retval MSG_RESET if the event has been disabled while the thread was * waiting for an edge. * * @api */ msg_t palWaitLineTimeout(ioline_t line, sysinterval_t timeout);
When called in a thread, this API suspends the thread until a specified event occurs or a timeout is reached. The timeout
parameter must be expressed as an Interval in System Ticks. However, this can be challenging as it depends on how the RTOS is configured in chconf.h
. To ensure application portability, ChibiOS\RT provides three macros that automatically convert Seconds, Milliseconds, and Microseconds into an Interval.
/** * @brief Seconds to time interval. * @details Converts from seconds to system ticks number. * @note The result is rounded upward to the next tick boundary. * @note Use of this macro for large values is not secure because * integer overflows, make sure your value can be correctly * converted. * * @param[in] secs number of seconds * @return The number of ticks. * * @api */ #define TIME_S2I(secs) \ ((sysinterval_t)((time_conv_t)(secs) * (time_conv_t)CH_CFG_ST_FREQUENCY)) /** * @brief Milliseconds to time interval. * @details Converts from milliseconds to system ticks number. * @note The result is rounded upward to the next tick boundary. * @note Use of this macro for large values is not secure because * integer overflows, make sure your value can be correctly * converted. * * @param[in] msecs number of milliseconds * @return The number of ticks. * * @api */ #define TIME_MS2I(msecs) \ ((sysinterval_t)((((time_conv_t)(msecs) * \ (time_conv_t)CH_CFG_ST_FREQUENCY) + \ (time_conv_t)999) / (time_conv_t)1000)) /** * @brief Microseconds to time interval. * @details Converts from microseconds to system ticks number. * @note The result is rounded upward to the next tick boundary. * @note Use of this macro for large values is not secure because * integer overflows, make sure your value can be correctly * converted. * * @param[in] usecs number of microseconds * @return The number of ticks. * * @api */ #define TIME_US2I(usecs) \ ((sysinterval_t)((((time_conv_t)(usecs) * \ (time_conv_t)CH_CFG_ST_FREQUENCY) + \ (time_conv_t)999999) / (time_conv_t)1000000))
Additionally to those macros, it is possible to use other special time constants:
/** * @name Special time constants * @{ */ /** * @brief Zero interval specification for some functions with a timeout * specification. * @note Not all functions accept @p TIME_IMMEDIATE as timeout parameter, * see the specific function documentation. */ #define TIME_IMMEDIATE ((sysinterval_t)0) /** * @brief Infinite interval specification for all functions with a timeout * specification. * @note Not all functions accept @p TIME_INFINITE as timeout parameter, * see the specific function documentation. */ #define TIME_INFINITE ((sysinterval_t)-1) /** * @brief Maximum interval constant usable as timeout. */ #define TIME_MAX_INTERVAL ((sysinterval_t)-2) /** * @brief Maximum system of system time before it wraps. */ #define TIME_MAX_SYSTIME ((systime_t)-1) /** @} */
The TIME_INFINITE
constant is particularly useful in this scenario as it allows us to wait indefinitely until the event occurs, as demonstrated in the following example. In this example, we are waiting for both rising and falling edges and synchronizing the thread upon the event. Based on whether the edge is rising or falling, the thread will take different actions.
#include "ch.h" #include "hal.h" #define MY_LINE PAL_LINE(GPIOA, 3U) /* Application entry point. */ int main(void) { /* ChibiOS/HAL and ChibiOS/RT initialization. */ halInit(); chSysInit(); /* Configuring the Line as Input Pull Up.*/ palSetLineMode(MY_LINE, PAL_MODE_INPUT_PULLUP); /* Enabling the event on the Line for a Both the edge. */ palEnableLineEvent(MY_LINE, PAL_EVENT_MODE_BOTH_EDGES), /* main() thread loop. */ while (true) { /* Waiting indefinitely on for an Edge. */ palWaitLineTimeout(MY_LINE, TIME_INIFINITE); /* Checking if it is a falling or a rising edge. */ if(palReadLine(MY_LINE) == PAL_LOW) { palSetLine(LINE_LED_GREEN); } else { palClearLine(LINE_LED_GREEN); } } }
The Wait for Event paradigm is helpful when edge detection triggers a complex task that cannot be executed in an interrupt context and requires a thread. If the timeout is not infinite, it’s important to retrieve the return value of palWaitLineTimeout
. The return value provides valuable information about why the thread was woken up. The following thread loop demonstrates an example where the timeout is set to 500 milliseconds.
/* main() thread loop. */ while (true) { /* Waiting on for an Edge with a timeout of 500ms. */ msg_t ret = palWaitLineTimeout(MY_LINE, TIME_MS2I(500)); /* Checking on the return type. */ if(ret == MSG_OK) { /* An edge has been detected. */ } else if(ret == MSG_TIMEOUT ) { /* A timeout occurred before an edge cound be detected. */ } else { /* ret is MSG_RESET, the event has been disabled while the thread was waiting for an edge. */ } }
Checking if an event is enabled
In a multithreading system where multiple threads often share resources, it is essential to check whether an event is enabled on a specific line at any given time. The following API can accomplish this task.
/** * @brief Pad event enable check. * * @param[in] port port identifier * @param[in] pad pad number within the port * @return Pad event status. * @retval false if the pad event is disabled. * @retval true if the pad event is enabled. * * @xclass */ #define palIsPadEventEnabledX(port, pad) \ pal_lld_ispadeventenabled(port, pad) /** * @brief Line event enable check. * * @param[in] line line identifier * @return Line event status. * @retval false if the line event is disabled. * @retval true if the line event is enabled. * * @xclass */ #define palIsLineEventEnabledX(line) \ pal_lld_islineeventenabled(line)
Note that this is an X-Class API that can be called safely from any context hence also from a callback associated with a PAL Event.
Conclusions
In conclusion, the PAL Event API offered by the ChibiOS/HAL provides a powerful and efficient way to leverage interrupts and asynchronous events on microcontrollers. The Event-Callback and Wait for Event programming paradigms allow for more precise and timely detection of external events compared to the limitations of polling. With the ability to configure and handle up to 16 external interrupt channels on STM32 microcontrollers, developers can easily implement event-driven applications with minimal CPU load and maximum responsiveness. By using the PAL Event API, developers can overcome the limitations of polling and design more efficient and reliable embedded systems.
If you are interested in examples related to polling and events, you may want to check out the article titled Dealing with push-buttons using an STM32. While we plan to revise this article soon, it’s currently the best reference available on PLAY Embedded for this topic.
Hi Rocco, Salvatore,
I arrived at your website on a tangent from a project involving a STM32F405 MCU running ChibiOS(The VESC project).
While pouring through the ChibiOS documentation/website for a few hours, I realized the documentation was too detailed, too complicated, for me to best learn the fundamentals of Chibi. I wanted example code, heavily explained, and beginner friendly. Your PlayEmbedded site is the best place to familiarize oneself with ChibiOS/embedded systems that I’ve found– and therefore, this website should be glaringly obvious on the ChibiOS homepage, so that novices know to go here upon arrival.
So, my constructive criticism of PlayEmbedded, is actually a constructive criticism for Giovanni’s ChibiOS website: I almost never stumbled upon your website! I spent a couple hours exploring the 10 or so tabs before the “Articles and Guides” tab where your PlayEmbedded site is linked. Its a rather hidden tab. I suggest he adds a note on his ChibiOS homepage, saying explicitly that your site has examples and tutorials of actual embedded systems in action.
Thanks!
Thanks Robert, I will speak with Giovanni thanks. In reality, we are doing some major rewritings now and I did not have all figured out.
Please keep the feedback coming in.
[EDIT]: I spoke with Giovanni and we updated the homepage as well as the article page on chibios.org. Thanks for the feedback.