一、TWS 按键交互背景
耳机可以通过电容触摸、压力感应、实体按键或敲击检测等方式接收用户操作。

我接触的这份 BES 项目使用 CAPSENSOR 电容感应方案。 以“双击暂停音乐”为例,先要完成触摸检测,得到“某个事件”,应用才能继续执行对应功能。
二、触摸检测的基本流程
手指接近或接触感应区域,会引起电容变化。硬件按一定节奏测量,将结果转换成程序能读取的数值,这个过程叫作采样。软件再结合数据随时间的变化,识别单击、双击或长按。

采样数据是测量结果,触摸事件是识别结果。 在实际开发中,重点先确认检测能启动、事件能产生,再将事件接入业务。
三、开机后,从应用初始化到触摸初始化
先看开机时这条主线
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}, "themeCSS": ".nodeLabel { white-space: nowrap; } .nodeLabel p { margin: 0; }"}}%%
flowchart TB
subgraph INIT["① 应用初始化"]
direction LR
A["应用入口<br/>app_init()"]
B["初始化触摸模块<br/>app_mcu_core_capsensor_init()"]
C["准备时钟和模拟电路<br/>capsensor_driver_init()"]
D["创建检测线程<br/>cap_sensor_core_thread_init()"]
A --> B --> C --> D
end
subgraph TASK["② 检测线程"]
direction LR
E["线程开始执行<br/>capsensor_thread()"]
F["配置并启动采样<br/>capsensor_drv_start()"]
G["设置采样中断<br/>cap_sensor_irqinit()"]
E --> F --> G
end
INIT -..->|"创建线程后,由 RTOS 调度运行"| TASK
classDef initPhase fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef taskPhase fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
class A,B,C,D initPhase;
class E,F,G taskPhase;
style INIT fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
style TASK fill:#f0f8f3,stroke:#b9d7c4,color:#24563a3.1 从应用入口进入检测初始化
app_init() 在 CAPSENSOR_ENABLE 开启时,调用 app_mcu_core_capsensor_init()。图中前一个函数用于恢复佩戴检测的校准参数,这里继续沿第二个函数看触摸初始化。

进入 app_mcu_core_capsensor_init(),先关注第 205 行和第 210 行:

capsensor_driver_init()
配置检测相关时钟和模拟电路,使硬件具备基本工作条件。cap_sensor_core_thread_init()
在 MCU 路径下创建软件任务,负责后续采样配置和数据处理。
第 205 行的实现如下,两个调用分别完成底层时钟和模拟电路设置:

本项目的模块开关在 target.mk,MCU 处理路径等配置在 capsensor_cfg.mk。沿这条链路检查时,需要确认 CAPSENSOR_ENABLE 和 CAPSENSOR_AT_MCU 已开启。
查看触摸模块的配置开关

3.2 创建线程,找到后续代码的入口
进入 cap_sensor_core_thread_init(),通过 osThreadCreate() 创建检测线程。

线程从哪个函数开始执行,由 osThreadDef(...) 指定。本项目对应图中第 47 行,入口是 capsensor_thread()。

创建后由 RTOS 调度线程运行,所以接下来要找 capsensor_thread() 的实现,继续追踪它调用了什么。
3.3 配置采样和中断
检测线程进入后,先调用两个函数:

capsensor_drv_start():设置采样通道、采样周期等参数。cap_sensor_irqinit():设置采样中断的处理函数,使程序能收到数据通知。
进入 capsensor_drv_start(),可以看到参数先填入 capsensor_cfg,再通过 capsensor_setup_cfg() 交给硬件。

到这里,检测所需的初始化已接上。应用层调试时,先确认这些调用是否执行,再继续观察运行时有没有数据和事件。
四、运行时,触摸事件怎样产生
4.1 采样中断读取数据,并通知线程
前面的 cap_sensor_irqinit() 将 cap_sensor_irq_handler() 登记为中断处理函数。采样中断到来后,程序进入它:

这里做两件事:read_capsensor_fifo() 把采样数据读入 capsense_sample_buff 数组;osSignalSet() 再通知检测线程“数据到了”。

补充:驱动怎样读取采样数据

图中按 1 → 2 → 3 依次调用 read_capsensor_fifo()、capsensor_get_raw_data()、get_sample(),最终把寄存器中的通道编号和采样值填入数组。应用层先掌握“这批数据已经读入数组”,具体位运算在需要调试底层时再展开。
4.2 检测线程收到通知,识别操作事件
检测线程在 osSignalWait() 处等待通知。收到前面发出的 0x0001 后,将 rawdata_ready 设为 1,再把数组交给 capsensor_touch_process() 识别。
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 采样中断"]
direction LR
R0N0["读取数据"]
R0N1["存入数组<br/>capsense_sample_buff"]
R0N2["发通知"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 检测线程"]
direction LR
R1N0["接到通知"]
R1N1["调用识别函数<br/>capsensor_touch_process()"]
R1N2["得到识别结果<br/>status.event"]
R1N0 --> R1N1 --> R1N2
end
class R1N0,R1N1,R1N2 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;
status.event 是返回的事件类型:
| 结果 | 含义 |
|---|---|
CAP_KEY_EVENT_NONE | 当前没有可上报的操作 |
CAP_KEY_EVENT_CLICK | 单击 |
CAP_KEY_EVENT_DOUBLECLICK | 双击 |
算法需要结合一段时间的数据判断操作,所以并非每批采样都有事件。点击时长、双击间隔等可以查看 capsensor_driver.c 中的触摸识别配置;完整算法实现在 SDK 的预编译库中。

拿到双击事件后,下一步就是交给应用,再决定播放、暂停或接听等业务:见《外设适配(02):触摸事件怎样交给应用》 。