Maker.io main logo

Introduction to Zephyr Part 9: Interrupts and Workqueues

72

2025-05-01 | By ShawnHymel

ESP32

When working with embedded systems, developers often face the challenge of managing tasks that require quick responses, such as reacting to button presses, sensor readings, or other hardware events. Interrupts are an essential tool in handling these real-time tasks efficiently. However, interrupts come with their own complexities and limitations, which is where workqueues (or work queues) in Zephyr can help bridge the gap. In this post, we’ll discuss why interrupts are necessary, how they work, and how Zephyr’s workqueues provide a robust mechanism for handling tasks that need to be deferred or debounced.

At the end of the video, you are given a challenge to implement a Zephyr workqueue to call a function whenever a button is pressed and use it to debounce the button. This written tutorial acts a solution to that challenge and describes how workqueues operate.

Code for this Zephyr series can be found here: https://github.com/ShawnHymel/introduction-to-zephyr

Hardware Connections

For this demonstration, we will use a pushbutton switch connected to pin 5 on the ESP32-S3-DevKitC.Here is a Fritzing diagram showing all of the connections we will use throughout this series:

Introduction to Zephyr Part 9: Interrupts and Workqueues

Interrupts

Interrupts allow computing systems to respond immediately to external or internal events. For instance, a button press might generate an interrupt signal, which tells the processor to temporarily pause its current task and execute a specific Interrupt Service Routine (ISR) to handle the event. This approach ensures low-latency responses, making interrupts vital for time-sensitive applications.

However, ISRs need to be minimal and efficient. Long or complex operations in an ISR can lead to issues like interrupt latency, where subsequent interrupts are delayed, potentially causing the system to miss critical events. This limitation calls for a way to offload longer operations from the ISR to a context where they can be handled without affecting real-time performance.

Most microcontrollers implement two basic types of counters:

  • Internal: Internal interrupts occur when something happens inside the microcontroller, such as a timer event, direct memory access (DMA) transfer complete, register overflow, etc.
  • External: External interrupts happen when something happens outside the microcontroller, such as a pin change, data transfer complete (e.g., UART, SPI) and so on.

Interrupts can also be hardware-based (like our timers) or software-based (such as a divide-by-zero error). Interrupts (of all varieties) are extremely important to most embedded systems, as it allows them to process events in a deterministic fashion quickly. They are widely used for things like controlling motors, transferring data to and from external devices, and responding to user input.

Zephyr Timers vs. Counters

Zephyr has two different types of timer interrupts:

  • Timers are software-based utilities that can be used to call functions after some time has passed. They are easy-to-use software-based interrupts that rely on the internal kernel timer. As a result, their maximum resolution is determined by the kernel timer as set by the SYS_CLOCK_TICKS_PER_SEC Kconfig symbol, which is usually around 1 ms for most systems.
  • Counters are hardware-based interrupts that rely on the internal timers of the system. Note that “counters” are often called “timers” in most microcontroller literature. Because they rely on the underlying hardware, some Devicetree setup is usually required to connect the counter API to the hardware. However, they offer greater precision than Zephyr timers, often in the micro- or nanosecond range.

Zephyr GPIO Interrupt

Zephyr offers a number of common APIs for working with external interrupts, including executing ISRs for GPIO pin changes, UART, SPI, I2C, etc. Each subsystem (e.g., interrupt-driven UART) will have its own information about how to configure interrupts for that subsystem.

This example shows a barebones Zephyr GPIO interrupt (edge-to-active). Note that one issue with the example is that it is prone to button bounce. There are a number of ways to debounce a button, and we’ll look at one such way in the next section.

Workqueues

Workqueues (also spelled “work queues”) provide a mechanism to defer tasks from an ISR to the system’s thread context. This allows the ISR to remain lightweight while ensuring more complex or time-consuming operations are executed later. In Zephyr, work queues operate in the kernel context and process work items sequentially, making them ideal for tasks like debouncing buttons or handling non-critical operations triggered by interrupts.

This is similar to setting a global flag in an ISR and having the main thread execute some task when that flag is set. The difference is that we can do this with multiple ISRs and functions without the need for manually setting flags. It essentially places a function in a queue to be executed by a worker thread.

To understand how work queues function in practice, let’s explore a demo application where we debounce a button press using a work queue. The full code for this solution can be found here.

Project Setup

Create a new project directory structure:

Copy Code
 /workspace/apps/09_solution_workqueue/ 
├─ boards/ 
│ └─ esp32s3_devkitc.overlay 
├─ src/ 
│ └─ main.c 
├─ CMakeLists.txt 
└─ prj.conf

CMakeLists.txt:

We will use the same boilerplate CMakeLists.txt file that we’ve been using for most of the series:

Copy Code
cmake_minimum_required(VERSION 3.22.0) 
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(button_demo) 
target_sources(app PRIVATE src/main.c) 
prj.conf
 

Leave empty. Workqueues (and the associated default workqueue thread) are enabled by default in Zephyr.

boards/esp32s3_devkitc.overlay

We need to enable our button connected to pin 5:

Copy Code
/ { 
aliases { 
my-button = &button_1;
 };
buttons {
compatible = "gpio-keys";
// debounce-interval-ms = <50>; 
button_1: d5 {
gpios = <&gpio0 5 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
 };
 };
 };

src/main.c

Here is the complete application code for our demonstration:

Copy Code
#include <stdio.h>
    #include <zephyr/kernel.h>
    #include <zephyr/drivers/gpio.h>
    
    // Settings
    #define DEBOUNCE_DELAY_MS 50
    
    // Button struct (from Devicetree)
    static const struct gpio_dt_spec btn = GPIO_DT_SPEC_GET(DT_ALIAS(my_button), gpios);
    
    // Struct for holding GPIO-related callback functions
    static struct gpio_callback btn_cb_data;
    
    // Struct for holding the workqueue
    static struct k_work_delayable button_work;
    
    // GPIO callback (ISR)
    void button_isr(const struct device *dev, 
                    struct gpio_callback *cb, 
                    uint32_t pins)
    {
        // Add work to the workqueue
        k_work_reschedule(&button_work, K_MSEC(DEBOUNCE_DELAY_MS));
    }
    
    // Work handler: button pressed
    void button_work_handler(struct k_work *work)
    {
        int state;
    
        // Read the state of the button (after the debounce delay)
        state = gpio_pin_get_dt(&btn);
        if (state < 0) {
            printk("Error (%d): failed to read button pin\r\n", state);
        } else if (state) {
            printk("Doing some work...now with debounce!\r\n");
        }
    }
    
    int main(void)
    {
        int ret;
    
        // Initialize work item
        k_work_init_delayable(&button_work, button_work_handler);
    
        // Make sure that the button was initialized
        if (!gpio_is_ready_dt(&btn)) {
            printk("ERROR: button not ready\r\n");
            return 0;
        }
    
        // Set the button as input (apply extra flags if needed)
        ret = gpio_pin_configure_dt(&btn, GPIO_INPUT);
        if (ret < 0) {
            printk("ERROR: could not set button to input\r\n");
            return 0;
        }
    
        // Configure the interrupt
        ret = gpio_pin_interrupt_configure_dt(&btn, GPIO_INT_EDGE_TO_ACTIVE);
        if (ret < 0) {
            printk("ERROR: could not configure button as interrupt source\r\n");
            return 0;
        }
    
        // Connect callback function (ISR) to interrupt source
        gpio_init_callback(&btn_cb_data, button_isr, BIT(btn.pin));
        gpio_add_callback(btn.port, &btn_cb_data);
    
        // Do nothing
        while (1) {
            k_sleep(K_FOREVER);
        }
    
        return 0;
    }

 

Build and Flash

In the Docker container, build the demo application:

Copy Code
cd /workspace/apps/09_solution_workqueue/ west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay

On your host computer, flash the application (replace <PORT>  with the USB port for your ESP32-S3-DevKitC):

Copy Code
python -m esptool --port " " --chip auto --baud 921600 --before default_reset --after hard_reset write_flash -u --flash_size detect 0x0 workspace/apps/09_solution_workqueue/build/zephyr/zephyr.bin

After flashing completes, open a serial port:

Copy Code
python -m serial.tools.miniterm "<PORT>" 115200‎
 

Press the button connected to pin 5, and you should see "Doing some work...now with debounce!” appear in the console. Because we added a delay to the work callback function (and checked the button state again), you should no longer see repeat actions on a single button press–we successfully debounced with the workqueue!

Breaking Down the Code

Let’s break down some of the code.

Setting Up the Button

The gpio_dt_spec structure retrieves the button’s configuration from the Devicetree. This ensures that the button’s pin and properties are initialized correctly.

Copy Code
static const struct gpio_dt_spec btn = GPIO_DT_SPEC_GET(DT_ALIAS(my_button), gpios);

The gpio_pin_configure_dt function is then used to set the button as an input pin.

Configuring the Interrupt

We configure the button to trigger an interrupt on an active edge (button press) using gpio_pin_interrupt_configure_dt. This ensures that our ISR is called whenever the button is pressed.

The ISR

The button_isr function is called whenever the interrupt is triggered. Instead of handling the button press directly, it defers the work by scheduling a delayed work item to the work queue:

Copy Code
k_work_reschedule(&button_work, K_MSEC(DEBOUNCE_DELAY_MS)); 

The DEBOUNCE_DELAY_MS ensures that only button presses sustained beyond this delay are processed, effectively debouncing the input.

The Work Queue Handler

The button_work_handler function executes in a thread context after the debounce delay. It reads the button state and performs the desired action:

Copy Code
state = gpio_pin_get_dt(&btn);
 if (state) {
 printk("Doing some work...now with debounce!\r\n"); }

This approach keeps the ISR lightweight and offloads the debounce logic and state checking to the workqueue.

Infinite Sleep

Finally, the main function puts the system into an infinite sleep loop:

Copy Code
while (1) {
 k_sleep(K_FOREVER);
 }

This ensures that the system remains active but does not consume unnecessary CPU cycles.

Going Further

Zephyr’s workqueues are a powerful tool for embedded developers. They allow you to defer and manage tasks efficiently, reducing ISR complexity while maintaining real-time responsiveness. The example above demonstrates how to debounce a button using a work queue, but the same principles can be applied to other scenarios, such as processing sensor data or triggering state changes in your application. By combining interrupts and work queues, you can build robust, responsive embedded systems.

If you would like to learn more about Zephyr Interrupts and Workqueues, check out the following content:

Circuit Dojo’s Threads, Work Queues, and Timers video

制造商零件编号 ESP32-S3-DEVKITC-1-N32R8V
ESP32-S3-WROOM-2-N32R8V DEV BRD
Espressif Systems
制造商零件编号 3533
GRAPHIC DISPLAY TFT RGB 0.96"
Adafruit Industries LLC
制造商零件编号 1782
MCP9808 TEMP I2C BREAKOUT BRD
Adafruit Industries LLC
制造商零件编号 3386P-1-103TLF
TRIMMER 10K OHM 0.5W PC PIN TOP
Bourns Inc.
制造商零件编号 1825910-6
SWITCH TACTILE SPST-NO 0.05A 24V
TE Connectivity ALCOSWITCH Switches
制造商零件编号 CF14JT220R
RES 220 OHM 5% 1/4W AXIAL
Stackpole Electronics Inc
制造商零件编号 LTL-4224
LED RED CLEAR T-1 3/4 T/H
Lite-On Inc.
制造商零件编号 FIT0096
BREADBRD TERM STRIP 3.20X2.00"
DFRobot
制造商零件编号 DH-20M50055
USB AM TO USB MICRO, USB 2.0 - 1
Cvilux USA
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.