Embedded engineers know the joys and pains with button inputs: debouncing them, triggering actions, and more advanced input patterns like long holding the button down or quickly clicking more than once. If your project is based on Zephyr, you should take a look at the input subsystem to see if it can do some of the hard work for you. Today we’ll look at using input events to handle button presses, double-taps, and longpresses.
Input Events in Zephyr
The input event system has been around for the last few version of Zephyr, but starting with the next release it will be the default system for the samples/basic/button application, replacing the more esoteric system of setting up callbacks. For me this is a welcome change as it makes a lot more sense:
#include <zephyr/input/input.h>
static void button_input_cb(struct input_event *evt, void *user_data)
{
if (evt->sync == 0) {
return;
}
printk("Button %d %s at %" PRIu32 "\n",
evt->code,
evt->value ? "pressed" : "released",
k_cycle_get_32());
}
INPUT_CALLBACK_DEFINE(NULL, button_input_cb, NULL);
The code above is all that it takes to register a callback that runs on every input event. The event includes a key code (evt->code) to test which keypress triggered the callback. The evt->value will be 1 for “pressed” and 0 for “released”.
In your devicetree, ensure that each button has a input code associated with it:
#include <zephyr/dt-bindings/input/input-event-codes.h>
/ {
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button switch 0";
zephyr,code = <INPUT_KEY_0>;
};
};
};
This is already a fantastic shortcut for controlling your application based on button presses, but there are two additional bindings that you may find useful: input-longpress and input-double-tap.
Detect Long and Short Button Presses
Long and short button presses are the most fully-featured pseudo input device implementation that I’ve found in Zephyr. This feature will be automatically selected in Kconfig by adding a node to your devicetree:
/{
longpress {
compatible = "zephyr,input-longpress";
input-codes = <INPUT_KEY_0>, <INPUT_KEY_1>;
short-codes = <INPUT_KEY_A>, <INPUT_KEY_B>;
long-codes = <INPUT_KEY_X>, <INPUT_KEY_Y>;
long-delay-ms = <1000>;
};
};
Let’s walk through what this node accomplishes:
- compatible: enables the input-longpress binding.
- input-codes: list of input codes (the keys you plan to press) to listen for.
- short-codes: the input event code to send when the button is pressed, but not held.
- long-codes: the input event code to send when the button is held.
- long-delay-ms: how long a button must be held before it is considered a longpress.
In your C code, we need to get the pseudo device from the devicetree, which we use as a filter when registering the callback.
#include <zephyr/input/input.h>
static const struct device *const longpress_dev = DEVICE_DT_GET(DT_PATH(longpress));
static void longpress_cb(struct input_event *evt, void *user_data)
{
if (evt->sync == 0) {
return;
}
int button_number = -1;
if (0 != evt->value) {
switch (evt->code) {
case INPUT_KEY_A:
case INPUT_KEY_X:
button_number = 1;
break;
case INPUT_KEY_B:
case INPUT_KEY_Y:
button_number = 2;
break;
default:
return;
}
switch (evt->code) {
case INPUT_KEY_A:
case INPUT_KEY_B:
printk("Short press: button %i\n", button_number);
break;
case INPUT_KEY_X:
case INPUT_KEY_Y:
printk("Long press: button %i\n", button_number);
break;
default:
return;
}
}
}
INPUT_CALLBACK_DEFINE(longpress_dev, longpress_cb, NULL);
At the top of this code example we use the longpress path to get the instance from the devicetree. At the bottom of the example we register the callback, passing longpress_dev as the first argument so that only input events from this device will cause the longpress_cb callback function to run.
The callback function itself really just filters for the desired button actions. First, it checks to see if the sync flag is set; some input events use this sync flag to indicate a stable state. From there, we use a conditional on evt->value to filter out the button release events. There are two nested switch statement, the first translates the event to a button number (this nRF52840dk board labels the buttons starting with 1 for key0 and 2 for key1) and the second prints a message for short or long presses.
The input-longpress binding uses the short-press parameter (which is optional) to remap the button presses. This way you will either get the new short keycode or the long keycode. The actual button code event (INPUT_KEY_0 or INPUT_KEY_1 in our example) will still occur but we have filtered them out from this callback.
Detect Double Tap Button Presses
The input-double-tap binding is a newer addition and not quite as useful as the longpress binding but still worth mentioning here. It is also a pseudo-device that you enable by adding a node in devicetree.
/{
double_tap: double_tap {
compatible = "zephyr,input-double-tap";
input-codes = <INPUT_KEY_2>, <INPUT_KEY_3>;
double-tap-codes = <INPUT_KEY_C>, <INPUT_KEY_D>;
double-tap-delay-ms = <300>;
};
};
This functions in a similar way to the previous example:
- compatible: enable the
input-double-tapbinding - input-codes: list of input codes (the keys you plan to press) to listen for
- double-tap-codes: the input event code to send when the button is quickly pressed twice
- double-tap-delay-ms: maximum amount of time between presses to be considered a double-tap
This issue with this binding is that it lacks the short-press member that was present in the longpress binding. Because of this, there is no way to separate the double-tap event from two key input events. Nonetheless, let’s implement callbacks for this binding:
#include <zephyr/input/input.h>
static const struct device *const double_tap_dev = DEVICE_DT_GET(DT_NODELABEL(double_tap));
static void double_tap_cb(struct input_event *evt, void *user_data)
{
if (evt->sync == 0) {
return;
}
int button_number = -1;
if (evt->value) {
switch (evt->code) {
case INPUT_KEY_C:
button_number = 3;
break;
case INPUT_KEY_D:
button_number = 4;
break;
default:
return;
}
printk("Double tap: button %i\n", button_number);
}
}
INPUT_CALLBACK_DEFINE(double_tap_dev, double_tap_cb, NULL);
The double_tap_cb callback is registered using the double_tap_dev pseudo-device as a filter. This makes it quite easy to detect two quick button presses. However, there is no detection for single button presses on those key. We could use the general input event callback show at the top of this post, but the downside is that we will receive two key events each time a double-tap event happens.
Implementing single-tap support on this binding is a great place to contribute some code improvements to the Zephyr project. We’ll see if I can find some time in the near future to work on this.
Start Using the Zephyr Input Subsystem
The input subsystem is a great way to offload some of the input monitoring for your projects. Here I’ve specifically detailed its use for button inputs, but the input subsystem is a great place to start for any of your GPIO input handling needs. Expect to see these implemented in reference designs and examples coming from Golioth soon.


No comments yet! Start the discussion at forum.golioth.io