zephyr-gpio-api-unificada

GPIO API in Zephyr: Controlling Inputs and Outputs

  • 5 min

A GPIO is a digital pin configurable as input or output to read buttons, control LEDs, activate relays, or detect limit switches.

In Arduino, this was as simple as digitalWrite(13, HIGH). In Zephyr, the philosophy is different. Zephyr aims for your code to be hardware agnostic. The code you write today to read a button should work tomorrow on an ESP32, a Nordic, or an STM32 without changing a single comma in the .c file.

To achieve this, Zephyr uses a Unified GPIO API. Today, we will master it.

The gpio_dt_spec Object

In the Devicetree-based API, pins are represented by a gpio_dt_spec (GPIO Devicetree Specification) structure instead of repeating physical numbers in the code.

This structure contains:

  1. The Port: The pointer to the controller (e.g., GPIOA, GPIO0).
  2. The Pin: The bit number (e.g., 13, 5).
  3. The Flags: Default configuration (e.g., GPIO_ACTIVE_LOW, GPIO_PULL_UP) extracted from the Devicetree.

To use a pin, we first define it in the Devicetree (using an alias, as we saw in previous chapters) and then capture it in C:

/* In Devicetree (app.overlay) */
/ {
    aliases {
        led0 = &my_led;
        sw0 = &my_button;
    };
};

/* In C (main.c) */
#define LED0_NODE DT_ALIAS(led0)
#define SW0_NODE  DT_ALIAS(sw0)

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);
Copied!

This macro GPIO_DT_SPEC_GET reads all the hardware properties and prepares the structure ready for use.

Digital Outputs

Once we have our led variable, using it is very intuitive, always using functions with the _dt suffix (DeviceTree).

Configuration

Before using the pin, we must tell the driver whether it is an input or output.

if (!gpio_is_ready_dt(&led)) {
    return 0; // Error: Driver is not ready
}

/* Configure as output and initialize to inactive state */
gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
Copied!

Writing

We can set the pin to 1, 0, or toggle it.

gpio_pin_set_dt(&led, 1);    // Turn on (Activate)
gpio_pin_set_dt(&led, 0);    // Turn off (Deactivate)
gpio_pin_toggle_dt(&led);    // Toggle state
Copied!

Notice I say “Activate” and not “Set to 5V”. If in the Devicetree you defined the LED as GPIO_ACTIVE_LOW, when setting a logical 1, Zephyr will put 0V on the physical pin. The API handles the negative logic for us.

Digital Inputs

To read a button, we first configure it. Here it is very useful to activate the internal Pull-Up or Pull-Down resistors if the hardware requires it.

/* Configure as input with Pull-Up */
/* GPIO_INPUT is mandatory. GPIO_PULL_UP is optional but recommended for buttons */
gpio_pin_configure_dt(&button, GPIO_INPUT | GPIO_PULL_UP);
Copied!

Polling Read

We can read the state in a loop.

int val = gpio_pin_get_dt(&button);
if (val > 0) {
    printk("Button pressed\n");
}
Copied!

This is fine for tests, but it wastes CPU power by constantly asking “Now? Now? Now?”. The professional way is to use Interrupts.

Interrupts (Callbacks)

The callback system for GPIO interrupts requires three steps:

Configure the trigger: When should it fire? (Rising edge, falling edge, both, level…).

Prepare the Callback structure: Zephyr needs a “record” to store which function to call.

Add the Callback: Subscribe our function to the GPIO port.

Code Structure

We need a struct gpio_callback variable that must live for the entire execution (usually static or global).

static struct gpio_callback button_cb_data;

/* This is the ISR function that will execute when the button is pressed */
void button_pressed(const struct device *dev, struct gpio_callback *cb,
                    uint32_t pins)
{
    printk("Button pressed in ISR!\n");
    /* The ISR must be brief: cannot sleep or execute heavy tasks. */
}
Copied!

Initialization in main

/* 1. Configure the interrupt in hardware */
/* GPIO_INT_EDGE_TO_ACTIVE: Triggers when it transitions to active state (e.g., pressed) */
gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

/* 2. Initialize the callback structure */
/* We tell it: "Use the function 'button_pressed' for the pin of this button" */
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));

/* 3. Add the callback to the port */
gpio_add_callback(button.port, &button_cb_data);
Copied!

Complete Example: Light Switch

Let’s put it all together. A button that, when pressed, changes the state of an LED using interrupts.

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <errno.h>

/* Get nodes from Devicetree */
#define LED0_NODE DT_ALIAS(led0)
#define SW0_NODE  DT_ALIAS(sw0)

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);

/* Structure to manage the callback */
static struct gpio_callback button_cb_data;

/* Interrupt function (ISR) */
void button_handler(const struct device *dev, struct gpio_callback *cb,
                    uint32_t pins)
{
    /* Toggle the LED instantly */
    gpio_pin_toggle_dt(&led);
}

int main(void)
{
    /* Safety checks */
    if (!gpio_is_ready_dt(&led) || !gpio_is_ready_dt(&button)) {
        return 0;
    }

    /* 1. Configure LED as output */
    if (gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE) < 0) {
        return -EIO;
    }

    /* 2. Configure Button as input with Pull-Up */
    if (gpio_pin_configure_dt(&button, GPIO_INPUT | GPIO_PULL_UP) < 0) {
        return -EIO;
    }
    /* 3. Register Callback */
    gpio_init_callback(&button_cb_data, button_handler, BIT(button.pin));
    if (gpio_add_callback(button.port, &button_cb_data) < 0) {
        return -EIO;
    }

    /* 4. Enable the interrupt after registering the callback */
    if (gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_BOTH) < 0) {
        return -EIO;
    }

    /* The main thread has nothing to do, it sleeps forever */
    while (1) {
        k_sleep(K_FOREVER);
    }
    return 0;
}
Copied!

Considerations on Debouncing

Mechanical buttons are noisy. When pressed, the electrical signal “bounces” several times, generating multiple interrupts in milliseconds.

Zephyr does not include automatic “Software Debouncing” in the basic GPIO driver (some hardware controllers do have it). If your button fires 10 times when pressed once, you have two options:

  1. Hardware: Use an RC network or a suitable conditioning circuit for the input.
  2. Software: Use a Timer or a Delayed Workqueue (as we saw in the previous article) to ignore successive presses.