按键扫描与输入处理
AD23N SDK 的按键子系统:以 10ms 周期轮询各类按键(IO / AD / 矩阵 / 红外 / 触摸),经统一状态机识别短按、长按、连按、抬起(及可选双击)等事件,再通过消息过滤器转换为系统消息(MSG_xxx)投递到消息队列,驱动上层应用逻辑。
Purpose and Scope
本页面向 sdk/app/bsp/common/key/ 目录下的按键扫描与输入处理机制,完整覆盖:
- 统一按键抽象
key_interface_t与编译期注册表key_list[] - 核心扫描状态机
key_scan()(短按 / 长按 / 连按 / 抬起 / 双击) - 各类型按键驱动(IO、AD、矩阵、红外、触摸)的接入方式与寄存器级初始化示例
- 按键事件到系统消息(
post_msg)的转换链路(key2msg_emit+key_msg_filter) - 各工作模式的按键映射表(如
music_key_table.c的红外键表)
以下内容刻意留给其他页面:消息队列本身的实现(msg.c / post_msg)、按键唤醒电源逻辑(sdk/app/bsp/cpu/sh59/power/key_wakeup.c)、各音频模式对 MSG_xxx 的具体业务处理,以及 GPIO 驱动层细节。本页聚焦"从按键物理信号到系统消息"这一段完整链路。
概述
按键输入是蓝牙音箱 / MCU 类产品最重要的人机接口,AD23N SDK 将其设计为三层解耦结构:
- 驱动层(
key_drv_io.c、key_drv_ad.c、key_matrix.c、key_ir.c、key_touch.c):每种物理按键类型实现同一份key_interface_t接口(初始化 + 读值),具体差异(寄存器、ADC、红外解码、触摸检测)被完全封装。 - 扫描层(
key.c):核心状态机key_scan()在 10ms 定时器中被周期调用,通过get_key_value()轮询驱动层获得当前键值,再用计数状态机把"持续按住时间"翻译成按键事件(短按、长按、连按、抬起)。 - 应用层(各模式
*_key_table.c/*_mode_key.c):通过key_table_sel()注册一个消息过滤函数,把(key_status, key_num, key_type)三元组查表映射为MSG_xxx系统消息,经post_msg()投递。
设计意图:把"物理按键怎么读"和"按键含义是什么"彻底分离。换板子改驱动、换产品改映射表,扫描状态机与消息机制完全不用动;同时所有按键类型共享一套消抖、长按、连按逻辑,保证行为一致性。
关键时间参数(由 10ms 扫描周期折算):短按判定约 30ms(3 次连续采样,兼作消抖),长按判定约 750ms,长按后每 150ms 重复产生连按事件,双击窗口约 350ms(默认关闭)。
架构
flowchart TD
subgraph sg_Drv["驱动层 key_drv_*"]
IO["key_io_info (IO)"]
AD["key_ad_info (AD)"]
MX["key_matrix_info (矩阵)"]
IR["key_ir_info (红外)"]
TC["key_touch_info (触摸)"]
end
subgraph sg_Core["扫描层 key.c"]
INIT["key_init()"]
SCAN["key_scan() 10ms 周期"]
GV["get_key_value()"]
EMIT["key2msg_emit()"]
end
subgraph sg_App["应用层"]
FILT["key_msg_filter 消息过滤"]
TABLE["模式按键表 xxx_key_table.c"]
QUEUE["消息队列 post_msg()"]
KICK["d_key_voice_kick() 按键音"]
end
INIT --> IO
INIT --> AD
INIT --> MX
INIT --> IR
INIT --> TC
SCAN --> GV
GV --> IO
GV --> AD
GV --> MX
GV --> IR
GV --> TC
SCAN --> EMIT
EMIT --> FILT
TABLE --> FILT
EMIT --> KICK
FILT --> QUEUE
各层职责说明:
- 驱动层:每个驱动导出一个
const key_interface_t实例(如key_io_info),内含key_type、key_init、key_get_value。key_get_value()返回当前按下的键号(NO_KEY表示无按键,-1表示本次读取失败)。驱动是否编译由KEY_IO_EN/KEY_AD_EN/KEY_MATRIX_EN/KEY_IR_EN/KEY_TOUCH_EN宏在编译期裁剪。 - 扫描层:
key_init()遍历key_list[]逐个调用驱动的初始化;key_scan()是唯一的状态机入口,输出按键事件;key2msg_emit()是事件出口,负责查表与投递。 - 应用层:模式按键表以"按键号 → 消息"的查表宏定义事件语义;
key_msg_filter函数指针由key_table_sel()在模式切换时动态更换,因此同一套物理按键在不同工作模式下可以有完全不同的含义。
核心实现
统一按键接口 key_interface_t
所有按键驱动都实现同一份接口,这是整个框架可扩展的基石:
typedef struct {
KEY_TYPE key_type;
void (*key_init)(void);
u32(*key_get_value)(void);
} key_interface_t;
Source: key.h
key_type:声明本驱动的按键类型(IO / AD / MATRIX / IR / TOUCH),由KEY_TYPE枚举定义;key_init:上电时被key_init()调用,完成 GPIO / ADC / 解码器配置;key_get_value:每次扫描被轮询,返回当前按下的键号。返回值约定:NO_KEY(0xff)表示无按键,-1表示驱动尚未就绪(本次扫描放弃)。
配套的 key_io_t 结构体用于在驱动层与扫描层之间传递结果:
typedef struct {
u8 key_type;
u8 key_num;
} key_io_t;
Source: key.h
按键类型、状态与门槛值
key.h 集中定义了全部按键语义常量:
typedef enum {
KEY_TYPE_IO,
KEY_TYPE_AD,
KEY_TYPE_MATRIX,
KEY_TYPE_IR,
KEY_TYPE_TOUCH,
} KEY_TYPE;
#define KEY_BASE_CNT 2
#define KEY_LONG_CNT 75
#define KEY_HOLD_CNT 15
#define KEY_SHORT_CNT 3
enum {
KEY_SHORT = 0x0,
KEY_SHORT_UP = 0x1,
KEY_LONG = 0x2,
KEY_HOLD = 0x3,
KEY_LONG_UP = 0x4,
KEY_DOUBLE = 0x5,
KEY_MAX_STATUS,
};
Source: key.h
按键状态机共产生 6 种事件,含义与触发时机如下(均以 10ms 扫描周期折算):
| 事件 | 值 | 触发条件 | 典型用途 |
|---|---|---|---|
KEY_SHORT | 0x0 | 按下持续 KEY_SHORT_CNT(3 次 ≈ 30ms) | 消抖后的"按下沿",一般与 SHORT_UP 成对 |
KEY_SHORT_UP | 0x1 | 在短按窗口内抬起(3 < counter < 75) | 单击动作执行点,如播放/暂停 |
KEY_LONG | 0x2 | 持续按住达 KEY_LONG_CNT(75 次 ≈ 750ms) | 长按动作,如开机/关机 |
KEY_HOLD | 0x3 | 长按后再持续 KEY_HOLD_CNT(15 次 ≈ 150ms),且周期重置 | 连续调节,如音量/快进退 |
KEY_LONG_UP | 0x4 | 长按后抬起 | 长按结束点,可与 KEY_LONG 配合 |
KEY_DOUBLE | 0x5 | 双击窗口(KEY_DOUBLE_CLICK_CNT ≈ 350ms)内第二次短按 | 双击快捷功能(默认关闭) |
门槛值设计的意图:KEY_SHORT_CNT = 3 同时充当软件消抖——物理抖动通常小于 30ms,连续 3 次采样一致才认为按键稳定按下;KEY_LONG_CNT = 75 把长按与短按清晰分隔(750ms 足够用户区分两种操作);KEY_HOLD_CNT = 15 是长按后连按的重复周期,过长会感觉迟钝、过短会误触发重复。各模式可自行调整这些宏以适配产品手感。
扫描状态机 key_scan()
key_scan() 是整个按键子系统的核心,使用三个 static 变量跨周期保持状态:
static u8 key_type = NO_KEY; ///<按键类型
static u8 last_key = NO_KEY;
static u8 key_press_counter = 0;
Source: key.c
每次调用流程:先处理可选的双击计时 → 读取当前键值 cur_key → 与 last_key 比较,分两大分支:
分支一:cur_key == last_key(键值不变,持续按住)——计数递增,命中以下阈值时产生事件:
if (cur_key == last_key) { //长时间按键
if (cur_key == NO_KEY) {
return;
}
key_press_counter++;
if (key_press_counter == KEY_SHORT_CNT) {
key_status = KEY_SHORT;
} else if (key_press_counter == KEY_LONG_CNT) { //长按
key_status = KEY_LONG;
} else if (key_press_counter == (KEY_LONG_CNT + KEY_HOLD_CNT)) { //连按
key_status = KEY_HOLD;
key_press_counter = KEY_LONG_CNT;
} else {
return;
}
}
Source: key.c
注意 KEY_HOLD 分支把计数器重置回 KEY_LONG_CNT:此后每 KEY_HOLD_CNT 个周期再次触发 KEY_HOLD,形成"长按后持续重复"的事件流(这正是音量连续加减的实现基础),直到用户抬手。
分支二:cur_key != last_key(键值变化,通常是抬键)——根据释放前累计的计数判定短按抬起或长按抬起:
} else { //cur_key = NO_KEY, 抬键
last_key = cur_key;
if ((key_press_counter > KEY_SHORT_CNT) && (key_press_counter < KEY_LONG_CNT) && (cur_key == NO_KEY)) { //短按抬起
key_press_counter = 0;
key_status = KEY_SHORT_UP;
} else if ((cur_key == NO_KEY) && (key_press_counter >= KEY_LONG_CNT)) { //长按抬起
key_press_counter = 0;
key_status = KEY_LONG_UP;
} else {
key_press_counter = 0;
return;
}
}
Source: key.c
该分支的设计要点:若按下期间键值发生变化但又不是抬键(例如误触到另一颗键),计数清零并直接返回,不产生任何事件,避免在按键切换瞬间误报。
状态机判定逻辑可概括为:
flowchart TD
Start(["key_scan() 每 10ms"]) --> Read["get_key_value()"]
Read -->|"返回 -1 读取失败"| R1["return 放弃本次扫描"]
Read -->|"成功"| Cmp{"cur_key == last_key ?"}
Cmp -->|"是 且 NO_KEY"| R2["return 无按键"]
Cmp -->|"是 且按下"| Inc["key_press_counter++"]
Inc --> Cnt{"计数命中?"}
Cnt -->|"KEY_SHORT_CNT(3)"| S["KEY_SHORT 短按"]
Cnt -->|"KEY_LONG_CNT(75)"| L["KEY_LONG 长按"]
Cnt -->|"LONG+HOLD(90)"| H["KEY_HOLD 连按<br/>counter 重置回 75"]
Cmp -->|"否 键值变化"| Rel{"cur_key == NO_KEY ?"}
Rel -->|"是 且 3<counter<75"| SU["KEY_SHORT_UP 短按抬起"]
Rel -->|"是 且 counter>=75"| LU["KEY_LONG_UP 长按抬起"]
Rel -->|"否 换键误触"| R3["counter=0, return"]
S --> Em["key2msg_emit(status, key_num, key_type)"]
L --> Em
H --> Em
SU --> Em
LU --> Em
事件产生顺序与业务语义:短按抬起 / 长按 → 连按。也就是说,短按动作在"抬起"时才上报(配合消抖后按下 30ms、抬起判定的时间窗),长按动作在按下 750ms 时上报一次、之后每 150ms 重复一次;应用层通常以 KEY_SHORT_UP 执行单击功能、以 KEY_HOLD 执行连续调节,二者互不干扰。
驱动层注册与轮询
key.c 通过编译期宏裁剪的 key_list[] 数组聚合所有使能的驱动:
static const key_interface_t *key_list[] = {
#if KEY_IO_EN
&key_io_info,
#endif
#if KEY_AD_EN
&key_ad_info,
#endif
#if KEY_MATRIX_EN
&key_matrix_info,
#endif
#if KEY_IR_EN
&key_ir_info,
#endif
#if KEY_TOUCH_EN
&key_touch_info,
#endif
};
Source: key.c
初始化入口 key_init() 逐个调用驱动实例的 key_init:
void key_init(void)
{
key_puts("key init\n");
#if (KEY_IO_EN || KEY_AD_EN || KEY_MATRIX_EN || KEY_IR_EN || KEY_TOUCH_EN)
for (int i = 0; i < (sizeof(key_list) / sizeof(key_list[0])); i++) {
if (key_list[i]->key_init) {
key_list[i]->key_init();
}
}
#endif
}
Source: key.c
轮询入口 get_key_value() 按数组顺序遍历驱动,返回第一个有效键值;某驱动返回 -1 表示未就绪,立即放弃本次扫描:
bool get_key_value(key_io_t *p_key)
{
u8 i;
p_key->key_type = NO_KEY;
for (i = 0; i < (sizeof(key_list) / sizeof(key_list[0])); i++) {
if (key_list[i]->key_get_value) {
u32 key = key_list[i]->key_get_value();
if (-1 == key) {
return false;
}
p_key->key_num = key_list[i]->key_get_value();
if (NO_KEY != p_key->key_num) {
p_key->key_type = key_list[i]->key_type;
return true;
}
}
}
p_key->key_num = NO_KEY;
return true;
}
Source: key.c
这种"数组 + 宏裁剪"的设计让添加新按键类型只需两步:写驱动、加进数组;未使能的驱动连代码都不链接,ROM 占用最小。
IO 按键驱动示例(key_drv_io)
IO 按键驱动展示了一个典型的驱动层实现:读取 GPIO 电平判断按下,初始化时配置上下拉。AD23N 与早期平台(AD24)的差异点在于:AD23N 用 2 bit 控制一个 IO 的上下拉电阻,上下拉各自集成在同一个寄存器中,寄存器 bit 与 io_index 的对应关系为 bit0 = io_index * 2, bit1 = io_index * 2 + 1:
//AD23需要2bit控制IO上下拉电阻,并且上下拉各自集成在一个寄存器
//所以寄存器bit和io_index对应关系bit0 = io_index * 2, bit1 = io_index * 2 + 1
//比如需要设置PA3的上拉电阻,则配置SFR(JL_PORTA->PU, 3 * 2, 2, 0b01)
#define KEY0_IO_INDEX 10
#define KEY1_IO_INDEX 11
#define KEY2_IO_INDEX 12
#define IS_KEY0_DOWN() \t(!(JL_PORTA->IN & BIT(KEY0_IO_INDEX)))
#define IS_KEY1_DOWN() \t(!(JL_PORTA->IN & BIT(KEY1_IO_INDEX)))
#define IS_KEY2_DOWN() \t(!(JL_PORTA->IN & BIT(KEY2_IO_INDEX)))
#define KEY_INIT() do{\
/**key0 init*/\
SFR(JL_PORTA->PU, KEY0_IO_INDEX * 2, 2, 0b01),\
SFR(JL_PORTA->PD, KEY0_IO_INDEX * 2, 2, 0b00),\
JL_PORTA->DIE |= BIT(KEY0_IO_INDEX),\
JL_PORTA->DIR |= BIT(KEY0_IO_INDEX);\
...
}while(0)
extern const key_interface_t key_io_info;
Source: key_drv_io.h
要点:
IS_KEYx_DOWN()用低电平有效(!(IN & BIT))表示按键按下,这是按键通常接 GND 的硬件习惯;SFR(JL_PORTA->PU, idx*2, 2, 0b01)配置上拉、SFR(JL_PORTA->PD, idx*2, 2, 0b00)关闭下拉,DIE使能数字输入,DIR置为输入方向;- 驱动文件导出
const key_interface_t key_io_info,内部把IS_KEYx_DOWN()组合成键号返回,对上层完全屏蔽寄存器细节。
其余驱动同理:key_drv_ad.c 通过 ADC 采样分压电阻网络识别多键(AD 键位共享一个 ADC 通道,不同电阻对应不同键号);key_matrix.c 通过行列扫描识别矩阵键盘;key_ir.c 解码红外遥控码;key_touch.c 读取触摸检测结果。
按键 → 消息转换
扫描状态机产生的 (key_status, key_num, key_type) 三元组由 key2msg_emit() 统一出口处理:
static void key2msg_emit(u8 key_status, u8 key_num, u8 key_type)
{
key_printf("key_status:%d key_num:%d key_type:%d \n", key_status, key_num, key_type);
if (key_msg_filter) {
u16 msg = key_msg_filter(key_status, key_num, key_type);
if (msg != NO_MSG) {
d_key_voice_kick();
int ret = post_msg(1, msg);
if (ret != 0) {
key_printf("error !!! msg pool full \n");
}
}
}
}
Source: key.c
链路语义:
key_msg_filter是当前模式注册的消息过滤器(函数指针,默认 NULL,未注册时按键事件被丢弃);- 过滤器返回
NO_MSG表示该按键在该状态无动作,链路直接终止; - 有动作时先
d_key_voice_kick()触发按键音,再post_msg(1, msg)投递到消息队列; - 投递失败(队列满)打印
error !!! msg pool full,本次按键消息丢弃——扫描状态机本身不受影响,下一周期继续工作。
过滤器由 key_table_sel() 动态切换,切换过程关中断保护,避免在中断上下文读到半更新指针:
void key_table_sel(void *msg_filter)
{
local_irq_disable();
key_msg_filter = msg_filter;
local_irq_enable();
}
Source: key.c
模式按键表(查表映射)
应用层以"按键号 → 消息"的查表宏定义每个模式下每个按键的含义。以音乐模式的红外键表为例,IRFF00_MUSIC_SHORT_UP 定义 21 个 IR 键(IR_KEY_MAX_NUM = 21)在短按抬起时的动作:
#define IRFF00_MUSIC_SHORT_UP \
/*00*/ MSG_PP,\
/*01*/ MSG_NEXT_WORKMODE,\
/*02*/ MSG_MUTE,\
/*03*/ MSG_PP,\
/*04*/ MSG_PREV_FILE,\
/*05*/ MSG_NEXT_FILE,\
/*06*/ MSG_DEC_EQ,\
/*07*/ MSG_VOL_DOWN,\
/*08*/ MSG_VOL_UP,\
/*09*/ MSG_0,\
/*10*/ MSG_NEXT_PLAYMODE,\
/*11*/ MSG_NEXT_DEVICE,\
/*12*/ MSG_1,\
...
/*20*/ MSG_9
Source: music_key_table.c
同一文件还定义了 IRFF00_MUSIC_LONG、IRFF00_MUSIC_HOLD、IRFF00_MUSIC_LONG_UP 等状态表:长按/连按通常只对音量与快进退有效(如 MSG_VOL_UP、MSG_MUSIC_FF),其余键位填 NO_MSG 保持静默。这种"每状态一张表"的结构与 key_scan 的 6 种事件一一对应,并且按键表用 #pragma 分段指令放入独立的 .music_key_table.* 链接段,便于厂商固件分区管理。
各模式(音乐 / 录音 / 收音 / 对箱 / linein / 等)均有自己的 *_mode_key.c 与按键表,模式切换时调用 key_table_sel() 换表,实现同一颗物理按键在不同模式下复用。
核心流程
sequenceDiagram
participant T as 系统定时器 (10ms)
participant S as key_scan()
participant G as get_key_value()
participant D as 按键驱动 key_get_value()
participant E as key2msg_emit()
participant F as key_msg_filter 查表
participant Q as post_msg() 消息队列
T->>S: 周期调用
S->>G: 读取当前键值
G->>D: 遍历 key_list[] 查询各驱动
D-->>G: key_num / key_type (或 -1)
G-->>S: key_io_t
S->>S: 状态机判定事件<br/>(短按/长按/连按/抬起)
S->>E: key2msg_emit(status, num, type)
E->>F: msg = key_msg_filter(...)
F-->>E: MSG_xxx 或 NO_MSG
E->>Q: post_msg(1, msg)
Q-->>E: 0=成功 / 非0=队列满
端到端时序(以"短按播放键一次"为例):
- 10ms 定时器触发
key_scan(); - 连续 3 个周期读到同一键号,第 3 个周期产生
KEY_SHORT(此时按键已消抖确认); - 用户抬手,下一周期键值变
NO_KEY,计数器位于(3, 75)区间 → 产生KEY_SHORT_UP; key2msg_emit(KEY_SHORT_UP, key_num, key_type)调用音乐模式的过滤器,查IRFF00_MUSIC_SHORT_UP表得到MSG_PP;- 触发按键音
d_key_voice_kick(),post_msg(1, MSG_PP)入队; - 音乐模式消息循环收到
MSG_PP,执行播放/暂停切换。
若按住不放:750ms 时产生一次 KEY_LONG,之后每 150ms 产生一次 KEY_HOLD,抬起时产生 KEY_LONG_UP——同一颗键在一条时间线上自然形成"短按、长按、连按"三种操作。
配置选项
按键子系统全部通过编译期宏配置,无运行时配置项。宏定义位置:key.h(门槛值、最大键数)与 app_config.h(类型使能开关)。
| 宏 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KEY_IO_EN | 宏开关 | 由 app_config.h 定义 | 使能 IO 按键驱动,参与编译 |
KEY_AD_EN | 宏开关 | 由 app_config.h 定义 | 使能 AD(ADC 分压)按键驱动 |
KEY_MATRIX_EN | 宏开关 | 由 app_config.h 定义 | 使能矩阵键盘驱动 |
KEY_IR_EN | 宏开关 | 由 app_config.h 定义 | 使能红外遥控驱动 |
KEY_TOUCH_EN | 宏开关 | 由 app_config.h 定义 | 使能触摸按键驱动 |
KEY_DOUBLE_CLICK_EN | 宏开关 | 0 | 双击使能;置 1 后还需定义对应 DOUBLE_KICK 表 |
KEY_BASE_CNT | 计数 | 2 | 按键基础计数值(消抖前预留) |
KEY_SHORT_CNT | 计数 | 3 | 短按判定阈值,兼作软件消抖(≈30ms) |
KEY_LONG_CNT | 计数 | 75 | 长按判定阈值(≈750ms) |
KEY_HOLD_CNT | 计数 | 15 | 长按后连按重复周期(≈150ms) |
KEY_DOUBLE_CLICK_CNT | 计数 | 35 | 双击判定窗口(≈350ms),仅 KEY_DOUBLE_CLICK_EN 时生效 |
IO_KEY_MAX_NUM | 常量 | 10 | IO 按键最大个数 |
AD_KEY_MAX_NUM | 常量 | 10 | AD 按键最大个数 |
MATRIX_KEY_MAX_NUM | 常量 | 10 | 矩阵按键最大个数 |
IR_KEY_MAX_NUM | 常量 | 21 | 红外按键最大个数(与遥控器键位表对应) |
TOUCH_KEY_MAX_NUM | 常量 | 10 | 触摸按键最大个数 |
NO_KEY | 常量 | 0xff | "无按键"哨兵值 |
KEY_UART_DEBUG | 宏开关 | 关闭 | 打开后 key_puts/key_printf/key_buf 映射到 log_info,输出扫描与投递日志 |
Source: key.h
调整建议:手感调校只需改 KEY_SHORT_CNT(消抖灵敏度)、KEY_LONG_CNT(长按时长)、KEY_HOLD_CNT(连按速率)三个值;量产不同硬件平台时通过 app_config.h 开关按键类型,无需改动 key.c。
API 参考
void key_init(void)
初始化所有使能的按键驱动。系统上电阶段调用一次。
说明:遍历 key_list[],调用每个驱动实例的 key_init 函数指针;未使能的驱动不参与编译。
void key_scan(void)
按键扫描状态机入口,需按固定周期(10ms)重复调用。
说明:内部使用 static 变量跨周期保持状态(last_key、key_press_counter、key_type),通过计数判定产生 KEY_SHORT / KEY_SHORT_UP / KEY_LONG / KEY_HOLD / KEY_LONG_UP / KEY_DOUBLE 事件。不可重入,必须在单一上下文(系统定时器或主循环)中周期调用。
void key_table_sel(void *msg_filter)
注册/切换按键消息过滤器。
参数:msg_filter —— 函数指针 u16 (*)(u8 key_status, u8 key_num, u8 key_type),返回 MSG_xxx 或 NO_MSG。传入 NULL 可屏蔽所有按键。
说明:切换过程以 local_irq_disable()/local_irq_enable() 保护,保证与中断上下文切换安全。各模式进入/退出时调用以更换按键语义表。
bool get_key_value(key_io_t *p_key)
轮询驱动层获取当前键值。
参数:p_key —— 输出参数,成功时填充 key_num 与 key_type。
返回:true 表示读取有效(key_num 可能为 NO_KEY);false 表示某驱动返回 -1(未就绪),本次扫描应放弃。
static void key2msg_emit(u8 key_status, u8 key_num, u8 key_type)
按键事件出口:查表 → 触发按键音 → 投递消息。
参数:key_status 为 6 种事件之一;key_num 为键号;key_type 为按键类型。
副作用:过滤器返回非 NO_MSG 时调用 d_key_voice_kick() 播放按键音,并 post_msg(1, msg) 入队;队列满时打印 error !!! msg pool full 并丢弃该消息。
typedef int (*key_emit_t)(u8 key_status, u8 key_num, u8 key_type)
按键输出注册接口类型(key.h 中定义)。当前框架内部通过静态 key2msg_emit 实现,该类型为上层预留的自定义输出通道。
失败模式、边界与并发
消息队列满(按键消息丢失)
post_msg(1, msg) 返回非 0 时打印 error !!! msg pool full 并丢弃本次消息。这是按键链路唯一的"数据丢失"点:快速连按超过消息队列吞吐时,中间事件可能被丢弃,但扫描状态机状态不受影响,后续按键仍正常上报。应用层应把关键操作放在 KEY_SHORT_UP(单击)而非 KEY_SHORT(按下沿),因为抬起事件天然对操作做了节流。
驱动读取失败(-1)
get_key_value() 中任一驱动返回 -1 即放弃本次扫描(return false),key_scan() 直接返回。这在驱动初始化未完成或 ADC/红外解码器忙时保护状态机不被脏数据污染;下一个 10ms 周期自动恢复。
按键切换误触
按下 A 未抬起又触发 B(cur_key != last_key 且 cur_key != NO_KEY):计数器清零并返回,不产生任何事件。设计意图是:换键瞬间的物理状态不可信,宁可漏报一次也不误报,保证单键语义的纯粹性。
长按重复与抬起竞态
KEY_HOLD 触发时计数器被重置回 KEY_LONG_CNT,因此只要不抬手,KEY_HOLD 会以 KEY_HOLD_CNT(150ms)周期无限重复;抬手瞬间计数器 >= KEY_LONG_CNT 触发 KEY_LONG_UP。若在长按与连按之间(counter ∈ [75, 90))抬手,仍归为长按抬起,不会产生歧义。
双击窗口(默认关闭)
KEY_DOUBLE_CLICK_EN = 0 时双击逻辑不编译。置 1 后,第一次 KEY_SHORT_UP 进入等待窗口(KEY_DOUBLE_CLICK_CNT ≈ 350ms):窗口内第二次短按产生 KEY_DOUBLE 并清窗口;窗口超时则补发第一次的 KEY_SHORT_UP。代价是单击响应被延迟最多 350ms,因此默认关闭,产品需要双击功能时需权衡手感。
并发与中断安全
key_scan()依赖static状态变量,不可重入,必须保证 10ms 周期内单上下文调用(典型实现是系统定时器回调或主循环,二者选一);key_table_sel()用local_irq_disable()/enable()保护函数指针的赋值,防止中断上下文(如key_scan运行于定时器中断)读到半更新的过滤器指针;key2msg_emit中key_msg_filter的读取未加锁——由于key_scan与key_table_sel通常在同一中断优先级上下文切换,实际不会并发,但若把key_scan移入低优先级任务则需自行加锁。
性能与运维
- 轮询开销:10ms 一次
key_scan,每次最多遍历 5 个驱动、各调用一次key_get_value,均为常数时间,对 MCU 负载可忽略; - ROM 优化:所有按键类型、双击逻辑、调试日志均通过编译期宏裁剪,未使能功能零开销;按键表经
#pragma放入独立链接段(如.music_key_table.text.const),便于厂商分区分包; - 现场调试:打开
KEY_UART_DEBUG后,key2msg_emit会打印key_status / key_num / key_type三元组,可快速定位"按键识别正确但无动作"(查表问题)与"状态机乱序"(干扰/抖动问题); - 手感调校路径:先调
KEY_SHORT_CNT消除抖动误触,再调KEY_LONG_CNT区分长短按,最后调KEY_HOLD_CNT校准连按速率;每次改动后需验证各状态事件在串口日志中的时序。
扩展点
新增按键类型(如霍尔、编码器)
- 在
KEY_TYPE枚举中追加类型; - 实现
key_interface_t实例(key_init+key_get_value,键号约定NO_KEY/-1语义); - 在
key.c的key_list[]中按宏条件加入; - 在
app_config.h定义对应KEY_xxx_EN使能宏,并在key.h增加KEY_xxx_MAX_NUM。 驱动层与状态机完全解耦,新增类型不需要改动key_scan()。
新增/定制模式按键表
仿照 music_key_table.c:为每个事件状态定义 xxx_SHORT / SHORT_UP / LONG / HOLD / LONG_UP(双击开启时还有 DOUBLE_KICK / TRIPLE_KICK)查表宏,按"键号 → MSG_xxx 或 NO_MSG"填表,在模式入口调用 key_table_sel(过滤器) 注册。同一键在不同模式可映射不同消息,实现按键复用。
使能双击
将 KEY_DOUBLE_CLICK_EN 置 1,并在按键表中补齐 DOUBLE_KICK(双击)与 TRIPLE_KICK(三击,红外键表已预留)条目;注意单击响应会延迟一个双击窗口。
相关链接
- key.h — 类型/状态/门槛值/接口定义
- key.c — 扫描状态机与消息转换
- key_drv_io.h / key_drv_io.c — IO 按键驱动
- key_drv_ad.h / key_drv_ad.c — AD 按键驱动
- key_matrix.h / key_matrix.c — 矩阵键盘驱动
- key_ir.h — 红外遥控驱动
- key_touch.h — 触摸按键驱动
- music_key_table.c — 音乐模式按键表示例
- key_wakeup.c — 按键唤醒(电源域,另见电源管理页面)
相关主题页面:消息系统与消息队列(post_msg / MSG_xxx 处理)、各音频工作模式(音乐 / 录音 / 对箱等模式对按键消息的业务处理)、电源管理与唤醒(低功耗下按键唤醒路径)。