20. 按键库(MultiButton)
📝本节您将学习如何使用状态机实现多功能按键检测,支持短按、长按、双击、按下、释放等丰富的按键事件,构建响应灵敏的人机交互。
🏆本章目标
1️⃣ 理解按键状态机的工作原理
2️⃣ 掌握 MultiButton 按键库的使用方法
3️⃣ 实现短按、长按、双击等按键事件检测
4️⃣ 将按键事件与 LED、蜂鸣器、OLED 等外设联动
20.1 为什么需要按键库
在前面的章节中,我们使用简单的延时消抖 + 轮询/中断方式检测按键。这种方式只能检测"按下"和"释放"两种状态,无法区分短按、长按、双击等复杂操作。
实际产品中常见的按键交互:
| 操作 | 典型用途 |
|---|---|
| 单击(短按) | 确认选择、切换选项 |
| 双击 | 快捷操作、特殊功能 |
| 长按开始 | 进入设置模式 |
| 长按持续 | 连续调节(音量+、亮度+) |
| 按下/释放 | 按住说话(PTT)、游戏控制 |
手动实现这些逻辑需要大量状态变量和定时器,代码复杂且容易出错。按键库通过状态机封装了所有检测逻辑,开发者只需注册回调函数即可。
20.2 MultiButton 状态机原理
天巧星出厂固件使用的是开源 MultiButton 库(作者 Zibin Zheng),核心是一个有限状态机:
┌──────────────────────────────────┐
▼ │
[IDLE] ──按下──→ [PRESS_DOWN] │
│ │
短于阈值释放 │
▼ │
[SINGLE_CLICK] │
│ │
等待双击超时 │
┌─────┼─────┐ │
▼ ▼ │
[再次按下] [确认单击] │
│ │ │
▼ │ │
[DOUBLE_CLICK] │ │
▼ │
(事件回调) │
│ │
┌─────────────────────┘ │
│ │
[PRESS_DOWN] ──超过长按阈值──→ [LONG_PRESS_START]
│
持续按住
▼
[LONG_PRESS_HOLD]
│
释放
▼
[PRESS_UP] ──→ [IDLE]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
支持的事件类型
| 事件 | 触发条件 | 事件常量 |
|---|---|---|
| PRESS_DOWN | 按键刚按下 | PRESS_DOWN |
| PRESS_UP | 按键刚释放 | PRESS_UP |
| PRESS_REPEAT | 重复按下(连续快按) | PRESS_REPEAT |
| SINGLE_CLICK | 单击确认(等待双击超时后) | SINGLE_CLICK |
| DOUBLE_CLICK | 双击检测到 | DOUBLE_CLICK |
| LONG_PRESS_START | 长按达到阈值瞬间 | LONG_PRESS_START |
| LONG_PRESS_HOLD | 长按持续中(周期触发) | LONG_PRESS_HOLD |
20.3 按键库实现
20.3.1 数据结构
c
#ifndef MID_BUTTON_H
#define MID_BUTTON_H
#include <stdint.h>
// 按键事件类型
typedef enum {
PRESS_DOWN = 0,
PRESS_UP,
PRESS_REPEAT,
SINGLE_CLICK,
DOUBLE_CLICK,
LONG_PRESS_START,
LONG_PRESS_HOLD,
EVENT_NUM, // 事件总数
} ButtonEvent;
// 按键状态
typedef enum {
BTN_STATE_IDLE = 0,
BTN_STATE_PRESS,
BTN_STATE_LONG,
} ButtonState;
// 按键结构体
typedef struct Button {
uint8_t id;
uint8_t state;
uint8_t debounce_cnt;
uint8_t active_level; // 按下时的电平(0 或 1)
uint8_t button_level; // 当前电平
uint8_t event;
uint8_t repeat_cnt;
uint16_t ticks;
uint16_t long_press_ticks; // 长按阈值(tick 数)
uint16_t short_press_ticks; // 短按最小时间
uint8_t (*read_pin)(void); // 读取引脚电平的函数指针
void (*callback[EVENT_NUM])(void *); // 事件回调函数
struct Button *next; // 链表指针
} Button_t;
// API
void button_init(Button_t *btn, uint8_t (*read_pin)(void), uint8_t active_level);
void button_attach(Button_t *btn, ButtonEvent event, void (*callback)(void *));
void button_tick(void); // 在定时器中断中周期调用
#endif1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
20.3.2 核心状态机
c
#include "mid_button.h"
#define DEBOUNCE_TICKS 3 // 消抖(3 tick = 15ms @5ms周期)
#define SHORT_TICKS 20 // 短按最小时间(100ms)
#define LONG_TICKS 100 // 长按阈值(500ms)
static Button_t *btn_list_head = NULL;
void button_init(Button_t *btn, uint8_t (*read_pin)(void), uint8_t active_level)
{
btn->read_pin = read_pin;
btn->active_level = active_level;
btn->state = BTN_STATE_IDLE;
btn->event = 0;
btn->ticks = 0;
btn->debounce_cnt = 0;
btn->repeat_cnt = 0;
btn->long_press_ticks = LONG_TICKS;
btn->short_press_ticks = SHORT_TICKS;
// 加入链表
btn->next = btn_list_head;
btn_list_head = btn;
}
void button_attach(Button_t *btn, ButtonEvent event, void (*callback)(void *))
{
if (event < EVENT_NUM) {
btn->callback[event] = callback;
}
}
// 触发事件回调
static void button_fire_event(Button_t *btn, ButtonEvent event)
{
if (btn->callback[event]) {
btn->callback[event]((void *)btn);
}
}
// 单个按键状态机更新
static void button_process(Button_t *btn)
{
uint8_t pin_level = btn->read_pin();
// 消抖
if (pin_level != btn->button_level) {
if (++btn->debounce_cnt >= DEBOUNCE_TICKS) {
btn->button_level = pin_level;
btn->debounce_cnt = 0;
}
} else {
btn->debounce_cnt = 0;
}
uint8_t pressed = (btn->button_level == btn->active_level);
switch (btn->state) {
case BTN_STATE_IDLE:
if (pressed) {
btn->state = BTN_STATE_PRESS;
btn->ticks = 0;
btn->repeat_cnt = 1;
button_fire_event(btn, PRESS_DOWN);
}
break;
case BTN_STATE_PRESS:
if (pressed) {
btn->ticks++;
if (btn->ticks >= btn->long_press_ticks) {
btn->state = BTN_STATE_LONG;
button_fire_event(btn, LONG_PRESS_START);
}
} else {
// 释放
button_fire_event(btn, PRESS_UP);
if (btn->ticks < btn->short_press_ticks) {
// 太短,忽略
btn->state = BTN_STATE_IDLE;
} else {
button_fire_event(btn, SINGLE_CLICK);
btn->state = BTN_STATE_IDLE;
}
}
break;
case BTN_STATE_LONG:
if (pressed) {
btn->ticks++;
// 长按持续中,每隔一定时间触发
if (btn->ticks % 10 == 0) {
button_fire_event(btn, LONG_PRESS_HOLD);
}
} else {
button_fire_event(btn, PRESS_UP);
btn->state = BTN_STATE_IDLE;
}
break;
}
}
// 遍历所有注册的按键并更新状态(在定时器中断中调用)
void button_tick(void)
{
Button_t *btn = btn_list_head;
while (btn) {
button_process(btn);
btn = btn->next;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
简化说明
上述代码是简化版本,省略了双击检测逻辑。完整的 MultiButton 库支持双击,通过在单击后等待一小段时间(约 200ms)判断是否有第二次按下。出厂固件使用的完整版本位于 middle/mid_button.c。
20.4 使用方法
20.4.1 定义按键实例和读取函数
c
#include "ti_msp_dl_config.h"
#include "mid_button.h"
// 按键实例
static Button_t btn_key1;
static Button_t btn_key2;
static Button_t btn_enter;
// 引脚读取函数(返回当前电平,0 或 1)
static uint8_t read_key1(void)
{
return (DL_GPIO_readPins(GPIO_KEY_PORT, GPIO_KEY_PIN_KEY1_PIN) != 0) ? 1 : 0;
}
static uint8_t read_key2(void)
{
return (DL_GPIO_readPins(GPIO_KEY2_PORT, GPIO_KEY2_PIN_KEY2_PIN) != 0) ? 1 : 0;
}
static uint8_t read_enter(void)
{
return (DL_GPIO_readPins(GPIO_ENTER_PORT, GPIO_ENTER_PIN_PIN) != 0) ? 1 : 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
20.4.2 注册事件回调
c
// 事件回调函数
void on_key1_click(void *arg)
{
DL_GPIO_togglePins(GPIO_LED_PORT, GPIO_LED_PIN_PIN);
buzzer_beep(NOTE_C6, 30); // 按键提示音
}
void on_key1_long_press(void *arg)
{
// 长按进入设置模式
uart_send_string("Enter settings\r\n");
}
void on_key2_click(void *arg)
{
uart_send_string("KEY2 clicked\r\n");
}
void on_enter_double_click(void *arg)
{
uart_send_string("ENTER double clicked\r\n");
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
20.4.3 初始化和启动
c
void user_key_init(void)
{
// 初始化按键(引脚读取函数, 按下时的电平)
button_init(&btn_key1, read_key1, 0); // PA24 上拉,按下低电平
button_init(&btn_key2, read_key2, 0); // PB24 上拉,按下低电平
button_init(&btn_enter, read_enter, 0); // PB21 上拉,按下低电平
// 注册事件回调
button_attach(&btn_key1, SINGLE_CLICK, on_key1_click);
button_attach(&btn_key1, LONG_PRESS_START, on_key1_long_press);
button_attach(&btn_key2, SINGLE_CLICK, on_key2_click);
button_attach(&btn_enter, DOUBLE_CLICK, on_enter_double_click);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
20.4.4 定时驱动
在系统定时器中断中周期调用 button_tick():
c
// TIMA0 5ms 周期中断
void TIMA0_IRQHandler(void)
{
switch (DL_TimerA_getPendingInterrupt(TIMER_TICK_INST)) {
case DL_TIMER_IIDX_ZERO:
button_tick(); // 每 5ms 扫描一次按键状态
break;
default:
break;
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
20.4.5 主函数
c
int main(void)
{
SYSCFG_DL_init();
user_key_init();
// 配置并启动 5ms 周期定时器
NVIC_EnableIRQ(TIMER_TICK_INT_IRQN);
DL_TimerA_startCounter(TIMER_TICK_INST);
uart_send_string("Button library ready.\r\n");
while (1) {
// 主循环可以做其他事情
// 按键事件在中断回调中处理
__WFI();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
20.5 参数调节
| 参数 | 默认值(@5ms tick) | 实际时间 | 调节建议 |
|---|---|---|---|
| DEBOUNCE_TICKS | 3 | 15ms | 机械按键通常 5~20ms |
| SHORT_TICKS | 20 | 100ms | 低于此时间的按下被忽略 |
| LONG_TICKS | 100 | 500ms | 超过此时间判定为长按 |
| 双击间隔 | 40 | 200ms | 两次点击间最大间隔 |
c
// 自定义单个按键的长按阈值
btn_key1.long_press_ticks = 200; // 1 秒才算长按1
2
2
20.6 应用示例:OLED 菜单导航
将按键事件与 OLED UI 联动,实现菜单浏览:
c
static int8_t menu_index = 0;
#define MENU_ITEMS 5
const char *menu_labels[] = {
"1. LED Control",
"2. Buzzer Test",
"3. IMU Data",
"4. Wireless",
"5. Settings"
};
void on_key1_click(void *arg)
{
// KEY1: 上移
menu_index--;
if (menu_index < 0) menu_index = MENU_ITEMS - 1;
menu_refresh();
}
void on_key2_click(void *arg)
{
// KEY2: 下移
menu_index++;
if (menu_index >= MENU_ITEMS) menu_index = 0;
menu_refresh();
}
void on_enter_click(void *arg)
{
// ENTER: 确认选择
switch (menu_index) {
case 0: led_control_page(); break;
case 1: buzzer_test_page(); break;
case 2: imu_data_page(); break;
case 3: wireless_page(); break;
case 4: settings_page(); break;
}
}
void menu_refresh(void)
{
OLED_Clear();
for (int i = 0; i < MENU_ITEMS; i++) {
if (i == menu_index) {
OLED_ShowString(0, i * 12, ">");
}
OLED_ShowString(8, i * 12, menu_labels[i]);
}
OLED_Refresh();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
20.7 天巧星按键映射
出厂固件中的按键逻辑映射:
| 物理按键 | 引脚 | 逻辑功能 |
|---|---|---|
| KEY1 (PA24) | 上拉,按下低电平 | 返回 / 向上 |
| KEY2 (PB24) | 上拉,按下低电平 | 向下 |
| ENTER (PB21) | 上拉,按下低电平 | 确认 / 进入 |
| BACK (PA18) | 下拉,按下高电平 | 返回(等同 KEY1) |
| 编码器按键 (PA31) | 上拉,按下低电平 | 确认(等同 ENTER) |
| 编码器旋转 | PA29/PA30 QEI | 上/下滚动 |
编码器与按键的融合
出厂固件将旋转编码器的左旋/右旋映射为"上/下"操作,编码器按键映射为"确认"。这样用户既可以用独立按键操作,也可以用编码器单手操作——体验更统一。
20.8 知识总结
| 内容 | 说明 |
|---|---|
| 核心原理 | 有限状态机 + 定时器周期扫描 |
| 支持事件 | 按下、释放、单击、双击、长按开始、长按持续 |
| 驱动方式 | 定时器中断中调用 button_tick()(5ms 周期) |
| 消抖 | 3 个 tick(15ms)软件消抖 |
| 长按阈值 | 默认 500ms,可自定义 |
| 扩展性 | 链表管理,支持任意数量按键 |
| 回调机制 | 事件触发时自动调用注册的回调函数 |
至此,天巧星 MSPM0G3519 开发板的入门手册和板载外设高阶教程全部完成。你已经掌握了从基础 GPIO 到复杂外设驱动的完整知识体系。可以将这些模块自由组合,构建你自己的嵌入式应用。