【中科蓝讯】耳机电量是怎样更新到手机的

从功能表面看,耳机电量更新并不复杂:ADC 采到电池电压,再查表换算成百分比,手机就能显示。 但在实际项目中,电量更新并不是采样后直接查表:采样值要先经过滤波,查表得到的也只是参考电量,手机最终显示的还要由软件继续维护。先把实际电压、参考电量和显示电量这三个值分清楚,整条链路就容易理解了。 先看完整链路:前 3 步把采样值处理成参考电量,后 3 步由软件维护显示值、完成双耳同步,最后上报手机。 电池电压是怎么采出来的? 顺着中科蓝讯 SDK 的调用关系看,系统会在 5ms 任务中检查 ADC 转换状态。转换完成后,读取电池通道和内部参考通道的 ADC 值。内部参考电压的已知值用于校准换算比例,再结合电池采样值和分压系数,得到实际电池电压。 为什么采到电压后还不能直接使用? 播放音乐或者蓝牙发射时,电池电压会短暂下降,负载变小后又会回升,所以一次采样不能直接拿来估算电量。 结合耳机电压变化缓慢、但采样又容易受负载影响的特点,我更推荐用递推平均的方式更新电压:每来一个新采样值,只让平均电压向它靠近一小步。平均电压变化达到条件后,才更新稳定电压。下降条件更容易触发,上升条件更严格,可以减少电压回弹的影响。 参考电量为什么不直接给手机? 稳定电压通过“电压—电量对照表”换算成参考电量。它只用来判断当前显示偏高还是偏低,不是手机最终看到的电量。 如果每次都用参考电量覆盖显示电量,负载变化仍然会造成百分比跳变。 显示电量是怎么变化的? 电量管理任务大约每 10 秒运行一次,并根据当前状态选择更新方式。 放电时,参考电量低于显示电量,软件就让显示电量逐级下降,不会一次跳到参考值;电压短暂回升时,也不会把电量加回来。 充电时,电压回升很快,不能直接用它增加百分比。软件根据充电电流和时间估算充入容量,再换算成显示电量;底层确认充满后,才显示 100%。 温度决定当前能不能充电、使用多大的充电电流。低电提醒主要看显示电量,低压关机最终看实际电压。 为什么内部电量和手机显示还可能不同? 显示电量发生变化后会被保存,耳机下次开机可以从上次的结果继续计算。耳机内部按 1% 维护电量,当前 App 路径按 5% 一档显示。 例如内部电量是 59%,手机仍然显示 60%;内部电量下降到 55% 后,手机才显示 55%。 左右耳电量最后由谁上报? 左右耳各自计算本耳电量,再通过 TWS 交换耳机和充电盒电量。主耳汇总左耳、右耳和充电盒数据后,统一上报给手机系统和配套 App。 现在再看这条链路,重点就是分清三份数据:实际电压反映电池状态,参考电量判断变化方向,显示电量负责最终呈现。 原文:CSDN · 【中科蓝讯】耳机电量是怎样更新到手机的?

【中科蓝讯】耳机电量与充电问题复盘

本文先整理电压采样的方案设计思路,再复盘下面五类电量与充电问题: 电压稳定方案的设计思路 电量曲线 温度采样 上报精度 电量管控 状态刷新 一、电压稳定方案的设计思路 电量计算不会直接使用一次 ADC 采样结果。播放音乐、蓝牙发射和负载切换都会造成短时波动,因此在进入电量估算前,需要先得到一份相对稳定的电压。 这里先分清三个值: 数据 含义 新采样电压 ADC 本次换算得到的电压,每次都可能变化 平均电压 经过滤波后缓慢上升或下降的中间值 稳定电压 达到更新条件后才改变,提供给后续电量算法使用 稳定电压不是手机显示的百分比,也不是永远固定不变。它只是在平均电压没有达到更新条件时保持原值。 方案一:去极值后取平均值 先连续采集一组电压,去掉其中几个最大值和最小值,再对剩余数据求平均。偶发的异常高值和异常低值不会直接影响结果,适合 ADC 容易出现尖峰干扰的场景。 这种方案的优点是抗单点异常能力强,思路也比较直观。缺点是需要保存一组采样数据,还要完成极值查找或排序,会占用更多内存和计算资源。采样窗口越长,结果越稳定,但输出延迟也越大,真实电压发生变化时不容易及时跟上。 方案二:递推平均加更新阈值 递推平均不需要保存整组数据。每得到一个有效采样值,就让原来的平均电压向新采样值移动一小步。新采样值低于旧平均值时向下移动,高于旧平均值时向上移动,每次移动的大小由两者差值和递推系数决定。 这种方案的优点是内存和计算开销小,适合持续采样,也可以通过调整递推系数改变响应速度。缺点是它不会自动剔除异常值,一个突发尖峰会影响后续一段时间;系数过小,电压变化跟得慢,系数过大,又容易把短时波动带进结果。 以递推系数 1/32 为例: 当前平均电压和稳定电压都是 4000mV。 本次新采样电压为 3800mV,与平均电压相差 200mV。 本次只向下移动差值的 1/32,也就是 6.25mV,新的平均电压约为 3993.75mV。 新的平均电压再与稳定电压比较。两者向下相差约 6.25mV,达到当前项目的下降更新条件,稳定电压才更新。 如果旧平均电压低于新采样电压,处理方向相反:旧平均电压加上两者差值的 1/32,得到新的平均电压。无论差值大小,每次采样都会参与递推;更新阈值只负责决定稳定电压是否改变。 当前项目在平均电压下降约 5mV 时允许更新稳定电压,向上则需要变化约 10mV。下降阈值更小,可以及时反映放电趋势;上升阈值更大,可以减少负载解除后电压回弹带来的影响。 对于 TWS 耳机这种电压变化缓慢、需要持续采样的设备,递推平均占用资源少,也方便调整响应速度,当前 SDK 采用的就是第二种方案。如果还会出现明显的异常采样,可以先剔除异常值,再进入递推平均。 二、电量曲线 问题现象 耳机从满电持续播放音乐时,有些 10% 很快掉完,有些区间长时间不变化;左右耳在同一电量区间的续航也可能相差较大。总续航接近要求,但百分比曲线不均匀。 问题原因 这次问题的直接原因是软件仍使用旧电池的电压—电量表,与当前电芯的充放电曲线不匹配,导致各百分比区间对应的实际续航不均匀。左右耳的主从角色还会带来功耗差异,因此双耳验证时也要把主从切换考虑进去。 处理结果 根据当前电芯的实测充放电数据更新电量表,同时调整电量相关的主从切换逻辑。修改后分别在 ANC 开启和关闭场景下完成整轮放电验证,各 10% 区间的续航恢复到要求范围内。 三、温度采样 问题现象 ...

【中科蓝讯】机盒通信问题单复盘

本文围绕下面四类问题,整理对应的现象、问题原因和处理结果: 快速开关盖后异常断连 左右耳回复不一致,盒子灯效频闪 耳机已经进入配对,盒子却不显示配对灯效 配对超时后,盒子显示了错误灯效 一、快速开关盖后异常断连 问题现象 快速开关盖时,耳机低概率断开。日志中没有完整的关盖命令,耳机却进入了充电状态。 问题原因 快速开关盖时,充电盒重新拉起供电。电平持续时间超过原来的防抖阈值,耳机提前判断为稳定入盒。状态切换由充电检测触发,不是串口丢包。 处理结果 将充电电平持续检测时间从约 600 ms 延长到 1 s,避免短暂电平触发充电模式。连续快速开关盖验证后,未再出现异常断连。 二、左右耳回复不一致,盒子灯效频闪 问题现象 耳机进入配对状态并放入盒内后,充电盒白灯闪烁不均匀,部分场景出现频闪或亮度异常。 问题原因 左右耳返回给充电盒的配对状态不一致。盒子先收到一只耳机的配对状态,又收到另一只耳机的非配对状态,灯效状态被反复切换。 左耳回复:配对中 右耳回复:非配对 ↓ 盒子灯效在两个状态之间反复切换 处理结果 由从耳同步主耳的配对状态,确保左右耳向充电盒返回一致的数据。双耳状态一致后,配对灯效恢复正常。 三、耳机进入配对,盒子却不显示配对灯效 问题现象 耳机已经连接一台手机,再进入配对模式并放入盒内。充电盒只显示耳机电量,没有显示配对灯效。 问题原因 耳机同时存在“已连接”和“可发现可连接”两个状态。原来的回复逻辑优先返回已连接状态,覆盖了正在配对的状态,盒子因此没有进入配对灯效。 /* 修改前 */ if (phone_connected()) { report_connected(); } else if (pairing()) { report_pairing(); } /* 修改后 */ if (pairing()) { report_pairing(); } else if (phone_connected()) { report_connected(); } 处理结果 回复状态时优先判断配对状态,再判断手机连接状态。修改后,耳机在已连接状态下重新进入配对,盒子能够正常显示配对灯效。 四、配对超时后,盒子显示了错误灯效 问题现象 耳机在配对状态下入盒,配对超时后,充电盒显示了配对成功灯效,而不是结束灯效。 问题原因 耳机在配对超时后返回无效状态,充电盒却把这个状态按配对成功处理。数据传输正常,但两端对同一状态的含义没有统一。 处理结果 统一耳机与充电盒对回复状态的定义。收到无效状态时直接结束灯效,不再进入配对成功灯效。 这四类问题对应的检查入口是:充电电平、左右耳回复是否一致、状态判断优先级、两端状态定义。

【中科蓝讯】TWS 耳机机盒通信链路

机盒通信的主链路是:UART 收包、协议解析、校验与寻址、命令分发、业务执行、应答返回。这篇记录我负责该模块时使用的应用层调用链。 一、UART 收包与协议解析 %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart LR N0["充电盒发送数据"] N1["UART 中断接收字节<br/>并写入环形缓冲区"] N2["协议状态机<br/>拼接完整数据包"] N3["应用任务<br/>从缓冲区取包并校验"] N0 --> N1 N1 --> N2 N2 --> N3 class N0,N1,N2,N3 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; UART 中断侧负责收字节和快速识别数据包: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 中断与接收回调"] direction LR R0N0["uart1_isr()"] R0N1["uart1_rx_done_cb()"] R0N0 --> R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 解析与接收处理"] direction LR R1N0["bsp_uart_packet_parse()"] R1N1["charge_box_packet_recv()"] 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; 中断将字节写入环形缓冲区,快速解析一次以记录收包时间及开盖、心跳等标志。charge_box_ssw_process(charge_sta) 再从缓冲区取出字节,重新组包、校验并处理命令。 ...

【中科蓝讯】TWS音量是怎样同步的

这篇梳理音乐音量在应用层的处理:手机下发的音量怎样换算为本地等级,左右耳怎样同时生效,以及耳机按键调节后怎样同步手机音量条。通话音量只作区分,不展开 HFP 细节。 一、手机调节音量后的主流程 一次音量调节要对应下面四处状态: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart LR N0["手机音量条"] N1["耳机内部音量级"] N2["左右耳同步状态"] N3["DAC 实际输出音量"] N0 --> N1 N1 --> N2 N2 --> N3 class N0,N1,N2,N3 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; 手机和耳机的音量范围并不相同。AVRCP 绝对音量是 0~127,SDK 内部可配置为 16 级或 32 级,最后还要通过音量表转换为 DAC 使用的数字音量。 因此,手机显示 50% 不等于把数值 50 直接写入 DAC,而是先换算、再查表。 这里的“绝对音量”指手机音量条和耳机媒体音量相互对应:任一侧调节,另一侧都要更新。 手机调节音乐音量后,协议栈将新的媒体音量交给应用层,主线如下: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 接收与换算"] direction LR R0N0["收到手机设置的媒体音量"] R0N1["把 0~127<br/>换算为本地音量级"] R0N2["发起 TWS 定时音量请求"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 定时执行与保存"] direction LR R1N0["左右耳在约定时间设置 DAC"] R1N1["更新当前音量<br/>后续再延时保存"] 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; TWS 路径不会在当前回调里直接设置 DAC,而是先发起定时请求;到达定时点后,左右耳再一起设置实际音量。 ...

【中科蓝讯】TWS 耳机音量问题单复盘

写在开始 这篇文章整理项目中比较有代表性的音量问题单。只保留已经写明原因和处理方向的内容,并按音量控制链路归为三类。 排查音量问题时,可以先确认四个结果: 1. 收到的目标音量是否正确 ↓ 2. 左右耳保存的音量是否一致 ↓ 3. 两耳应用音量的时间是否一致 ↓ 4. 实际写入硬件的音量是否正确 本文围绕下面三类问题,整理对应的现象和排查思路: 回连后的初始音量不对 ; 快速调节时双耳音量不同步 ; 出盒恢复时单耳突然变大 。 一、回连后的初始音量不对 问题现象 1 耳机重新组队并连接手机后,预期使用耳机的默认音量,实际却变成手机之前保存的最大音量。 排查思路 1 按时间确认三个值:重新组队后恢复的默认音量、手机首次下发的历史音量,以及最终实际生效的音量。若默认值恢复正确,但最终值变成手机历史值,说明问题出在手机下发后的覆盖。 耳机重新组队后已恢复到默认音量,但手机连接时下发的历史音量又覆盖了本地值。如果要求重新组队后采用耳机默认音量,可以在首次连接的短暂保护窗口内忽略手机下发的音量,使用耳机的默认音量,同时将默认音量写回给 App。 问题现象 2 一拖二场景下,设备 A 已连接耳机且音量调到最大;设备 B 的音量较小,在播放音乐时抢连耳机。抢连成功后,音乐开始时仍按设备 A 的最大音量播放,随后才突然降到设备 B 的低音量。 排查思路 2 检查抢连后耳机采用的是哪台设备的音量,以及 A2DP 开始播放前硬件音量是否已经更新。 已有问题中,设备 B 抢连成功后,硬件音量设置晚于音乐开始,导致开始播放时仍保留设备 A 的最大音量。处理时要在 A2DP 出声前,按设备 B 的音量设置好硬件音量。 二、快速调节时双耳音量不同步 问题现象 1 长按手机音量键或快速滑动音量条时,声音会短暂偏向一边;从最大音量降到静音时,一只耳机比另一只更晚无声。反复在最大和最小音量之间切换时,还可能出现音量跳变。 排查思路 1 先看手机下发音量后,主从耳是不是在 alarm 到点前就各自更新了本地媒体音量;再对比 alarm 回调里两边实际采用的目标值。 已有问题中,主从耳在 alarm 到点前更新本地媒体音量的路径不同。快速滑动时,虽然两边在同一时刻进入 alarm 回调,但取到的音量基准已经不一致,所以会出现一边先变大、先变小或更晚静音。 ...

【中科蓝讯】ANC 模式是怎样切换的

最开始看原厂 SDK 时,我觉得它的切换很简单:确定关闭、降噪或通透,再调用模式接口即可。 随着 App 控制、按键配置、单双耳记忆和出入盒切换逐渐加进来,我才发现应用层维护的并不只是一个 ANC 模式。提示音何时播放、左右耳怎样同步、状态什么时候保存和上报,也都在这条链路里。 一、先把 ANC 的几条链路放在一起 ANC 模式可能因为三类事件发生变化: App 下发一个明确的目标模式; 用户通过耳机按键切换模式; 耳机出入盒或 TWS 连接状态变化,需要重新选择单双耳记忆。 把三个入口放在一起,我目前是这样理解的: %%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%% flowchart TB subgraph R0["① 确定目标模式"] direction LR R0N0["收到 ANC 相关事件<br/>App 指令、按键事件<br/>出入盒或 TWS 状态变化"] R0N1["确定当前使用场景<br/>单耳、双耳<br/>或者双耳都在盒内"] R0N2["得到本次目标模式<br/>关闭、通透、不同等级的降噪<br/>或自适应降噪"] R0N0 --> R0N1 --> R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1["② 同步并应用"] direction LR R1N0["更新并同步状态<br/>选择或更新单耳、双耳记忆<br/>必要时通知对耳"] R1N1["处理提示音并应用模式<br/>按触发来源处理提示音<br/>再转换成 SDK 模式"] R1N2["保存并更新 App<br/>写入记忆;按键或状态变化时<br/>更新 App 显示"] 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、按键和状态变化的处理顺序并不完全相同,后面再分别展开。 ...

【中科蓝讯】TWS 耳机 ANC 问题单复盘

写在开始 这篇文章整理项目中比较有代表性的 ANC 问题单,并按问题现象归为五类。 有些问题标题虽然带有 ANC,但最后确认是电量、功耗或连接问题,这里不展开。本文只保留能够放回 ANC 控制链路解释的问题,没有明确结论的地方也不补充猜测。 先把 ANC 控制流程简化成四步: 1. 收到切换请求 ↓ 2. 确定并同步目标模式 ↓ 3. 加载参数,更新实际效果 ↓ 4. 保存状态并上报 App 本文围绕下面五类问题,整理对应的问题现象和排查思路: 实际模式正常,App 显示却不对 ; 单耳和双耳切换后,ANC 状态混乱 ; 按键或 App 操作后,ANC 没有切换 ; 模式切换成功,降噪效果却不对 ; 切换 ANC 时出现杂音、短暂无声或突变 。 一、实际模式正常,App 显示却不对 问题现象 1 从耳长按切换 ANC 后,耳机实际已经切换,App 仍显示原来的模式; 排查思路 1 先确认三个结果:从耳是否识别到长按、左右耳实际模式是否变化、主耳是否向 App 上报了新状态。 如果从耳和两耳实际模式都正常,重点查主耳是否收到新的目标模式、更新本地状态并上报 App。问题单中,从耳已经完成 ANC 切换,但主耳没有更新状态并发送新模式,因此问题出在主耳的状态同步和上报链路。 问题现象 2 耳机实际是高降噪,App 却显示低降噪;重新进入 App 后,等级还会发生跳变。 排查思路 2 对照耳机内部等级、协议值和 App 显示值是否一致。问题单中,耳机使用 1 表示低降噪、3 表示高降噪,App 却映射成1是高降噪,3是低降噪,导致高低等级颠倒。这是状态映射错误,不是 ANC 参数没有生效。 ...

【中科蓝讯】TWS 耳机提示音问题单复盘

一、本文要解决哪些提示音问题 这篇文章记录的是我对 TWS 耳机提示音问题的整理和理解。我结合问题单、原厂 SDK 以及实际修改点,把零散的问题按现象和根因重新归类,并从业务请求、队列、播放到状态恢复,整理出一套排查思路。 具体问题的修改结论,以问题单和现有代码记录为准;没有明确依据的地方,我不额外延伸。理解提示音问题,可以先把流程分成四步: 1. 业务事件发生 ↓ 2. 提示音请求进入队列 ↓ 3. 提示音开始播放 ↓ 4. 播放结束,恢复音频状态 有些提示音只是给用户提供声音反馈,有些提示音还和业务回调绑定在一起。原厂 SDK 中,ANC 和音乐/游戏模式会在提示音播放完成后更新;接听、拒接和回拨也会在提示音相关回调中触发。 所以排查时不能只看“有没有声音”,还要看对应的业务回调是否按预期执行。遇到问题时,先分清是“声音没有正常播放”,还是“提示音对应的业务动作没有执行”:前者检查资源、音量和播放链路,后者检查业务请求和回调。 本文主要围绕以下 7 类问题展开,后文会分别说明问题现象、原因和排查思路: 业务已经发生,提示音却没响或播错 ; 提示音太大、太小,或换资源后异常 ; 连续或快速操作后,提示音消失、滞后或重复 ; 提示音响错耳朵 ; 双耳提示音不同步,或音乐恢复不同步 ; 提示音响完,音乐却出现杂音或无声 ; 来电和双连接时,响铃状态混乱 。 二、业务已经发生,提示音却没响或播错 问题现象 1 电量已经低于 10%,却没有低电提示音。 排查思路 1 先确认低电条件是否真的触发。比如电量从 11% 直接变成 9% 时,可能跳过“等于 10%”的判断,后面就不会提交低电提示音。确认条件后,再检查代码是否走到提示音调用位置。 问题现象 2 来电双击已经接听,却播放成三击提示音。 排查思路 2 这个问题说明提示音请求已经提交,但事件和提示音没有正确对应。重点检查双击事件最终传入的提示音资源编号,确认是否误用了三击提示音。 这类问题可以按“业务条件、提示音调用、资源编号”的顺序检查。前三步没有问题后,再进入队列和播放流程。 三、提示音太大、太小,或换资源后异常 问题现象 1 通过 App 的“查找耳机”功能播放正常,但在手机设置中的 Google“查找设备”页面,点击“让耳机响铃”时声音偏小。 ...

【中科蓝讯】TWS 耳机提示音是怎样播放出来的

在 TWS 耳机里,开机、蓝牙连接、低电量和降噪模式切换,通常都会伴随一段提示音。 从表面看,提示音像是准备一个音频文件,需要时调用播放函数即可。实际上,一段提示音从代码触发到左右耳真正出声,中间还要经过资源查找、播放排队、双耳同步和音频恢复。 本文以中科蓝讯 BT891x 原厂 SDK 的实现为例,重点分析提示音播放的核心流程。不同 SDK 的接口名称可能不一样,但整体思路通常可以互相参考。 一、提示音播放的完整链路 一段提示音的完整流程,可以先简化成下面五步: %%{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["确定哪只耳机播放<br/>并完成双耳同步"] R1N1["解码播放<br/>结束后恢复原来的音频状态"] 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; 比如手机连接耳机成功以后,蓝牙业务只需要提出“播放连接成功提示音”这个请求。至于提示音文件放在哪里、左右耳什么时候开始播放、当前音乐要不要降低音量,都会继续交给提示音模块处理。 ...

【中科蓝讯】TWS 按键问题单复盘

一、写在开始 我参与的是一款基于中科蓝讯 BT8910 的 TWS 耳机品牌项目,目前这个项目已经完成。 项目结束后,我想把开发过程中接触到的 Jira 问题单重新梳理一遍。其中既有我参与处理的内容,也有同事负责的模块。 现在查一个函数、一个配置项并不难,真正值得记录的是:面对一个问题现象时,应该先判断哪一层,为什么往这个方向查,以及怎么通过日志一步步缩小范围。 所以这次复盘不是给出一套标准答案,而是站在我目前对应用层的理解上,把问题现象、判断过程和排查思路整理下来,供遇到类似问题的同学参考。 整理过程中我还发现,很多反馈看起来不一样,根因却有重复。按根因归类后,下次遇到类似现象时,就可以先判断问题属于哪个环节,再沿对应链路往下查。 后续我会继续按模块整理项目中的问题单,包括按键、提示音、机盒通信、ANC、音量控制、功耗管理、出入盒、双耳同步、蓝牙问题和连接问题等内容。这一篇先从按键开始,归纳其中比较常见的 4 类问题。 阅读提示 全文约 2 300 字,预计 10 分钟读完。 本文包含 4 类按键问题:完全无响应、场景功能错误、误触,以及复杂手势互相干扰。重点不是讲基础原理,而是遇到类似反馈时,应该先查哪里、再往哪里查。 本文只讨论按键链路本身。如果按键已经触发正确功能,只是提示音没有播放或播放不对,就归到后续的提示音问题中。 二、所有手势都没有反应 客户反馈 其中一只耳机的触控按键完全无效,双击、三击、长按都没有反应。 排查思路 第一步:确认触控输入 先看这只耳机的触控按键是否有按下、松开的日志。 没有日志:先查这个触控按键的输入本身,例如触控是否启用、耳机是否在盒内、是否正处于禁键时间、触控阈值或通道是否正常。 有日志:说明这个触控按键已经有输入,继续往下查。 第二步:确认手势识别 再看目标手势有没有识别出来。 没有识别出来:重点查双击间隔、长按时间、松手后状态是否复位,以及组合手势是否把普通手势挡住了。 识别出来:说明手势已经产生,继续看功能。 第三步:确认功能消息 最后看手势有没有转换成正确的功能消息。 没有、为空或错误:查按键定义、场景切换和配置更新。 有正确消息:再确认功能处理是否执行;如果功能已经执行,后面只是没有提示音,就属于提示音问题,不再按按键问题处理。 判断与处理 “完全无效”不一定是触控硬件问题,也可能是手势已经产生、但后面没有正确链接到功能。处理时不要一开始就调灵敏度或改功能定义,先通过日志确认问题停在哪一层。 三、同一手势,功能却错了 客户反馈 听歌时双击正常,来电时双击却不能接听或挂断。 长按在听歌时能切换功能,到响铃或通话时却失效。 App 改完触控后,耳机执行的功能不对。 排查思路 第一步:固定一个场景复现 只测听歌、只测来电响铃,或只测通话,避免混在一起判断。 第二步:确认手势已经识别 如果手势都没出来,回到第一类问题排查。 第三步:核对当前场景的功能 手势正常后,直接看耳机最后想执行的是什么功能。 同样是“双击”:听歌时可能是切歌,来电时应该是接听或拒接,通话中又可能是其他操作。 来电时双击却去切歌:说明双击识别没有问题,只是耳机还在按“听歌时的规则”处理。 只有通话异常:重点检查进入通话后,双击和长按是否已经改成通话时该做的功能。 App 改完才异常:重点检查新设置是否把听歌、来电或通话时的功能改错了。 判断与处理 听歌、来电响铃、通话时都可以识别同一个“双击”或“长按”,但后续要执行的功能并不相同。 这类问题通常是耳机没有按当前使用状态执行对应功能。处理重点是把每种状态下的功能对应关系确认清楚。 四、不该响应时仍然响应 客户反馈 耳机放在盒内还能切歌;刚出盒或拿取耳机时,容易误触切换功能。 排查思路 第一步:确认误触发生的时机 ...

【中科蓝讯】TWS 耳机按键处理流程

上一篇已经把 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 步开始,软件才把按键行为转换成功能。 ...

【中科蓝讯】聊聊我对这套SDK的理解

前两篇记录了环境搭建和 com/bank 内存的事。这篇主要想讲一下中科蓝汛的 SDK——不讲具体细节,也不列目录结构,每个项目的目录可能都不太一样,列了也没太大意义。主要是聊一下我对这套 SDK 几个核心概念的理解。以后不管拿到哪个版本、做什么功能,这几个概念应该都不会变。 一、SDK 就是一个"模式状态机" 这是做完项目之后最大的感受。一颗 TWS 耳机芯片,任何时候只处于一个"模式"——蓝牙模式、充电模式、IDLE 待机、关机。切模式就是改一个变量 func_cb.sta。 // func_run() —— 整个 SDK 的主循环就长这样 while (1) { func_enter(); switch (func_cb.sta) { case FUNC_BT: func_bt(); break; // 蓝牙 case FUNC_CHARGE: func_charge(); break; // 充电 case FUNC_IDLE: func_idle(); break; // 待机 case FUNC_PWROFF: func_pwroff(); break; // 关机 default: func_exit(); break; // 退出,切下一个 } } 虽然每个模式做的事情不一样,但内部结构都有共同点。拿蓝牙模式看: void func_bt(void) { func_bt_enter(); // 进来先初始化 while (func_cb.sta == FUNC_BT) { // 没切模式就一直在这转 func_bt_process(); // 轮询事件 func_bt_message(msg_dequeue()); // 处理消息 func_bt_display(); // 刷新显示 } func_bt_exit(); // 走之前收尾 } 就是 enter → while → process/message/display → exit。再看 idle 模式,几乎一模一样——func_idle_enter() → while(func_cb.sta == FUNC_IDLE) → process/message/display → 退出。充电和关机模式虽然内部细节不同,但也是 enter → 做事 → exit 这个三段式。 ...

【中科蓝讯】从两次偶发死机,理解 com 区和 bank 区

刚接触中科蓝讯 SDK 时,我经常看到函数前面有 AT(.com_text.xxx)。一开始我只知道照着原有代码写,却不明白它为什么在这里。后来看了资料和工程里的写法,我才认识到:SDK 把代码分区域存放,不同区域的运行方式不一样。 这篇只是我目前从应用侧整理的一点理解,重点放在看到 AT(...) 时怎么读。 一、com 区和 bank 区有什么区别 按照官方资料的说明,芯片上电后,一部分代码会从 Flash 搬到 RAM 里常驻下来,这部分就是 com 区;剩下的代码继续待在 Flash,用到时才被加载到 bank 运行区。 区域 简单理解 适合放什么 com 区 上电后会加载到 RAM,并在运行期间常驻 中断函数、实时性要求高的 bank 区 代码存在 Flash,运行时按需加载到 bank 运行区 大多数普通应用逻辑 com 区的空间有限,所以不要因为更快,就把普通功能都放进去。 二、真正要注意的是中断和它调用的函数 中断函数和它调到的关键函数,通常需要放在 com 区。因为中断来了要马上处理,不能等到需要时再去加载代码。 在工程里看定时和一些中断相关代码时,我能看到类似下面的写法: AT(.com_text.user) void user_timer_isr(void) { // 中断中的处理 } 实际用的时候,我是先看同模块已有代码的写法,不凭空猜。但照抄还不够——踩过两次坑才知道,光盯着函数本身的 AT,容易漏掉更深层的依赖。 第一次是在中断里调了一个普通函数,我给那个函数加了 AT(.com_text.xxx),以为这就完事了。结果还是偶发异常。排查后发现,这个函数内部又调了另一个普通函数,那个子函数没加 AT。中断触发时 CPU 跳到 com 区,执行到嵌套的子函数时,它的代码还在 bank 区没加载进来,一样挂。这件事的风险在于,被调函数可能还调了其他函数,嵌套几层之后很容易漏掉。所以给函数加 AT 时,得顺着调用链往下检查,确保整条执行路径上的函数都在 com 区。如果调用深度比较大,更稳妥的做法是通过消息转发把逻辑放到其他线程执行,避免在 com 区里越套越深。 ...

【中科蓝讯】SDK 环境搭建与烧录

以我最近实际接触的中科蓝讯 BT891x 项目为例,刚开始最先要做的就是把开发环境搭起来:准备资料和工具、打开工程、编译生成 app.dcf,再用 Downloader 完成后续烧录。 不同 SDK 版本和开发板的目录、工具版本可能不一样,最终以当前资料包和工程说明为准。 一、准备资料和工具 如果手上的 SDK 对应特定的 TWS 开发板,原理图和相关硬件资料也要一并保存好,后面核对硬件配置、下载和外设问题时都会用到。 刚开始做 BT891x 时,主要先准备下面四项资料和工具: 内容 先拿它做什么 BT891x 固件 SDK 后续的功能开发主要就在这里进行 CodeBlocks 开发环境 打开 .cbp 工程并发起编译 RV32 Toolchain 供 CodeBlocks 调用的 RISC-V 编译工具链 Downloader 读取 app.dcf,进行配置和烧录处理 如果当前使用的是串口下载方式,还要保留蓝讯提供的串口驱动。资料包里的驱动通常以 CP210x 驱动包的形式提供,用于让电脑识别 CP2102 或 XLink 模块对应的 COM 口。 CodeBlocks 和 RV32 Toolchain 都是双击安装程序后,按提示一路选择 Next、默认路径安装即可。CodeBlocks 用来打开 .cbp 工程并编译,RV32 Toolchain 是真正执行编译的工具。 如果使用串口下载,按当前下载器对应的驱动安装即可。下载器和设备接入电脑后,系统能够识别出 COM 口,Downloader 才能继续扫描设备。 Downloader 不需要安装,解压后直接运行 Downloader.exe 即可。 二、编译与烧录 环境装完后,我不会马上改功能,而是先在 CodeBlocks 中打开 SDK 里的 .cbp 工程并编译一次。 ...