上一篇已经把 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 切换。

遇到新增按键功能时,我一般先确认三个地方:

  1. 手势能不能被识别? 三击属于多击,先确认当前配置支持三击;只支持单击和长按时,表里填了三击功能也不会生效。
  2. 要改表里的哪个格子? 这里就是音乐表中 KEY_1 的“三击”列,把原来的消息改成 ANC 切换消息。
  3. 这个消息有没有人处理? 如果 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;

大多数按键问题都可以先按这个顺序排查,不要一开始就去调触控灵敏度或改底层扫描。


原文:CSDN · 【中科蓝讯】TWS 耳机按键处理流程