From 54d3517b12e8933c88e4de03b27d2ec024f6709d Mon Sep 17 00:00:00 2001 From: "Mr.Ag" Date: Fri, 24 Jul 2026 14:20:03 +0800 Subject: [PATCH] feat: add low-power button timing support --- CMakeLists.txt | 12 +++ Makefile | 8 +- README.md | 108 ++++++++++++++++++++++- README_CN.md | 102 +++++++++++++++++++++- multi_button.c | 137 +++++++++++++++++++++++++---- multi_button.h | 52 ++++++++--- tests/test_button.c | 158 ++++++++++++++++++++++++++++++++-- tests/test_button_no_double.c | 80 +++++++++++++++++ 8 files changed, 618 insertions(+), 39 deletions(-) create mode 100644 tests/test_button_no_double.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 12d6bf5..3c6a104 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,4 +26,16 @@ if(MULTIBUTTON_BUILD_TESTS) add_executable(test_button tests/test_button.c) target_link_libraries(test_button multibutton) add_test(NAME button_tests COMMAND test_button) + + add_executable(test_button_no_double + multi_button.c + tests/test_button_no_double.c + ) + target_compile_definitions(test_button_no_double + PRIVATE MULTIBUTTON_ENABLE_DOUBLE_CLICK=0 + ) + target_include_directories(test_button_no_double + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME button_no_double_tests COMMAND test_button_no_double) endif() diff --git a/Makefile b/Makefile index cc6717e..71ba723 100644 --- a/Makefile +++ b/Makefile @@ -92,9 +92,10 @@ $(BIN_DIR)/poll_example: $(OBJ_DIR)/poll_example.o $(STATIC_LIB) | $(BIN_DIR) examples: $(addprefix $(BIN_DIR)/, $(EXAMPLES)) # Test target -test: $(BIN_DIR)/test_button +test: $(BIN_DIR)/test_button $(BIN_DIR)/test_button_no_double @echo "Running unit tests..." @$(BIN_DIR)/test_button + @$(BIN_DIR)/test_button_no_double # Build test binary $(BIN_DIR)/test_button: $(OBJ_DIR)/test_button.o $(STATIC_LIB) | $(BIN_DIR) @@ -103,6 +104,9 @@ $(BIN_DIR)/test_button: $(OBJ_DIR)/test_button.o $(STATIC_LIB) | $(BIN_DIR) $(OBJ_DIR)/test_button.o: tests/test_button.c multi_button.h | $(OBJ_DIR) $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ +$(BIN_DIR)/test_button_no_double: multi_button.c tests/test_button_no_double.c multi_button.h | $(BIN_DIR) + $(CC) $(CFLAGS) $(INCLUDES) -DMULTIBUTTON_ENABLE_DOUBLE_CLICK=0 multi_button.c tests/test_button_no_double.c -o $@ + # Clean build files clean: $(RM) -r $(BUILD_DIR) @@ -162,4 +166,4 @@ $(OBJ_DIR)/test_button.o: tests/test_button.c multi_button.h $(OBJ_DIR)/multi_button.o: multi_button.c multi_button.h $(OBJ_DIR)/basic_example.o: $(EXAMPLES_DIR)/basic_example.c multi_button.h $(OBJ_DIR)/advanced_example.o: $(EXAMPLES_DIR)/advanced_example.c multi_button.h -$(OBJ_DIR)/poll_example.o: $(EXAMPLES_DIR)/poll_example.c multi_button.h \ No newline at end of file +$(OBJ_DIR)/poll_example.o: $(EXAMPLES_DIR)/poll_example.c multi_button.h diff --git a/README.md b/README.md index b6cf825..344ad04 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ A compact and flexible multi-button state machine library for embedded systems. ## Features - **7 event types**: press down, press up, single click, double click, long press start, long press hold, repeat press -- **Hardware debounce**: built-in digital filter eliminates contact bounce +- **Software debounce**: deferred level confirmation filters contact bounce - **State machine driven**: reliable state transitions with clear logic - **Unlimited buttons**: linked-list architecture supports any number of button instances - **Callback & polling**: flexible event handling via callbacks or polling `button_get_event()` - **Memory efficient**: compact bitfield struct (~30 bytes per button) -- **Configurable**: adjustable timing thresholds and debounce depth +- **Configurable**: adjustable timing thresholds and debounce duration - **Thread-safe option**: optional RTOS lock hooks with zero overhead on bare-metal ## Quick Start @@ -101,6 +101,7 @@ void button_detach(Button* handle, ButtonEvent event); int button_start(Button* handle); // returns 0=ok, -1=duplicate, -2=invalid void button_stop(Button* handle); void button_ticks(void); // call every 5ms from timer +uint32_t button_ticks_low_power(uint32_t elapsed_ms); ``` ### Utility Functions @@ -142,12 +143,113 @@ Edit the defines in `multi_button.h`: ```c #define TICKS_INTERVAL 5 // timer tick interval (ms) -#define DEBOUNCE_TICKS 3 // debounce filter depth (max 7) +#define DEBOUNCE_TICKS 3 // debounce duration in legacy ticks #define SHORT_TICKS (300 / TICKS_INTERVAL) // short press threshold #define LONG_TICKS (1000 / TICKS_INTERVAL) // long press threshold #define PRESS_REPEAT_MAX_NUM 15 // max repeat counter +#define MULTIBUTTON_ENABLE_DOUBLE_CLICK 1 // enable double-click detection ``` +### Optional double-click detection + +Double-click detection is enabled by default for backward compatibility. It +can be disabled before including the header: + +```c +#define MULTIBUTTON_ENABLE_DOUBLE_CLICK 0 +#include "multi_button.h" +``` + +It can also be disabled with +`-DMULTIBUTTON_ENABLE_DOUBLE_CLICK=0`. When disabled, release debounce +immediately emits `BTN_PRESS_UP` followed by `BTN_SINGLE_CLICK`, returns the +button to idle, and does not schedule the `SHORT_TICKS` double-click window. +`BTN_DOUBLE_CLICK` and `BTN_PRESS_REPEAT` are not generated. This removes the +extra wake-up after every short press. + +## Low-power/event-driven operation + +`button_ticks()` remains available for applications using a fixed periodic +timer. Low-power applications can use: + +```c +uint32_t button_ticks_low_power(uint32_t elapsed_ms); +``` + +`elapsed_ms` is the actual number of milliseconds since the previous call. +The return value is the delay before the next required scan: + +- A non-zero value means that a one-shot timer must be armed for that delay. +- Zero means that no timer is required. The MCU may sleep until a button GPIO + edge occurs. +- On a GPIO edge, call the function again and re-arm the one-shot timer from + the new return value. +- With multiple buttons, the return value is the earliest deadline required by + any registered button. + +```c +static uint32_t last_scan_ms; + +static uint32_t elapsed_ms_since(uint32_t now, uint32_t previous) +{ + /* + * Unsigned subtraction is modulo 2^32, so this remains correct across + * one platform_millis() wrap, provided the real interval is < 2^32 ms. + */ + return now - previous; +} + +static void button_scan_and_reschedule(void) +{ + uint32_t now = platform_millis(); + uint32_t elapsed = elapsed_ms_since(now, last_scan_ms); + uint32_t delay = button_ticks_low_power(elapsed); + last_scan_ms = now; + + platform_cancel_button_timer(); + if (delay != 0U) { + platform_start_button_oneshot(delay, + button_scan_and_reschedule); + } +} + +void button_gpio_edge_isr(void) +{ + platform_defer_from_isr(button_scan_and_reschedule); +} + +void button_low_power_start(void) +{ + last_scan_ms = platform_millis(); + platform_enable_button_both_edge_irq(); +} +``` + +The GPIO interrupt must cover both press and release edges. Run the state +machine in task or main-loop context unless every registered callback is +ISR-safe. + +The example assumes that `platform_millis()` returns a monotonically +incrementing `uint32_t` counter that wraps at `UINT32_MAX`. Unsigned subtraction +handles one such wrap without a conditional branch. A platform using a +different counter width or an earlier custom modulus must provide its own +elapsed-time conversion. + +Debouncing uses one deferred confirmation read. The first changed sample +schedules a delay of `DEBOUNCE_TICKS * TICKS_INTERVAL`; the new level is +accepted only if it is still different from the previous stable level at the +deadline. The library therefore does not require periodic timer wake-ups +during the debounce interval. + +Mechanical bounce can still wake the MCU through repeated GPIO interrupts. A +platform seeking the lowest possible power may mask that button's edge +interrupt after the first edge, keep it masked for the debounce interval, and +restore it after the deferred confirmation read. + +If no `BTN_LONG_PRESS_HOLD` callback is attached, the timer stops after +`BTN_LONG_PRESS_START` and resumes on the release edge. Attaching a hold +callback intentionally keeps a timer active at the `LONG_HOLD_TICKS` period. + ## Thread Safety (RTOS) For RTOS environments, define lock macros before including the header: diff --git a/README_CN.md b/README_CN.md index 4cd0979..2942cbb 100644 --- a/README_CN.md +++ b/README_CN.md @@ -5,7 +5,7 @@ ## 功能特性 - **多种按键事件**: 按下、抬起、单击、双击、长按开始、长按保持、重复按下 -- **硬件去抖**: 内置数字滤波,消除按键抖动 +- **软件去抖**: 延迟确认电平,过滤机械按键抖动 - **状态机驱动**: 清晰的状态转换逻辑,可靠性高 - **多按键支持**: 支持无限数量的按键实例 - **回调机制**: 灵活的事件回调函数注册,支持 `void* user_data` 上下文指针 @@ -160,6 +160,16 @@ typedef enum { #### `void button_ticks(void)` **功能**: 后台处理函数 (每 5ms 调用一次) +#### `uint32_t button_ticks_low_power(uint32_t elapsed_ms)` +**功能**: 按实际经过时间推进状态机,并返回下一次必要扫描前的毫秒延时 + +**参数**: +- `elapsed_ms`: 距离上一次调用实际经过的毫秒数 + +**返回值**: +- 非零:下一次一次性定时器的延时 +- 0:当前不需要定时扫描,可等待 GPIO 边沿唤醒 + ### 工具函数 #### `ButtonEvent button_get_event(Button* handle)` @@ -217,18 +227,106 @@ button_attach(&btn, BTN_PRESS_REPEAT, on_repeat, NULL); 说明: `BTN_SINGLE_CLICK` 在 repeat==1 时触发,`BTN_DOUBLE_CLICK` 在 repeat==2 时触发。repeat>=3 时,仅 `BTN_PRESS_REPEAT` 在按下过程中触发。 +## 低功耗/事件驱动模式 + +原有的 `button_ticks()` 固定周期接口保持兼容。低功耗应用可以改用: + +```c +uint32_t button_ticks_low_power(uint32_t elapsed_ms); +``` + +`elapsed_ms` 是距离上一次调用实际经过的毫秒数。返回值表示下一次必须扫描 +前的延时: + +- 返回非零值:启动对应延时的一次性定时器; +- 返回 0:不再需要定时扫描,MCU 可以休眠并等待按键 GPIO 边沿; +- GPIO 边沿唤醒后再次调用本函数,并根据新的返回值重设一次性定时器。 +- 存在多个按键时,返回值是所有已注册按键中最早的下一次期限。 + +去抖采用延迟复读:首次检测到电平变化后,只安排一次 +`DEBOUNCE_TICKS * TICKS_INTERVAL` 延时;到期复读时若仍与原稳定电平不同, +才确认本次变化。去抖窗口内不需要周期唤醒。 + +```c +static uint32_t last_scan_ms; + +static uint32_t elapsed_ms_since(uint32_t now, uint32_t previous) +{ + /* + * uint32_t 无符号减法按模 2^32 运算。只要实际间隔小于 2^32 ms, + * platform_millis() 发生一次回绕后仍能得到正确的经过时间。 + */ + return now - previous; +} + +static void scan_and_reschedule(void) +{ + uint32_t now = platform_millis(); + uint32_t elapsed = elapsed_ms_since(now, last_scan_ms); + uint32_t delay = button_ticks_low_power(elapsed); + last_scan_ms = now; + + platform_cancel_button_timer(); + if (delay != 0) { + platform_start_button_oneshot(delay, scan_and_reschedule); + } +} + +void button_gpio_edge_isr(void) +{ + platform_defer_from_isr(scan_and_reschedule); +} + +void button_low_power_start(void) +{ + last_scan_ms = platform_millis(); + platform_enable_button_both_edge_irq(); +} +``` + +GPIO 中断必须同时覆盖按下和松开边沿。若回调函数不能在中断环境运行,应将 +实际扫描延后到主循环或任务上下文。 + +示例假定 `platform_millis()` 返回在 `UINT32_MAX` 后回绕的单调递增 +`uint32_t` 计数器。无符号减法可以自动处理一次这种回绕,无需额外分支。如果 +平台采用其他位宽或提前回绕的自定义模数,应由平台层自行换算经过时间。 + +延迟复读只消除了去抖期间的软件定时器周期唤醒。机械抖动仍可能通过多个 GPIO +边沿中断唤醒 MCU。追求最低功耗的平台可以在首次边沿后临时屏蔽该按键中断, +保持屏蔽直到去抖定时器到期并完成复读,然后恢复按下和松开双边沿中断。 + +未注册 `BTN_LONG_PRESS_HOLD` 回调时,组件在发出 +`BTN_LONG_PRESS_START` 后停止定时扫描,直到松开边沿唤醒。注册保持回调后, +组件会按照 `LONG_HOLD_TICKS` 周期继续唤醒。 + ## 配置选项 在 `multi_button.h` 中可以自定义以下参数: ```c #define TICKS_INTERVAL 5 // 定时器中断间隔 (ms) -#define DEBOUNCE_TICKS 3 // 去抖深度 (最大 7) +#define DEBOUNCE_TICKS 3 // 去抖时间,单位为兼容接口 tick #define SHORT_TICKS (300 / TICKS_INTERVAL) // 短按阈值 #define LONG_TICKS (1000 / TICKS_INTERVAL) // 长按阈值 #define PRESS_REPEAT_MAX_NUM 15 // 最大重复计数 +#define MULTIBUTTON_ENABLE_DOUBLE_CLICK 1 // 是否检测双击 ``` +### 可选双击检测 + +为保持兼容,双击检测默认开启。可在包含头文件前关闭: + +```c +#define MULTIBUTTON_ENABLE_DOUBLE_CLICK 0 +#include "multi_button.h" +``` + +也可以使用编译参数 `-DMULTIBUTTON_ENABLE_DOUBLE_CLICK=0`。关闭后,松开去抖 +完成时会立即依次产生 `BTN_PRESS_UP` 和 `BTN_SINGLE_CLICK`,随后直接返回空闲 +状态,不再启动 `SHORT_TICKS` 双击等待定时器,也不会产生 +`BTN_DOUBLE_CLICK` 和 `BTN_PRESS_REPEAT`。因此每次短按可以少一次双击窗口结束 +时的唤醒。 + ## 重要注意事项 ### BTN_LONG_PRESS_HOLD 每 tick 触发 diff --git a/multi_button.c b/multi_button.c index e79c2b7..ef4b31a 100644 --- a/multi_button.c +++ b/multi_button.c @@ -12,8 +12,12 @@ static Button* head_handle = NULL; // Forward declarations -static void button_handler(Button* handle); +static uint32_t button_handler(Button* handle, uint32_t elapsed_ms); static inline uint8_t button_read_level(Button* handle); +static uint32_t ticks_to_ms(uint32_t ticks); +static uint32_t ticks_remaining_ms(uint16_t elapsed_ticks, + uint32_t remainder_ms, + uint32_t target_ticks); /** * @brief Initialize the button struct handle @@ -96,9 +100,11 @@ void button_reset(Button* handle) if (!handle) return; handle->state = BTN_STATE_IDLE; handle->ticks = 0; + handle->tick_remainder_ms = 0; handle->repeat = 0; handle->event = (uint8_t)BTN_NONE_PRESS; handle->debounce_cnt = 0; + handle->debounce_elapsed_ms = 0; } /** @@ -127,27 +133,73 @@ static inline uint8_t button_read_level(Button* handle) * @param handle: the button handle struct * @retval None */ -static void button_handler(Button* handle) +static uint32_t ticks_to_ms(uint32_t ticks) { + if (ticks > UINT32_MAX / TICKS_INTERVAL) { + return UINT32_MAX; + } + return ticks * TICKS_INTERVAL; +} + +static uint32_t ticks_remaining_ms(uint16_t elapsed_ticks, + uint32_t remainder_ms, + uint32_t target_ticks) +{ + uint32_t remaining_ms; + + if (elapsed_ticks >= target_ticks) { + return 1U; + } + + remaining_ms = ticks_to_ms(target_ticks - elapsed_ticks); + if (remainder_ms >= remaining_ms) { + return 1U; + } + return remaining_ms - remainder_ms; +} + +static uint32_t button_handler(Button* handle, uint32_t elapsed_ms) +{ + uint32_t next_time = 0; + uint32_t debounce_time_ms = ticks_to_ms(DEBOUNCE_TICKS); uint8_t read_gpio_level = button_read_level(handle); - // Increment ticks counter when not in idle state (with saturation) + // Accumulate actual elapsed time without losing sub-tick remainders. if (handle->state > BTN_STATE_IDLE) { - if (handle->ticks < UINT16_MAX) { - handle->ticks++; + uint32_t total_ms = (uint32_t)handle->tick_remainder_ms + elapsed_ms; + uint32_t elapsed_ticks = total_ms / TICKS_INTERVAL; + handle->tick_remainder_ms = total_ms % TICKS_INTERVAL; + if (elapsed_ticks > (uint32_t)UINT16_MAX - handle->ticks) { + handle->ticks = UINT16_MAX; + } else { + handle->ticks = (uint16_t)(handle->ticks + elapsed_ticks); } } - /* Button debounce handling */ + /* + * Deferred debounce: after the first changed sample, wait for the complete + * debounce interval and read once more. Low-power users therefore need only + * one timer wake-up instead of waking for every debounce sample. + */ if (read_gpio_level != handle->button_level) { - // Continue reading same new level for debounce - if (++(handle->debounce_cnt) >= DEBOUNCE_TICKS) { + if (handle->debounce_cnt == 0) { + handle->debounce_cnt = 1; + handle->debounce_elapsed_ms = 0; + return debounce_time_ms; + } + + if (elapsed_ms >= debounce_time_ms - handle->debounce_elapsed_ms) { handle->button_level = read_gpio_level; handle->debounce_cnt = 0; + handle->debounce_elapsed_ms = 0; + } else { + handle->debounce_elapsed_ms += elapsed_ms; + return debounce_time_ms - handle->debounce_elapsed_ms; } } else { - // Level not changed, reset counter + // The signal returned to its stable level before the deadline. handle->debounce_cnt = 0; + handle->debounce_elapsed_ms = 0; } /* State machine */ @@ -158,8 +210,10 @@ static void button_handler(Button* handle) handle->event = (uint8_t)BTN_PRESS_DOWN; EVENT_CB(BTN_PRESS_DOWN); handle->ticks = 0; + handle->tick_remainder_ms = 0; handle->repeat = 1; handle->state = BTN_STATE_PRESS; + next_time = ticks_to_ms(LONG_TICKS); } else { handle->event = (uint8_t)BTN_NONE_PRESS; } @@ -171,15 +225,32 @@ static void button_handler(Button* handle) handle->event = (uint8_t)BTN_PRESS_UP; EVENT_CB(BTN_PRESS_UP); handle->ticks = 0; + handle->tick_remainder_ms = 0; +#if MULTIBUTTON_ENABLE_DOUBLE_CLICK handle->state = BTN_STATE_RELEASE; - } else if (handle->ticks > LONG_TICKS) { + next_time = ticks_to_ms(SHORT_TICKS); +#else + handle->repeat = 0; + handle->state = BTN_STATE_IDLE; + handle->event = (uint8_t)BTN_SINGLE_CLICK; + EVENT_CB(BTN_SINGLE_CLICK); +#endif + } else if (handle->ticks >= LONG_TICKS) { // Long press detected handle->event = (uint8_t)BTN_LONG_PRESS_START; EVENT_CB(BTN_LONG_PRESS_START); handle->state = BTN_STATE_LONG_HOLD; + if (handle->cb[BTN_LONG_PRESS_HOLD]) { + next_time = ticks_to_ms(LONG_HOLD_TICKS); + } + } else { + next_time = ticks_remaining_ms(handle->ticks, + handle->tick_remainder_ms, + LONG_TICKS); } break; +#if MULTIBUTTON_ENABLE_DOUBLE_CLICK case BTN_STATE_RELEASE: if (handle->button_level == handle->active_level) { // Button pressed again @@ -191,8 +262,10 @@ static void button_handler(Button* handle) handle->event = (uint8_t)BTN_PRESS_REPEAT; EVENT_CB(BTN_PRESS_REPEAT); handle->ticks = 0; + handle->tick_remainder_ms = 0; handle->state = BTN_STATE_REPEAT; - } else if (handle->ticks > SHORT_TICKS) { + next_time = ticks_to_ms(SHORT_TICKS); + } else if (handle->ticks >= SHORT_TICKS) { // Timeout reached, determine click type if (handle->repeat == 1) { handle->event = (uint8_t)BTN_SINGLE_CLICK; @@ -202,6 +275,12 @@ static void button_handler(Button* handle) EVENT_CB(BTN_DOUBLE_CLICK); } handle->state = BTN_STATE_IDLE; + handle->ticks = 0; + handle->tick_remainder_ms = 0; + } else { + next_time = ticks_remaining_ms(handle->ticks, + handle->tick_remainder_ms, + SHORT_TICKS); } break; @@ -212,28 +291,41 @@ static void button_handler(Button* handle) EVENT_CB(BTN_PRESS_UP); if (handle->ticks < SHORT_TICKS) { handle->ticks = 0; + handle->tick_remainder_ms = 0; handle->state = BTN_STATE_RELEASE; // Continue waiting for more presses + next_time = ticks_to_ms(SHORT_TICKS); } else { handle->state = BTN_STATE_IDLE; // End of sequence } - } else if (handle->ticks > SHORT_TICKS) { - // Held down too long, treat as normal press - handle->ticks = 0; // reset for fresh long-press timing - handle->repeat = 0; // clear repeat count for new press cycle + } else if (handle->ticks >= SHORT_TICKS) { + // Continue timing the second press toward the long-press threshold. handle->state = BTN_STATE_PRESS; + next_time = ticks_remaining_ms(handle->ticks, + handle->tick_remainder_ms, + LONG_TICKS); + } else { + next_time = ticks_remaining_ms(handle->ticks, + handle->tick_remainder_ms, + SHORT_TICKS); } break; +#endif case BTN_STATE_LONG_HOLD: if (handle->button_level == handle->active_level) { // Continue holding handle->event = (uint8_t)BTN_LONG_PRESS_HOLD; EVENT_CB(BTN_LONG_PRESS_HOLD); + if (handle->cb[BTN_LONG_PRESS_HOLD]) { + next_time = ticks_to_ms(LONG_HOLD_TICKS); + } } else { // Released from long press handle->event = (uint8_t)BTN_PRESS_UP; EVENT_CB(BTN_PRESS_UP); handle->state = BTN_STATE_IDLE; + handle->ticks = 0; + handle->tick_remainder_ms = 0; } break; @@ -242,6 +334,7 @@ static void button_handler(Button* handle) handle->state = BTN_STATE_IDLE; break; } + return next_time; } /** @@ -302,9 +395,16 @@ void button_stop(Button* handle) * @retval None */ void button_ticks(void) +{ + (void)button_ticks_low_power(TICKS_INTERVAL); +} + +uint32_t button_ticks_low_power(uint32_t elapsed_ms) { Button* target; Button* next; + uint32_t button_next; + uint32_t next_scan_time = UINT32_MAX; MULTIBUTTON_LOCK(); target = head_handle; @@ -315,7 +415,12 @@ void button_ticks(void) next = target->next; MULTIBUTTON_UNLOCK(); - button_handler(target); + button_next = button_handler(target, elapsed_ms); + if (button_next != 0 && button_next < next_scan_time) { + next_scan_time = button_next; + } target = next; } + + return next_scan_time == UINT32_MAX ? 0 : next_scan_time; } diff --git a/multi_button.h b/multi_button.h index 3372a7a..9a93f00 100644 --- a/multi_button.h +++ b/multi_button.h @@ -14,16 +14,37 @@ #define MULTIBUTTON_VERSION_MINOR 1 #define MULTIBUTTON_VERSION_PATCH 1 -// Configuration constants - can be modified according to your needs -#define TICKS_INTERVAL 5 // ms - timer interrupt interval -#define DEBOUNCE_TICKS 3 // MAX 7 (0 ~ 7) - debounce filter depth -#define SHORT_TICKS (300 / TICKS_INTERVAL) // short press threshold -#define LONG_TICKS (1000 / TICKS_INTERVAL) // long press threshold -#define PRESS_REPEAT_MAX_NUM 15 // maximum repeat counter value - -// Compile-time check: debounce_cnt is a 3-bit field, max value is 7 -#if DEBOUNCE_TICKS > 7 - #error "DEBOUNCE_TICKS exceeds 3-bit field maximum (7)" +// Configuration constants. They may be overridden by compiler definitions. +#ifndef TICKS_INTERVAL +#define TICKS_INTERVAL 5U // ms - legacy periodic timer interval +#endif +#ifndef DEBOUNCE_TICKS +#define DEBOUNCE_TICKS 3U // debounce duration in legacy ticks +#endif +#ifndef SHORT_TICKS +#define SHORT_TICKS (300U / TICKS_INTERVAL) +#endif +#ifndef LONG_TICKS +#define LONG_TICKS (1000U / TICKS_INTERVAL) +#endif +#ifndef LONG_HOLD_TICKS +#define LONG_HOLD_TICKS 1U // hold callback period in legacy ticks +#endif +#ifndef PRESS_REPEAT_MAX_NUM +#define PRESS_REPEAT_MAX_NUM 15U +#endif +#ifndef MULTIBUTTON_ENABLE_DOUBLE_CLICK +#define MULTIBUTTON_ENABLE_DOUBLE_CLICK 1 +#endif + +#if DEBOUNCE_TICKS < 1 + #error "DEBOUNCE_TICKS must be at least 1" +#endif +#if TICKS_INTERVAL < 1 + #error "TICKS_INTERVAL must be at least 1 ms" +#endif +#if MULTIBUTTON_ENABLE_DOUBLE_CLICK != 0 && MULTIBUTTON_ENABLE_DOUBLE_CLICK != 1 + #error "MULTIBUTTON_ENABLE_DOUBLE_CLICK must be 0 or 1" #endif // Forward declaration @@ -67,6 +88,8 @@ struct _Button { uint8_t (*hal_button_level)(uint8_t button_id); // HAL function to read GPIO BtnCallback cb[BTN_EVENT_COUNT]; // callback function array void* user_data; // user context pointer passed to callbacks + uint32_t tick_remainder_ms; // elapsed time not forming a complete tick + uint32_t debounce_elapsed_ms; // time elapsed since a level change Button* next; // next button in linked list }; @@ -104,6 +127,15 @@ int button_start(Button* handle); void button_stop(Button* handle); void button_ticks(void); +/** + * Process elapsed time and return the delay until the next required scan. + * + * Call this function from a one-shot timer and whenever a GPIO edge wakes the + * system. elapsed_ms is the actual time since the previous call. A return value + * of 0 means no timer is needed; scanning can remain stopped until a GPIO edge. + */ +uint32_t button_ticks_low_power(uint32_t elapsed_ms); + // Utility functions uint8_t button_get_repeat_count(Button* handle); void button_reset(Button* handle); diff --git a/tests/test_button.c b/tests/test_button.c index 043c85c..4df75dc 100644 --- a/tests/test_button.c +++ b/tests/test_button.c @@ -450,13 +450,9 @@ static int test_debounce_boundary(void) { setup_button(); - /* Hold for exactly DEBOUNCE_TICKS - should register */ + /* Detection starts now; confirmation occurs after the full duration. */ mock_gpio_value = 1; - tick_n(DEBOUNCE_TICKS); - - /* After exactly DEBOUNCE_TICKS of consistent high reading, - * the level should have changed and press detected */ - tick_n(5); /* a few more ticks for state machine to process */ + tick_n(DEBOUNCE_TICKS + 1); ASSERT(has_event(BTN_PRESS_DOWN)); @@ -487,6 +483,150 @@ static int test_rapid_press_release(void) return 0; } +/* Test 16: low-power mode stops its timer while all buttons are idle */ +static int test_low_power_idle(void) +{ + setup_button(); + + ASSERT(button_ticks_low_power(0) == 0); + + teardown_button(); + return 0; +} + +/* Test 17: an edge starts debounce and schedules only required deadlines */ +static int test_low_power_long_press(void) +{ + uint32_t next; + + setup_button(); + mock_gpio_value = 1; + + next = button_ticks_low_power(0); /* GPIO edge wake-up */ + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + + next = button_ticks_low_power(next); /* one deferred debounce read */ + ASSERT(has_event(BTN_PRESS_DOWN)); + ASSERT(next == LONG_TICKS * TICKS_INTERVAL); + + next = button_ticks_low_power(next); + ASSERT(count_event(BTN_LONG_PRESS_START) == 1); + ASSERT(next == LONG_HOLD_TICKS * TICKS_INTERVAL); + + teardown_button(); + return 0; +} + +/* Test 18: without a hold callback, long press can sleep until release edge */ +static int test_low_power_long_press_without_hold(void) +{ + uint32_t next; + + setup_button(); + button_detach(&test_btn, BTN_LONG_PRESS_HOLD); + mock_gpio_value = 1; + + next = button_ticks_low_power(0); + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + next = button_ticks_low_power(next); + next = button_ticks_low_power(next); + + ASSERT(has_event(BTN_LONG_PRESS_START)); + ASSERT(!has_event(BTN_LONG_PRESS_HOLD)); + ASSERT(next == 0); + + mock_gpio_value = 0; + next = button_ticks_low_power(100); /* release GPIO edge */ + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + next = button_ticks_low_power(next); + ASSERT(has_event(BTN_PRESS_UP)); + ASSERT(next == 0); + + teardown_button(); + return 0; +} + +/* Test 19: a held second press still schedules and reaches long press */ +static int test_low_power_second_press_long(void) +{ + uint32_t next; + + setup_button(); + button_detach(&test_btn, BTN_LONG_PRESS_HOLD); + + mock_gpio_value = 1; + next = button_ticks_low_power(0); + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + next = button_ticks_low_power(next); + + mock_gpio_value = 0; + next = button_ticks_low_power(50); + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + next = button_ticks_low_power(next); + + mock_gpio_value = 1; + next = button_ticks_low_power(50); + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + next = button_ticks_low_power(next); + ASSERT(has_event(BTN_PRESS_REPEAT)); + + next = button_ticks_low_power(next); + ASSERT(next > 0); + next = button_ticks_low_power(next); + ASSERT(has_event(BTN_LONG_PRESS_START)); + ASSERT(next == 0); + + teardown_button(); + return 0; +} + +/* Test 20: a transient edge is rejected by the single deferred read */ +static int test_low_power_debounce_rejects_glitch(void) +{ + uint32_t next; + + setup_button(); + mock_gpio_value = 1; + + next = button_ticks_low_power(0); + ASSERT(next == DEBOUNCE_TICKS * TICKS_INTERVAL); + + /* The input returned to the old stable level before the timer expired. */ + mock_gpio_value = 0; + next = button_ticks_low_power(next); + + ASSERT(!has_event(BTN_PRESS_DOWN)); + ASSERT(next == 0); + + teardown_button(); + return 0; +} + +/* Test 21: sub-tick remainder is deducted from the next deadline */ +static int test_low_power_sub_tick_deadline(void) +{ + uint32_t next; + + setup_button(); + button_detach(&test_btn, BTN_LONG_PRESS_HOLD); + mock_gpio_value = 1; + + next = button_ticks_low_power(0); + next = button_ticks_low_power(next); + ASSERT(next == LONG_TICKS * TICKS_INTERVAL); + + next = button_ticks_low_power(next - 1U); + ASSERT(!has_event(BTN_LONG_PRESS_START)); + ASSERT(next == 1U); + + next = button_ticks_low_power(next); + ASSERT(has_event(BTN_LONG_PRESS_START)); + ASSERT(next == 0); + + teardown_button(); + return 0; +} + /* ============================================================ */ int main(void) @@ -511,6 +651,12 @@ int main(void) RUN_TEST(test_user_data); RUN_TEST(test_debounce_boundary); RUN_TEST(test_rapid_press_release); + RUN_TEST(test_low_power_idle); + RUN_TEST(test_low_power_long_press); + RUN_TEST(test_low_power_long_press_without_hold); + RUN_TEST(test_low_power_second_press_long); + RUN_TEST(test_low_power_debounce_rejects_glitch); + RUN_TEST(test_low_power_sub_tick_deadline); printf("\nResults: %d/%d passed", tests_passed, tests_run); if (tests_failed > 0) { diff --git a/tests/test_button_no_double.c b/tests/test_button_no_double.c new file mode 100644 index 0000000..82aa059 --- /dev/null +++ b/tests/test_button_no_double.c @@ -0,0 +1,80 @@ +#include "multi_button.h" + +#include + +#if MULTIBUTTON_ENABLE_DOUBLE_CLICK +#error "This test must be compiled with MULTIBUTTON_ENABLE_DOUBLE_CLICK=0" +#endif + +static uint8_t gpio_level; +static unsigned int press_up_count; +static unsigned int single_click_count; +static unsigned int double_click_count; + +static uint8_t read_gpio(uint8_t button_id) +{ + (void)button_id; + return gpio_level; +} + +static void on_press_up(Button* button, void* user_data) +{ + (void)button; + (void)user_data; + press_up_count++; +} + +static void on_single_click(Button* button, void* user_data) +{ + (void)button; + (void)user_data; + single_click_count++; +} + +static void on_double_click(Button* button, void* user_data) +{ + (void)button; + (void)user_data; + double_click_count++; +} + +int main(void) +{ + Button button; + uint32_t next; + + gpio_level = 0; + button_init(&button, read_gpio, 1, 0); + button_attach(&button, BTN_PRESS_UP, on_press_up, NULL); + button_attach(&button, BTN_SINGLE_CLICK, on_single_click, NULL); + button_attach(&button, BTN_DOUBLE_CLICK, on_double_click, NULL); + if (button_start(&button) != 0) { + return 1; + } + + gpio_level = 1; + next = button_ticks_low_power(0); + if (next != DEBOUNCE_TICKS * TICKS_INTERVAL) { + return 2; + } + next = button_ticks_low_power(next); + if (next != LONG_TICKS * TICKS_INTERVAL) { + return 3; + } + + gpio_level = 0; + next = button_ticks_low_power(25); + if (next != DEBOUNCE_TICKS * TICKS_INTERVAL) { + return 4; + } + next = button_ticks_low_power(next); + + if (press_up_count != 1 || single_click_count != 1 || + double_click_count != 0 || next != 0) { + return 5; + } + + button_stop(&button); + printf("Double-click disabled test passed\n"); + return 0; +}