上一篇已经把 SDK 的几个核心概念过了一遍,包括模式状态机、process/message/display 和消息分层。
这一篇就拿按键功能做一次具体分析,从一个按键事件出发,一直追到最终执行的位置。
以我接触到的 TWS 项目来看,按键是应用层里相对好理解、也比较容易上手的一部分:改单击功能、加长按、加多击,或者调整不同模式下的按键行为。这些需求在 SDK 里都对应到同一套机制,弄懂之后,定位修改位置会更快。
一、先跑一遍完整链路
一个按键从手指按下到功能执行,在 SDK 里走了这几步:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 输入与识别"]
direction LR
R0N0["手指按下<br/>用户触发按键"]
R0N1["硬件扫描<br/>读取按键编号"]
R0N2["消抖 / 识别<br/>判断短按、长按、多击"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 消息与业务"]
direction LR
R1N0["查按键表<br/>找到对应消息"]
R1N1["消息入队<br/>放入消息队列"]
R1N2["模式分发<br/>按当前模式处理"]
R1N3["功能执行<br/>播放、切歌、接听等"]
R1N0 --> R1N1 --> R1N2 --> R1N3
end
class R1N0,R1N1,R1N2,R1N3 green;
style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
R0 --> R1
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;前两步主要回答“这个按键物理上发生了什么”。从第 3 步开始,软件才把按键行为转换成功能。
理解这条链路后,改按键时就能快速判断:是改按键表的映射,还是改消息处理逻辑,还是两处都要改。
先看最核心的一段代码——bsp_key_scan()。它不是只做“扫描”,而是把扫描、手势识别、查按键表和消息入队串了起来:
u8 bsp_key_scan(void)
{
// 第 1 步:读取当前按键
u8 key_uid = bsp_key_scan_do();
// 第 2 步:消抖并识别按键事件
u16 key_evt = bsp_key_process(key_uid);
if (key_evt != NO_KEY) {
// 第 3 步:查表,得到功能消息
u16 key_msg = bsp_key_get_msg(key_evt);
if (key_msg != MSG_NO) {
// 第 4 步:功能消息入队
msg_enqueue(key_msg);
}
}
return key_uid;
}
先不用记住每个函数的细节,先区分下面三个结果分别代表什么。
key_uid:按的是哪个物理按键。对单个 TWS 耳机来说,通常就是TKEY_EN对应的唯一按键编号;没有触控时就是“没有按键”。key_evt:这个按键最后被识别成什么操作,例如单击、双击或长按。key_msg:这个操作在当前状态下要做什么,例如听歌时双击切歌、来电时双击接听。它是查按键表后才得到的,随后才进入消息队列。
所以,bsp_key_scan_do() 只产生第一个结果;消息不是它产生的,而是在 bsp_key_scan() 里识别出手势、查完按键表后才得到。
后面几步——消息出队、模式分发、功能执行——在前一篇已经讲过。
对应的是 func_bt() 循环里的 func_bt_message(msg_dequeue()) → func_bt_message_do();没有处理的消息会落到 func_message()。
二、硬件扫描:按键从哪来
对这类 TWS 耳机来说,单只耳机通常只启用一个 TKEY_EN 触控按键。负责读取它的函数叫 bsp_key_scan_do()。
u8 bsp_key_scan_do(void)
{
u8 key_uid = KEY_NULL;
#if TKEY_EN
key_uid = bsp_tkey_scan(); // 读取这一个触控按键当前有没有被按
#endif
return key_uid;
}
三、手势识别:短按、长按和多击
bsp_key_process() 拿到 key_uid 之后,做两件事:消抖,然后判断是短按、长按、还是多击。这部分代码在库里面,应用层不需要改,但需要知道它产出了什么。
产出的东西叫 key_evt,一个 16 位的值,编码了两层信息:
key_evt:
┌─────────────────┬──────────────────┐
│ 高4位: 事件类型 │ 低8位: 按键编号 │
│ (KEY_EVT) │ (KEY_ID) │
└─────────────────┴──────────────────┘
事件类型有这些:
| 事件 | 含义 | 例子 |
|---|---|---|
KEY_SHORT_UP | 短按抬起 | 单击播放/暂停 |
KEY_LONG | 长按(按下达到阈值) | 长按拒接来电 |
KEY_HOLD | 按住不放(持续触发) | 按住快进 |
KEY_LONG_UP | 长按后抬起 | 长按松开执行关机 |
KEY_DOUBLE | 双击 | 双击切下一曲 |
KEY_THREE | 三击 | 三击回拨最后一个号码 |
KEY_FOUR | 四击 | 预留 |
KEY_FIVE | 五击 | 预留 |
消抖和时长阈值由 key_shake_table 控制:
const key_shake_tbl_t key_shake_table = {
.scan_cnt = 6, // 按下消抖:连续 6 次扫描确认按下
.up_cnt = 6, // 抬起消抖:连续 6 次扫描确认抬起
.long_cnt = 150, // 长按阈值:150 次扫描 × 5ms,约 0.75 秒
.hold_cnt = 185, // HOLD 开始阈值:185 次扫描 × 5ms,约 0.93 秒
};
这几个值一般不需要改。如果客户反馈“长按要按很久才有反应”,先确认长按事件是不是出现得太晚。
确认是后,就把 KEY_LONG_TIMES 适当调小。它会赋给 long_cnt,不需要先去改定时器或状态机。
如果长按功能一定要等松手才执行,或者日志里已经有长按事件、功能却迟迟没有反应,问题就不在这个参数,要继续看按键表和后面的功能处理。
四、按键表:把手势翻译成消息
拿到 key_evt(哪个按键 + 什么事件)之后,下一步是把它翻译成消息号。这个翻译靠的就是按键表。
按键表的结构很直观——一个二维数组,行是按键编号,列是事件类型,每个格子里填的是对应的消息号。
为了把这张二维表讲清楚,这里暂时不拿单触控的 TWS 耳机举例,而是用一副有三个实体按键的有线耳机说明。
KEY_1 是多功能键,KEY_2 是上一首键,KEY_3 是下一首键。单触控 TWS 通常只会用到其中一个按键编号,反而不容易看出“行”和“列”的关系。
// BT 音乐模式的按键表
const u8 bt_music_key_msg_tbl[KEY_TBL_MAX_NB][KEY_MSG_MAX_IDX] = {
[KEY_1] = {
MSG_MUSIC_PLAY_PAUSE, // 单击
MSG_SWITCH_SIRI, // 长按下
MSG_PWR_HOLD, // HOLD
MSG_NO, // 长按抬起
MSG_CHANGE_MODE, // 双击
MSG_CALL_REDIAL_LAST, // 三击
MSG_NO, // 四击
MSG_NO, // 五击
},
[KEY_2] = {
MSG_MUSIC_PREV, // 单击
MSG_MUSIC_FB_START, // 长按下
MSG_NO, // HOLD
MSG_MUSIC_FB_END, // 长按抬起
MSG_VOL_DOWN, // 双击
MSG_NO, // 三击
MSG_NO, // 四击
MSG_NO, // 五击
},
[KEY_3] = {
MSG_MUSIC_NEXT, // 单击
MSG_MUSIC_FF_START, // 长按下
MSG_NO, // HOLD
MSG_MUSIC_FF_END, // 长按抬起
MSG_VOL_UP, // 双击
MSG_NO, // 三击
MSG_NO, // 四击
MSG_NO, // 五击
},
};
把这张表读出来就是:
- KEY_1(多功能键):单击 → 播放暂停,长按 → Siri,按住 → 关机,双击 → 切换模式,三击 → 回拨
- KEY_2(上一首键):单击 → 上一曲,长按 → 快退开始,长按抬起 → 快退结束,双击 → 音量减
- KEY_3(下一首键):单击 → 下一曲,长按 → 快进开始,长按抬起 → 快进结束,双击 → 音量加
如果只是修改按键功能,通常只需要改这张表,不需要动后面的处理逻辑,把对应位置的消息号换掉就可以了。
比如想把 KEY_1 的长按从 Siri 改成 ANC 切换,把 MSG_SWITCH_SIRI 换成 MSG_CHANGE_ANC 就可以了。
消息的处理逻辑 SDK 已经写好,改的是“什么按键触发什么消息”这一层。
在单触控 TWS 中,原理完全一样,通常只需要关心 KEY_1 这一行:同一个 TKEY_EN 的单击、双击、长按分别对应什么功能。
五、不同场景,按键表不同
同一个 KEY_1 在不同使用状态下,拿到的表可以不同:
| 使用状态 | KEY_1 单击的常见结果 |
|---|---|
| 听歌 | 播放/暂停 |
| 来电响铃 | 接听或拒接 |
| 通话中 | 挂断或执行通话控制 |
这就是“同一颗触控按键不同行为”的原因:进入来电或通话后,系统会切到对应的按键表;回到听歌状态后,再恢复音乐表。
通话表里有些消息不会直接写成“接听”或“挂断”,而是先交给通话处理,再由当前状态决定最终动作。
如果听歌正常、只有来电或通话时按键不对,先检查当前是不是取到了正确的通话按键表,而不是先调触控灵敏度。
六、新按键功能怎么加
以单触控 TWS 为例:想让 KEY_1 的三击触发 ANC 切换。
遇到新增按键功能时,我一般先确认三个地方:
- 手势能不能被识别? 三击属于多击,先确认当前配置支持三击;只支持单击和长按时,表里填了三击功能也不会生效。
- 要改表里的哪个格子? 这里就是音乐表中
KEY_1的“三击”列,把原来的消息改成 ANC 切换消息。 - 这个消息有没有人处理? 如果 ANC 切换功能本身已经存在,通常不用新写功能处理,只要把消息映射过去。
还有一个容易忽略的情况:前面改的是代码里的默认按键表。
有些项目会在烧录时,通过 Downloader 配置重新指定 KEY_1 的单击、双击、三击和长按功能。
所以遇到“代码明明改了,烧录后还是旧功能”时,不要马上怀疑表没改对。
先确认烧录工具有没有启用按键自定义。启用后,工具里的配置会覆盖代码默认值,排查时也要把这一层考虑进去。
七、写在最后
以后改按键时,先按这个顺序判断:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
N0["先看手势<br/>有没有识别出来"]
N1["再看当前场景<br/>取到的按键表是否正确"]
N2["代码改了不生效<br/>再查 Downloader 是否覆盖默认配置"]
N0 --> N1
N1 --> N2
class N0,N1,N2 blue;
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;大多数按键问题都可以先按这个顺序排查,不要一开始就去调触控灵敏度或改底层扫描。