[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 :
- Clean:
rm -rf build
- Compile:
west build -p auto -b esp32_devkitc_wroom .
- Flash:
west flash
- 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.
switch_cb_data
: This is a data structure that holds information about the callback. Think of it as a registration form for your interrupt handler.- The
switch_cb_data
structure holds information about the callback, including a pointer to the callback function and the pin(s) it's associated with. It's used to package all the necessary information about a callback together. - a
single switch_cb_data
structure is associated with one callback function. However, that single callback function can handle multiple pins. If you want separate functions for different pins, you'd need to create multiple gpio_callback structures
- The
switch_pressed_callback
: This is the actual function that will be called when the interrupt occurs. It's equivalent to your ISR in traditional embedded programming.BIT(SWITCH_PIN)
: This specifies which pin(s) this callback is for. It's a bitmask, allowing you to use the same callback for multiple pins if needed.gpio_add_callback(gpio_dev, &switch_cb_data)
: This function registers the callback with the specified GPIO device. It tells the Zephyr GPIO subsystem, "When an interrupt occurs on this GPIO device, and it matches the pin mask in this callback structure, call this function."
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?
- Flexibility: You can use the same callback function for multiple pins or change callbacks dynamically.
- Information Passing: The
switch_cb_data
structure can contain additional information that gets passed to your callback function. - 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