stateDiagram-v2
direction LR
prj_conf : CONFIG_GPIO=y
note right of prj_conf : This enables gpio.h to be included
overlay : nrf52833dk_nrf52833.overlay
overlay : Enable the GPIO ports/pins
overlay : Create aliases
state "main.c" as mainc {
Preamble : Create GPIO struct from DT alias
Preamble : Declare and define callback function
state "main()" as main {
state "Configure port & pin" as config {
direction LR
[*] --> configA
configA : Check GPIO port is ready
configA --> configB
configB : Configure/initialize GPIO pin
}
state "Associate GPIO pin with ISR/CB" as isrcb {
direction LR
[*] --> isrcbA
isrcbA : Attach interrupt to pin
isrcbA --> isrcbB
isrcbB : Declare and initialize callback function
isrcbB --> isrcbC
isrcbC : Activate (add) the callback
}
config --> isrcb
}
}
prj_conf --> overlay
overlay --> mainc
GPIO / ISR / Callbacks
BME554L - Fall 2026
Why Callbacks?
Interrupts & Callbacks
Interrupts

What do you do when the phone rings?
You might not be able to take a 2-hour phone call right when your phone rings, but you can:
Acknowledge that you received the call (e.g., thumbs-up emoji text message)
Add an item to your “to do” list (i.e., “queue”) to call the person back when you have time
What you do when the phone rings (the interrupt) is captured in a callback function.
ISR -> Callback Function
An interrupt can be used to call a callback function (“callback” = a function executed in response to an interrupt or event).
Need to execute the callback / ISR quickly to not paralyze the rest of
main()/ other threads from running, otherwise device is paralyzed from acting.Avoid calculations, significant IO, data Tx/Rx, etc.
Prefer simple actions, like toggling the state of a Boolean variable or posting an event.
Callback functions can be removed (gpio_remove_callback_dt()) or re-assigned from an ISR.
Button function can change as a function of the state of the system.
Button can be disabled or re-enabled based on the state of the system.
Atomicity and Thread Safety
Wait, with multiple threads being able to operate on things, are variables being handled by ISRs “safe” to use if they can be called at any time?
Solution: Atomicity and thread safety.
Atomic Operations
- An atomic operation is an indivisible operation that either completes entirely or does not execute at all.
- Atomic operations are used to prevent race conditions and ensure thread safety when multiple threads access shared resources.
- In Zephyr, atomic operations can be performed using the
atomicAPI, which provides functions for atomic read, write, and modify operations on variables.
Zephyr Implementation
Overview
prj.conf- enable GPIO, enable logging librariesdevicetree.overlay- define GPIO pin as an input, define callback functionmain.c- initialize GPIO struct, initialize callback struct, associate callback with GPIO pin, define callback function, test for callback event state in your code
Devicetree (overlay): gpio-keys
The Devicetree is used to separate hardware-specific definitions from the firmware logic. Your development kit has a pre-defined devicetree in Zephyr that can be modified with an overlay file. This overlay file can be:
Manually edited (YAML format), or
Edited/visualized with the nRF DeviceTree extension in VS Code
/ {
aliases {
sw0: &button0;
}
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button";
};
};
};What is “ACTIVE” state?
GPIO_ACTIVE_LOW- button is active when pulled to low voltage (GND)GPIO_ACTIVE_HIGH- button is active when pulled to high voltage (VDD)

https://docs.nordicsemi.com/bundle/ug_nrf52833_dk/page/UG/dk/hw_buttons_leds.html
Collecting GPIO Information from the Devicetree into a struct
// create this struct before main()
// initialize GPIO struct
static const struct gpio_dt_spec sw0 = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);GPIO_DT_SPEC_GET: macro to get GPIO information from the DT
DT_ALIAS: reference the pin of interest by an alias (sw0) in the DTgpio_dt_spec: struct prototype to store all of the information about this GPIO pin
sw0: name of the struct that will store the information about the GPIO pin
GPIO Input Functionality
Define Callback Function
// declare callback function
void sw0_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins);// initialize GPIO callback struct
static struct gpio_callback sw0_cb; gpio_callback: struct prototype is defined in gpio.h
sw0_cb: name of the struct based on thegpio_callbackprototype that will store the information about the callback function
#include <zephyr/sys/atomic.h>
// Declare atomic variable
static atomic_t sw0_event = ATOMIC_INIT(0);
// define callback function
void sw0_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
// Set the event flag when the button is pressed
atomic_set(&sw0_event, 1); // conditional statement in main() can now do something based on the
// event detection we can also use actual system kernel event flags,
// but this is simpler (for now)
}Constraints on Callback Functions
The contents of this function should consume minimal resources / time (i.e., cannot “block”).
Common action is to set an
eventor toggle the state of a Boolean, the value of which is reset after action is taken in the main code.
Within main()
// check if interface is ready
if (!gpio_is_ready_dt(&sw0)) {
LOG_ERR("gpio0 interface not ready."); // logging module output
return -ENODEV; // exit code that will exit main()
}
// configure GPIO pin
int err;
err = gpio_pin_configure_dt(&sw0, GPIO_INPUT);
if (err < 0) {
LOG_ERR("Cannot configure sw0 pin [%d].", err);
return err;
// -ENOTSUP if any of the configuration options is not supported
// -EINVAL Invalid argument
// -EIO I/O error when accessing an external GPIO chip
// -EWOULDBLOCK if operation would block
}
// associate callback with GPIO pin
err = gpio_pin_interrupt_configure_dt(&sw0, GPIO_INT_EDGE_TO_ACTIVE); // trigger on transition from INACTIVE -> ACTIVE
// ACTIVE could be HIGH or LOW
if (err < 0) {
LOG_ERR("Cannot attach callback to sw0 [%d].", err);
}
gpio_init_callback(&sw0_cb, sw0_callback, BIT(sw0.pin)); // populate CB struct with information about the CB function and pin
gpio_add_callback_dt(sw0, &sw0_cb); // associate callback with GPIO pin
// test for the callback event state in your code...
while () {
// Check if the button was pressed
if (atomic_clear(&sw0_event)) {
// Button was pressed, do work here
LOG_INF("Button press detected!");
gpio_pin_toggle_dt(&led);
}
}Some useful API documentation
Placed outside of while loop
// declare second callback function
void sw0_callback_2(const struct device *dev, struct gpio_callback *cb, uint32_t pins);
// initialize second GPIO Callback Struct}
static struct gpio_callback sw0_cb_2;
// define second callback function.
void sw0_callback_2(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
atomic_set(&different_event, 1) ;
//This callback now toggles a different event trigger
}
//Associate the second callback function to the second callback struct
gpio_init_callback(&sw0_cb_2, sw0_callback_2, BIT(sw0.pin));The gpio_callback struct is used to store information about the callback function. This includes the function and the GPIO pin that it is associated with.
Once this is setup, the following syntax will switch the function associated with the button press:
gpio_remove_callback_dt(sw0, &sw0_cb);
gpio_add_callback_dt(sw0, &sw0_cb_2);Callback Functions Should Not Test For State
void callback_function(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
if (state == AWAKE) {
state = NEW_STATE_A;
} else (state == SLEEP) {
state = NEW_STATE_B;
}
}Instead, have state-specific callbacks for each ISR:
Detach a callback function in an exit transition state.
Attach a new state-specific callback function in an entry transition state.
GPIO Output Functionality
Configure GPIO Pin as Output
// led is a gpio struct you have already created from the devicetree
// check if interface is ready
if (!gpio_is_ready_dt(&led)) {
LOG_ERR("gpio0 interface not ready."); // logging module output
return -ENODEV; // exit code that will exit main()
}
// configure GPIO pin
int err;
err = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); // ACTIVE referes to ON, not HIGH
if (err < 0) {
LOG_ERR("Cannot configure GPIO output pin.");
return err;
}Set Pin State
#define SLEEP_TIME_MS 1000
bool led_state = true;
int ret = gpio_pin_toggle_dt(&led);
// can explicitly set with gpio_pin_set_dt(&led, led_state);
if (ret < 0) {
LOG_ERR("Cannot toggle GPIO output pin.");
return ret;
}
led_state = !led_state;
LOG_INF("LED state: %s\n", led_state ? "ON" : "OFF");
k_msleep(SLEEP_TIME_MS); // this is BLOCKINGInterrupt Configuration Flags
There are several other interrupt configuration flags that can be used to toggle the interrupt to trigger on falling edge, on both edges, etc.:
Relevant MACROS:
GPIO_INT_EDGE_[TO_ACTIVE/TO_INACTIVE/BOTH]GPIO_INT_DISABLE