【BES】外设适配(02):触摸事件怎样交给应用

上一篇 讲到检测线程得到单击、双击等事件。这篇接着看:事件怎样进入应用,最后找到对应业务。 本项目的 MCU 处理路径如下: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 检测线程:调用接收函数"] direction LR R0N0["得到检测事件<br/>status.event"] R0N1["转换为应用按键事件<br/>app_capsensor_click_event()"] R0N2["放入应用邮箱<br/>capsensor_key_event_process()"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 应用线程:取消息并分发"] direction LR R1N0["还原按键消息<br/>app_key_handle_process()"] R1N1["进入产品处理<br/>app_ibrt_user_key()"] R1N2["按产品状态、左右耳和事件类型<br/>分发到对应业务函数"] 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; 一、初始化时,登记事件接收函数 app_mcu_core_capsensor_init() 中有一行: ...

【BES】外设适配(01):触摸输入从采样到事件

一、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:#24563a 3.1 从应用入口进入检测初始化 app_init() 在 CAPSENSOR_ENABLE 开启时,调用 app_mcu_core_capsensor_init()。图中前一个函数用于恢复佩戴检测的校准参数,这里继续沿第二个函数看触摸初始化。 ...

【BES】编译配置(04):源码怎样编译成固件

前一篇 说明了配置怎样变成编译参数。除了参数,构建系统还需要知道哪些目录、哪些源文件参与构建。这里继续借助本地 BES SDK,从源码清单查到编译命令,再看目标文件怎样链接成固件。 一、源码选择与编译 1.1 core-y:选择顶层目录 要选择参与构建的源码,先要确定它们所在的目录。这份 SDK 在 target.mk 中通过 core-y 列出顶层构建目录: 图 1:target.mk 用 core-y 指定顶层构建目录 当前面没有指定 EXT_SRC_DIR 时,会把 apps/ 和 services/ 加入这部分构建列表。后续再进入这些目录,读取各自的 Makefile。 1.2 obj-y:列出子目录和目标文件 core-y 加入 services/ 后,构建系统会读取 services/Makefile。里面的 obj-y 再决定这个目录下哪些内容参与构建。 在这套构建系统里,obj-y 是当前目录的构建清单,里面既可以写子目录,也可以写目标文件。清单的名称和处理方式由 SDK 的构建脚本约定,Make 本身不会因为变量叫 obj-y 就自动编译它。 (1)子目录条目 在 services/Makefile 的 obj-y 列表中,可以找到: 图 2:services/Makefile 用 obj-y 列出子目录 audio_process/ 末尾带 /,表示还要进入这个子目录,继续读取 services/audio_process/Makefile。 (2)目标文件条目 进入 services/audio_process/ 后,里面有这些源文件: audio_process.c audio_process_vol.c audio_spectrum.cpp 这些源码需要经过编译,生成对应的 .o 文件。例如,audio_process.c 编译后生成 audio_process.o。 ...

【BES】编译配置(03):Make 变量怎样变成 C 宏

前一篇 讨论了 Make 变量从哪里来、怎样覆盖。接下来还要弄清楚:配置文件中的开关,怎样影响 C 源码里的 #ifdef? 一、从变量到宏的传递过程 程序 主要读取什么 在构建中负责什么 Make Makefile、.mk 文件 根据配置安排构建,并组织传给编译器的参数 C 编译器 .c、.h 等源码文件 根据宏和源码内容,生成目标文件 写在 .mk 文件中的变量,是给 Make 使用的。C 编译器不会自动读取这些变量。 先用一个简单的 MY_FEATURE 开关说明。 1.1 定义 Make 变量 假设 .mk 文件里写了: MY_FEATURE = 1 MY_FEATURE 是 Make 变量,Make 用 $(MY_FEATURE) 取值。例如,判断这个开关是否等于 1: ifeq ($(MY_FEATURE),1) # 在这里安排开启功能时的构建操作 endif 1.2 export:传递给子 Make 如果顶层 Make 另外启动子 Make 来构建模块,后面的子 Make 也可能需要这个开关。这时可以前面加 export 将变量传过去: export MY_FEATURE = 1 export 会把变量带给后续子进程,子 Make 因而也能读取 MY_FEATURE。 这里说的子 Make,是另外启动的 Make 进程。如果是同一个 Make, 通过 include 读入多个配置文件时,变量可以在该进程内使用,无需为了 include 单独加 export。 ...

【BES】编译配置(02):构建目标与配置覆盖

前一篇 介绍了如何执行构建命令。这篇看配置怎样被选中和加载。 一、构建目标与配置加载 1.1 T:选择构建目标 要知道本次构建用了哪套配置,可以先查构建命令传入了哪些变量、Makefile 怎样使用它们。 这份 SDK 用 T 选择构建目标,顶层 Makefile 根据T的值,拼出配置文件路径。传入 T=best1306p,选中的就是 config/best1306p/target.mk: 图 1:顶层 Makefile 根据 T 拼出配置路径 默认的构建输出目录如下: 图 2:顶层 Makefile 中的输出目录设置与目标检查 Makefile 还会检查这个文件是否存在。不存在时,会报 Invalid target。 修改配置前先看构建命令里的 T,再去找对应目录,避免改了另一套配置。 1.2 CHIP:指定芯片平台 选出产品配置后,还需要指定它使用的芯片平台。 CHIP ?= best1306p 这里用的也是 best1306p 作为默认值。 两个变量的用途分别是: T:选择哪套产品或构建配置。 CHIP:指定这套配置使用的芯片平台。 这次两者恰好都叫 best1306p。换成产品目标时,它们可能不同。 1.3 配置文件的加载顺序 配置拆在多个文件中时,Make 可以用 include 读入其他文件。因此,查配置除了找赋值,还要沿着 include 看它们的加载顺序。 这份工程的顶层 Makefile 先读入下面两个文件: 图 3:先读取目标配置,再读取公共配置 srctree 表示源码根目录。这里先读取 target.mk,再读取 common.mk。 target.mk 里面还有其他 include。Make 读到这里,会先处理被包含的文件,再回来接着读后面的配置。 ...

【BES】编译配置(01):编译命令与构建脚本

在《系统总览(01):编译配置如何决定固件功能》 中,用 ANC 介绍了配置对源码的影响。实际构建项目时,还要先弄清楚该执行什么命令、得到什么文件,以及是否需要后续处理。 这里通过我在实际项目开发中所使用的BES 2700这款芯片的SDK 所用的构建命令进行介绍。包含构建目标、输出路径和 CRC 处理方式。 一、执行编译命令 编译bes工程,可以在工程根目录打开终端,输入: make T=best1306p -j Make 会读取工程里的 Makefile,并按其中的规则完成构建。这条命令中,T=best1306p 是用于选择这份工程的构建配置,-j 用于并行构建。 平时修改的 .c、.cpp 源码。经过编译、链接,源码会变成芯片能够执行的程序,并与相关数据一起组成固件。本例生成的固件文件路径是 out/best1306p/best1306p.bin。 二、为固件补充校验信息 2.1 CRC 后处理 编译生成固件后,有些项目还需要为后续升级补充校验信息。例如,接收方要检查收到的数据是否与准备好的固件一致,就需要一个用于比较的校验值。这里以 CRC32 为例。 CRC 的中文名称是“循环冗余校验”。电脑先算好一个值,接收方拿到数据后,再按相同范围、相同算法算一次,与保存的值比较: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 电脑上准备固件时"] direction LR R0N0["生成固件"] R0N1["计算 CRC"] R0N2["将校验值填入固件"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 后续设备接收后"] direction LR R1N0["读取收到的固件数据"] R1N1["耳机重新计算 CRC"] R1N2["与固件中保存的 CRC 比较"] 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; 对比结果不同,说明数据在传输中发生了变化;结果相同,表示通过校验,数据传输正常。 ...

【BES】系统总览(03):事件如何触发业务处理

【BES】系统总览(03):事件如何触发业务处理 上一篇介绍了开机初始化:创建线程、邮箱,初始化按键、蓝牙、音频等模块。 初始化完成后,耳机就可以响应各种变化。例如,用户双击耳机、手机连接成功、App 切换降噪模式,都会触发相应的处理。 这一篇主要介绍:一个事件发生后,是怎样进入业务代码的。 一、事件与业务的区别 事件描述“发生了什么”,业务决定“接下来做什么”。 例如,用户双击左耳: 事件:左耳发生双击。 业务:根据当前状态,执行播放、暂停或者接听电话等操作。 同一个操作,在不同状态下可能执行不同功能。 整体流程可以这样理解: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 事件传递"] direction LR R0N0["检测到变化"] R0N1["传递事件"] R0N2["找到处理函数"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 业务处理"] direction LR R1N0["判断当前状态"] R1N1["执行具体操作"] R1N0 --> R1N1 end class R1N0,R1N1 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; 下面结合 BES 的触摸链路来看。 ...

【BES】系统总览(02):开机初始化到应用模块

耳机开机后,应用层需要先准备好运行环境:读取配置数据、创建消息处理机制、初始化各业务模块,之后才能处理按键、连接、音乐播放等事件。 一、应用层从哪里开始 正常启动时,main() 完成平台侧准备,然后调用 app_init(): app_init() 是应用层初始化的主要入口。看这个函数,可以了解 SDK 开机时启用了哪些模块,以及模块之间的初始化顺序和条件。 二、app_init() 初始化系统和模块 app_init() 下,关键的初始化函数有: nv_record_init():准备 NV 数据、工厂参数 app_os_init():创建 app 邮箱和线程 app_battery_open():检查电池、充电状态,确定启动分支 app_key_open():按键检测及开机按键处理 bt_app_init():蓝牙模块 app_audio_open():音频模块 这里的初始化,是为了让模块具备处理业务的条件。例如,音频模块初始化完成,并不代表已经开始播放音乐,后面还需要播放事件触发。 三、建立应用线程和邮箱 在 app_init() 中,会调用 app_os_init(): 这个函数主要完成两件事:先创建 app 邮箱,再创建 app 线程。 邮箱用于保存待处理的应用消息,app_thread 负责取出消息,并根据模块编号交给对应的处理函数。所以,这里的消息链路大致为: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 消息投递"] direction LR R0N0["模块产生消息"] R0N1["放入 app 邮箱"] R0N0 --> R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 应用线程处理"] direction LR R1N0["app_thread 取出消息"] R1N1["调用对应模块的处理函数"] R1N0 --> R1N1 end class R1N0,R1N1 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; 这是 app 线程的处理方式。蓝牙和音频等子系统也有各自的线程或回调,整个 SDK 由这些任务协作运行。 ...

【BES】系统总览(01):编译配置如何决定固件功能

一、编译配置是什么 配置主要由 .mk 文件决定。看这些文件,可以了解当前选择了什么平台、加入了哪些模块,以及使用了哪些配置。 编译配置发生在电脑生成固件时。耳机上电后,运行的是已经编译好的固件。 整体流程如下: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 电脑上构建"] direction LR R0N0["读取配置"] R0N1["编译、链接"] R0N2["生成固件"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 耳机上运行"] direction LR R1N0["烧录固件"] R1N1["上电启动"] R1N2["运行固件中的代码"] 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; 二、ANC 配置如何影响源码 1. 判断是否加载 ANC 配置 下面是 target.mk 中的判断: ...