GPIO / ISR / Callbacks
BME554L -Fall 2025 - Palmeri
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.
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 DT- gpio_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_callback
prototype that will store the information about the callback function
// define callback function
void sw0_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
= 1; // conditional statement in main() can now do something based on the event detection
sw0_event // 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
event
or 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 (!device_is_ready(sw0.port)) {
("gpio0 interface not ready."); // logging module output
LOG_ERRreturn -1; // exit code that will exit main()
}
// configure GPIO pin
int err;
= gpio_pin_configure_dt(&sw0, GPIO_INPUT);
err if (err < 0) {
("Cannot configure sw0 pin.");
LOG_ERRreturn err;
}
// associate callback with GPIO pin
= gpio_pin_interrupt_configure_dt(&sw0, GPIO_INT_EDGE_TO_ACTIVE); // trigger on transition from INACTIVE -> ACTIVE
err // ACTIVE could be HIGH or LOW
if (err < 0) {
("Cannot attach callback to sw0.");
LOG_ERR}
(&sw0_cb, sw0_callback, BIT(sw0.pin)); // populate CB struct with information about the CB function and pin
gpio_init_callback(sw0, &sw0_cb); // associate callback with GPIO pin
gpio_add_callback_dt
// test for the callback event state in your code...
while () {
if (sw0_event) {
(); // this can take more time than the callback function
do_something_less_trivial= 0;
sw0_event }
}
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)
{
= 1;
different_event //This callback now toggles a different event trigger
}
//Associate the second callback function to the second callback struct
(&sw0_cb_2, sw0_callback_2, BIT(sw0.pin)); gpio_init_callback
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:
(sw0, &sw0_cb);
gpio_remove_callback_dt(sw0, &sw0_cb_2); gpio_add_callback_dt
Callback Functions Should Not Test For State
void callback_function(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
if (state == AWAKE) {
= NEW_STATE_A;
state } else (state == SLEEP) {
= NEW_STATE_B;
state }
}
- 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 (!device_is_ready(led.port)) {
("gpio0 interface not ready."); // logging module output
LOG_ERRreturn -1; // exit code that will exit main()
}
// configure GPIO pin
int err;
= gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); // ACTIVE referes to ON, not HIGH
err if (err < 0) {
("Cannot configure GPIO output pin.");
LOG_ERRreturn 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) {
("Cannot toggle GPIO output pin.");
LOG_ERRreturn ret;
}
= !led_state;
led_state ("LED state: %s\n", led_state ? "ON" : "OFF");
LOG_INF(SLEEP_TIME_MS); // this is BLOCKING k_msleep
Interrupt 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.: