我为什么越来越看好 AI 眼镜

最近研究了一圈 AI 眼镜,我越来越觉得,这个东西值得认真关注。 一、为什么这么多厂商都在做 按照 Omdia 的估算,2025 年全球 AI 眼镜出货量约 870 万副,接近 900 万副。市场还不大,但已经吸引了不少厂商进场。大家争的除了眼前的销量,还有未来 AI 入口的位置。 眼镜的位置,确实太好了。眼睛、耳朵、嘴巴都在附近,适合采集画面、接收声音,再通过语音或者显示给你反馈。遇到不认识的东西,看着它问一句,AI 就能结合画面回答,不用再拿着手机对准、拍照、上传。 手机当然也能做,但眼镜有机会把这些操作变得更自然。尤其当你正在走路、搬东西,或者双手忙着的时候,这个区别就出来了。 我看好的,其实是 AI 从“等你提问”,慢慢变成一个能理解你当下处境的助手。它知道你刚才让它看了什么,正在处理什么事情,再结合这些信息提供帮助。如果做得好,交流起来就不用每次都从头解释。 二、眼镜能帮我们做什么 翻译,导航,拍摄,录屏,听歌,提词、消息提醒,扫码付款,全天候记录等等,这些都是比较容易想到的场景。并且,交互也不只有语音。或者触摸按键,眨眼这种控制,Meta 已经推出配合眼镜使用的肌电腕带,通过手腕处的肌肉电信号识别细微动作,让人动动手指就能操作。在不方便开口的时候,也能通过手势完成选择、确认等操作。 三、一个能帮人记住事情的助手 不过,最让我感兴趣的,还是“帮人记住事情”。临时冒出一个想法,说一句就保存下来;过了几天想不起细节,再问它:“上次那件事,我们最后怎么说的?” 如果这些能力足够可靠,眼镜就不只是拍摄工具了。它可能逐渐积累起一个和你生活有关的资料库,帮你找回那些经历过、却已经忘掉的信息。 四、长期记录会带来多少数据 顺着这个方向想,背后的存储需求也很值得关注。 我们现在拍照录像,通常是主动选一个时刻。如果未来变成每天长时间记录,数据量就完全不同了。做个简单假设:视频平均码率为 5 Mbps,每天记录 8 小时,**一天大约就是 18 GB,一年约 6.6 TB。**这还只是一个人的一份视频,没有算备份。 这不是在预测未来每个人都会这样录,而是说,一旦长时间记录普及,个人存储需求确实有可能上一个台阶。再乘上大量用户,云端存储、数据上传,以及 AI 检索和处理的需求,都可能跟着增加。 当然,也不一定所有原始视频都往云端传。上传要电量和带宽,长期保存要花钱,用户也未必愿意把生活完整交出去。更现实的做法,可能是设备先筛选,保留重要片段,把普通内容整理成文字摘要,再由用户决定哪些上传、哪些删除。 这么看,未来值得竞争的能力还有:**怎样用更少的空间,留下真正有用的信息。**毕竟存了一年的录像,问一句却找不到答案,那也只是多了一堆文件。 这也让我想到另一件事:买眼镜可能只是第一笔费用。如果后面还有云空间、AI 服务和数据处理,用户每个月愿意花多少钱?厂商能不能靠这些收入覆盖成本?这些问题,可能会影响产品最终怎么设计。 五、隐私是绕不开的问题 但越往“随身记忆助手”发展,隐私问题就越突出。 手机举起来拍,别人通常看得出来。眼镜一直戴在脸上,即便有拍摄指示灯,对方也未必留意。如果连聊天内容、生活轨迹都长期保存,一旦泄露,影响也会更大。记录是否经过同意,拍摄状态是否明显,数据能否导出和彻底删除,都会影响人们愿不愿意接受它。 六、续航、重量和颜值都很重要 硬件也有实实在在的难题。芯片、电池、摄像头和光学模组要塞进小小的镜架,功能越多,重量、发热和续航就越难兼顾。能戴一天,不代表能连续录一天;拿在手里觉得不重,也不代表架在鼻梁上几个小时还舒服。 针对续航,已经有厂商采用可换电设计。例如,夸克 AI 眼镜 S1 使用可更换的双电池方案,用户可以通过更换电池延长使用时间。此外,部分产品配有充电盒,方便外出补电。 还有颜值,哈哈,这个真的不能忽略。我之前在线下体验过,有些款式对我来说确实有点影响颜值。眼镜每天都在脸上,功能再强,如果不好看、夹头、压鼻梁,最后可能还是放进抽屉里。 对本来就需要戴眼镜的人来说,它又有天然的吸引力。如果度数配得合适,外观和舒适度也过关,相当于把原来就要戴的东西升级了。但不同人的脸型、鼻梁和度数不同,厂商还得把配镜和售后这些事情做好。 七、它会取代手机吗 眼镜取代手机,会成为一个趋势,现在更可能先发生的,是一些简单、频繁的操作逐渐转移到眼镜上,手机继续承担大屏操作和其他任务。 眼镜最后能走多远,既要看 AI 有多聪明,硬件发展有多快,也要看它能不能让人舒服地戴着,持续解决问题,还负担得起长期使用的费用。 如果以后出门,一副眼镜就能帮我翻译、导航、记录重点,需要时还能找回忘记的事情,那我确实会很想买。再往后想,它带来的变化可能还会延伸到存储、计算、服务和我们管理个人数据的方式。 越研究,越觉得这件事有意思。但真正让我期待的,还是它什么时候能从“演示起来很酷”,变成我每天都愿意戴的东西。 参考资料与视频 Omdia:2025 年全球 AI 眼镜出货量约 870 万副 阿里巴巴:夸克 AI 眼镜 S1 的可换电双电池方案 扎克伯格演示智能眼镜与肌电腕带(YouTube 官方视频,约 4 分钟) 注:存储容量为指定码率和记录时长下的简单测算;关于存储、云服务和商业模式的内容属于个人推演,并非确定的行业预测。

周末重拾 STM32,从点灯开始

背景介绍 毕业后很久没用过 STM32 了,今天刚好周末,看到家里还有一块 STM32F103C8T6 最小系统开发板,就拿出来玩一下。这次从点灯开始,回顾从 CubeMX 配置到 VS Code 编译、烧录并运行的流程,顺手记录下来,方便以后回顾。 这次使用 MacBook 开发,硬件是 STM32F103C8T6 最小系统开发板和 USB 转 TTL 模块,通过串口烧录。 软件用到以下四款工具: STM32CubeMX:配置时钟、GPIO 等,生成工程。 STM32CubeIDE:提供当前工程使用的 ARM GCC 和 Make 编译工具。 STM32CubeProgrammer:负责烧录、校验,通过命令行工具发送启动命令。 VS Code:编辑代码,通过“项目助手”扩展调用上述编译、烧录工具,实现一键编译烧录并运行。 最终实现的效果是:日常修改和下载都在 VS Code 中完成,每次下载前手动按一下开发板 RESET,再点击“编译烧录并运行”即可。 下面按实际操作顺序,记录具体的配置和烧录流程。 一、CubeMX 配置工程 1. 配置外部时钟 在 System Core → RCC 中,将 HSE 设置为 Crystal/Ceramic Resonator,使用开发板上的外部晶振。 2. 配置时钟树 在 Clock Configuration 中,使用 8 MHz 外部晶振,经 PLL 倍频至 72 MHz,选择 PLLCLK 作为系统时钟;APB1 二分频至 36 MHz。 ...

【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 中的判断: ...

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

从功能表面看,耳机电量更新并不复杂: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“查找设备”页面,点击“让耳机响铃”时声音偏小。 ...