Mastering Button Management in Embedded C with lwbtn: A Deep Dive
This article introduces the lightweight lwbtn library for embedded C, explains its core features, shows how to integrate it on STM32 with a complete demo, and provides an in‑depth analysis of its architecture, state‑machine processing, click detection algorithm, configuration options, and practical usage tips.
lwbtn Overview
lwbtn (Lightweight Button) is a tiny, pure‑C button management library designed for embedded systems. The entire source is under 500 lines, uses no dynamic memory, and can be configured via macros to include only the needed features, making it ideal for resource‑constrained microcontrollers.
https://github.com/MaJerle/lwbtn
Basic Features
Pure C implementation : Compatible with the C11 standard and runs on any MCU.
Platform‑agnostic : Requires only a millisecond‑resolution time source; everything else is handled by the library.
Zero dynamic allocation : Critical for memory‑tight devices.
Event‑driven architecture : Uses callbacks to report button events.
Rich event set : Press, release, click, multi‑click, long‑press keep‑alive, etc.
Software debounce : Built‑in debounce algorithm eliminates the need for external hardware debounce circuits.
STM32 Demo Usage
The following STM32 demo showcases all core lwbtn functions. Buttons are mapped to GPIO pins, and a terminal input simulates press/release events.
Key Functions
Button state getter bridges lwbtn with hardware:
/**
* @brief Get button state
* @param lw LwBTN instance
* @param btn Button instance
* @return 1 = pressed, 0 = released
*/
uint8_t get_key_state(struct lwbtn* lw, struct lwbtn_btn* btn) {
(void)lw;
// Retrieve configuration from btn->arg
btn_config_t* config = (btn_config_t*)btn->arg;
// Read GPIO (low = pressed)
GPIO_PinState pin_state = HAL_GPIO_ReadPin(config->port, config->pin);
return (pin_state == GPIO_PIN_RESET) ? 1 : 0;
}Event handler is called for each detected event:
/**
* @brief Button event handler
* @param lw LwBTN instance
* @param btn Button instance
* @param evt Event type
*/
void button_event_handler(struct lwbtn* lw, struct lwbtn_btn* btn, lwbtn_evt_t evt) {
(void)lw;
btn_config_t* config = (btn_config_t*)btn->arg;
char msg[100];
switch (evt) {
case LWBTN_EVT_ONPRESS:
snprintf(msg, sizeof(msg), "[%lu] %s pressed
", system_tick_ms, config->name);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
LED_Toggle();
break;
case LWBTN_EVT_ONRELEASE:
snprintf(msg, sizeof(msg), "[%lu] %s released
", system_tick_ms, config->name);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
break;
#if LWBTN_CFG_USE_CLICK
case LWBTN_EVT_ONCLICK:
snprintf(msg, sizeof(msg), "[%lu] %s click (count=%d)
", system_tick_ms, config->name, btn->click.cnt);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
if (config->id == BTN_ID_1) {
if (btn->click.cnt == 1) {
// single click: short LED flash
for (int i = 0; i < 3; i++) {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_RESET);
HAL_Delay(50);
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
HAL_Delay(50);
}
} else if (btn->click.cnt == 2) {
// double click: longer LED flash
for (int i = 0; i < 3; i++) {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_RESET);
HAL_Delay(200);
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
HAL_Delay(200);
}
}
}
break;
#endif
default:
break;
}
}Main loop periodically calls the processing function (every 5 ms in the demo) to let lwbtn handle debouncing, state changes, and event generation.
int main(void) {
char welcome_msg[] = "
=== LwBTN STM32F103 Demo ===
"
"Button map:
"
" BTN1(PA0) - click test
"
" BTN2(PA1) - long‑press keep‑alive
"
" BTN3(PA2) - normal
"
" BTN4(PA3) - normal
"
"LED: PC13
"
"Starting event monitoring...
";
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
HAL_UART_Transmit(&huart1, (uint8_t*)welcome_msg, strlen(welcome_msg), 1000);
for (int i = 0; i < BTN_COUNT; i++) {
memset(&btns[i], 0, sizeof(lwbtn_btn_t));
btns[i].arg = (void*)&btn_configs[i];
}
if (!lwbtn_init_ex(NULL, btns, BTN_COUNT, get_key_state, button_event_handler)) {
char err[] = "LwBTN init failed!
";
HAL_UART_Transmit(&huart1, (uint8_t*)err, strlen(err), 100);
while (1);
}
HAL_UART_Transmit(&huart1, (uint8_t*)"LwBTN init success!
", 23, 100);
uint32_t last_process_time = get_system_time_ms();
while (1) {
uint32_t now = get_system_time_ms();
if (now - last_process_time >= 5) {
lwbtn_process_ex(NULL, now);
last_process_time = now;
}
HAL_Delay(1);
}
}Architecture Deep Dive
The library revolves around three core data structures: lwbtn_t: Manager that tracks all buttons. lwbtn_btn_t: Per‑button object storing state, timers, click counters, etc.
Configuration macros that enable/disable features and set timing parameters.
State‑Machine Processing
The heart of lwbtn is the prv_process_btn function, which follows these steps:
State acquisition : new_state = LWBTN_BTN_GET_STATE(lwobj, btn); – supports callback, manual set, or combined mode.
First‑inactive check : Skips processing if the button was already pressed at power‑up.
State change detection : Updates time_state_change when the raw state differs from the previous one.
Press handling : After the debounce interval, sets the ONPRESS_SENT flag and emits LWBTN_EVT_ONPRESS.
Keep‑alive handling : For long‑presses, periodically sends LWBTN_EVT_KEEPALIVE based on KEEPALIVE_PERIOD.
Click Detection Algorithm
The click detector distinguishes short taps, multi‑clicks, and long presses by measuring press duration against LWBTN_CFG_TIME_CLICK_MIN and LWBTN_CFG_TIME_CLICK_MAX, and by checking inter‑click intervals. It also caps the maximum consecutive clicks with LWBTN_CFG_CLICK_MAX_CONSECUTIVE.
Configuration Options
All behaviour is controlled by compile‑time macros: LWBTN_CFG_TIME_DEBOUNCE_PRESS (default 20 ms) LWBTN_CFG_TIME_DEBOUNCE_RELEASE (default 0 ms) LWBTN_CFG_TIME_CLICK_MIN / _MAX (20 ms / 300 ms) LWBTN_CFG_USE_CLICK – enable click detection. LWBTN_CFG_USE_KEEPALIVE – enable long‑press keep‑alive. LWBTN_CFG_CLICK_MAX_CONSECUTIVE – limit multi‑click count.
By disabling unused features you can shrink RAM/ROM footprints, which is crucial for low‑end MCUs.
Practical Recommendations
Adjust debounce times to match the mechanical characteristics of your switches (20 ms for typical mechanical keys, shorter for membrane keys).
Ensure the time source provides accurate millisecond ticks; otherwise button timing may become unreliable.
Process the library at a reasonable interval (5 ms works for most cases). Faster polling wastes CPU, slower may miss events.
When many buttons are used, monitor memory consumption; disabling unnecessary events reduces per‑button overhead.
Conclusion
lwbtn offers a clean, event‑driven solution for complex button handling in embedded C. Its state‑machine design, configurable debounce and click logic, and zero‑dynamic‑allocation approach make it both powerful and lightweight, providing a solid foundation for reliable user‑input handling on resource‑constrained MCUs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
