indiantinker's blog

[Zephyr on ESP32] : GPIOzzz

In the previous post, we explored the basics of UART communication using ZephyrOS. In this post, we will build on that and I will expatiate on using GPIO inputs and setting up interrupts. ESP32 is the cynosure of the DIY world these days and I hope some people, including my future self, can benefit from this.

Let's get right into it.

Basic pin-state logging

Let's combine our knowledge of UART communication and read the state of a pin. Let's see if you can guess what the code does.

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>

#define SWITCH_PIN 23
#define SLEEP_TIME_MS 100

static const struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0));

void main(void)
{
    int ret;

    if (!device_is_ready(gpio_dev))
    {
        printk("Error: GPIO device is not ready\n");
        return;
    }

    ret = gpio_pin_configure(gpio_dev, SWITCH_PIN, GPIO_INPUT | GPIO_PULL_UP);
    if (ret != 0)
    {
        printk("Error: Failed to configure switch GPIO pin %d\n", ret);
        return;
    }

    ret = gpio_pin_configure(gpio_dev, 25, GPIO_OUTPUT_ACTIVE);
    if (ret != 0)
    {
        printk("Error: Failed to configure led GPIO pin %d\n", ret);
        return;
    }

    printk("Switch monitoring started. Press the switch to see changes.\n");

    while (1)
    {
        int switch_state = gpio_pin_get(gpio_dev, SWITCH_PIN);
        printk("Switch state: %s\n", switch_state ? "OFF" : "ON");
        ret = gpio_pin_set_raw(gpio_dev, 25, !switch_state);
        if (ret != 0)
        {
            return;
        }
        k_msleep(SLEEP_TIME_MS);
    }
}

The key statement here is gpio_pin_configure(gpio_dev, SWITCH_PIN, GPIO_INPUT | GPIO_PULL_UP); Here, we go to gpio0 device tree (which has our pin and referenced by gpio_dev) and ask it to enable INPUT and PULLUP on SWITCH_PIN i.e physical pin 23. It is similar to pinMode(23, INPUT_PULLUP) on an Arduino. The remaining code is quite straightforward. We read the pin using gpio_pin_get(gpio_dev, SWITCH_PIN); and then send the state over Serial. Additionally, we also make the LED on pin 25 reflect the state.

Remember the main commands, once again :

  1. Clean: rm -rf build
  2. Compile: west build -p auto -b esp32_devkitc_wroom .
  3. Flash: west flash
  4. Use terminal: screen /dev/cu.usbserial-0001 115200

The code is available here.

This is an easy way to read inputs. Let's make things tortuous.

Input interrupts and the peculiar callback

Let's do interrupts on pins. This is one of the most conventional ways to use GPIOs in a physical UX context. Imagine a button on a key panel. Something is happening, and the user presses the button. The CPU drops everything, attends to the button, checks the state diagram and reacts. An interrupt is the way to do it.

Let's convert our previous polling input example to the one using interrupts. Let's see if you get what is happening here:

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>

#define SWITCH_PIN 23
#define LED_PIN 25

static const struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0));
static struct gpio_callback switch_cb_data;


void print_callback_info(const struct gpio_callback *cb)
{
    printk("Callback Information:\n");
    printk("  Handler function address: %p\n", cb->handler);
    printk("  Pin mask: 0x%08x\n", cb->pin_mask);
}

void switch_pressed_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    int switch_state = gpio_pin_get(gpio_dev, SWITCH_PIN);
    printk("Switch state changed: %s\n", switch_state ? "OFF" : "ON");
    
    gpio_pin_set(gpio_dev, LED_PIN, !switch_state);
}

void main(void)
{
    int ret;

    if (!device_is_ready(gpio_dev)) {
        printk("Error: GPIO device is not ready\n");
        return;
    }

    ret = gpio_pin_configure(gpio_dev, SWITCH_PIN, GPIO_INPUT | GPIO_PULL_UP);
    if (ret != 0) {
        printk("Error: Failed to configure switch GPIO pin %d\n", ret);
        return;
    }

    ret = gpio_pin_configure(gpio_dev, LED_PIN, GPIO_OUTPUT_INACTIVE);
    if (ret != 0) {
        printk("Error: Failed to configure LED GPIO pin %d\n", ret);
        return;
    }

    ret = gpio_pin_interrupt_configure(gpio_dev, SWITCH_PIN, GPIO_INT_EDGE_BOTH);
    if (ret != 0) {
        printk("Error: Failed to configure interrupt %d\n", ret);
        return;
    }

    gpio_init_callback(&switch_cb_data, switch_pressed_callback, BIT(SWITCH_PIN));
    print_callback_info(&switch_cb_data);  // Print callback info

    gpio_add_callback(gpio_dev, &switch_cb_data);

    printk("Switch monitoring started. Press the switch to toggle LED.\n");

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

gpio_pin_interrupt_configure(gpio_dev, SWITCH_PIN, GPIO_INT_EDGE_BOTH) enables the interrupts on SWITCH_PIN and the interrupt will be triggered on both rising and falling edges, defined by GPIO_INT_EDGE_BOTH. Whenever the interrupt happens switch_pressed_callback is called like an ISR and it is executed.

Let's jump to the elephant in the room.

gpio_init_callback(&switch_cb_data, switch_pressed_callback, BIT(SWITCH_PIN));
gpio_add_callback(gpio_dev, &switch_cb_data);

Ahem. What is happening over here? This part took me a bit to wrap my head around.

In Arduino terms, it would be like if instead of writing:

attachInterrupt(digitalPinToInterrupt(pin), ISR_function, CHANGE);

You had to do:

InterruptInfo info;
setupInterruptInfo(&info, ISR_function, pin);
registerInterrupt(&info);

This makes it much clearer. I hope the readers get the logic. But, Why is this done?

  1. Flexibility: You can use the same callback function for multiple pins or change callbacks dynamically.
  2. Information Passing: The switch_cb_data structure can contain additional information that gets passed to your callback function.
  3. Abstraction: It separates the interrupt mechanism (handled by Zephyr) from your application logic.

In any case, the code is available here.

LED Toggle using interrupts and debouncing

Now that we know so much, let's do something useful (finally). This is your challenge :

Implement a version of the previous code, to toggle an LED with the switch i.e. when the switch is pressed, the LED is turned on and when it is pressed again, it turns off.

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>

#define SWITCH_PIN 23
#define LED_PIN 25
#define DEBOUNCE_DELAY_MS 50  // 50ms debounce delay

static const struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0));
static struct gpio_callback switch_cb_data;

static bool led_state = false;  // Track LED state
static int64_t last_time = 0;   // For debounce

void switch_pressed_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    int64_t current_time = k_uptime_get();
    
    // Simple debounce: ignore interrupts that occur too soon after the previous one
    if ((current_time - last_time) < DEBOUNCE_DELAY_MS) {
        return;
    }
    
    last_time = current_time;

    // Toggle LED state
    led_state = !led_state;
    gpio_pin_set(gpio_dev, LED_PIN, led_state);
    
    printk("Switch pressed. LED is now %s\n", led_state ? "ON" : "OFF");
}

void main(void)
{
    int ret;

    if (!device_is_ready(gpio_dev)) {
        printk("Error: GPIO device is not ready\n");
        return;
    }

    // Configure switch pin (input with pull-up)
    ret = gpio_pin_configure(gpio_dev, SWITCH_PIN, GPIO_INPUT | GPIO_PULL_UP);
    if (ret != 0) {
        printk("Error: Failed to configure switch GPIO pin %d\n", ret);
        return;
    }

    // Configure LED pin (output, initially off)
    ret = gpio_pin_configure(gpio_dev, LED_PIN, GPIO_OUTPUT_INACTIVE);
    if (ret != 0) {
        printk("Error: Failed to configure LED GPIO pin %d\n", ret);
        return;
    }

    // Configure interrupt for switch (falling edge only)
    ret = gpio_pin_interrupt_configure(gpio_dev, SWITCH_PIN, GPIO_INT_EDGE_FALLING);
    if (ret != 0) {
        printk("Error: Failed to configure interrupt %d\n", ret);
        return;
    }

    gpio_init_callback(&switch_cb_data, switch_pressed_callback, BIT(SWITCH_PIN));
    gpio_add_callback(gpio_dev, &switch_cb_data);

    printk("Switch monitoring started. Press the switch to toggle LED.\n");

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

Did it work? How did it go? The code is available here.

Hope this was useful. Feel free to chime in on twitter @rohit7gupta.

Cheers, Rohit