# MultiButton: A Button State Machine for Embedded C

> MultiButton is a small C library that turns raw GPIO reads into click, double-click and long-press events. It suits firmware engineers who want debouncing and event logic handled in one 5ms tick, and it is not a UI toolkit or a host-side input library.

**0x1abin/MultiButton** — Button driver for embedded system

- Repository: https://github.com/0x1abin/MultiButton
- Stars: 2,380 · Forks: 740
- Language: C
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/0x1abin-multibutton

## The problem MultiButton solves on a microcontroller

Reading a button on a microcontroller looks trivial until you need a double click. A raw GPIO read gives you a level, not an intent. Between the level and the intent sit contact bounce, the difference between a short tap and a hold, and the question of how long to wait after a release before you declare that no second press is coming. Firmware projects usually grow this logic ad hoc, one counter at a time, until a single button handler has five timers and a bug where a long press also fires a click.

MultiButton puts that logic in a state machine. It is a C library aimed at embedded firmware, and the README describes it as a compact and flexible multi-button state machine library for embedded systems. The unit of work is a Button struct plus a periodic tick. You supply a function that reads the pin, you attach callbacks to the events you care about, and you call button_ticks() from a timer. The library decides which event has occurred.

It is for people writing firmware in C on bare metal or an RTOS. It is not for application code on a phone or a desktop, and the name collides with several unrelated products, so search results for it are noisy. The repository itself is unambiguous: multi_button.c, multi_button.h, an examples directory with three programs, and a Makefile that builds both a static and a shared library.

## How the state machine advances: ticks, debounce and the linked list

The core mechanism is a digital filter plus a state variable. The README states that button_ticks() should be called every 5ms from a timer, and that DEBOUNCE_TICKS is a filter depth with a maximum of 7. The button level must match the active level for that many consecutive ticks before the state machine accepts it, which is where contact bounce disappears. That also means the debounce window is a function of your tick rate: at the default tick interval, three ticks is 15ms of filtering.

Timing thresholds are expressed in ticks, not milliseconds. SHORT_TICKS is defined as (300 / TICKS_INTERVAL) and LONG_TICKS as (1000 / TICKS_INTERVAL), so changing TICKS_INTERVAL rescales both. The state machine moves through IDLE, PRESS, RELEASE, REPEAT and LONG_HOLD. A press that is released before the long threshold lands in RELEASE, and if no re-press arrives within the short timeout the library fires BTN_SINGLE_CLICK when the repeat count is 1 or BTN_DOUBLE_CLICK when it is 2. A press held past LONG_TICKS fires BTN_LONG_PRESS_START once, then BTN_LONG_PRESS_HOLD on every subsequent tick.

That last behaviour is the sharpest constraint in the library. The README warns that BTN_LONG_PRESS_HOLD fires on every tick, which at the default is 200Hz. If your callback does anything heavier than setting a flag, you are running that work inside your timer interrupt.

Button instances live in a linked list, which is how the library supports an arbitrary number of buttons without a fixed array. Each instance is described as roughly 30 bytes. Events reach your code either through callbacks attached per event type or by polling button_get_event().

## Installing MultiButton and getting a first click out of it

There is no package manager step. The repository is the distribution: copy multi_button.c and multi_button.h into your project, or build the library from the included Makefile. The Makefile uses gcc, ar and the flags -Wall -Wextra -std=c99 -O2 -g, and produces build/lib/libmultibutton.a for the library target and build/bin/basic_example for the example.

```bash
make library
make basic_example
./build/bin/basic_example
```

The first two commands build the static library and one of the example programs; the third runs it. The examples are host programs, so they compile and run on a desktop, but they need a pin-read function to be useful.

Wiring a button into your own firmware follows four steps from the README. You implement a GPIO read function, define a callback, initialise the button with button_init(), attach handlers with button_attach(), and start it with button_start(). Then you call button_ticks() from your timer.

```c
#include "multi_button.h"

static Button btn1;

uint8_t read_button_gpio(uint8_t button_id)
{
    return HAL_GPIO_ReadPin(BUTTON1_GPIO_Port, BUTTON1_Pin);
}

void on_single_click(Button* btn, void* user_data)
{
    // handle single click
}

void setup(void)
{
    button_init(&btn1, read_button_gpio, 0, 1);  // active low
    button_attach(&btn1, BTN_SINGLE_CLICK, on_single_click, NULL);
    button_start(&btn1);
}

void timer_5ms_isr(void)
{
    button_ticks();
}
```

button_init() takes the handle, the pin-read function, the active level (0 here, meaning active low) and a button identifier that is passed back to your read function. button_start() returns 0 on success, -1 for a duplicate and -2 for an invalid argument, so the return value is worth checking rather than discarding. Every callback receives the Button pointer and the void* user_data you passed to button_attach(); the README notes that user_data is stored per button, so all callbacks for one button share the same pointer.

## Triple click and N-click are not built in

The library ships seven events, and single click and double click are among them. Triple click is not. The README is explicit that for repeat counts of three or more, only BTN_PRESS_REPEAT fires during the press sequence, and that BTN_SINGLE_CLICK fires when the repeat count is 1 while BTN_DOUBLE_CLICK fires when it is 2 after the short-press timeout.

To build an N-click gesture you attach a handler to BTN_SINGLE_CLICK and read button_get_repeat_count() inside it, or attach to BTN_PRESS_REPEAT and watch the count as it climbs. The README's own example registers on_click_resolve for BTN_SINGLE_CLICK and checks whether the count equals 3. That works, but it means your triple-click handler runs on the single-click path, which is a slightly odd place for it. If you need triple click and single click to mean different things, you have to disambiguate yourself, and the library gives you the count rather than the gesture.

This is a reasonable design choice for a small library: the count is more general than a fixed set of N-click events, and it avoids a configuration knob per gesture. It does mean the README's N-click section is really a recipe rather than a feature.

## Thread safety, callback context and the locking trade-off

On bare metal the library has no locking at all. For RTOS environments, the README says to define MULTIBUTTON_THREAD_SAFE and the MULTIBUTTON_LOCK() and MULTIBUTTON_UNLOCK() macros before including the header, pointing them at your mutex acquire and release calls. Without MULTIBUTTON_THREAD_SAFE defined, the lock macros compile to nothing.

The design detail worth noting is that callbacks run outside the lock. The README states this is deliberate, so that button_stop() and button_start() can be called from inside a callback without deadlock, and that a regular non-recursive mutex is therefore sufficient. That is a sensible arrangement, but it also means the Button struct is not protected while your callback runs. If your callback touches shared state, the mutex the library uses will not cover it.

The RTOS path also assumes you have a mutex available at include time. The README's example uses osMutexAcquire and osMutexRelease from CMSIS-RTOS. If your RTOS names those differently, you supply your own macros; the library does not ship an abstraction over them.

## Where MultiButton is the wrong tool

The clearest failure mode is a tick that does not arrive. Every timing threshold in the library is counted in ticks, so if button_ticks() is called from a timer that drifts, is starved by a higher-priority interrupt, or stops when the MCU sleeps, the click and long-press boundaries move with it. There is no wall-clock fallback. A design that wakes the CPU only on a GPIO edge will not drive this library correctly, because the state machine needs to be told that time has passed even when nothing has changed on the pin.

BTN_LONG_PRESS_HOLD is the second constraint. Firing a callback at 200Hz is fine for a flag and wrong for anything that blocks, allocates, or writes to a bus. The README flags this behaviour rather than hiding it, which is the right call, but it puts the responsibility on you to keep that handler short.

The third case is scope. MultiButton handles buttons. It has no notion of encoders, key matrices, capacitive touch, or gesture recognition beyond counting presses. If your input is a rotary encoder or a touch panel, this library has nothing to offer. It also assumes one GPIO per button through the pin-read callback, with no built-in matrix scanning.

Finally, the memory claim of roughly 30 bytes per button is a per-instance figure for the struct. The linked list, the callbacks you attach and any user_data you pass are separate, so the total cost depends on how you use it.

## How MultiButton compares with writing the debounce yourself

The realistic alternative is not another library. It is a counter in your own timer interrupt: read the pin, increment or decrement a debounce counter, compare against a threshold, and set flags. That approach has no dependency, no header to include, and no state machine to learn. For a single button that only needs press and release, it is fewer lines than MultiButton's four-step setup.

The difference in approach shows up when the gesture set grows. Hand-rolled debounce code typically keeps one counter and one boolean, and adding double click means adding a timeout, then adding long press means adding a second timeout, then adding repeat counting means tracking a count across releases. MultiButton encodes those transitions as named states with documented edges, so the behaviour is inspectable rather than emergent. The README lists each transition, including the ones that are easy to get wrong, such as REPEAT to PRESS when a button is held too long during a repeat sequence.

The cost is the tick dependency and the 200Hz hold event. If your firmware already has a 5ms tick and more than one button, the library is likely less code than the equivalent hand-rolled version. If you have one button and no tick, the trade goes the other way.

## Licence, maintenance and the cost of upgrading

MultiButton is MIT licensed. In practice that means you can copy multi_button.c and multi_button.h into a proprietary firmware tree, keep the copyright notice, and ship. The repository has no releases, so there is no versioned artefact to pin against and no changelog entries tied to tags. Upgrading means diffing the two source files against your vendored copy.

The last push to master was on 2026-03-17, roughly six months before this writing. That is recent enough that the code is not stale, but the absence of releases and the presence of a CHANGELOG.md without retrieved entries means you should read the file history rather than assume a stable interface. The public API is small, which limits the blast radius of a change, but the configuration defines in multi_button.h are part of your build and a change to their defaults would alter timing behaviour silently.

MIT imposes no obligation to publish modifications, so if you fork the timing constants for your hardware, you carry that divergence yourself. There is no upstream to merge it back into unless you send a patch.

## Conclusion

Adopt MultiButton if you have a bare-metal or RTOS firmware project with a periodic tick and want click, double-click and long-press logic out of your application code. Do not adopt it if you are building a desktop or mobile UI, or if you cannot supply a fixed 5ms tick, because the timing thresholds are expressed in ticks and the whole state machine advances only when button_ticks() runs. Before wiring it in, read the BTN_LONG_PRESS_HOLD note in the README and decide whether a 200Hz callback fits your interrupt budget, then check the defines in multi_button.h against your hardware's bounce behaviour. The last push to master was on 2026-03-17.

## FAQ

### How do I use MultiButton with an RTOS?

Define MULTIBUTTON_THREAD_SAFE and the MULTIBUTTON_LOCK() and MULTIBUTTON_UNLOCK() macros before including multi_button.h, pointing them at your mutex calls. Callbacks run outside the lock, so a regular non-recursive mutex is enough.

### Does MultiButton support triple click or N-click gestures?

Not as dedicated events. The README says only BTN_PRESS_REPEAT fires during the press sequence for repeat counts of three or more, and you read button_get_repeat_count() from a callback to detect the pattern.

### How often should button_ticks() be called?

The README's quick start calls it from a 5ms timer interrupt, and TICKS_INTERVAL defaults to 5. The short and long press thresholds are defined as tick counts derived from that interval, so the two must stay consistent.

### How many buttons can one MultiButton instance handle?

The README describes a linked-list architecture that supports any number of button instances, with each instance taking roughly 30 bytes. There is no fixed array size to configure.

## Sources

- [0x1abin/MultiButton on GitHub](https://github.com/0x1abin/MultiButton)
- [Issues](https://github.com/0x1abin/MultiButton/issues)
- [License: MIT](https://github.com/0x1abin/MultiButton/blob/master/LICENSE)
- [README](https://github.com/0x1abin/MultiButton/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/0x1abin-multibutton
