Federico Fuga

Engineering, Tech, Informatics & science

Interrupts and Workqueues

28 Mar 2023 10:25 CEST


In this post I’ll show how to enable interrupts for a GPIO and how to use workqueues to handle code that must be triggered when the interrupt occurs.

You should spend as less time as possible in the interrupt context, to ensure the OS is always responsive and that other interrupts are not delayed.

In RTOS, indeed, you must always consider which tasks (meaning “things to do” and not tasks in the concurrency terms) must be completed ASAP and which may be delayed for later.

For example, in a recent project I had, a RF sniffer using a CC1101 transceiver tuned at 866Mhz, the chip was triggering an interrupt every time a packet is received. The app had to

  1. receive the interrupt
  2. read the data available from the chip FIFO
  3. split packets in separate structures
  4. parse them for the user – that is, output their hex representation with other metadata to the UART.

If all of this was implemented in the interrupt context, packet loss would be very likely.

This must be done instead by splitting the tasks in high priority (read from the chip FIFO) and low priority (parsing data and exposing it to the user).

In this post we’ll use WorkQueues to implement this separation, but mailboxes are also available for this task, when we want to pass data from one context to another.


But first, let’s talk about interrupts.

To enable interrupts for source, most of the time it’s a signal arriving from external circuitry, you need to first, configure the GPIO using devicetree to map it to a dt node (and alias), configure it as input, then configure the interrupt on it and attach a “callback” function (the handler).

Let’s see how.

I’ll use an expressif ESP32, with a pushbutton connected between GPIO 12 and GND. The wroom board has a user LED connected to GPIO 13.


This is the esp32 overlay:

/ {
    aliases {
        led0 = &led_blue;
        sw = &sw0;

    leds {
        compatible = "gpio-leds";
        led_blue: led_0 {
            gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;

    buttons {
        compatible = "gpio-keys";
        sw0: sw_0 {
            gpios = <&gpio0 12 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
            label = "Button 0";

Note something very important, I learnt at my expenses: it is very important to not clash names of the resources. Always check the resulting dts file (in build/zephyr/zephyr.dts) to see if the node you’re using is correctly merged and GPIOs are assigned as intended. It’s easy to use button0 for your button, and then find that button0 is assigned to reset button, and wondering why pressing your user button is not working as expected.

Also note to use alias names when using DT_ALIAS macro. This line

#define SW_NODE DT_ALIAS(sw0)

will not work as intended and it will compile correctly, but code will not work because it will assign GPIO 0. sw0 is a node, sw is the alias name.

For this reason, it is very useful to output the configuration in the debug log, for example

	printk("Set up button at %s pin %d\n", button.port->name, button.pin);

This way you should easily spot if button.pin is 0 and GPIO is misconfigured.

Setup and configuration

Next, in main.c let’s use the following functions to setup and configure the GPIO:

  1. gpio_is_ready_dt(&sw) to check if the resource is ready
  2. gpio_pin_configure_dt(&sw, GPIO_INPUT) to configure the GPIO as input
  3. gpio_pin_interrupt_configure_dt(&sw, GPIO_INT_EDGE_TO_ACTIVE) to configure it as a source of interrupt
  4. gpio_init_callback(&sw_cb_data, button_pressed, BIT(sw.pin)) to configure the callback function for the GPIO pin
  5. gpio_add_callback(sw.port, &sw_cb_data) to enable it

at this point, the gpio is ready to process interrupts in the button_pressed callback.

Work Queue

A work queue is a worker thread that can execute functions when required. It’s the faster way to start a task without the overhead of creating a new thread, because the thread is already allocated. On the other hand, each work queue has just one thread to serve jobs, so if the worker thread is busy processing another job, the new job will be queued.

Zephyr provides a System Workqueue, but you can instantiate other if required. Each workqueue has a stack, and if you have multiple cores, different workqueues can run concurrently. If they are allocated in a single core, they’ll share timeslices depending on the type of multi tasking used (preemptive or cooperative).

You need 2 things to use work queues:

  1. the handler (a function with void work_handler(struct k_work *work) prototype)
  2. the definition of the work to be done (with the K_WORK_DEFINE(work, work_handler) macro).

When needed, you can use k_work_submit(&work) call to submit new jobs to the workqueue.

This is our interrupt handler for the GPIO:

void button_pressed(const struct device *dev, struct gpio_callback *cb,
		    uint32_t pins)

This will release the interrupt as soon as the work is submitted, freeing resources to serve other interrupts. In facts, if an interrupt occurs when the button_action_work is still processing, it will be interrupted and a new instance of the job will be queued, avoiding losing the interrupt.

The example

In the example, we’re using the interrupt to change the blinking rate of the led. The interrupt handler simply fires a new work wich will change the internal state.

Here is the processing that occurs when the gpio triggers:

/* Work handler function */
void button_action_work_handler(struct k_work *work) {
    fast = !fast;
    LOG_INF("Pressing the button at %" PRIu32 "; period is %dms", k_cycle_get_32(), period());

/* Register the work handler */
K_WORK_DEFINE(button_action_work, button_action_work_handler);

void button_pressed(const struct device *dev, struct gpio_callback *cb,
		    uint32_t pins)

And this is the code that configures the interrupt:

	// switch

	if (!gpio_is_ready_dt(&sw)) {
		LOG_ERR("SW not running");

	ret = gpio_pin_configure_dt(&sw, GPIO_INPUT);
	if (ret < 0) {
		LOG_ERR("Bad SW Configuration");

	// interrupts

	ret = gpio_pin_interrupt_configure_dt(&sw, GPIO_INT_EDGE_TO_ACTIVE);
	if (ret < 0) {
		LOG_ERR("Bad INTR configuration");

	// setup the button press callback
	gpio_init_callback(&sw_cb_data, button_pressed, BIT(sw.pin));
	gpio_add_callback(sw.port, &sw_cb_data);

The full code is available in the project repository under the 6.interrupts subdirectory.