Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
$(OBJ_DIR)/poll_example.o: $(EXAMPLES_DIR)/poll_example.c multi_button.h
108 changes: 105 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
102 changes: 100 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## 功能特性

- **多种按键事件**: 按下、抬起、单击、双击、长按开始、长按保持、重复按下
- **硬件去抖**: 内置数字滤波,消除按键抖动
- **软件去抖**: 延迟确认电平,过滤机械按键抖动
- **状态机驱动**: 清晰的状态转换逻辑,可靠性高
- **多按键支持**: 支持无限数量的按键实例
- **回调机制**: 灵活的事件回调函数注册,支持 `void* user_data` 上下文指针
Expand Down Expand Up @@ -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)`
Expand Down Expand Up @@ -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 触发
Expand Down
Loading