A robust, feature-rich button handling library for embedded systems with advanced state machine implementation, debouncing, and multi-click detection.
✅ State Machine Based - Clean, predictable button behavior
✅ Advanced Debouncing - Eliminates mechanical bounce (press & release)
✅ Long Press Detection - Configurable long press threshold
✅ Auto-Repeat - Continuous callbacks while button held
✅ Multi-Click Support - Double and triple click detection (normal & combined modes)
✅ Reverse Logic - Support for active-high and active-low buttons
✅ Idle Detection - Callback for button inactivity
✅ Error Handling - All functions return status codes
✅ NULL-Safe - Built-in pointer validation
✅ Portable - Works with HAL or bare-metal
#include "button.h"
// Global tick variable (updated in SysTick interrupt)
volatile uint32_t g_system_tick = 0;
// Button instance
button_t my_button;
int main(void) {
// Register tick source
BTN_tick_variable_register(&g_system_tick);
// Initialize button (default timings: 50ms debounce, 500ms long press, 300ms repeat)
ButtonInitKeyDefault(&my_button, GPIOA, GPIO_PIN_0, NON_REVERSE, 1);
// Register callbacks
ButtonRegisterPressCallback(&my_button, on_button_press);
ButtonRegisterReleaseCallback(&my_button, on_button_release);
while(1) {
ButtonTask(&my_button); // Call regularly in main loop
// ... other tasks
}
}
void on_button_press(uint16_t button_id) {
printf("Button %d pressed!\n", button_id);
}
void on_button_release(uint16_t button_id) {
printf("Button %d released!\n", button_id);
}// Custom timings: 100ms debounce, 1000ms long press, 200ms repeat
ButtonInitKey(&my_button, GPIOB, GPIO_PIN_5, 100, 1000, 200, NON_REVERSE, 2);The library uses a well-defined state machine for reliable button handling:
┌─────────────────────────────────────────────────────────────┐
│ Button States │
└─────────────────────────────────────────────────────────────┘
IDLE ──[press]──> DEBOUNCE ──[stable]──> PRESSED
↑ │ │
│ [unstable] [hold > long_time]
│ │ │
│ ↓ ↓
│ IDLE REPEAT ─┐
│ │ │ [hold > repeat_time]
│ [release] │
│ │ │
│ ↓ ↓
└─────────[release]────── DEBOUNCE_RELEASE* ────┘
│
[stable release]
│
↓
┌───────────────┴────────────────┐
│ │
RELEASE RELEASE_AFTER_REPEAT*
│ │
└────────> IDLE <─────────────────┘
* Optional states (configurable via macros)
| State | Description |
|---|---|
IDLE |
Button is not pressed, waiting for input |
DEBOUNCE |
Filtering mechanical bounce after press detected |
PRESSED |
Button press confirmed, waiting for long press or release |
REPEAT |
Long press detected, triggering repeat callbacks |
DEBOUNCE_RELEASE |
Filtering mechanical bounce on release (optional) |
RELEASE |
Normal button release |
RELEASE_AFTER_REPEAT |
Release after repeat state (optional) |
BTN_operate_status BTN_tick_variable_register(BTN_TIME_t *Variable);Must be called first! Registers the system tick variable.
Parameters:
Variable- Pointer to system tick counter (incremented in interrupt)
Returns: BTN_OK or BTN_ERROR
BTN_operate_status ButtonInitKey(button_t *Key, GPIO_TypeDef *GpioPort,
uint16_t GpioPin, BTN_TIME_t TimerDebounce,
BTN_TIME_t TimerLongPressed, BTN_TIME_t TimerRepeat,
ReverseLogicGpio_t ReverseLogic, uint16_t Number);Initialize button with custom parameters.
Parameters:
Key- Pointer to button structureGpioPort- GPIO port (e.g., GPIOA)GpioPin- GPIO pin (e.g., GPIO_PIN_0)TimerDebounce- Debounce time in ms (recommended: 20-100)TimerLongPressed- Long press threshold in ms (typical: 500-2000)TimerRepeat- Repeat interval in ms (typical: 100-500)ReverseLogic-NON_REVERSE(active-low) orREVERSE(active-high)Number- Button identifier (passed to callbacks)
Returns: BTN_OK or BTN_ERROR
BTN_operate_status ButtonInitKeyDefault(button_t *Key, GPIO_TypeDef *GpioPort,
uint16_t GpioPin, ReverseLogicGpio_t ReverseLogic,
uint16_t Number);Initialize button with default timings (50ms debounce, 500ms long press, 300ms repeat).
BTN_operate_status ButtonTask(button_t *Key);Must be called regularly! Processes button state machine. Call in main loop or timer interrupt.
Returns: BTN_OK or BTN_ERROR
All callback functions have signature: void callback(uint16_t button_number)
Triggered when button press is confirmed (after debounce).
Triggered once when button held longer than TimerLongPressed.
Triggered repeatedly every TimerRepeat ms while button is held.
Triggered when button released after short press.
Triggered when button released after long press/repeat (requires BTN_RELEASE_AFTER_REPEAT).
Triggered on double-click (requires BTN_MULTIPLE_CLICK).
Triggered on triple-click (requires BTN_MULTIPLE_CLICK).
Example:
void my_callback(uint16_t btn_id) {
switch(btn_id) {
case 1: handle_button1(); break;
case 2: handle_button2(); break;
}
}
ButtonRegisterPressCallback(&button, my_callback);BTN_operate_status ButtonSetDebounceTime(button_t *Key, BTN_TIME_t Milliseconds);Change debounce time at runtime.
Change long press threshold.
Change repeat interval.
Change release debounce time (requires BTN_DOUBLE_DEBOUNCING).
Change time window for multi-click detection (requires BTN_MULTIPLE_CLICK).
BTN_operate_status ButtonSetMultipleClick(button_t *Key,
MultipleClickMode_t Mode,
BTN_TIME_t TimerBetweenClick);Modes:
BTN_MULTIPLE_CLICK_OFF- Disable multi-clickBTN_MULTIPLE_CLICK_NORMAL_MODE- Immediate callbacks for each clickBTN_MULTIPLE_CLICK_COMBINED_MODE- Delayed callbacks after click sequence
Normal Mode Behavior:
Click 1: ButtonPressed(1)
Click 2: ButtonPressed(2) + ButtonDoubleClick(1)
Click 3: ButtonPressed(3) + ButtonDoubleClick(1) + ButtonTripleClick(1)
Combined Mode Behavior:
Click 1: [wait] → ButtonPressed(1)
Click 2: [wait] → ButtonDoubleClick(1)
Click 3: [wait] → ButtonTripleClick(1)
Example:
ButtonSetMultipleClick(&btn, BTN_MULTIPLE_CLICK_NORMAL_MODE, 300);
ButtonRegisterDoubleClickCallback(&btn, on_double_click);
ButtonRegisterTripleClickCallback(&btn, on_triple_click);BTN_operate_status ButtonSetNonUsed(button_t *Key, BTN_TIME_t Milliseconds,
void *Callback);Trigger callback after button is idle for specified time. Useful for sleep/timeout.
Example:
void on_idle(uint16_t btn) {
enter_low_power_mode();
}
ButtonSetNonUsed(&btn, 30000, on_idle); // 30 seconds#define BTN_RELEASE_AFTER_REPEAT 1 // Enable separate release callback after repeat
#define BTN_DOUBLE_DEBOUNCING 1 // Enable debouncing on release
#define BTN_MULTIPLE_CLICK 1 // Enable multi-click detection
#define BTN_DEFAULT_INIT 1 // Enable ButtonInitKeyDefault()
#define BTN_NON_USED_CALLBACK 1 // Enable idle detection#define BTN_DEFAULT_TIME_DEBOUNCE 50 // ms
#define BTN_DEFAULT_TIME_LONG_PRESS 500 // ms
#define BTN_DEFAULT_TIME_REPEAT 300 // ms#define BTN_FORCE_NON_HAL 1 // Use direct register access instead of HAL
#define BTN_USER_READ_PIN_ROUTINE 0 // Use custom GPIO read function
#define BTN_GPIO_PORT_T GPIO_TypeDef
#define BTN_GPIO_PIN_T uint16_tbutton_t btn_up, btn_down, btn_enter;
void init_buttons(void) {
BTN_tick_variable_register(&system_tick);
ButtonInitKeyDefault(&btn_up, GPIOA, GPIO_PIN_0, NON_REVERSE, 1);
ButtonInitKeyDefault(&btn_down, GPIOA, GPIO_PIN_1, NON_REVERSE, 2);
ButtonInitKeyDefault(&btn_enter, GPIOA, GPIO_PIN_2, NON_REVERSE, 3);
ButtonRegisterPressCallback(&btn_up, menu_up);
ButtonRegisterPressCallback(&btn_down, menu_down);
ButtonRegisterPressCallback(&btn_enter, menu_select);
}
void main_loop(void) {
while(1) {
ButtonTask(&btn_up);
ButtonTask(&btn_down);
ButtonTask(&btn_enter);
}
}button_t vol_up, vol_down;
void init_volume_buttons(void) {
ButtonInitKeyDefault(&vol_up, GPIOB, GPIO_PIN_0, NON_REVERSE, 10);
ButtonInitKeyDefault(&vol_down, GPIOB, GPIO_PIN_1, NON_REVERSE, 11);
// Single press and auto-repeat
ButtonRegisterPressCallback(&vol_up, volume_change);
ButtonRegisterRepeatCallback(&vol_up, volume_change);
ButtonRegisterPressCallback(&vol_down, volume_change);
ButtonRegisterRepeatCallback(&vol_down, volume_change);
// Faster repeat for volume
ButtonSetRepeatTime(&vol_up, 100);
ButtonSetRepeatTime(&vol_down, 100);
}
void volume_change(uint16_t btn) {
if(btn == 10) volume++;
else volume--;
update_display();
}button_t power_btn;
void init_power_button(void) {
ButtonInitKeyDefault(&power_btn, GPIOC, GPIO_PIN_13, NON_REVERSE, 99);
ButtonRegisterPressCallback(&power_btn, toggle_backlight);
ButtonRegisterLongPressedCallback(&power_btn, power_off_dialog);
}
void toggle_backlight(uint16_t btn) {
backlight_enabled = !backlight_enabled;
}
void power_off_dialog(uint16_t btn) {
show_shutdown_confirmation();
}button_t settings_btn;
void init_settings_button(void) {
ButtonInitKeyDefault(&settings_btn, GPIOD, GPIO_PIN_8, NON_REVERSE, 50);
ButtonSetMultipleClick(&settings_btn, BTN_MULTIPLE_CLICK_NORMAL_MODE, 400);
ButtonRegisterPressCallback(&settings_btn, normal_settings);
ButtonRegisterTripleClickCallback(&settings_btn, advanced_settings);
}
void normal_settings(uint16_t btn) {
open_settings_menu();
}
void advanced_settings(uint16_t btn) {
open_developer_menu(); // Hidden feature!
}- Call
ButtonTask()regularly (every 1-10ms recommended) - Register tick variable before initializing buttons
- Check return values in critical applications
- Use debounce times appropriate for your buttons (mechanical: 20-100ms, membrane: 10-50ms)
- Test multi-click timings with real users
- Initialize callbacks to NULL if not used (done automatically)
- Don't call
ButtonTask()from multiple contexts without synchronization - Don't use very short debounce times (<10ms) - wastes CPU
- Don't ignore
BTN_ERRORreturns in production code - Don't modify button structure directly - use API functions
- Don't forget to call
BTN_tick_variable_register()before initialization
- ✓ Check
BTN_tick_variable_register()was called - ✓ Verify
ButtonTask()is being called regularly - ✓ Check GPIO initialization (pull-up/down resistors)
- ✓ Verify correct
ReverseLogicsetting
- ✓ Increase debounce time (
ButtonSetDebounceTime()) - ✓ Enable
BTN_DOUBLE_DEBOUNCINGfor noisy buttons - ✓ Check for electrical noise (add hardware filtering)
- ✓ Verify
BTN_MULTIPLE_CLICKis enabled inbutton_cfg.h - ✓ Check
TimerBetweenClickis appropriate (200-500ms typical) - ✓ Ensure callbacks are registered
- ✓ Try
BTN_MULTIPLE_CLICK_NORMAL_MODEfirst (simpler)
- ✓ Adjust with
ButtonSetLongPressedTime() - ✓ Typical range: 500-2000ms
The library includes a comprehensive test suite (button_test.c). To run tests:
gcc -DUNIT_TESTING button.c button_test.c -o button_test
./button_testTest Coverage:
- ✅ Tick registration
- ✅ Initialization (normal & default)
- ✅ Simple press/release
- ✅ Debouncing
- ✅ Long press detection
- ✅ Repeat functionality
- ✅ Release after repeat
- ✅ Double/triple click (normal & combined modes)
- ✅ State machine transitions
- ✅ Reverse logic
- ✅ Multiple independent buttons
- ✅ Runtime configuration changes
- ✅ Timer wraparound handling
Per button instance:
- Without optional features: ~40 bytes
- With all features enabled: ~72 bytes
Code size (ARM Cortex-M, -Os):
- Core functionality: ~1.2 KB
- With all features: ~2.5 KB
Tested on:
- ✅ STM32 (HAL & LL)
- ✅ ESP32
- ✅ Nordic nRF52
- ✅ Generic ARM Cortex-M
Requirements:
- C99 or later
- System tick counter (1ms resolution recommended)
- GPIO read capability
Mozilla Public License 2.0 (MPL-2.0)
Adrian Pietrzak
GitHub: @AdrianPietrzak1998
- ✨ Added comprehensive error handling (BTN_ERROR returns)
- ✨ Added NULL pointer validation
- ✨ Added tick registration API
- ✨ Fixed multi-click counting bug (now starts at 0)
- 🐛 Fixed uninitialized callback pointers
- 📝 Improved documentation
- ✅ Added comprehensive test suite
- 🎉 Initial release