Interrupts¶
Learning goals: Interrupts and their usage in an embedded device
In a computer, an interrupt is an internal or external signal for a processor, that literally interrupts other functions of the processor. When an interruption occurs, the processor saves its state (state of the current program, registers, etc.) and starts the execution of the handler routine associated with the interruption. When the handler has been executed, the processor loads back to its previous state, and continues to execute the program precisely where it left off.
In the picture below is the pin layout of ATmel's ATtiny2313 microcontroller, where the red markings indicate an external hardware interruption pin (INT0 and INT1). We could connect the outgoing interrupt line of some peripheral device to these pins, and this way capture the interruption signals sent by the device. In microcontrollers, there are often pins for external interruptions connected directly to the interruption handling logic of the circuit. Naturally, the possible interruptions that a certain peripheral device / component is able to produce, are carefully defined in datasheets.
There are two types of interruptions. An interrupt can be hardware-based (internal to the main unit or coming from a peripheral device), that is typically caused by an asynchronous (independent of the scheduling of program) event inside of the processor or in a peripheral device. As a result, an interrupt occurs unexpectedtly from the perspective of the program being run, at any time. Hardware interruptions are caused inside of the main unit by for example dividing by zero (error) or externally by a message from a peripheral device that tells the processor that it needs attention because there is new data available or an error has occured.
A software interruption can be caused by a special machine instruction, for example
INT-instruction in Intel's x86 processors. A software interruption triggers an interruption signal in the main unit, that is handled in the operating system / firmware in a similar manner as a hardware interruption, and consequently a handler function defined for this interruption is executed. Some timers can generate this type of interrupts. Interruptions have some priority. An interruption with a higher priority is executed first. Priorities matter when multiple interruptions occur at the same time, or when a lower priority interruption is being executed. In a situation like this, the interruption with a higher priority stops the execution of the handler routine with a lower priority, and executes its own handler routine before giving the control of the system back. For example, in embedded systems, the hardware interruptions originating from a RESET-pin (
RUN in pico) have the highest priority. The second highest priority interruptions are hardware interruptions, because they are typically time-critical. The interruptions with the lowest priority are software interruptions. A programmer can set a priority for a software interruption, and they are executed according to the programmers will (they can also override each other).The handler routine (also handler) of an interruption is almost like a function, but it is never called from the program being executed. This is important to know, because before an interruption handler can be executed, the state of the processor needs to be saved, and by calling the handler as a function, this operation would not occur. In addition to this, the routine does not return any value, but global variables and registers can be used inside of it to deliver information. Now, in C the handlers are implemented as functions, and we need to tell that those functions are actually handlers to FreeRTOS and Pico SDK.
Interruption handlers are time critical for two reasons: they stop the program being executed currently and they can override each other. Because of this, the execution of the entire program is easy to block/ prevent (by mistake) with a higher priority interrupt handler, which is taking too much time to execute. Now, RTOS instructs that a hardware interrupt should not take more than 5 microseconds and for software interrupt, the same metric is 100 microseconds. In practice, only minimal amount of code should be executed in a handler. Because of this, handlers usually just modify the values of registers or global state variables. For example, it is a good practice that from a handler we inform the program that there is new data availabe in a peripheral device, and then read the data once the handler has run. Again, one of these golden tips. Well, more on this in the Finite state machines-material...
Interrupts in the Raspberry Pi Pico¶
The Pico supports interrupts for all its peripherals. For example, timers, UART, I2C, SPI, DMA, PIO and others can generate an interrupt signal when an event occurs. These interruptions are managed by the Cortex-M0+ NVIC (Nested Vectored Interrupt Controller). In the Pico SDK this is exposed through the hardware_irq package. With these functions you can:
- Associate an interrupt line to a specific handler function.
- Define the priority of the interruption.
- Enable or disable a particular interruption.
For example:
irq_set_exclusive_handler(TIMER_IRQ_0, my_timer_handler); irq_set_priority(TIMER_IRQ_0, 0); irq_set_enabled(TIMER_IRQ_0, true);
Special case with GPIOS: All the GPIO pins are connected to a single interrupt line per core (SIO_IRQ_PROC0 and SIO_IRQ_PROC1). This means that, at the hardware level, the processor only sees 'a GPIO interrupt happened. The Pico SDK provides some magic (aka extra logic) that reads which pin triggered the event and then calls the adequate handler. In this way you can easily associate an interrupt handler to each GPIO, even if the hardware only exposes one shared line.
GPIO interrupts¶
In a previous chapter we presented an interruption that reacts to a state change of a pin. Let's get back to it.
...
// Interruption handler
void buttonFxn(uint gpio, uint32_t eventMask) {
...
}
int main() {
...
// We assign the interrupt handler buttonFxn to a pin
gpio_set_irq_enabled_with_callback(BUTTON1, GPIO_IRQ_EDGE_FALL, true, buttonFxn);
...
}
We can see that in the function, the constant
GPIO_IRQ_EDGE_FALL has been assigned to the interruption. Here the interruption will occur, when the state of the pin changes on falling edge, or in other words in transition HIGH (operating voltage) -> LOW (ground). A pin interruption can also be set to occur on rising edge, which means the state transition LOW -> HIGH, with the constant GPIO_IRQ_EDGE_RISE.A handler for the interruption signal is set with the last argument of the function call
gpio_set_irq_enabled_with_callback, which here is the function buttonFxn. The interruption-specific functionality is then implemented inside of this function.The third parameters (bool) indicates if we should activate or deactivate the interrupt.
Peripheral device interrupts¶
As an example of peripheral device interrupt in Pico, we introduce the multi-functional ICM-42670-sensor. Integrated to this single sensor, we have gyroscope and an accelerometer.
We can use the interruption functionality of ICM-42670, because its external interruption line has been connected to Pico,
ICM42670_INT (in header file pins.h). So, the interrupt is enabled in a similar manner as the pin interrupt above. The ICM-42670 can trigger an interrupt in different circunstances that can be programmed by the user: for instance, when it has capture a determined number of samples, when the gyroscope detects certain inclination or when the pedometer integrated in the chip detect a step. In the following example we will learn how to enable / disable external interrupts, coming from the ICM42670.
void ICMFxn(uint gpio, uint32_t event_mask) {
...
}
void sensorTask(void *pvParameters) {
// Allowing interrupts from ICM-42670 in rising edge
gpio_set_irq_enabled(ICM42670_INT, GPIO_IRQ_EDGE_RISE, true);
// Disallowing interrupts
gpio_set_irq_enabled(ICM42670_INT, GPIO_IRQ_EDGE_RISE, false);
}
int main(void) {
...
// Enabling ICM-42670 interrupt pin
gpio_init(ICM42670_INT);
gpio_set_dir(ICM42670_INT, GPIO_IN);
// Setting an interrupt handler function
gpio_set_irq_enabled_with_callback(ICM42670_INT, GPIO_IRQ_EDGE_RISE, false, ICMFxn);
...
}
Let's review this example step by step. Now in function
main we enable the interrupt line in our program and assign the interrupt handler function ICMFxn.When setting the interrupt handler for the function
gpio_set_irq_enabled_with_callback, the third argument is given the value false. This means that interrupts are initially disabled. When we want to receive interrupts, for example within a task, we can use the Pico SDK function gpio_set_irq_enabled. For this function, we specify the desired interrupt pin, determine when the interrupt should be handled (using constants GPIO_IRQ_EDGE_RISE or GPIO_IRQ_EDGE_FALL), and enable or disable the interrupt with the values true or false. Typically, it’s best to enable interrupts at the last possible moment in the code to avoid disrupting program execution before we actually need them.In the example, the implementation of
sensorTask also includes enabling interrupts using the gpio_set_irq_enabled function. By toggling interrupts on and off programmatically, we can control their behavior.Multiple GPIO interrupt sources¶
Sometimes we need to connect multiple GPIOs that can generate interrupts, for example when using buttons or sensors.
Let us imagine that we have two buttons,
Let us imagine that we have two buttons,
BUTTON1 and BUTTON2. Pressing each button should activate a different function: in our case BUTTON1 will turn on LED1 and BUTTON2 will turn it off. There are two ways to implement this behaviour. The first approach is to use a generic callback that will be called for all GPIO events. In this case the SDK provides us with the information of which GPIO triggered the interruption and what event (rising or falling edge, level high or low) occurred. Inside this generic function we can then distinguish which button was pressed and act accordingly.
#include "pico/stdlib.h"
#include "hardware/gpio.h"
static void gpio_callback(uint gpio, uint32_t events) {
if (gpio == BUTTON1 && (events & GPIO_IRQ_EDGE_FALL)) {
gpio_put(LED1, true); // turn on LED
}
if (gpio == BUTTON2 && (events & GPIO_IRQ_EDGE_FALL)) {
gpio_put(LED1, false); // turn off LED
}
}
int main() {
stdio_init_all();
gpio_init(LED1);
gpio_set_dir(LED1, GPIO_OUT);
gpio_init(BUTTON1);
gpio_init(BUTTON2);
// Register the generic callback
gpio_set_irq_enabled_with_callback(BUTTON1, GPIO_IRQ_EDGE_FALL, true, &gpio_callback);
gpio_set_irq_enabled(BUTTON2, GPIO_IRQ_EDGE_FALL, true);
while (1) { tight_loop_contents(); }
}
The second approach is to give each button its own raw interrupt handler by using
This method bypasses the generic callback mechanism and lets us assign a dedicated function to each GPIO pin. In this case, the handler should explicitly communicate that it has process the interrutption using
gpio_add_raw_irq_handler. This method bypasses the generic callback mechanism and lets us assign a dedicated function to each GPIO pin. In this case, the handler should explicitly communicate that it has process the interrutption using
gpio_acknowledge_irq(uint gpio, uint32_t event_mask)#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/irq.h"
static void button1_handler(void) {
if (gpio_get_irq_event_mask(BUTTON1) & GPIO_IRQ_EDGE_FALL) {
gpio_acknowledge_irq(BUTTON1, GPIO_IRQ_EDGE_FALL);
gpio_put(LED1, true); // turn on LED
}
}
static void button2_handler(void) {
if (gpio_get_irq_event_mask(BUTTON2) & GPIO_IRQ_EDGE_FALL) {
gpio_acknowledge_irq(BUTTON2, GPIO_IRQ_EDGE_FALL);
gpio_put(LED1, false); // turn off LED
}
}
int main() {
stdio_init_all();
gpio_init(LED1);
gpio_set_dir(LED1, GPIO_OUT);
gpio_init(BUTTON1);
gpio_init(BUTTON2);
// Register dedicated handlers
gpio_add_raw_irq_handler(BUTTON1, button1_handler);
gpio_add_raw_irq_handler(BUTTON2, button2_handler);
gpio_set_irq_enabled(BUTTON1, GPIO_IRQ_EDGE_FALL, true);
gpio_set_irq_enabled(BUTTON2, GPIO_IRQ_EDGE_FALL, true);
//irq_set_enabled(SIO_IRQ_PROC0, true);
while (1) { tight_loop_contents(); }
}
One important point is that the maximum number of handlers that can be associated with the GPIO is
In practice, these functions call
PICO_MAX_IRQ_SHARED_HANDLERS. In practice, these functions call
irq_add_shared_handler internally, and that is what imposes this limitation. Timers¶
FreeRTOS also provides us timers in the
timers.h-library, which we can use to implement timed events, or in other words, interruptions in fixed rates. For example, with the help of a timer, we could read sensor data, communicate with a peripheral device or blink a LED once per second. There are two different type of timers: those which repeat the same function periodically after a given time and "one-shot" timers, those that only exeucte once
An example tells us more than a thousand words, so lets go!!!
...
#include <timers.h>
...
void timerFxn(TimerHandle_t timer) {
printf("%li\n", (xTaskGetTickCountFromISR() / portTICK_PERIOD_MS));
}
int main(void) {
stdio_init_all();
TimerHandle_t timerHandle;
// Initializing a timer
timerHandle = xTimerCreate(
"Timer", // Name of the timer
pdMS_TO_TICKS(1000), // Time period
pdTRUE, // We want to automatically restart this timer
(void *) 0, // Identification number of this timer
timerFxn // Timer function
);
if(timerHandle == NULL) {
printf("Timer create failed\n");
return 0;
}
else {
// Starting the timer
if(xTimerStart(timerHandle, 0) != pdPASS) {
printf("Timer start failed\n");
return 0;
}
}
vTaskStartScheduler();
return 0;
}
You may observe some similarities between this code and the creation of tasks that was covered earlier. At first, we create a handle (
TimerHandle_t) for our timer, which we can use to manage this timer later in the program. With the help of a handle, we can also later check if our timer was created successfully.Next, we will attach a new timer to our handle, which is first created with the function
xTimerCreate. This function takes in as arguments the name of the timer, which does not really matter for the RTOS, but it can help us humans to identify the timer for example in the debugger. As a second argument to the timer we give it its time period. This is the time that the timer waits before it calls the function that was given. The third argument given to the timer tells the RTOS if the timer needs to be restarted at the end of its period. Here, the value pdTRUE means that the restart is desired. The value pdFALSE would mean that the timer is "one-shot", so it would be executed only once. Well, these are RTOS definitions again.The second-to last argument given to the timer is its number, which we can use to distinguish between different timers at the program-level. If we set up multiple timers to call the same function with different time intervals, with this identifier we could distinguish between those timers. As the last argument, we set the timer function, that this timer calls at the end of its period. Observe, how the timer function has to take in as argument the handle of the timer. With this handle we can get information about the timer inside of the function, that triggered the execution of this function. (For example, inside of the timer function we could call the function
pvTimerGetTimerID and we would get the identification number we set for this timer)When the timer has been created, we should check that no errors have occured. In the example, we first check that the value of the handle is not
NULL at this point. If it is, the creation of the timer failed, and we print this information to the terminal-window, after which we exit the program. Another check is performed when starting the timer. The starting is done with the function xTimerStart, which is given the handle of the timer to be started, and the amount of RTOS ticks to wait before starting the timer. In this case we don't want to wait at all, so we set 0 as the wait value. If the return value of xTimerStart is not the RTOS constant pdPASS, starting of the timer did not succeed. In this case we will inform the terminal window about this and exit the program.We also notice that generally the constant
pdPASS is used as FreeRTOS's way of telling that some operation was successful, and the constants pdTRUE and pdFALSE are available for the programmer to use when setting desired and undesired setting values.With this library, we can create multiple concurrent timers to our program, we just need to introduce one handle per timer and settings arguments for the function
xTimerCreate, and start each of the timers with the function xTimerStart. Well, this is not the best way to do things, as we can have only one timer interrupt, where we calculate multiple time periods.Running a task periodically with vTaskDelayUntil¶
Sometimes we want a task to run in a strict periodic way, for example every 250 ms, always keeping the same rhythm. For that purpose FreeRTOS gives us the function
vTaskDelayUntil(). This function is similar to
vTaskDelay(), but with one important difference: instead of saying "wait X ticks starting from now", it says "wake me up exactly at this point of time, no matter when vTaskDelayUntil() was called". That way, if the task is supposed to run every 250 ms, it will keep that exact rhythm even if the work inside the task takes a bit longer sometimes. In other words, it guarantees a steady and predictable pace. In order to really run periodically, this task usually needs to have a higher priority than other tasks. Otherwise, even if we ask it to wake up every 250 ms, it will only get CPU time when the scheduler decides. If other tasks with the same or higher priority are running, our periodic task will need to wait.
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
TickType_t xLastWakeTime;
// The task name is passed in as a parameter
pcTaskName = ( char * ) pvParameters;
// Initialize xLastWakeTime with the current tick count
// This is the ONLY place we assign a value to it
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
// Print out the name of this task
vPrintLine( pcTaskName );
// Delay task until the next 250 ms period
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
A few important things to notice here:
xLastWakeTime: this variable keeps track of when the task was last unblocked. It is only set once at the beginning with the current tick count. After that,vTaskDelayUntil()takes care of updating it internally.vTaskDelayUntil(): by giving it the address ofxLastWakeTimeand the period in ticks, it ensures the task keeps running exactly every 250 ms.pdMS_TO_TICKS(): here we have again our friend the macro, helping us to translate from human-friendly milliseconds into FreeRTOS ticks. If the tick rate is 1 kHz, 250 ms becomes 250 ticks.
So, with this mechanism we can make a task run at a precise interval, something very important in many real-time applications like sampling sensors or blinking LEDs with constant rhythm.
Timers in Pico¶
Although we recommend to write timers using FreeRTOS (because then they are fully integrated into the task scheduling),
it is also possible to use the Pico’s own timer system directly.
it is also possible to use the Pico’s own timer system directly.
The RP2040 chip has a hardware timer and an alarm unit that can be accessed through the pico_time library.
The idea is that you can schedule a callback to run after some milliseconds. If the callback returns 0 it is only executed once, but if it returns a non-zero value it will be rescheduled and run again, effectively creating a repeating timer.
The idea is that you can schedule a callback to run after some milliseconds. If the callback returns 0 it is only executed once, but if it returns a non-zero value it will be rescheduled and run again, effectively creating a repeating timer.
Let’s start with one example of a one-shot alarm that prints a message after one second:
#include "pico/stdlib.h" #include "pico/time.h" #includeint64_t one_shot_callback(alarm_id_t id, void *user_data) { printf("One-shot alarm fired!\n"); return 0; // 0 means do not repeat } int main() { stdio_init_all(); // Schedule a callback 1000 ms (1 second) from now add_alarm_in_ms(1000, one_shot_callback, NULL, false); while (1) { tight_loop_contents(); } }
Easy, isn’t it?
Here the function
add_alarm_in_ms() has four parameters: - the first one (1000) is the delay in milliseconds before the alarm fires,
- the second one (one_shot_callback) is the function that will be called,
- the third one (NULL) is a pointer you can use to pass user data to the callback,
- and the fourth one (false) defines whether the callback should fire immediately if the time has already passed (normally set to false).
The callback function itself must have the form
The parameter id identifies which alarm triggered (useful if you have several alarms), and user_data is the same pointer you passed when you created the alarm. The return value is crucial: returning 0 stops the alarm (one-shot), while returning a positive number schedules the next call after that many milliseconds.
int64_t callback(alarm_id_t id, void *user_data). The parameter id identifies which alarm triggered (useful if you have several alarms), and user_data is the same pointer you passed when you created the alarm. The return value is crucial: returning 0 stops the alarm (one-shot), while returning a positive number schedules the next call after that many milliseconds.
But you can also have repeating alarms. In this case the callback simply returns the interval in milliseconds for the next call. For example, this alarm will print a message every 500 ms:
#include "pico/stdlib.h" #include "pico/time.h" #includeint64_t repeating_callback(alarm_id_t id, void *user_data) { printf("Repeating alarm: 500 ms passed\n"); return 500; // return next interval in milliseconds } int main() { stdio_init_all(); // Schedule first alarm in 500 ms add_alarm_in_ms(500, repeating_callback, NULL, false); while (1) { tight_loop_contents(); } }
As you can see, Pico timers are powerful and allow very fine-grained control, but the code can quickly become more complicated to manage than using FreeRTOS timers. Still, it is useful to know that this option exists when you need precise alarms closer to the hardware and not trusting that much in FreeRTOS tasks.
Real-time clock¶
On top of all this, the Pico SDK provides us with
hardware_rtc-library for real-time clock, that runs in the same time as us humans. The only downside of this is that we need to initialize the clock ourselves by setting it to right time before using it.Again, we will trust in the power of an example.
...
#include <hardware/rtc.h>
...
// Timer function
void timerFxn(TimerHandle_t timer) {
// Let's see the time from clock...
datetime_t aika;
while (1){
rtc_get_datetime(&aika);
// ...and print it out
printf("Time is %02d:%02d:%02d\n", aika.hour, aika.min, aika.sec);
// (en) Politely sleeping for a moment
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
int main(void) {
stdio_init_all();
// We create a timer that automatically restarts and calls timerFxn approximately once per second
// We can do this based on the previous example
TimerHandle_t timerHandle;
timerHandle = xTimerCreate(
"Timer",
pdMS_TO_TICKS(1000),
pdTRUE,
(void *) 0,
timerFxn
);
if(timerHandle == NULL) {
printf("Timer create failed\n");
return 0;
}
else {
if(xTimerStart(timerHandle, 0) != pdPASS) {
printf("Timer start failed\n");
return 0;
}
}
// Initializing the time
datetime_t aika = {
.year = 2024,
.month = 06,
.day = 20,
.dotw = 4,
.hour = 11,
.min = 59,
.sec = 59
};
// Initializing the clock and setting it to right time
rtc_init();
rtc_set_datetime(&aika);
vTaskStartScheduler();
return 0;
}
In
main-function we create the timer, which we have already familiarized ourselves with. In this example we use the timer to see the time from the real-time clock with Pico SDK's function rtc_get_datetime and to print it to the terminal window. You may also notice that the timer function here is too slow because of the printf function call. This could be fixed by for example using a print task along with a state machine and a global variable that stores the time read from the clock.Before starting the program, we fill the desired start time for our clock by using the structure
datetime_t, and give it to the function rtc_set_datetime, of course after initializing the onboard real-time clock circuit with the function rtc_init.Interruptions in FreeRTOS¶
When we work with interrupts inside FreeRTOS we need to be careful: not all API functions can be called from an interrupt service routine (ISR). Many FreeRTOS functions assume they are called from a task and might try to block that task if a resource is not available. But inside an ISR there is no calling task to block!
That is why FreeRTOS provides special versions of some functions, with the suffix
That is why FreeRTOS provides special versions of some functions, with the suffix
FromISR, that are safe to use inside an interrupt. The rule is simple: never call a FreeRTOS API function from an ISR unless its name ends with
If we want to resume a suspended task from an interrupt, we use
FromISR. For example, if we want to start or reset a software timer from inside an interrupt, we should call xTimerStartFromISR() or xTimerResetFromISR(). If we want to resume a suspended task from an interrupt, we use
vTaskResumeFromISR(). These versions are lightweight and avoid the checks needed for task context, making both the ISRs and the kernel more efficient. For instance, if an interrupt should restart a timer (for example to measure inactivity), we could write:
void gpio_irq_handler(uint gpio, uint32_t events) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerResetFromISR(myTimerHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
These interrupt-safe APIs make it possible to connect the hardware world (interrupts) with the FreeRTOS world (tasks and timers) without breaking the scheduler.
To conclude¶
RTOS abstracts the usage of interrupts into fairly simple process, which involves writing the handler as a function. In fact, we don't even know if the library works based on interruptions or is it just a program function. To gain this knowledge, it might be beneficial to read the documentation of the library to know what is really going on. If we don't know, the handler we implement for the interruption might be too heavy.