Handling Button Press, Longpress, and Double-Tap with the Zephyr Input Subsystem

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-tap binding
  • 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.

Mike Szczys
Mike Szczys
Mike is a Senior Firmware Engineer at Golioth. His deep love of microcontrollers began in the early 2000s, growing from the desire to make more of the BEAM robotics he was building. During his 12 years at Hackaday (eight of them as Editor in Chief), he had a front-row seat for the growth of the industry, and was active in developing a number of custom electronic conference badges. When he's not reading data sheets he's busy as an orchestra musician in Madison, Wisconsin.

Post Comments

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

More from this author

Related posts

spot_img

Latest posts

WiFi HaLoW with Morse Micro, Zephyr, and Golioth

Wi-Fi HaLoW enables high throughput, at a long distance using ISM band radios (850-950 MHz, depending on location). This post introduces the concepts behind the demo and shows how to use Morse Micro, Zephyr, and Golioth to send data through walls and up to the cloud.

When Rapid Prototyping becomes Rapid Deployment

Modern IoT teams can move beyond rapid prototyping to rapid deployment. By combining LLM-assisted development with Golioth’s device management, connectivity, and fleet orchestration capabilities, it’s now possible to create IoT fleets with a range of connectivity options in days instead of months.

New Pipelines Data Destination: Amazon Kinesis

Amazon Kinesis is especially well-suited for high volume data and applications in which there are multiple consumers of the streamed data. Golioth pipelines added a destination that works directly with the Kinesis service.

Want to stay up to date with the latest news?

Subscribe to our newsletter and get updates every 2 weeks. Follow the latest blogs and industry trends.