How to Debounce Button Inputs in a Real Time Operating System
Embedded systems are riddled with complexity, mainly because they are at the intersection of expertise. Problem are often a mixture of software, electrical and mechanical issues. This is the case even for seemingly simple tasks, such as reliably detecting a button-press.
“Wait — a button-press??”
Let’s think for a second, how a button press is detected:
- Initial State: a button-press typically consists of a “normally-open switch”, which through a pull-up/down resistor, is normally “high” or normally ‘low’: this is the initial state.
- Press Event: then, when the button is pressed, the switch is closed and the signal transitions towards the opposite state (e.g, low for normally “high” state), and it’s sustained for as long as the button is held by the user.
- Release Event: Finally, when the button is released, it goes back to its initial state.
In an ideal world, we could just look at the signal edges to keep track of the transitions and assume a falling edge is a “press-event” and rising edge the “release event”. In our world, these transitions are affected by electrical transients caused by the mechanical properties of the button actuator. The suppression of this noise it’s commonly called “debouncing”.
“Ok, I get it. How can we `debounce` button-presses?”
There are two main ways we can approach this: the hardware-way and the software-way. Today I’ll touch on both, and detail the technique I prefer to use when debouncing button input with the Zephyr RTOS.
The Hardware Way
The hardware-way focuses on the root cause: the electrical noise. It guarantees the digital signal won’t have such noise during transitions; and it does it through the use of low-pass filters (most probably RC-filters). There are some pretty cool articles that detail this approach (see references at the end of the article).
The Software Way
On the other hand, the software-way is about “ignoring” these false positives on the signal transitions to determine which ones are the real events we’re looking for, and which ones aren’t. Even though there are many ways implement software debouncing, there are two main approaches, depending on whether the variable of interest is the signal state or the transitions: periodic sampling and tracking edge interrupts.
A. Periodic Button State Sampling
Button sampling works by periodically acquiring the signal state, which is buffered on a continuously rolling sample-set. Through detection of consecutive states, the signal change event is detected (either pressed or released). The rule is simple: if there are X-number of consecutive samples with a changed state, we assume the transition really happened. This periodic sampling is often in the order of 10 to 25-ms and is commonly paced by a hardware timer to guarantee fixed intervals and to free some CPU usage.
B. Tracking Edge-Interrupts with Minimum Cooldown
This works by coordinating the detection of edge changes with the spacing between these: there must be a minimum duration before legitimate signal transition. This cooldown phase is commonly implemented through a timer, which kicks-off on the edge-interrupts: the timer gets restarted on each transition and only when it expires (after 10-ms to 25-ms of no edge-changes), the firmware handles the transition as an event.
In multi-threaded systems, we can leverage the use of RTOS primitives to make the code more modular while simplifying the logic to achieve the same purpose: featuring a thread and semaphores to control the state transitions and decide when to notify the user of the module when an event occurred.
Example Code – Zephyr RTOS
The following code presents an example of achieving the software approach B on Zephyr, with the following observations:
- We’re using Zephyr GPIO interrupt APIs to keep track of the edge-changes.
- We’re using the system workqueue as the cooldown mechanism for false positives.
- Our button-detection module, abstracts both of these details, and only notifies the user of the relevant events: pressed or released.
- Note: the user callback context is the workqueue handler, therefore: actions on this context shall be kept brief to allow proper functioning of other parts of the system.
#ifndef _BUTTON_H_ #define _BUTTON_H_ enum button_evt { BUTTON_EVT_PRESSED, BUTTON_EVT_RELEASED }; typedef void (*button_event_handler_t)(enum button_evt evt); int button_init(button_event_handler_t handler); #endif /* _BUTTON_H_ */
#include <zephyr/kernel.h> #include <zephyr/drivers/gpio.h> #include "button.h" #define SW0_NODE DT_ALIAS(sw0) static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios); static struct gpio_callback button_cb_data; static button_event_handler_t user_cb; static void cooldown_expired(struct k_work *work) { ARG_UNUSED(work); int val = gpio_pin_get_dt(&button); enum button_evt evt = val ? BUTTON_EVT_PRESSED : BUTTON_EVT_RELEASED; if (user_cb) { user_cb(evt); } } static K_WORK_DELAYABLE_DEFINE(cooldown_work, cooldown_expired); void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { k_work_reschedule(&cooldown_work, K_MSEC(15)); } int button_init(button_event_handler_t handler) { int err = -1; if (!handler) { return -EINVAL; } user_cb = handler; if (!device_is_ready(button.port)) { return -EIO; } err = gpio_pin_configure_dt(&button, GPIO_INPUT); if (err) { return err; } err = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_BOTH); if (err) { return err; } gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin)); err = gpio_add_callback(button.port, &button_cb_data); if (err) { return err; } return 0; }
Check out the working sample code on https://github.com/ubieda/zephyr_button_debouncing
Conclusion
The most important part of debouncing inputs is to understand how far you should go and which approach best suits you. Cost sensitivity tends to favor software debouncing, whereas less CPU usage favors offloading it to the hardware approach. Like any engineering problem, there are 1000 ways to solve it: always favor the simplest (yet effective) solution that works for you.
Start the discussion at forum.golioth.io