BME554L - Spring 2026
Duke University
February 1, 2026
read() in main()void main() {
while (1) {
// do stuff
k_msleep(1000);
sw0_event = command_to_read_digital_pin();
if (sw0_event) {
// do stuff
}
}
} What if you press the button while the code is “sleeping”?
What if “do stuff” is time consuming / blocking?
Trying to read a button at a specific time is like only checking if you have an incoming phone call at a specific time interval (i.e., no ring).
Instead, think of the ringing phone as an event trigger that you allow to happen “whenever”.
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.
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.
prj.conf - enable GPIO, enable logging libraries
devicetree.overlay - define GPIO pin as an input, define callback function
main.c - initialize GPIO struct, initialize callback struct, associate callback with GPIO pin, define callback function, test for callback event state in your code
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
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
// 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
// declare callback function
void sw0_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins);gpio_callback: struct prototype is defined in gpio.h
sw0_cb: name of the struct based on the gpio_callback prototype that will store the information about the callback function
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.
main()// check if interface is ready
if (!device_is_ready(sw0.port)) {
LOG_ERR("gpio0 interface not ready."); // logging module output
return -1; // 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.");
return err;
}
// 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.");
}
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 () {
if (sw0_event) {
do_something_less_trivial(); // this can take more time than the callback function
sw0_event = 0;
}
}If you want a button to have multiple functionalities, you can do so by changing the callback associated with the button.
Next are the steps to set up a button with 2 different callbacks.
// 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)
{
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:
Other times you’ll wish to deactivate the button entirely.
However, removing the callback like above will still cause the interrupt to trigger.
Instead, it is best to remove the interrupt with gpio_pin_configure_dt:
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.
// led is a gpio struct you have already created from the devicetree
// check if interface is ready
if (!device_is_ready(led.port)) {
LOG_ERR("gpio0 interface not ready."); // logging module output
return -1; // 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;
}#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 BLOCKINGThere 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
What is debouncing?
“Debouncing” is a technique used to ensure that only a single signal is registered when a button is pressed, despite the fact that mechanical buttons can produce multiple rapid on/off signals (bounces) when pressed or released.