[{"content":"最近研究了一圈 AI 眼镜，我越来越觉得，这个东西值得认真关注。\n一、为什么这么多厂商都在做 按照 Omdia 的估算，2025 年全球 AI 眼镜出货量约 870 万副，接近 900 万副。市场还不大，但已经吸引了不少厂商进场。大家争的除了眼前的销量，还有未来 AI 入口的位置。\n眼镜的位置，确实太好了。眼睛、耳朵、嘴巴都在附近，适合采集画面、接收声音，再通过语音或者显示给你反馈。遇到不认识的东西，看着它问一句，AI 就能结合画面回答，不用再拿着手机对准、拍照、上传。\n手机当然也能做，但眼镜有机会把这些操作变得更自然。尤其当你正在走路、搬东西，或者双手忙着的时候，这个区别就出来了。\n我看好的，其实是 AI 从“等你提问”，慢慢变成一个能理解你当下处境的助手。它知道你刚才让它看了什么，正在处理什么事情，再结合这些信息提供帮助。如果做得好，交流起来就不用每次都从头解释。\n二、眼镜能帮我们做什么 翻译，导航，拍摄，录屏，听歌，提词、消息提醒，扫码付款，全天候记录等等，这些都是比较容易想到的场景。并且，交互也不只有语音。或者触摸按键，眨眼这种控制，Meta 已经推出配合眼镜使用的肌电腕带，通过手腕处的肌肉电信号识别细微动作，让人动动手指就能操作。在不方便开口的时候，也能通过手势完成选择、确认等操作。\n三、一个能帮人记住事情的助手 不过，最让我感兴趣的，还是“帮人记住事情”。临时冒出一个想法，说一句就保存下来；过了几天想不起细节，再问它：“上次那件事，我们最后怎么说的？”\n如果这些能力足够可靠，眼镜就不只是拍摄工具了。它可能逐渐积累起一个和你生活有关的资料库，帮你找回那些经历过、却已经忘掉的信息。\n四、长期记录会带来多少数据 顺着这个方向想，背后的存储需求也很值得关注。\n我们现在拍照录像，通常是主动选一个时刻。如果未来变成每天长时间记录，数据量就完全不同了。做个简单假设：视频平均码率为 5 Mbps，每天记录 8 小时，**一天大约就是 18 GB，一年约 6.6 TB。**这还只是一个人的一份视频，没有算备份。\n这不是在预测未来每个人都会这样录，而是说，一旦长时间记录普及，个人存储需求确实有可能上一个台阶。再乘上大量用户，云端存储、数据上传，以及 AI 检索和处理的需求，都可能跟着增加。\n当然，也不一定所有原始视频都往云端传。上传要电量和带宽，长期保存要花钱，用户也未必愿意把生活完整交出去。更现实的做法，可能是设备先筛选，保留重要片段，把普通内容整理成文字摘要，再由用户决定哪些上传、哪些删除。\n这么看，未来值得竞争的能力还有：**怎样用更少的空间，留下真正有用的信息。**毕竟存了一年的录像，问一句却找不到答案，那也只是多了一堆文件。\n这也让我想到另一件事：买眼镜可能只是第一笔费用。如果后面还有云空间、AI 服务和数据处理，用户每个月愿意花多少钱？厂商能不能靠这些收入覆盖成本？这些问题，可能会影响产品最终怎么设计。\n五、隐私是绕不开的问题 但越往“随身记忆助手”发展，隐私问题就越突出。\n手机举起来拍，别人通常看得出来。眼镜一直戴在脸上，即便有拍摄指示灯，对方也未必留意。如果连聊天内容、生活轨迹都长期保存，一旦泄露，影响也会更大。记录是否经过同意，拍摄状态是否明显，数据能否导出和彻底删除，都会影响人们愿不愿意接受它。\n六、续航、重量和颜值都很重要 硬件也有实实在在的难题。芯片、电池、摄像头和光学模组要塞进小小的镜架，功能越多，重量、发热和续航就越难兼顾。能戴一天，不代表能连续录一天；拿在手里觉得不重，也不代表架在鼻梁上几个小时还舒服。\n针对续航，已经有厂商采用可换电设计。例如，夸克 AI 眼镜 S1 使用可更换的双电池方案，用户可以通过更换电池延长使用时间。此外，部分产品配有充电盒，方便外出补电。\n还有颜值，哈哈，这个真的不能忽略。我之前在线下体验过，有些款式对我来说确实有点影响颜值。眼镜每天都在脸上，功能再强，如果不好看、夹头、压鼻梁，最后可能还是放进抽屉里。\n对本来就需要戴眼镜的人来说，它又有天然的吸引力。如果度数配得合适，外观和舒适度也过关，相当于把原来就要戴的东西升级了。但不同人的脸型、鼻梁和度数不同，厂商还得把配镜和售后这些事情做好。\n七、它会取代手机吗 眼镜取代手机，会成为一个趋势，现在更可能先发生的，是一些简单、频繁的操作逐渐转移到眼镜上，手机继续承担大屏操作和其他任务。\n眼镜最后能走多远，既要看 AI 有多聪明，硬件发展有多快，也要看它能不能让人舒服地戴着，持续解决问题，还负担得起长期使用的费用。\n如果以后出门，一副眼镜就能帮我翻译、导航、记录重点，需要时还能找回忘记的事情，那我确实会很想买。再往后想，它带来的变化可能还会延伸到存储、计算、服务和我们管理个人数据的方式。\n越研究，越觉得这件事有意思。但真正让我期待的，还是它什么时候能从“演示起来很酷”，变成我每天都愿意戴的东西。\n参考资料与视频 Omdia：2025 年全球 AI 眼镜出货量约 870 万副 阿里巴巴：夸克 AI 眼镜 S1 的可换电双电池方案 扎克伯格演示智能眼镜与肌电腕带（YouTube 官方视频，约 4 分钟） 注：存储容量为指定码率和记录时长下的简单测算；关于存储、云服务和商业模式的内容属于个人推演，并非确定的行业预测。\n","permalink":"https://btio.cn/posts/2026/11/01/%E6%88%91%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B6%8A%E6%9D%A5%E8%B6%8A%E7%9C%8B%E5%A5%BD-ai-%E7%9C%BC%E9%95%9C/","summary":"\u003cp\u003e最近研究了一圈 AI 眼镜，我越来越觉得，这个东西值得认真关注。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e一、为什么这么多厂商都在做\u003c/h2\u003e\n\u003cp\u003e按照 Omdia 的估算，2025 年全球 AI 眼镜出货量约 870 万副，接近 900 万副。市场还不大，但已经吸引了不少厂商进场。\u003cstrong\u003e大家争的除了眼前的销量，还有未来 AI 入口的位置。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e眼镜的位置，确实太好了。眼睛、耳朵、嘴巴都在附近，适合采集画面、接收声音，再通过语音或者显示给你反馈。遇到不认识的东西，看着它问一句，AI 就能结合画面回答，不用再拿着手机对准、拍照、上传。\u003c/p\u003e\n\u003cp\u003e手机当然也能做，但眼镜有机会把这些操作变得更自然。尤其当你正在走路、搬东西，或者双手忙着的时候，这个区别就出来了。\u003c/p\u003e\n\u003cp\u003e我看好的，其实是 AI 从“等你提问”，慢慢变成一个能理解你当下处境的助手。它知道你刚才让它看了什么，正在处理什么事情，再结合这些信息提供帮助。如果做得好，交流起来就不用每次都从头解释。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e二、眼镜能帮我们做什么\u003c/h2\u003e\n\u003cp\u003e翻译，导航，拍摄，录屏，听歌，提词、消息提醒，扫码付款，全天候记录等等，这些都是比较容易想到的场景。并且，交互也不只有语音。或者触摸按键，眨眼这种控制，Meta 已经推出配合眼镜使用的肌电腕带，通过手腕处的肌肉电信号识别细微动作，让人动动手指就能操作。在不方便开口的时候，也能通过手势完成选择、确认等操作。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e三、一个能帮人记住事情的助手\u003c/h2\u003e\n\u003cp\u003e不过，最让我感兴趣的，还是“帮人记住事情”。临时冒出一个想法，说一句就保存下来；过了几天想不起细节，再问它：“上次那件事，我们最后怎么说的？”\u003c/p\u003e\n\u003cp\u003e如果这些能力足够可靠，眼镜就不只是拍摄工具了。它可能逐渐积累起一个和你生活有关的资料库，帮你找回那些经历过、却已经忘掉的信息。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e四、长期记录会带来多少数据\u003c/h2\u003e\n\u003cp\u003e顺着这个方向想，背后的存储需求也很值得关注。\u003c/p\u003e\n\u003cp\u003e我们现在拍照录像，通常是主动选一个时刻。如果未来变成每天长时间记录，数据量就完全不同了。做个简单假设：视频平均码率为 5 Mbps，每天记录 8 小时，**一天大约就是 18 GB，一年约 6.6 TB。**这还只是一个人的一份视频，没有算备份。\u003c/p\u003e\n\u003cp\u003e这不是在预测未来每个人都会这样录，而是说，一旦长时间记录普及，个人存储需求确实有可能上一个台阶。再乘上大量用户，云端存储、数据上传，以及 AI 检索和处理的需求，都可能跟着增加。\u003c/p\u003e\n\u003cp\u003e当然，也不一定所有原始视频都往云端传。上传要电量和带宽，长期保存要花钱，用户也未必愿意把生活完整交出去。更现实的做法，可能是设备先筛选，保留重要片段，把普通内容整理成文字摘要，再由用户决定哪些上传、哪些删除。\u003c/p\u003e\n\u003cp\u003e这么看，未来值得竞争的能力还有：**怎样用更少的空间，留下真正有用的信息。**毕竟存了一年的录像，问一句却找不到答案，那也只是多了一堆文件。\u003c/p\u003e\n\u003cp\u003e这也让我想到另一件事：买眼镜可能只是第一笔费用。如果后面还有云空间、AI 服务和数据处理，用户每个月愿意花多少钱？厂商能不能靠这些收入覆盖成本？这些问题，可能会影响产品最终怎么设计。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e五、隐私是绕不开的问题\u003c/h2\u003e\n\u003cp\u003e但越往“随身记忆助手”发展，隐私问题就越突出。\u003c/p\u003e\n\u003cp\u003e手机举起来拍，别人通常看得出来。眼镜一直戴在脸上，即便有拍摄指示灯，对方也未必留意。如果连聊天内容、生活轨迹都长期保存，一旦泄露，影响也会更大。记录是否经过同意，拍摄状态是否明显，数据能否导出和彻底删除，都会影响人们愿不愿意接受它。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e六、续航、重量和颜值都很重要\u003c/h2\u003e\n\u003cp\u003e硬件也有实实在在的难题。芯片、电池、摄像头和光学模组要塞进小小的镜架，功能越多，重量、发热和续航就越难兼顾。能戴一天，不代表能连续录一天；拿在手里觉得不重，也不代表架在鼻梁上几个小时还舒服。\u003c/p\u003e\n\u003cp\u003e针对续航，已经有厂商采用可换电设计。例如，夸克 AI 眼镜 S1 使用可更换的双电池方案，用户可以通过更换电池延长使用时间。此外，部分产品配有充电盒，方便外出补电。\u003c/p\u003e\n\u003cp\u003e还有颜值，哈哈，这个真的不能忽略。我之前在线下体验过，有些款式对我来说确实有点影响颜值。眼镜每天都在脸上，功能再强，如果不好看、夹头、压鼻梁，最后可能还是放进抽屉里。\u003c/p\u003e\n\u003cp\u003e对本来就需要戴眼镜的人来说，它又有天然的吸引力。如果度数配得合适，外观和舒适度也过关，相当于把原来就要戴的东西升级了。但不同人的脸型、鼻梁和度数不同，厂商还得把配镜和售后这些事情做好。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e七、它会取代手机吗\u003c/h2\u003e\n\u003cp\u003e眼镜取代手机，会成为一个趋势，现在更可能先发生的，是一些简单、频繁的操作逐渐转移到眼镜上，手机继续承担大屏操作和其他任务。\u003c/p\u003e\n\u003cp\u003e眼镜最后能走多远，既要看 AI 有多聪明，硬件发展有多快，也要看它能不能让人舒服地戴着，持续解决问题，还负担得起长期使用的费用。\u003c/p\u003e\n\u003cp\u003e如果以后出门，一副眼镜就能帮我翻译、导航、记录重点，需要时还能找回忘记的事情，那我确实会很想买。再往后想，它带来的变化可能还会延伸到存储、计算、服务和我们管理个人数据的方式。\u003c/p\u003e\n\u003cp\u003e越研究，越觉得这件事有意思。但真正让我期待的，还是它什么时候能从“演示起来很酷”，变成我每天都愿意戴的东西。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"参考资料与视频\"\u003e参考资料与视频\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://omdia.tech.informa.com/pr/2026/mar/global-ai-glasses-shipments-reach-8point7-million-units-with-mainland-china-emerging-as-the-fastest-growing-market\"\u003eOmdia：2025 年全球 AI 眼镜出货量约 870 万副\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.alizila.com/alibaba-launches-new-quark-ai-glasses-series-in-china-deeply-integrated-with-qwen/\"\u003e阿里巴巴：夸克 AI 眼镜 S1 的可换电双电池方案\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=gZ9IsB72nVk\"\u003e扎克伯格演示智能眼镜与肌电腕带（YouTube 官方视频，约 4 分钟）\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e注：存储容量为指定码率和记录时长下的简单测算；关于存储、云服务和商业模式的内容属于个人推演，并非确定的行业预测。\u003c/p\u003e","title":"我为什么越来越看好 AI 眼镜"},{"content":"背景介绍 毕业后很久没用过 STM32 了，今天刚好周末，看到家里还有一块 STM32F103C8T6 最小系统开发板，就拿出来玩一下。这次从点灯开始，回顾从 CubeMX 配置到 VS Code 编译、烧录并运行的流程，顺手记录下来，方便以后回顾。\n这次使用 MacBook 开发，硬件是 STM32F103C8T6 最小系统开发板和 USB 转 TTL 模块，通过串口烧录。\n软件用到以下四款工具：\nSTM32CubeMX：配置时钟、GPIO 等，生成工程。 STM32CubeIDE：提供当前工程使用的 ARM GCC 和 Make 编译工具。 STM32CubeProgrammer：负责烧录、校验，通过命令行工具发送启动命令。 VS Code：编辑代码，通过“项目助手”扩展调用上述编译、烧录工具，实现一键编译烧录并运行。 最终实现的效果是：日常修改和下载都在 VS Code 中完成，每次下载前手动按一下开发板 RESET，再点击“编译烧录并运行”即可。\n下面按实际操作顺序，记录具体的配置和烧录流程。\n一、CubeMX 配置工程 1. 配置外部时钟 在 System Core → RCC 中，将 HSE 设置为 Crystal/Ceramic Resonator，使用开发板上的外部晶振。\n2. 配置时钟树 在 Clock Configuration 中，使用 8 MHz 外部晶振，经 PLL 倍频至 72 MHz，选择 PLLCLK 作为系统时钟；APB1 二分频至 36 MHz。\n3. 配置 GPIO 将 PC13 设置为 GPIO_Output，选择推挽输出，用户标签填写 LED，用于控制板载 LED。\n4. 配置工程与代码生成选项 在 Project Manager 中填写工程名称 test_led 和保存路径。\n注意：下图的工具链选择有误，后续打开工程时会遇到提示，修正过程见后文。\n在 Code Generator 中，选择只复制必要的库文件，并勾选为每个外设分别生成 .c、.h 文件，以及重新生成时保留用户代码。\n5. 生成代码 点击 Generate Code。首次缺少 STM32F1 固件包时，点击 Yes 下载 HAL 库及芯片支持文件。\n邮件更新提醒可点击 Don’t ask me again 跳过，不影响工程创建。\n阅读并接受固件包许可协议，完成安装。\n生成完成后，点击 Open Project 打开工程；Open Folder 仅打开所在文件夹。\n6. 工具链选择记录 首次生成时误选了 EWARM（IAR），因此打开工程时提示安装 IAR。\n回到 Project Manager，将 Toolchain / IDE 改为 STM32CubeIDE，重新生成工程即可。\n完成后，在 VS Code 中打开工程进行代码编辑，通过“项目助手”调用本机 STM32CubeIDE 的编译工具和 STM32CubeProgrammer 的烧录工具。\n二、串口接线与 BOOT 配置 USB 转 TTL 使用 3.3V 逻辑电平：TXD 接 STM32 的 PA10，RXD 接 PA9，GND 接 GND，开发板正常供电。\n下载时设置 BOOT0=1、BOOT1=0。芯片复位后进入 ST 出厂预置的 Bootloader，等待电脑发送固件，再将其写入 Flash。写入后断电不会丢失。\n三、在 VS Code 中编译烧录并运行 保持上述接线和 BOOT 配置，每次下载只需：按一下并松开 RESET，再点击“编译烧录并运行”。\n扩展自动保存代码、编译生成 ELF 固件，通过串口写入 Flash，校验成功后发送 Go 0x08000000，启动刚下载的程序。失败信息显示在按钮下方。\n编译和烧录入口集成在 VS Code 中，修改代码后即可在开发板上验证。若其他软件正在连接同一个串口，先断开连接。\n程序启动后，芯片已经离开 Bootloader。下次下载前，再按一下 RESET，让芯片重新进入下载模式，然后点击按钮即可。\n四、独立上电运行 开发期间保持 BOOT0=1、BOOT1=0，配合 RESET 和一键烧录反复下载。此时再次复位或上电，芯片仍会进入 Bootloader；Flash 中的程序还在，只是没有自动启动。\n需要上电直接运行时，将 BOOT0 改为 0，BOOT1 保持 0，再复位或重新上电，芯片就会从 Flash 启动自己的程序。\n","permalink":"https://btio.cn/posts/2026/10/31/%E9%87%8D%E6%8B%BE-stm32%E4%BB%8E%E7%82%B9%E7%81%AF%E5%BC%80%E5%A7%8B/","summary":"\u003ch2 align=\"center\"\u003e背景介绍\u003c/h2\u003e\n\u003cp\u003e毕业后很久没用过 STM32 了，今天刚好周末，看到家里还有一块 STM32F103C8T6 最小系统开发板，就拿出来玩一下。这次从点灯开始，回顾从 CubeMX 配置到 VS Code 编译、烧录并运行的流程，顺手记录下来，方便以后回顾。\u003c/p\u003e\n\u003cp\u003e这次使用 \u003cstrong\u003eMacBook\u003c/strong\u003e 开发，硬件是 \u003cstrong\u003eSTM32F103C8T6 最小系统开发板和 USB 转 TTL 模块\u003c/strong\u003e，通过串口烧录。\u003c/p\u003e\n\u003cp\u003e软件用到以下四款工具：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSTM32CubeMX\u003c/strong\u003e：配置时钟、GPIO 等，生成工程。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSTM32CubeIDE\u003c/strong\u003e：提供当前工程使用的 ARM GCC 和 Make 编译工具。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSTM32CubeProgrammer\u003c/strong\u003e：负责烧录、校验，通过命令行工具发送启动命令。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVS Code\u003c/strong\u003e：编辑代码，通过“项目助手”扩展调用上述编译、烧录工具，实现一键编译烧录并运行。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e最终实现的效果是：日常修改和下载都在 VS Code 中完成，每次下载前手动按一下开发板 RESET，再点击“编译烧录并运行”即可。\u003c/p\u003e\n\u003cp\u003e下面按实际操作顺序，记录具体的配置和烧录流程。\u003c/p\u003e\n\u003ch2 align=\"center\"\u003e一、CubeMX 配置工程\u003c/h2\u003e\n\u003ch3 align=\"center\"\u003e1. 配置外部时钟\u003c/h3\u003e\n\u003cp\u003e在 \u003cstrong\u003eSystem Core → RCC\u003c/strong\u003e 中，将 HSE 设置为 \u003cstrong\u003eCrystal/Ceramic Resonator\u003c/strong\u003e，使用开发板上的外部晶振。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"RCC 外部晶振配置\" loading=\"lazy\" src=\"https://picgocloud.com/m/a6a1b5a0-a3f1-4db4-b478-8e1c70dddbdf.png\"\u003e\u003c/p\u003e\n\u003ch3 align=\"center\"\u003e2. 配置时钟树\u003c/h3\u003e\n\u003cp\u003e在 \u003cstrong\u003eClock Configuration\u003c/strong\u003e 中，使用 8 MHz 外部晶振，经 PLL 倍频至 \u003cstrong\u003e72 MHz\u003c/strong\u003e，选择 PLLCLK 作为系统时钟；APB1 二分频至 \u003cstrong\u003e36 MHz\u003c/strong\u003e。\u003c/p\u003e","title":"周末重拾 STM32，从点灯开始"},{"content":"前一篇 讨论了 Make 变量从哪里来、怎样覆盖。接下来还要弄清楚：配置文件中的开关，怎样影响 C 源码里的 #ifdef？\n一、从变量到宏的传递过程 程序 主要读取什么 在构建中负责什么 Make Makefile、.mk 文件 根据配置安排构建，并组织传给编译器的参数 C 编译器 .c、.h 等源码文件 根据宏和源码内容，生成目标文件 写在 .mk 文件中的变量，是给 Make 使用的。C 编译器不会自动读取这些变量。\n先用一个简单的 MY_FEATURE 开关说明。\n1.1 定义 Make 变量 假设 .mk 文件里写了：\nMY_FEATURE = 1 MY_FEATURE 是 Make 变量，Make 用 $(MY_FEATURE) 取值。例如，判断这个开关是否等于 1：\nifeq ($(MY_FEATURE),1) # 在这里安排开启功能时的构建操作 endif 1.2 export：传递给子 Make 如果顶层 Make 另外启动子 Make 来构建模块，后面的子 Make 也可能需要这个开关。这时可以前面加 export 将变量传过去：\nexport MY_FEATURE = 1 export 会把变量带给后续子进程，子 Make 因而也能读取 MY_FEATURE。\n这里说的子 Make，是另外启动的 Make 进程。如果是同一个 Make， 通过 include 读入多个配置文件时，变量可以在该进程内使用，无需为了 include 单独加 export。\n注意：变量传到了子 Make，C 编译器仍然不会自动得到同名宏，所以 cpp文件仍然无法使用mk 中定义的宏\n1.3 -D：定义编译宏 要让 .mk 中的开关影响 C 编译，需要把对应的宏定义选项加入模块编译参数。这个 SDK 会用 ccflags-y 收集这些参数，例如：\nifeq ($(MY_FEATURE),1) ccflags-y += -DMY_FEATURE endif 开关为 1 时，模块参数 ccflags-y 就会多一个 -DMY_FEATURE。\n这里的 -D 才是编译器识别的宏定义选项。-DMY_FEATURE 表示本次编译时定义 MY_FEATURE，效果相当于：\n#define MY_FEATURE 1 ccflags-y 是这套 SDK 用来收集模块编译参数的变量；构建规则会把它的内容并入编译命令，再交给编译器。实际命令可以简化看成下面这样：\narm-none-eabi-gcc -DMY_FEATURE -c demo.c -o demo.o 其中 demo.c 是输入源码，demo.o 是编译生成的目标文件。编译器处理 demo.c 时会带着 MY_FEATURE 这个宏，C 源码就可以用条件编译判断是否保留相关代码：\n#ifdef MY_FEATURE /* 这部分功能代码参与编译 */ #endif %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;Make 变量\u0026lt;br/\u0026gt;MY_FEATURE=1\u0026#34;] N1[\u0026#34;模块添加\u0026lt;br/\u0026gt;-DMY_FEATURE\u0026#34;] N2[\u0026#34;条件分支参与编译\u0026lt;br/\u0026gt;#ifdef MY_FEATURE\u0026#34;] N0 --\u0026gt;|\u0026#34;ifeq 条件成立\u0026#34;| N1 N1 --\u0026gt;|\u0026#34;编译器定义宏\u0026#34;| 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; 二、在 BES 中追踪开关与宏的对应关系 回到工程里的 EQ 开关，Make 变量和 C 宏用的是两个名字：\n图 1：audio_process/Makefile 第 113～114 行添加编译宏\n第 113 行判断 HW_DAC_IIR_EQ_PROCESS，第 114 行添加 -D__HW_DAC_IIR_EQ_PROCESS__。开关为 1，编译器就会定义后一个宏。\nC 源码中的 #ifdef此时就要用后一个名字，Makefile 只是负责把两边对应起来。\n三、检查配置是否生效 这里要确认的是：前面加到 ccflags-y 的 -D__HW_DAC_IIR_EQ_PROCESS__，有没有真的出现在编译 audio_process.c 的命令里。\n执行下面的构建命令：\nmake T=best1306p V=1 -j8 其中，T=best1306p 选择本次构建目标；V=1 让构建过程打印完整的编译命令；-j8 表示并行使用 8 个任务，只影响构建速度。\n在输出中找到编译 audio_process.c 的那条命令，确认里面有 -D__HW_DAC_IIR_EQ_PROCESS__。省略其他编译参数后，大致会看到：\narm-none-eabi-gcc ... -D__HW_DAC_IIR_EQ_PROCESS__ ... -c audio_process.c -o audio_process.o 判断时看这三种情况：\n有 audio_process.c 的编译命令，且带有 -D__HW_DAC_IIR_EQ_PROCESS__：宏参与了这次编译。 有该文件的编译命令，但没有这个 -D 参数：这条配置添加的宏参数没有传入本次编译。 没有该文件的编译命令：这次构建没有重新编译 audio_process.c。增量构建会跳过输入文件和编译命令都没有变化的目标； 如果想从头编译 best1306p 这个目标，可以先清理它的构建输出，再重新编译：\nmake T=best1306p allclean make T=best1306p V=1 -j8 allclean 会删除 out/best1306p 中该目标的生成文件，第二条命令会重新构建。完整构建会比增量构建花更长时间，V=1 也会打印大量编译命令。\n这一步只能确认宏参数进入了编译命令。要确认功能在设备上实际运行，还要烧录本次生成的文件，再结合初始化、触发条件和设备日志判断。\n上一篇：编译配置（02）：构建目标与配置覆盖 下一篇：编译配置（04）：源码怎样编译成固件 ","permalink":"https://btio.cn/posts/2026/10/23/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE03make-%E5%8F%98%E9%87%8F%E6%80%8E%E6%A0%B7%E5%8F%98%E6%88%90-c-%E5%AE%8F/","summary":"\u003cp\u003e\u003ca href=\"/posts/2026/10/22/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE02%E6%9E%84%E5%BB%BA%E7%9B%AE%E6%A0%87%E4%B8%8E%E9%85%8D%E7%BD%AE%E8%A6%86%E7%9B%96/\"\u003e前一篇\u003c/a\u003e\n讨论了 Make 变量从哪里来、怎样覆盖。接下来还要弄清楚：配置文件中的开关，怎样影响 C 源码里的 \u003ccode\u003e#ifdef\u003c/code\u003e？\u003c/p\u003e\n\u003ch2 id=\"一从变量到宏的传递过程\"\u003e一、从变量到宏的传递过程\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e程序\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e主要读取什么\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e在构建中负责什么\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMake\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMakefile、\u003ccode\u003e.mk\u003c/code\u003e 文件\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e根据配置安排构建，并组织传给编译器的参数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eC 编译器\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e.c\u003c/code\u003e、\u003ccode\u003e.h\u003c/code\u003e 等源码文件\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e根据宏和源码内容，生成目标文件\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e写在 \u003ccode\u003e.mk\u003c/code\u003e 文件中的变量，是给 Make 使用的。C 编译器不会自动读取这些变量。\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e先用一个简单的 \u003ccode\u003eMY_FEATURE\u003c/code\u003e 开关说明。\u003c/p\u003e\n\u003ch3 id=\"11-定义-make-变量\"\u003e1.1 定义 Make 变量\u003c/h3\u003e\n\u003cp\u003e假设 \u003ccode\u003e.mk\u003c/code\u003e 文件里写了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eMY_FEATURE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eMY_FEATURE\u003c/code\u003e 是 Make 变量，Make 用 \u003ccode\u003e$(MY_FEATURE)\u003c/code\u003e 取值。例如，判断这个开关是否等于 \u003ccode\u003e1\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003eifeq\u003c/span\u003e \u003cspan class=\"err\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003e\u003cspan class=\"nv\"\u003eMY_FEATURE\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"err\"\u003e,1)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 在这里安排开启功能时的构建操作\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003eendif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"12-export传递给子-make\"\u003e1.2 \u003ccode\u003eexport\u003c/code\u003e：传递给子 Make\u003c/h3\u003e\n\u003cp\u003e如果顶层 Make 另外启动子 Make 来构建模块，后面的子 Make 也可能需要这个开关。这时可以前面加 \u003ccode\u003eexport\u003c/code\u003e 将变量传过去：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eexport \u003c/span\u003e\u003cspan class=\"nv\"\u003eMY_FEATURE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eexport\u003c/code\u003e 会把变量带给后续子进程，子 Make 因而也能读取 \u003ccode\u003eMY_FEATURE\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e这里说的子 Make，是另外启动的 Make 进程。如果是同一个 Make， 通过 \u003ccode\u003einclude\u003c/code\u003e 读入多个配置文件时，变量可以在该进程内使用，无需为了 \u003ccode\u003einclude\u003c/code\u003e 单独加 \u003ccode\u003eexport\u003c/code\u003e。\u003c/p\u003e","title":"【BES】编译配置（03）：Make 变量怎样变成 C 宏"},{"content":"前一篇 说明了配置怎样变成编译参数。除了参数，构建系统还需要知道哪些目录、哪些源文件参与构建。这里继续借助本地 BES SDK，从源码清单查到编译命令，再看目标文件怎样链接成固件。\n一、源码选择与编译 1.1 core-y：选择顶层目录 要选择参与构建的源码，先要确定它们所在的目录。这份 SDK 在 target.mk 中通过 core-y 列出顶层构建目录：\n图 1：target.mk 用 core-y 指定顶层构建目录\n当前面没有指定 EXT_SRC_DIR 时，会把 apps/ 和 services/ 加入这部分构建列表。后续再进入这些目录，读取各自的 Makefile。\n1.2 obj-y：列出子目录和目标文件 core-y 加入 services/ 后，构建系统会读取 services/Makefile。里面的 obj-y 再决定这个目录下哪些内容参与构建。\n在这套构建系统里，obj-y 是当前目录的构建清单，里面既可以写子目录，也可以写目标文件。清单的名称和处理方式由 SDK 的构建脚本约定，Make 本身不会因为变量叫 obj-y 就自动编译它。\n（1）子目录条目 在 services/Makefile 的 obj-y 列表中，可以找到：\n图 2：services/Makefile 用 obj-y 列出子目录\naudio_process/ 末尾带 /，表示还要进入这个子目录，继续读取 services/audio_process/Makefile。\n（2）目标文件条目 进入 services/audio_process/ 后，里面有这些源文件：\naudio_process.c audio_process_vol.c audio_spectrum.cpp 这些源码需要经过编译，生成对应的 .o 文件。例如，audio_process.c 编译后生成 audio_process.o。\n.o 叫作“目标文件”，是源码编译后的中间产物，后面还要参与汇总和链接，才能形成最终固件。\n如果 obj-y 清单中包含 audio_process.o，就表示本次需要构建这个目标，构建系统再按照规则找到对应源码进行编译。\n（3）自动收集源码 audio_process/Makefile 没有逐个填写 .o，而是先收集当前目录中的源码，再把对应的目标文件名放入 obj-y：\n图 3：audio_process/Makefile 收集源码并列出目标文件名\n其中：\nobj_c 保存收集到的 .c 文件名。 obj_cpp 保存收集到的 .cpp 文件名。 obj_s 保存收集到的 .S 汇编文件名。 接着通过图中第 13 行代码生成目标文件列表：\nobj-y := $(obj_c:.c=.o) $(obj_s:.S=.o) $(obj_cpp:.cpp=.o) 例如：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph MAP[\u0026#34;清单中的文件名转换：尚未编译\u0026#34;] direction LR A[\u0026#34;audio_process.c\u0026#34;] --\u0026gt; B[\u0026#34;audio_process.o\u0026#34;] C[\u0026#34;audio_process_vol.c\u0026#34;] --\u0026gt; D[\u0026#34;audio_process_vol.o\u0026#34;] E[\u0026#34;audio_spectrum.cpp\u0026#34;] --\u0026gt; F[\u0026#34;audio_spectrum.o\u0026#34;] end class A,C,E blue; class B,D,F green; style MAP fill:#f1f6fd,stroke:#b8cfe8,color:#23456b 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; 这里改的是清单里的文件名，还没有真正生成 .o 文件。\n（4）手动列出目标文件 同一份工程也有手动清单，以 A2DP 音频解码模块为例。这里分两步：先选中解码目录，再列出目录里需要编译的文件。\n前面的 core-y 将 apps/ 纳入构建后，apps/Makefile 又列出了 audioplayers/。继续读取 apps/audioplayers/Makefile 时，会根据 A2DP_DECODER_VER 选择解码实现。当前项目在 target.mk 中把它设为 2，所以会选中 a2dp_decoder/ 这个子目录：\n图中第 10 行先把 a2dp_decoder/ 加入 obj_cpp，后面再汇入 obj-y。末尾的 / 表示继续进入子目录，读取其中的 Makefile。\n进入 apps/audioplayers/a2dp_decoder/Makefile 后，才看到手动清单：直接写出要构建的 .o 文件名。\n第 4 行把 a2dp_decoder.o 加入清单，对应源码是同目录下的 a2dp_decoder.cpp。第 6～7 行表示：A2DP_SMF 不为 1 时，再加入 SBC 解码文件。\n第 12～17 行仍在这个分支内：A2DP_AAC_ON=1 时，追加 a2dp_decoder_aac_lc.o，对应 a2dp_decoder_aac_lc.cpp。因此，源码放在目录里，还需要清单把它选进来；这里就通过开关选择需要的解码文件。\n以后新增源码时，可以照所在目录的写法处理：audio_process 这种目录会自动收集源码；a2dp_decoder 这种目录则需要在清单里补上对应的 .o，并按功能需要放进相应的条件分支。\n（5）完整构建流程 把前面两种方式放在一起看：先进入对应目录，再生成 obj-y 目标清单，最后由通用规则调用编译器。区别在于清单是自动收集生成，还是直接写出目标文件名。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 26, \u0026#34;nodeSpacing\u0026#34;: 20, \u0026#34;padding\u0026#34;: 0}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}, \u0026#34;themeCSS\u0026#34;: \u0026#34;\\n.build-step {width:224px;text-align:center;white-space:nowrap;text-wrap:nowrap;}\\n.build-step-box {height:100px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1.45;text-wrap:nowrap;}\\n.build-blue {border:1.5px solid #7ba4d8;background:#e8f2ff;color:#23456b;}\\n.build-green {border:1.5px solid #78aa8b;background:#e7f4ec;color:#24563a;}\\n.build-amber {border:1.5px solid #d5a25b;background:#fff3df;color:#72501e;}\\n.build-step-note {padding-top:12px;padding-bottom:4px;font-size:15px;line-height:1.4;font-weight:500;text-wrap:nowrap;}\\n.build-note-blue {color:#23456b;}\\n.build-note-green {color:#24563a;}\\n.build-step span {color:inherit;}\\n\u0026#34;}}%% flowchart TB %% 每个节点包含上方代码框和下方步骤说明，均由 Mermaid 渲染。 subgraph AUTO[\u0026#34;① 自动收集：audio_process\u0026#34;] direction LR A0[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-blue\u0026#39;\u0026gt;\u0026lt;span\u0026gt;target.mk\u0026lt;br/\u0026gt;core-y 选择 services/\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-blue\u0026#39;\u0026gt;① 选定顶层构建目录\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] A1[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-blue\u0026#39;\u0026gt;\u0026lt;span\u0026gt;services/Makefile\u0026lt;br/\u0026gt;进入 audio_process/\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-blue\u0026#39;\u0026gt;② 找到音频处理模块\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] A2[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-blue\u0026#39;\u0026gt;\u0026lt;span\u0026gt;audio_process/Makefile\u0026lt;br/\u0026gt;wildcard 收集源码\u0026lt;br/\u0026gt;替换后缀，生成 obj-y\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-blue\u0026#39;\u0026gt;③ 自动整理目标清单\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] A3[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-amber\u0026#39;\u0026gt;\u0026lt;span\u0026gt;通用编译规则\u0026lt;br/\u0026gt;源码编译成 .o\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-blue\u0026#39;\u0026gt;④ 编译源码，生成 .o\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] A0 --\u0026gt; A1 --\u0026gt; A2 --\u0026gt; A3 end style AUTO fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph MANUAL[\u0026#34;② 手动清单：a2dp_decoder\u0026#34;] direction LR M0[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-green\u0026#39;\u0026gt;\u0026lt;span\u0026gt;target.mk → apps/\u0026lt;br/\u0026gt;apps/Makefile\u0026lt;br/\u0026gt;进入 audioplayers/\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-green\u0026#39;\u0026gt;① 找到音频播放模块\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] M1[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-green\u0026#39;\u0026gt;\u0026lt;span\u0026gt;audioplayers/Makefile\u0026lt;br/\u0026gt;A2DP_DECODER_VER=2\u0026lt;br/\u0026gt;进入 a2dp_decoder/\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-green\u0026#39;\u0026gt;② 按版本选择解码目录\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] M2[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-green\u0026#39;\u0026gt;\u0026lt;span\u0026gt;a2dp_decoder/Makefile\u0026lt;br/\u0026gt;直接列出 .o\u0026lt;br/\u0026gt;按开关追加到 obj-y\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-green\u0026#39;\u0026gt;③ 手动填写目标清单\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] M3[\u0026#34;\u0026lt;div class=\u0026#39;build-step\u0026#39;\u0026gt;\u0026lt;div class=\u0026#39;build-step-box build-amber\u0026#39;\u0026gt;\u0026lt;span\u0026gt;通用编译规则\u0026lt;br/\u0026gt;源码编译成 .o\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;div class=\u0026#39;build-step-note build-note-green\u0026#39;\u0026gt;④ 编译源码，生成 .o\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;] M0 --\u0026gt; M1 --\u0026gt; M2 --\u0026gt; M3 end style MANUAL fill:#f0f8f3,stroke:#b9d7c4,color:#24563a AUTO ~~~ MANUAL classDef stage fill:transparent,stroke:transparent; class A0,A1,A2,A3,M0,M1,M2,M3 stage; 1.3 调用编译器生成 .o 源码清单列好后，就由通用规则调用编译器。模块用 ccflags-y 收集 C 编译参数。这些参数会在 scripts/lib.mk 中与其他编译选项汇总，最终放入编译命令使用的 c_flags 中。\n实际编译规则在 scripts/build.mk 中：\n图 4：scripts/build.mk 中的编译命令和源码匹配规则\n看图中第 236～237 行：% 匹配相同的文件名主体。目标是 audio_process.o，就找到 audio_process.c，再按规则调用编译命令。\n第 229 行是命令本身：$(CC) 是编译器，$(c_flags) 是整理好的参数，$\u0026lt; 和 $@ 分别是输入源码、输出目标文件。\n以第三篇中的 EQ 开关 为例，启用它时添加的 -D__HW_DAC_IIR_EQ_PROCESS__ 会进入这些参数，并随编译命令交给编译器。\n二、链接与固件生成 2.1 链接生成 ELF 各个源文件编译成 .o 后，还需要通过链接，将不同文件中的代码、数据和函数引用组织起来。在这份示例工程中，链接先生成 .elf，再从中提取 .bin 固件二进制。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;各个源文件\u0026#34;] N1[\u0026#34;编译成 .o\u0026#34;] N2[\u0026#34;链接生成 .elf\u0026#34;] N3[\u0026#34;提取 .bin\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; N2 N2 --\u0026gt; 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; 图 5：顶层 Makefile 中的 ELF 链接命令\n2.2 提取 BIN 固件 ELF 保存链接后的程序以及符号、调试等信息；BIN 是从中提取出的原始二进制内容，供后续处理或烧录流程使用。\n链接完成后，构建系统通过 OBJCOPY 将 ELF 中的相关内容提取为 BIN：\n图 6：顶层 Makefile 从 ELF 提取 BIN 的规则\n本例提取出的文件是 out/best1306p/best1306p.bin。如果后续升级还需要补充 CRC 信息，可以接着执行第一篇介绍的后处理步骤 。烧录或升级时，应使用对应流程要求的文件。\n上一篇：编译配置（03）：Make 变量怎样变成 C 宏 ","permalink":"https://btio.cn/posts/2026/10/23/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE04%E6%BA%90%E7%A0%81%E6%80%8E%E6%A0%B7%E7%BC%96%E8%AF%91%E6%88%90%E5%9B%BA%E4%BB%B6/","summary":"\u003cp\u003e\u003ca href=\"/posts/2026/10/23/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE03make-%E5%8F%98%E9%87%8F%E6%80%8E%E6%A0%B7%E5%8F%98%E6%88%90-c-%E5%AE%8F/\"\u003e前一篇\u003c/a\u003e\n说明了配置怎样变成编译参数。除了参数，构建系统还需要知道哪些目录、哪些源文件参与构建。这里继续借助本地 BES SDK，从源码清单查到编译命令，再看目标文件怎样链接成固件。\u003c/p\u003e\n\u003ch2 id=\"一源码选择与编译\"\u003e一、源码选择与编译\u003c/h2\u003e\n\u003ch3 id=\"11-core-y选择顶层目录\"\u003e1.1 \u003ccode\u003ecore-y\u003c/code\u003e：选择顶层目录\u003c/h3\u003e\n\u003cp\u003e要选择参与构建的源码，先要确定它们所在的目录。这份 SDK 在 \u003ccode\u003etarget.mk\u003c/code\u003e 中通过 \u003ccode\u003ecore-y\u003c/code\u003e 列出顶层构建目录：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/71c4332f-e353-4fb1-a7fb-be5bbd0cc556.png\"\u003e\n\u003cem\u003e图 1：\u003ccode\u003etarget.mk\u003c/code\u003e 用 \u003ccode\u003ecore-y\u003c/code\u003e 指定顶层构建目录\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e当前面没有指定 \u003ccode\u003eEXT_SRC_DIR\u003c/code\u003e 时，会把 \u003ccode\u003eapps/\u003c/code\u003e 和 \u003ccode\u003eservices/\u003c/code\u003e 加入这部分构建列表。后续再进入这些目录，读取各自的 Makefile。\u003c/p\u003e\n\u003ch3 id=\"12-obj-y列出子目录和目标文件\"\u003e1.2 \u003ccode\u003eobj-y\u003c/code\u003e：列出子目录和目标文件\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003ecore-y\u003c/code\u003e 加入 \u003ccode\u003eservices/\u003c/code\u003e 后，构建系统会读取 \u003ccode\u003eservices/Makefile\u003c/code\u003e。里面的 \u003ccode\u003eobj-y\u003c/code\u003e 再决定这个目录下哪些内容参与构建。\u003c/p\u003e\n\u003cp\u003e在这套构建系统里，\u003ccode\u003eobj-y\u003c/code\u003e 是当前目录的构建清单，里面既可以写子目录，也可以写目标文件。清单的名称和处理方式由 SDK 的构建脚本约定，Make 本身不会因为变量叫 \u003ccode\u003eobj-y\u003c/code\u003e 就自动编译它。\u003c/p\u003e\n\u003ch4 id=\"1子目录条目\"\u003e（1）子目录条目\u003c/h4\u003e\n\u003cp\u003e在 \u003ccode\u003eservices/Makefile\u003c/code\u003e 的 \u003ccode\u003eobj-y\u003c/code\u003e 列表中，可以找到：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/2cd1527f-f90e-4558-8d91-4f5b94644d5b.png\"\u003e\n\u003cem\u003e图 2：\u003ccode\u003eservices/Makefile\u003c/code\u003e 用 \u003ccode\u003eobj-y\u003c/code\u003e 列出子目录\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eaudio_process/\u003c/code\u003e 末尾带 \u003ccode\u003e/\u003c/code\u003e，表示还要进入这个子目录，继续读取 \u003ccode\u003eservices/audio_process/Makefile\u003c/code\u003e。\u003c/p\u003e\n\u003ch4 id=\"2目标文件条目\"\u003e（2）目标文件条目\u003c/h4\u003e\n\u003cp\u003e进入 \u003ccode\u003eservices/audio_process/\u003c/code\u003e 后，里面有这些源文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaudio_process.c\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaudio_process_vol.c\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaudio_spectrum.cpp\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这些源码需要经过编译，生成对应的 \u003ccode\u003e.o\u003c/code\u003e 文件。例如，\u003ccode\u003eaudio_process.c\u003c/code\u003e 编译后生成 \u003ccode\u003eaudio_process.o\u003c/code\u003e。\u003c/p\u003e","title":"【BES】编译配置（04）：源码怎样编译成固件"},{"content":"一、TWS 按键交互背景 耳机可以通过电容触摸、压力感应、实体按键或敲击检测等方式接收用户操作。\n我接触的这份 BES 项目使用 CAPSENSOR 电容感应方案。 以“双击暂停音乐”为例，先要完成触摸检测，得到“某个事件”，应用才能继续执行对应功能。\n二、触摸检测的基本流程 手指接近或接触感应区域，会引起电容变化。硬件按一定节奏测量，将结果转换成程序能读取的数值，这个过程叫作采样。软件再结合数据随时间的变化，识别单击、双击或长按。\n采样数据是测量结果，触摸事件是识别结果。 在实际开发中，重点先确认检测能启动、事件能产生，再将事件接入业务。\n三、开机后，从应用初始化到触摸初始化 先看开机时这条主线\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}, \u0026#34;themeCSS\u0026#34;: \u0026#34;.nodeLabel { white-space: nowrap; } .nodeLabel p { margin: 0; }\u0026#34;}}%% flowchart TB subgraph INIT[\u0026#34;① 应用初始化\u0026#34;] direction LR A[\u0026#34;应用入口\u0026lt;br/\u0026gt;app_init()\u0026#34;] B[\u0026#34;初始化触摸模块\u0026lt;br/\u0026gt;app_mcu_core_capsensor_init()\u0026#34;] C[\u0026#34;准备时钟和模拟电路\u0026lt;br/\u0026gt;capsensor_driver_init()\u0026#34;] D[\u0026#34;创建检测线程\u0026lt;br/\u0026gt;cap_sensor_core_thread_init()\u0026#34;] A --\u0026gt; B --\u0026gt; C --\u0026gt; D end subgraph TASK[\u0026#34;② 检测线程\u0026#34;] direction LR E[\u0026#34;线程开始执行\u0026lt;br/\u0026gt;capsensor_thread()\u0026#34;] F[\u0026#34;配置并启动采样\u0026lt;br/\u0026gt;capsensor_drv_start()\u0026#34;] G[\u0026#34;设置采样中断\u0026lt;br/\u0026gt;cap_sensor_irqinit()\u0026#34;] E --\u0026gt; F --\u0026gt; G end INIT -..-\u0026gt;|\u0026#34;创建线程后，由 RTOS 调度运行\u0026#34;| 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()。图中前一个函数用于恢复佩戴检测的校准参数，这里继续沿第二个函数看触摸初始化。\n进入 app_mcu_core_capsensor_init()，先关注第 205 行和第 210 行：\ncapsensor_driver_init()\n配置检测相关时钟和模拟电路，使硬件具备基本工作条件。 cap_sensor_core_thread_init()\n在 MCU 路径下创建软件任务，负责后续采样配置和数据处理。 第 205 行的实现如下，两个调用分别完成底层时钟和模拟电路设置：\n本项目的模块开关在 target.mk，MCU 处理路径等配置在 capsensor_cfg.mk。沿这条链路检查时，需要确认 CAPSENSOR_ENABLE 和 CAPSENSOR_AT_MCU 已开启。\n查看触摸模块的配置开关 3.2 创建线程，找到后续代码的入口 进入 cap_sensor_core_thread_init()，通过 osThreadCreate() 创建检测线程。\n线程从哪个函数开始执行，由 osThreadDef(...) 指定。本项目对应图中第 47 行，入口是 capsensor_thread()。\n创建后由 RTOS 调度线程运行，所以接下来要找 capsensor_thread() 的实现，继续追踪它调用了什么。\n3.3 配置采样和中断 检测线程进入后，先调用两个函数：\ncapsensor_drv_start()：设置采样通道、采样周期等参数。 cap_sensor_irqinit()：设置采样中断的处理函数，使程序能收到数据通知。 进入 capsensor_drv_start()，可以看到参数先填入 capsensor_cfg，再通过 capsensor_setup_cfg() 交给硬件。\n到这里，检测所需的初始化已接上。应用层调试时，先确认这些调用是否执行，再继续观察运行时有没有数据和事件。\n四、运行时，触摸事件怎样产生 4.1 采样中断读取数据，并通知线程 前面的 cap_sensor_irqinit() 将 cap_sensor_irq_handler() 登记为中断处理函数。采样中断到来后，程序进入它：\n这里做两件事：read_capsensor_fifo() 把采样数据读入 capsense_sample_buff 数组；osSignalSet() 再通知检测线程“数据到了”。\n补充：驱动怎样读取采样数据 图中按 1 → 2 → 3 依次调用 read_capsensor_fifo()、capsensor_get_raw_data()、get_sample()，最终把寄存器中的通道编号和采样值填入数组。应用层先掌握“这批数据已经读入数组”，具体位运算在需要调试底层时再展开。\n4.2 检测线程收到通知，识别操作事件 检测线程在 osSignalWait() 处等待通知。收到前面发出的 0x0001 后，将 rawdata_ready 设为 1，再把数组交给 capsensor_touch_process() 识别。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 采样中断\u0026#34;] direction LR R0N0[\u0026#34;读取数据\u0026#34;] R0N1[\u0026#34;存入数组\u0026lt;br/\u0026gt;capsense_sample_buff\u0026#34;] R0N2[\u0026#34;发通知\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 检测线程\u0026#34;] direction LR R1N0[\u0026#34;接到通知\u0026#34;] R1N1[\u0026#34;调用识别函数\u0026lt;br/\u0026gt;capsensor_touch_process()\u0026#34;] R1N2[\u0026#34;得到识别结果\u0026lt;br/\u0026gt;status.event\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 -..-\u0026gt;|\u0026#34;通知检测线程\u0026#34;| 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 是返回的事件类型：\n结果 含义 CAP_KEY_EVENT_NONE 当前没有可上报的操作 CAP_KEY_EVENT_CLICK 单击 CAP_KEY_EVENT_DOUBLECLICK 双击 算法需要结合一段时间的数据判断操作，所以并非每批采样都有事件。点击时长、双击间隔等可以查看 capsensor_driver.c 中的触摸识别配置；完整算法实现在 SDK 的预编译库中。\n拿到双击事件后，下一步就是交给应用，再决定播放、暂停或接听等业务：见《外设适配（02）：触摸事件怎样交给应用》 。\n","permalink":"https://btio.cn/posts/2026/10/23/bes%E5%A4%96%E8%AE%BE%E9%80%82%E9%85%8D01%E8%A7%A6%E6%91%B8%E8%BE%93%E5%85%A5%E4%BB%8E%E9%87%87%E6%A0%B7%E5%88%B0%E4%BA%8B%E4%BB%B6/","summary":"\u003ch2 id=\"一tws-按键交互背景\"\u003e一、TWS 按键交互背景\u003c/h2\u003e\n\u003cp\u003e耳机可以通过电容触摸、压力感应、实体按键或敲击检测等方式接收用户操作。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/d818f1bd-4b8b-44c6-859a-2e64d98ab0ce.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我接触的这份 BES 项目使用 CAPSENSOR 电容感应方案。\n以“双击暂停音乐”为例，先要完成触摸检测，得到“某个事件”，应用才能继续执行对应功能。\u003c/p\u003e\n\u003ch2 id=\"二触摸检测的基本流程\"\u003e二、触摸检测的基本流程\u003c/h2\u003e\n\u003cp\u003e手指接近或接触感应区域，会引起电容变化。硬件按一定节奏测量，将结果转换成程序能读取的数值，这个过程叫作\u003cstrong\u003e采样\u003c/strong\u003e。软件再结合数据随时间的变化，识别单击、双击或长按。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/e4093ded-3ee8-48a1-8d76-e21d216407ec.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e采样数据是测量结果，触摸事件是识别结果。\u003c/strong\u003e 在实际开发中，重点先确认检测能启动、事件能产生，再将事件接入业务。\u003c/p\u003e\n\u003ch2 id=\"三开机后从应用初始化到触摸初始化\"\u003e三、开机后，从应用初始化到触摸初始化\u003c/h2\u003e\n\u003cp\u003e先看开机时这条主线\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}, \u0026#34;themeCSS\u0026#34;: \u0026#34;.nodeLabel { white-space: nowrap; } .nodeLabel p { margin: 0; }\u0026#34;}}%%\nflowchart TB\n    subgraph INIT[\u0026#34;① 应用初始化\u0026#34;]\n        direction LR\n        A[\u0026#34;应用入口\u0026lt;br/\u0026gt;app_init()\u0026#34;]\n        B[\u0026#34;初始化触摸模块\u0026lt;br/\u0026gt;app_mcu_core_capsensor_init()\u0026#34;]\n        C[\u0026#34;准备时钟和模拟电路\u0026lt;br/\u0026gt;capsensor_driver_init()\u0026#34;]\n        D[\u0026#34;创建检测线程\u0026lt;br/\u0026gt;cap_sensor_core_thread_init()\u0026#34;]\n        A --\u0026gt; B --\u0026gt; C --\u0026gt; D\n    end\n\n    subgraph TASK[\u0026#34;② 检测线程\u0026#34;]\n        direction LR\n        E[\u0026#34;线程开始执行\u0026lt;br/\u0026gt;capsensor_thread()\u0026#34;]\n        F[\u0026#34;配置并启动采样\u0026lt;br/\u0026gt;capsensor_drv_start()\u0026#34;]\n        G[\u0026#34;设置采样中断\u0026lt;br/\u0026gt;cap_sensor_irqinit()\u0026#34;]\n        E --\u0026gt; F --\u0026gt; G\n    end\n\n    INIT -..-\u0026gt;|\u0026#34;创建线程后，由 RTOS 调度运行\u0026#34;| TASK\n\n    classDef initPhase fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef taskPhase fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    class A,B,C,D initPhase;\n    class E,F,G taskPhase;\n    style INIT fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    style TASK fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\u003c/pre\u003e\n\u003c/div\u003e\u003ch3 id=\"31-从应用入口进入检测初始化\"\u003e3.1 从应用入口进入检测初始化\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eapp_init()\u003c/code\u003e 在 \u003ccode\u003eCAPSENSOR_ENABLE\u003c/code\u003e 开启时，调用 \u003ccode\u003eapp_mcu_core_capsensor_init()\u003c/code\u003e。图中前一个函数用于恢复佩戴检测的校准参数，这里继续沿第二个函数看触摸初始化。\u003c/p\u003e","title":"【BES】外设适配（01）：触摸输入从采样到事件"},{"content":"上一篇 讲到检测线程得到单击、双击等事件。这篇接着看：事件怎样进入应用，最后找到对应业务。\n本项目的 MCU 处理路径如下：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 检测线程：调用接收函数\u0026#34;] direction LR R0N0[\u0026#34;得到检测事件\u0026lt;br/\u0026gt;status.event\u0026#34;] R0N1[\u0026#34;转换为应用按键事件\u0026lt;br/\u0026gt;app_capsensor_click_event()\u0026#34;] R0N2[\u0026#34;放入应用邮箱\u0026lt;br/\u0026gt;capsensor_key_event_process()\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 应用线程：取消息并分发\u0026#34;] direction LR R1N0[\u0026#34;还原按键消息\u0026lt;br/\u0026gt;app_key_handle_process()\u0026#34;] R1N1[\u0026#34;进入产品处理\u0026lt;br/\u0026gt;app_ibrt_user_key()\u0026#34;] R1N2[\u0026#34;按产品状态、左右耳和事件类型\u0026lt;br/\u0026gt;分发到对应业务函数\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 -..-\u0026gt;|\u0026#34;通过邮箱交给应用线程\u0026#34;| 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() 中有一行：\nregister_capsensor_click_event_callback(app_capsensor_click_event); 它告诉检测模块：“有操作事件时，交给 app_capsensor_click_event 处理。”函数内部通过 capsensor_click_event_cb = p 保存这个函数的地址。登记时不执行它，等事件产生后再调用，这种方式叫作回调。\n二、检测到事件，进入应用接收函数 检测线程确认 status.event 不是 CAP_KEY_EVENT_NONE，且已经登记接收函数后，执行：\ncapsensor_click_event_cb(status.event); 由于保存的是 app_capsensor_click_event，这里便进入该函数，传入本次识别结果。这一步仍在检测线程中执行。\n这里看 MCU 对应的分支。\n以双击为例，接收函数把检测模块的 CAP_KEY_EVENT_DOUBLECLICK 转成按键模块使用的 APP_KEY_EVENT_DOUBLECLICK。这只是对应事件表示，具体功能由后续业务决定。\n三、通过邮箱，交给应用线程 转换完成后，调用：\ncapsensor_key_event_process(status.code, status.event); 两个参数分别是按键编号和操作类型。接收函数将它们打包成 APP_MODULE_KEY 消息，放入应用邮箱，等待应用线程处理。\n调用应用接收函数与切换到应用线程，是两件事。 前面直接调用接收函数；这里通过邮箱交接，后续按键业务才由应用线程处理。\n四、应用线程找到对应业务 应用线程按 APP_MODULE_KEY 分发消息，进入 app_key_handle_process()。它还原按键编号和事件，再调用 app_ibrt_user_key()，进入产品处理流程。\n后续还要检查产品状态，并可能把事件转给对耳。以“本机继续处理的右耳双击”为例：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 本机继续处理\u0026#34;] direction LR R0N0[\u0026#34;产品处理入口\u0026lt;br/\u0026gt;app_ibrt_user_key()\u0026#34;] R0N1[\u0026#34;区分左右耳\u0026lt;br/\u0026gt;app_wgr_ui_handle_key()\u0026#34;] R0N0 --\u0026gt; R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 右耳双击分支\u0026#34;] direction LR R1N0[\u0026#34;区分单击、双击等事件\u0026lt;br/\u0026gt;app_wgr_ui_handle_right_key()\u0026#34;] R1N1[\u0026#34;结合状态执行功能\u0026lt;br/\u0026gt;bt_key_handle_func_doubleclick_right()\u0026#34;] R1N0 --\u0026gt;|\u0026#34;双击\u0026#34;| R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt;|\u0026#34;右耳\u0026#34;| 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; 右耳双击业务会查看通话状态：无通话时读取双击功能配置，有来电时可以执行接听。因此，要修改“双击做什么”，应继续找业务配置和处理函数。\n具体业务分支可回看《系统总览（03）：事件如何触发业务处理》 。\n应用层调试时，可以依次观察：检测事件是否到达接收函数、消息是否进入按键模块、是否进入预期的左右耳和操作分支、当前业务条件是否满足。\n上一篇：外设适配（01）：触摸输入从采样到事件 。\n","permalink":"https://btio.cn/posts/2026/10/23/bes%E5%A4%96%E8%AE%BE%E9%80%82%E9%85%8D02%E8%A7%A6%E6%91%B8%E4%BA%8B%E4%BB%B6%E6%80%8E%E6%A0%B7%E4%BA%A4%E7%BB%99%E5%BA%94%E7%94%A8/","summary":"\u003cp\u003e\u003ca href=\"/posts/2026/10/23/bes%E5%A4%96%E8%AE%BE%E9%80%82%E9%85%8D01%E8%A7%A6%E6%91%B8%E8%BE%93%E5%85%A5%E4%BB%8E%E9%87%87%E6%A0%B7%E5%88%B0%E4%BA%8B%E4%BB%B6/\"\u003e上一篇\u003c/a\u003e\n讲到检测线程得到单击、双击等事件。这篇接着看：事件怎样进入应用，最后找到对应业务。\u003c/p\u003e\n\u003cp\u003e本项目的 MCU 处理路径如下：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 检测线程：调用接收函数\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;得到检测事件\u0026lt;br/\u0026gt;status.event\u0026#34;]\n        R0N1[\u0026#34;转换为应用按键事件\u0026lt;br/\u0026gt;app_capsensor_click_event()\u0026#34;]\n        R0N2[\u0026#34;放入应用邮箱\u0026lt;br/\u0026gt;capsensor_key_event_process()\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 应用线程：取消息并分发\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;还原按键消息\u0026lt;br/\u0026gt;app_key_handle_process()\u0026#34;]\n        R1N1[\u0026#34;进入产品处理\u0026lt;br/\u0026gt;app_ibrt_user_key()\u0026#34;]\n        R1N2[\u0026#34;按产品状态、左右耳和事件类型\u0026lt;br/\u0026gt;分发到对应业务函数\u0026#34;]\n        R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2\n    end\n    class R1N0,R1N1,R1N2 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 -..-\u0026gt;|\u0026#34;通过邮箱交给应用线程\u0026#34;| R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003ch2 id=\"一初始化时登记事件接收函数\"\u003e一、初始化时，登记事件接收函数\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eapp_mcu_core_capsensor_init()\u003c/code\u003e 中有一行：\u003c/p\u003e","title":"【BES】外设适配（02）：触摸事件怎样交给应用"},{"content":"在《系统总览（01）：编译配置如何决定固件功能》 中，用 ANC 介绍了配置对源码的影响。实际构建项目时，还要先弄清楚该执行什么命令、得到什么文件，以及是否需要后续处理。\n这里通过我在实际项目开发中所使用的BES 2700这款芯片的SDK 所用的构建命令进行介绍。包含构建目标、输出路径和 CRC 处理方式。\n一、执行编译命令 编译bes工程，可以在工程根目录打开终端，输入：\nmake T=best1306p -j Make 会读取工程里的 Makefile，并按其中的规则完成构建。这条命令中，T=best1306p 是用于选择这份工程的构建配置，-j 用于并行构建。\n平时修改的 .c、.cpp 源码。经过编译、链接，源码会变成芯片能够执行的程序，并与相关数据一起组成固件。本例生成的固件文件路径是 out/best1306p/best1306p.bin。\n二、为固件补充校验信息 2.1 CRC 后处理 编译生成固件后，有些项目还需要为后续升级补充校验信息。例如，接收方要检查收到的数据是否与准备好的固件一致，就需要一个用于比较的校验值。这里以 CRC32 为例。\nCRC 的中文名称是“循环冗余校验”。电脑先算好一个值，接收方拿到数据后，再按相同范围、相同算法算一次，与保存的值比较：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 电脑上准备固件时\u0026#34;] direction LR R0N0[\u0026#34;生成固件\u0026#34;] R0N1[\u0026#34;计算 CRC\u0026#34;] R0N2[\u0026#34;将校验值填入固件\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 后续设备接收后\u0026#34;] direction LR R1N0[\u0026#34;读取收到的固件数据\u0026#34;] R1N1[\u0026#34;耳机重新计算 CRC\u0026#34;] R1N2[\u0026#34;与固件中保存的 CRC 比较\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 -..-\u0026gt;|\u0026#34;后续设备收到文件\u0026#34;| 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; 对比结果不同，说明数据在传输中发生了变化；结果相同，表示通过校验，数据传输正常。\n这份工程在 build_info.c 中预留了 CRC 字段：\n图 1：build_info.c 预留的 CRC 校验字段\n这里代码中给了一个默认参数。等固件生成后，根据实际数据计算 CRC，将结果填入这个字段。\n在这份示例工程中，这个值用于 OTA 升级。在给设备升级固件中，耳机接收完成会调用下面的 CRC 检查。\n图 2：ota_control.cpp 中接收完成后的 CRC 检查调用\n2.2 Python 脚本的调用方式 计算 CRC、将结果写入文件，可以交给一个Python 脚本完成。工程中采用了generate_crc32_of_image.py，调用命令为：\npython3 generate_crc32_of_image.py out/best1306p/best1306p.bin 命令里有三个部分：\npython3：启动 Python 3 解释器。 generate_crc32_of_image.py：指定要执行的 CRC 处理脚本。 out/best1306p/best1306p.bin：指定本次要处理的输入固件。 这条命令的最后一个参数指定输入固件。脚本要读取它的数据，计算 CRC；在处理其他固件，也可以复用这个脚本，只需要指定输入固件路径就行。\n脚本处理前后的文件关系如下：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;best1306p.bin\u0026#34;] N1[\u0026#34;Python 脚本\u0026lt;br/\u0026gt;计算并填入 CRC\u0026#34;] N2[\u0026#34;best1306p_converted.bin\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; 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; 当前脚本对 CRC 数值字段之前的数据计算 CRC32，生成带 _converted.bin 后缀的新文件，这样就得到了 2 个固件，一个是不带 CRC 校验的固件，另一个是带 CRC 校验的固件。\n三、把重复命令写成脚本 为了方便编译，可以把编译和构建命令 写进一个文本文件，交给 Shell 脚本处理。\n例如，新建一个 build.sh脚本文件，将下面的编译命令写进去：\nmake T=best1306p -j \u0026amp;\u0026amp; python3 generate_crc32_of_image.py out/best1306p/best1306p.bin 中间的 \u0026amp;\u0026amp; 表示：只有前面的构建命令成功，才执行后面的 CRC 处理命令。\n然后下次编译项目时，只需要执行\nbash build.sh Bash 就会读取文件中的命令，依次完成构建和 CRC 后处理。 下一篇：编译配置（02）：构建目标与配置覆盖 ","permalink":"https://btio.cn/posts/2026/10/22/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE01%E7%BC%96%E8%AF%91%E5%91%BD%E4%BB%A4%E4%B8%8E%E6%9E%84%E5%BB%BA%E8%84%9A%E6%9C%AC/","summary":"\u003cp\u003e在\u003ca href=\"/posts/2026/10/20/bes%E7%B3%BB%E7%BB%9F%E6%80%BB%E8%A7%8801%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE%E5%A6%82%E4%BD%95%E5%86%B3%E5%AE%9A%E5%9B%BA%E4%BB%B6%E5%8A%9F%E8%83%BD/\"\u003e《系统总览（01）：编译配置如何决定固件功能》\u003c/a\u003e\n中，用 ANC 介绍了配置对源码的影响。实际构建项目时，还要先弄清楚该执行什么命令、得到什么文件，以及是否需要后续处理。\u003c/p\u003e\n\u003cp\u003e这里通过我在实际项目开发中所使用的BES  2700这款芯片的SDK 所用的构建命令进行介绍。包含构建目标、输出路径和 CRC 处理方式。\u003c/p\u003e\n\u003ch2 id=\"一执行编译命令\"\u003e一、执行编译命令\u003c/h2\u003e\n\u003cp\u003e编译bes工程，可以在工程根目录打开终端，输入：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emake \u003cspan class=\"nv\"\u003eT\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ebest1306p -j\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eMake 会读取工程里的 Makefile，并按其中的规则完成构建。这条命令中，\u003ccode\u003eT=best1306p\u003c/code\u003e 是用于选择这份工程的构建配置，\u003ccode\u003e-j\u003c/code\u003e 用于并行构建。\u003c/p\u003e\n\u003cp\u003e平时修改的 \u003ccode\u003e.c\u003c/code\u003e、\u003ccode\u003e.cpp\u003c/code\u003e 源码。经过编译、链接，源码会变成芯片能够执行的程序，并与相关数据一起组成固件。本例生成的固件文件路径是 \u003ccode\u003eout/best1306p/best1306p.bin\u003c/code\u003e。\u003c/p\u003e\n\u003ch2 id=\"二为固件补充校验信息\"\u003e二、为固件补充校验信息\u003c/h2\u003e\n\u003ch3 id=\"21-crc-后处理\"\u003e2.1 CRC 后处理\u003c/h3\u003e\n\u003cp\u003e编译生成固件后，有些项目还需要为后续升级补充校验信息。例如，接收方要检查收到的数据是否与准备好的固件一致，就需要一个用于比较的校验值。这里以 CRC32 为例。\u003c/p\u003e\n\u003cp\u003eCRC 的中文名称是“循环冗余校验”。电脑先算好一个值，接收方拿到数据后，再按相同范围、相同算法算一次，与保存的值比较：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 电脑上准备固件时\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;生成固件\u0026#34;]\n        R0N1[\u0026#34;计算 CRC\u0026#34;]\n        R0N2[\u0026#34;将校验值填入固件\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 后续设备接收后\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;读取收到的固件数据\u0026#34;]\n        R1N1[\u0026#34;耳机重新计算 CRC\u0026#34;]\n        R1N2[\u0026#34;与固件中保存的 CRC 比较\u0026#34;]\n        R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2\n    end\n    class R1N0,R1N1,R1N2 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 -..-\u0026gt;|\u0026#34;后续设备收到文件\u0026#34;| R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e对比结果不同，说明数据在传输中发生了变化；结果相同，表示通过校验，数据传输正常。\u003c/p\u003e","title":"【BES】编译配置（01）：编译命令与构建脚本"},{"content":"前一篇 介绍了如何执行构建命令。这篇看配置怎样被选中和加载。\n一、构建目标与配置加载 1.1 T：选择构建目标 要知道本次构建用了哪套配置，可以先查构建命令传入了哪些变量、Makefile 怎样使用它们。\n这份 SDK 用 T 选择构建目标，顶层 Makefile 根据T的值，拼出配置文件路径。传入 T=best1306p，选中的就是 config/best1306p/target.mk：\n图 1：顶层 Makefile 根据 T 拼出配置路径\n默认的构建输出目录如下：\n图 2：顶层 Makefile 中的输出目录设置与目标检查\nMakefile 还会检查这个文件是否存在。不存在时，会报 Invalid target。\n修改配置前先看构建命令里的 T，再去找对应目录，避免改了另一套配置。\n1.2 CHIP：指定芯片平台 选出产品配置后，还需要指定它使用的芯片平台。\nCHIP ?= best1306p 这里用的也是 best1306p 作为默认值。\n两个变量的用途分别是：\nT：选择哪套产品或构建配置。 CHIP：指定这套配置使用的芯片平台。 这次两者恰好都叫 best1306p。换成产品目标时，它们可能不同。\n1.3 配置文件的加载顺序 配置拆在多个文件中时，Make 可以用 include 读入其他文件。因此，查配置除了找赋值，还要沿着 include 看它们的加载顺序。\n这份工程的顶层 Makefile 先读入下面两个文件：\n图 3：先读取目标配置，再读取公共配置\nsrctree 表示源码根目录。这里先读取 target.mk，再读取 common.mk。\ntarget.mk 里面还有其他 include。Make 读到这里，会先处理被包含的文件，再回来接着读后面的配置。\n这个构建目标的配置加载顺序是：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR A[\u0026#34;顶层 Makefile\u0026#34;] B[\u0026#34;target.mk\u0026#34;] C[\u0026#34;按顺序读入专项配置\u0026#34;] D[\u0026#34;custom.mk\u0026#34;] E[\u0026#34;common.mk\u0026#34;] A --\u0026gt; B --\u0026gt; C --\u0026gt; D A --\u0026gt; E class A,B,C,D blue; class E green; 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; 图 4：target.mk 中专项配置与 custom.mk 的加载顺序\n图里的专项配置大多按 CHIP 选择路径，custom.mk 则按 T。其他产品目标可能有不同写法，需要看对应的 target.mk。\n还有一个容易漏掉的位置：custom.mk 后面仍然会读取 common.mk。查变量最终值时，不能看到 custom.mk 就停下，还要看公共配置有没有继续赋值或处理。\n《系统总览（01）》讲过按条件加载 ANC 配置 ：变量要在判断之前设置。后面改值，前面的 ifeq 不会重新执行。查配置时，除了看值，还得看赋值位置。\n二、配置变量的覆盖规则 配置按顺序读入，同一个变量也可能被设置多次。多音量表这个开关就是一个例子。\n2.1 默认配置与产品定制 target.mk 中有：\n图 5：target.mk 将多音量表的默认值设为 0\n后面加载的 custom.mk 中又有：\n图 6：custom.mk 将多音量表设为 1\n在没有外部传入该变量的情况下，前面的 ?= 0 先提供默认值，后面的普通赋值再将它设为 1。\n只查 target.mk 的话，容易误以为这个开关是关闭的。\n2.2 命令行变量的优先级 如果执行：\nmake T=best1306p CODEC_DAC_MULTI_VOLUME_TABLE=0 -j 这时，即使 custom.mk 里写了 = 1，命令行传入的 0 也会保留下来。\n普通的 =、:= 都不能覆盖命令行传入的同名变量的值；如果需要强覆盖，需要在赋值前面加 override，这样也会覆盖命令行传入的值。\n碰到同名变量，可以依次查：\n构建命令是否指定同名变量的值。 文件中有哪些有效赋值，使用什么赋值方式、按什么顺序加载。 文件是否使用 override强覆盖，以及变量在哪里被判断或使用。 上一篇：编译配置（01）：编译命令与构建脚本 下一篇：编译配置（03）：Make 变量怎样变成 C 宏 ","permalink":"https://btio.cn/posts/2026/10/22/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE02%E6%9E%84%E5%BB%BA%E7%9B%AE%E6%A0%87%E4%B8%8E%E9%85%8D%E7%BD%AE%E8%A6%86%E7%9B%96/","summary":"\u003cp\u003e\u003ca href=\"/posts/2026/10/22/bes%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE01%E7%BC%96%E8%AF%91%E5%91%BD%E4%BB%A4%E4%B8%8E%E6%9E%84%E5%BB%BA%E8%84%9A%E6%9C%AC/\"\u003e前一篇\u003c/a\u003e\n介绍了如何执行构建命令。这篇看配置怎样被选中和加载。\u003c/p\u003e\n\u003ch2 id=\"一构建目标与配置加载\"\u003e一、构建目标与配置加载\u003c/h2\u003e\n\u003ch3 id=\"11-t选择构建目标\"\u003e1.1 \u003ccode\u003eT\u003c/code\u003e：选择构建目标\u003c/h3\u003e\n\u003cp\u003e要知道本次构建用了哪套配置，可以先查构建命令传入了哪些变量、Makefile 怎样使用它们。\u003c/p\u003e\n\u003cp\u003e这份 SDK 用 \u003ccode\u003eT\u003c/code\u003e 选择构建目标，顶层 \u003ccode\u003eMakefile\u003c/code\u003e 根据T的值，拼出配置文件路径。传入 \u003ccode\u003eT=best1306p\u003c/code\u003e，选中的就是 \u003ccode\u003econfig/best1306p/target.mk\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/a4529f52-b717-46a8-aa9e-b7499d57cbee.png\"\u003e\n\u003cem\u003e图 1：顶层 \u003ccode\u003eMakefile\u003c/code\u003e 根据 \u003ccode\u003eT\u003c/code\u003e 拼出配置路径\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e默认的构建输出目录如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/ce61f938-c97f-4f36-b850-ba3bf3375d4f.png\"\u003e\n\u003cem\u003e图 2：顶层 \u003ccode\u003eMakefile\u003c/code\u003e 中的输出目录设置与目标检查\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eMakefile 还会检查这个文件是否存在。不存在时，会报 \u003ccode\u003eInvalid target\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e修改配置前先看构建命令里的 \u003ccode\u003eT\u003c/code\u003e，再去找对应目录，避免改了另一套配置。\u003c/p\u003e\n\u003ch3 id=\"12-chip指定芯片平台\"\u003e1.2 \u003ccode\u003eCHIP\u003c/code\u003e：指定芯片平台\u003c/h3\u003e\n\u003cp\u003e选出产品配置后，还需要指定它使用的芯片平台。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eCHIP\u003c/span\u003e \u003cspan class=\"o\"\u003e?=\u003c/span\u003e best1306p\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这里用的也是 \u003ccode\u003ebest1306p\u003c/code\u003e 作为默认值。\u003c/p\u003e\n\u003cp\u003e两个变量的用途分别是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eT\u003c/code\u003e\u003c/strong\u003e：选择哪套产品或构建配置。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eCHIP\u003c/code\u003e\u003c/strong\u003e：指定这套配置使用的芯片平台。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这次两者恰好都叫 \u003ccode\u003ebest1306p\u003c/code\u003e。换成产品目标时，它们可能不同。\u003c/p\u003e\n\u003ch3 id=\"13-配置文件的加载顺序\"\u003e1.3 配置文件的加载顺序\u003c/h3\u003e\n\u003cp\u003e配置拆在多个文件中时，Make 可以用 \u003ccode\u003einclude\u003c/code\u003e 读入其他文件。因此，查配置除了找赋值，还要沿着 \u003ccode\u003einclude\u003c/code\u003e 看它们的加载顺序。\u003c/p\u003e\n\u003cp\u003e这份工程的顶层 \u003ccode\u003eMakefile\u003c/code\u003e 先读入下面两个文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/7309d56c-162d-47de-b7ca-7618c2b455a3.png\"\u003e\n\u003cem\u003e图 3：先读取目标配置，再读取公共配置\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esrctree\u003c/code\u003e 表示源码根目录。这里先读取 \u003ccode\u003etarget.mk\u003c/code\u003e，再读取 \u003ccode\u003ecommon.mk\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003etarget.mk\u003c/code\u003e 里面还有其他 \u003ccode\u003einclude\u003c/code\u003e。Make 读到这里，会先处理被包含的文件，再回来接着读后面的配置。\u003c/p\u003e","title":"【BES】编译配置（02）：构建目标与配置覆盖"},{"content":"耳机开机后，应用层需要先准备好运行环境：读取配置数据、创建消息处理机制、初始化各业务模块，之后才能处理按键、连接、音乐播放等事件。\n一、应用层从哪里开始 正常启动时，main() 完成平台侧准备，然后调用 app_init()：\napp_init() 是应用层初始化的主要入口。看这个函数，可以了解 SDK 开机时启用了哪些模块，以及模块之间的初始化顺序和条件。\n二、app_init() 初始化系统和模块 app_init() 下，关键的初始化函数有：\nnv_record_init()：准备 NV 数据、工厂参数 app_os_init()：创建 app 邮箱和线程 app_battery_open()：检查电池、充电状态，确定启动分支 app_key_open()：按键检测及开机按键处理 bt_app_init()：蓝牙模块 app_audio_open()：音频模块 这里的初始化，是为了让模块具备处理业务的条件。例如，音频模块初始化完成，并不代表已经开始播放音乐，后面还需要播放事件触发。\n三、建立应用线程和邮箱 在 app_init() 中，会调用 app_os_init()：\n这个函数主要完成两件事：先创建 app 邮箱，再创建 app 线程。\n邮箱用于保存待处理的应用消息，app_thread 负责取出消息，并根据模块编号交给对应的处理函数。所以，这里的消息链路大致为：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 消息投递\u0026#34;] direction LR R0N0[\u0026#34;模块产生消息\u0026#34;] R0N1[\u0026#34;放入 app 邮箱\u0026#34;] R0N0 --\u0026gt; R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 应用线程处理\u0026#34;] direction LR R1N0[\u0026#34;app_thread 取出消息\u0026#34;] R1N1[\u0026#34;调用对应模块的处理函数\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 -..-\u0026gt;|\u0026#34;通过邮箱交接\u0026#34;| 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 由这些任务协作运行。\n四、初始化完成后的工作 以上工作处理完成之后，系统则从准备模块进入处理事件的运行阶段。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph KEY[\u0026#34;按键业务\u0026#34;] direction LR A[\u0026#34;按键事件\u0026#34;] --\u0026gt; B[\u0026#34;判断按键功能\u0026#34;] --\u0026gt; C[\u0026#34;执行播放、音量等操作\u0026#34;] end subgraph BT[\u0026#34;蓝牙业务\u0026#34;] direction LR D[\u0026#34;蓝牙事件\u0026#34;] --\u0026gt; E[\u0026#34;更新连接状态\u0026#34;] --\u0026gt; F[\u0026#34;执行连接后的业务\u0026#34;] end subgraph BAT[\u0026#34;电池业务\u0026#34;] direction LR G[\u0026#34;电池事件\u0026#34;] --\u0026gt; H[\u0026#34;更新电量状态\u0026#34;] --\u0026gt; I[\u0026#34;上报电量或处理低电量\u0026#34;] end KEY ~~~ BT ~~~ BAT class A,B,C blue; class D,E,F green; class G,H,I amber; style KEY fill:#f1f6fd,stroke:#b8cfe8,color:#23456b style BT fill:#f0f8f3,stroke:#b9d7c4,color:#24563a style BAT fill:#fffbf2,stroke:#e5d0ab,color:#72501e 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; 此时各线程由 RTOS 调度运行。main 线程则在 app_init() 初始化完成之后，进入 while 循环，通过 evt = osSignalWait(0x0, osWaitForever); 阻塞等待信号。如图所示：\n信号由其他模块主动发送。业务模块需要关机或复位时，通常调用 app_shutdown() 或 app_reset()，最终通过 osSignalSet() 通知 main 线程。这里关机信号为 0x04，复位信号为 0x08。\n其他模块负责提出关机、复位请求，main 线程负责统一收尾并执行最终操作。这里的信号属于 RTOS 的线程通知，与 app_thread 使用的应用邮箱是两套机制。\n五、邮箱和线程通知的区别 线程信号和邮箱都用于线程间通信。\n线程信号：用标志位表示某个事件发生了，例如关机、复位。同一标志位未清除前，重复发送会合并，不能记录发生次数。 邮箱：保存一条条消息，可以携带事件类型和参数。成功投递的消息会排队，等待线程逐条处理。 因此，main 用信号接收关机、复位请求；app_thread 用邮箱接收并分发各模块的业务消息。\n","permalink":"https://btio.cn/posts/2026/10/21/bes%E7%B3%BB%E7%BB%9F%E6%80%BB%E8%A7%8802%E5%BC%80%E6%9C%BA%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%B0%E5%BA%94%E7%94%A8%E6%A8%A1%E5%9D%97/","summary":"\u003cp\u003e耳机开机后，应用层需要先准备好运行环境：读取配置数据、创建消息处理机制、初始化各业务模块，之后才能处理按键、连接、音乐播放等事件。\u003c/p\u003e\n\u003ch2 id=\"一应用层从哪里开始\"\u003e一、应用层从哪里开始\u003c/h2\u003e\n\u003cp\u003e正常启动时，\u003ccode\u003emain()\u003c/code\u003e 完成平台侧准备，然后调用 \u003ccode\u003eapp_init()\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/30cdfc02-8d91-4454-a25d-e36f7f48da40.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eapp_init()\u003c/code\u003e 是应用层初始化的主要入口。看这个函数，可以了解 SDK 开机时启用了哪些模块，以及模块之间的初始化顺序和条件。\u003c/p\u003e\n\u003ch2 id=\"二app_init-初始化系统和模块\"\u003e二、\u003ccode\u003eapp_init()\u003c/code\u003e 初始化系统和模块\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eapp_init()\u003c/code\u003e 下，关键的初始化函数有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003env_record_init()\u003c/code\u003e：准备 NV 数据、工厂参数\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eapp_os_init()\u003c/code\u003e：创建 app 邮箱和线程\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eapp_battery_open()\u003c/code\u003e：检查电池、充电状态，确定启动分支\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eapp_key_open()\u003c/code\u003e：按键检测及开机按键处理\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ebt_app_init()\u003c/code\u003e：蓝牙模块\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eapp_audio_open()\u003c/code\u003e：音频模块\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这里的初始化，是为了让模块具备处理业务的条件。例如，音频模块初始化完成，并不代表已经开始播放音乐，后面还需要播放事件触发。\u003c/p\u003e\n\u003ch2 id=\"三建立应用线程和邮箱\"\u003e三、建立应用线程和邮箱\u003c/h2\u003e\n\u003cp\u003e在 \u003ccode\u003eapp_init()\u003c/code\u003e 中，会调用 \u003ccode\u003eapp_os_init()\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/9e87eacd-d73b-489b-8afb-56a30c4396a4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这个函数主要完成两件事：先创建 app 邮箱，再创建 app 线程。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"正文配图\" loading=\"lazy\" src=\"https://picgocloud.com/m/16f47e13-f664-487e-8a8c-c7b7463f2820.png\"\u003e\u003c/p\u003e\n\u003cp\u003e邮箱用于保存待处理的应用消息，\u003ccode\u003eapp_thread\u003c/code\u003e 负责取出消息，并根据模块编号交给对应的处理函数。所以，这里的消息链路大致为：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 消息投递\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;模块产生消息\u0026#34;]\n        R0N1[\u0026#34;放入 app 邮箱\u0026#34;]\n        R0N0 --\u0026gt; R0N1\n    end\n    class R0N0,R0N1 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 应用线程处理\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;app_thread 取出消息\u0026#34;]\n        R1N1[\u0026#34;调用对应模块的处理函数\u0026#34;]\n        R1N0 --\u0026gt; R1N1\n    end\n    class R1N0,R1N1 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 -..-\u0026gt;|\u0026#34;通过邮箱交接\u0026#34;| R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e这是 app 线程的处理方式。蓝牙和音频等子系统也有各自的线程或回调，整个 SDK 由这些任务协作运行。\u003c/p\u003e","title":"【BES】系统总览（02）：开机初始化到应用模块"},{"content":"【BES】系统总览（03）：事件如何触发业务处理 上一篇介绍了开机初始化：创建线程、邮箱，初始化按键、蓝牙、音频等模块。\n初始化完成后，耳机就可以响应各种变化。例如，用户双击耳机、手机连接成功、App 切换降噪模式，都会触发相应的处理。\n这一篇主要介绍：一个事件发生后，是怎样进入业务代码的。\n一、事件与业务的区别 事件描述“发生了什么”，业务决定“接下来做什么”。\n例如，用户双击左耳：\n事件：左耳发生双击。 业务：根据当前状态，执行播放、暂停或者接听电话等操作。 同一个操作，在不同状态下可能执行不同功能。\n整体流程可以这样理解：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 事件传递\u0026#34;] direction LR R0N0[\u0026#34;检测到变化\u0026#34;] R0N1[\u0026#34;传递事件\u0026#34;] R0N2[\u0026#34;找到处理函数\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 业务处理\u0026#34;] direction LR R1N0[\u0026#34;判断当前状态\u0026#34;] R1N1[\u0026#34;执行具体操作\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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 的触摸链路来看。\n二、触摸事件如何进入业务处理 1. 将触摸操作变成消息 触摸模块识别出操作后，会调用 capsensor_key_event_process()，传入按键编号和事件类型。\n该函数把“哪个按键、发生了什么操作”打包成按键消息，放入 app 邮箱，等待处理。此时还没有决定双击要执行什么功能。\n2. app 线程取出消息，交给按键模块 上一篇提到，app_thread 会从邮箱中取消息，再根据模块编号分发。\n这条消息的模块编号是 APP_MODULE_KEY，所以会交给按键处理函数 app_key_handle_process()。按键模块初始化时，会先登记由哪个函数处理按键消息；运行时再按登记结果分发。\n3. 按键模块进入产品业务 app_key_handle_process() 从消息中还原按键编号和事件类型，然后调用 app_ibrt_user_key()。\napp_ibrt_user_key() 会检查当前状态，并决定如何处理按键事件。例如，设备正在充电或耳机在充电仓内时会直接返回；如果当前耳机是从耳，会尝试把按键事件转发给对耳；否则在本机继续处理。\n这里的“主从”指 TWS 角色，“左右耳”是按键编号表示的左右身份，是两种不同判断。下面以本机继续处理的分支为例。\n4. 区分测试模式和左右耳 本机处理时会进入 app_wgr_ui_handle_key()，这是按键业务的统一入口。\n测试模式下，函数处理特定的超长按关机事件；正常模式下，根据 status-\u0026gt;code 区分左右耳，并调用对应的按键处理函数。该函数负责分流，不判断双击对应什么功能。\n5. 右耳函数根据事件类型分发 下面以右耳为例。\n右耳处理函数根据事件类型，分发到单击、双击、三击、长按等对应分支。源码中单击调用已注释，目前不会执行单击功能。\n6. 右耳双击根据当前状态执行功能 bt_key_handle_func_doubleclick_right() 会先读取当前通话状态，再决定双击的用途：\n空闲时：读取右耳双击配置。如果配置为播放/暂停，会继续进入播放处理函数，发送 AVRCP 播放或暂停请求。 来电时：接听电话。 拨出或通话中：按代码处理挂断。 三方通话时：根据当前通话组合处理接听、挂断等操作。 这里可以看出，同一个双击事件会根据耳机当前状态进入不同业务。 产品也可以按需求调整各状态下的处理逻辑。\n其他按键事件也是类似流程：先按左右耳和事件类型分发，再结合当前状态执行具体操作。\n三、其他事件也可以这样理解 看其他事件时，先关注：事件从哪里来、在哪里接收、由谁分发、最后执行什么业务。\n场景 事件从哪里来 业务主要做什么 手机连接耳机 蓝牙协议栈上报连接结果 更新连接状态，处理后续连接业务 App 切换 ANC 手机通过 BLE 发送设置命令 接收目标模式，进入 ANC 控制流程 电池电量变化 定时采样得到电池电压 更新电量，按条件上报或处理低电量 这些事件的入口和传递方式不完全相同，但都可以根据事件触发源头，顺着调用链查下去。\n四、看业务代码时，怎么找入口 先选一个具体操作，例如“双击暂停音乐”，然后顺着四步查：\n哪里检测到这个操作？ 事件通过邮箱还是回调传递？ 哪个函数判断状态和功能？ 最后调用什么接口执行？ 上一篇主要看“模块怎样初始化”，这一篇看“模块初始化后怎样响应事件”。 先把按键链路看通，再用同样的方法看连接、ANC 和电池等流程。\n","permalink":"https://btio.cn/posts/2026/10/21/bes%E7%B3%BB%E7%BB%9F%E6%80%BB%E8%A7%8803%E4%BA%8B%E4%BB%B6%E5%A6%82%E4%BD%95%E8%A7%A6%E5%8F%91%E4%B8%9A%E5%8A%A1%E5%A4%84%E7%90%86/","summary":"\u003ch1 id=\"bes系统总览03事件如何触发业务处理\"\u003e【BES】系统总览（03）：事件如何触发业务处理\u003c/h1\u003e\n\u003cp\u003e上一篇介绍了开机初始化：创建线程、邮箱，初始化按键、蓝牙、音频等模块。\u003c/p\u003e\n\u003cp\u003e初始化完成后，耳机就可以响应各种变化。例如，用户双击耳机、手机连接成功、App 切换降噪模式，都会触发相应的处理。\u003c/p\u003e\n\u003cp\u003e这一篇主要介绍：\u003cstrong\u003e一个事件发生后，是怎样进入业务代码的。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"一事件与业务的区别\"\u003e一、事件与业务的区别\u003c/h2\u003e\n\u003cp\u003e事件描述“发生了什么”，业务决定“接下来做什么”。\u003c/p\u003e\n\u003cp\u003e例如，用户双击左耳：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e事件\u003c/strong\u003e：左耳发生双击。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e业务\u003c/strong\u003e：根据当前状态，执行播放、暂停或者接听电话等操作。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e同一个操作，在不同状态下可能执行不同功能。\u003c/p\u003e\n\u003cp\u003e整体流程可以这样理解：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 事件传递\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;检测到变化\u0026#34;]\n        R0N1[\u0026#34;传递事件\u0026#34;]\n        R0N2[\u0026#34;找到处理函数\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 业务处理\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;判断当前状态\u0026#34;]\n        R1N1[\u0026#34;执行具体操作\u0026#34;]\n        R1N0 --\u0026gt; R1N1\n    end\n    class R1N0,R1N1 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e下面结合 BES 的触摸链路来看。\u003c/p\u003e","title":"【BES】系统总览（03）：事件如何触发业务处理"},{"content":"一、编译配置是什么 配置主要由 .mk 文件决定。看这些文件，可以了解当前选择了什么平台、加入了哪些模块，以及使用了哪些配置。\n编译配置发生在电脑生成固件时。耳机上电后，运行的是已经编译好的固件。\n整体流程如下：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 电脑上构建\u0026#34;] direction LR R0N0[\u0026#34;读取配置\u0026#34;] R0N1[\u0026#34;编译、链接\u0026#34;] R0N2[\u0026#34;生成固件\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 耳机上运行\u0026#34;] direction LR R1N0[\u0026#34;烧录固件\u0026#34;] R1N1[\u0026#34;上电启动\u0026#34;] R1N2[\u0026#34;运行固件中的代码\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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 中的判断：\n第 390 行检查 ANC_ENABLE 是否等于 1。如果是，就加载 anc_config.mk。\n这里要注意赋值的位置。Make 按顺序读取配置文件；如果在第 390 行之后才给 ANC_ENABLE 赋值，这个判断不会重新执行。因此，要让判断生效，就需要在判断之前设置好 ANC_ENABLE。\n2. 配置如何影响源码 下图展示 anc_config.mk 中的配置：\n这里扩展一下，Make 中常见的赋值方式有四种：\n=：使用变量时再展开 :=：定义时立即展开 ?=：变量未定义时，才设置默认值 +=：在原有值后追加内容 anc_config.mk 会设置 ANC_APP，并加入 ANC_APP 编译宏。\n源码可以通过这个宏来控制相关代码是否参与编译。下面这段代码就是一个例子：\n这个条件编译可以分成两个阶段来理解：\n编译时：决定是否保留开关中的调用。 运行时：耳机开机运行到这里，才执行具体的代码流程。 三、以后如何修改配置 可以在构建命令中传入配置，也可以修改工程里的默认配置。例如：\nmake T=best1306p ANC_ENABLE=1 -j 这里选择 best1306p 这套构建目标，并让 ANC_ENABLE 的判断成立，从而加载 anc_config.mk。不过，ANC 功能最终能否使用，还要看其他配置和硬件条件。\n四、常见问题 如果源码里有相关功能，但实际没有生效，先检查：\n当前编译的是哪个目标文件 配置变量的值是什么 对应模块有没有加入构建 初始化代码有没有被编译进去 编译配置决定固件中包含哪些功能代码；耳机启动后，固件再按运行流程执行这些代码。\n","permalink":"https://btio.cn/posts/2026/10/20/bes%E7%B3%BB%E7%BB%9F%E6%80%BB%E8%A7%8801%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE%E5%A6%82%E4%BD%95%E5%86%B3%E5%AE%9A%E5%9B%BA%E4%BB%B6%E5%8A%9F%E8%83%BD/","summary":"\u003ch2 id=\"一编译配置是什么\"\u003e一、编译配置是什么\u003c/h2\u003e\n\u003cp\u003e配置主要由 \u003ccode\u003e.mk\u003c/code\u003e 文件决定。看这些文件，可以了解当前选择了什么平台、加入了哪些模块，以及使用了哪些配置。\u003c/p\u003e\n\u003cp\u003e编译配置发生在电脑生成固件时。耳机上电后，运行的是已经编译好的固件。\u003c/p\u003e\n\u003cp\u003e整体流程如下：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 电脑上构建\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;读取配置\u0026#34;]\n        R0N1[\u0026#34;编译、链接\u0026#34;]\n        R0N2[\u0026#34;生成固件\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 耳机上运行\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;烧录固件\u0026#34;]\n        R1N1[\u0026#34;上电启动\u0026#34;]\n        R1N2[\u0026#34;运行固件中的代码\u0026#34;]\n        R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2\n    end\n    class R1N0,R1N1,R1N2 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003ch2 id=\"二anc-配置如何影响源码\"\u003e二、ANC 配置如何影响源码\u003c/h2\u003e\n\u003ch3 id=\"1-判断是否加载-anc-配置\"\u003e1. 判断是否加载 ANC 配置\u003c/h3\u003e\n\u003cp\u003e下面是 \u003ccode\u003etarget.mk\u003c/code\u003e 中的判断：\u003c/p\u003e","title":"【BES】系统总览（01）：编译配置如何决定固件功能"},{"content":"从功能表面看，耳机电量更新并不复杂：ADC 采到电池电压，再查表换算成百分比，手机就能显示。\n但在实际项目中，电量更新并不是采样后直接查表：采样值要先经过滤波，查表得到的也只是参考电量，手机最终显示的还要由软件继续维护。先把实际电压、参考电量和显示电量这三个值分清楚，整条链路就容易理解了。\n先看完整链路：前 3 步把采样值处理成参考电量，后 3 步由软件维护显示值、完成双耳同步，最后上报手机。\n电池电压是怎么采出来的？ 顺着中科蓝讯 SDK 的调用关系看，系统会在 5ms 任务中检查 ADC 转换状态。转换完成后，读取电池通道和内部参考通道的 ADC 值。内部参考电压的已知值用于校准换算比例，再结合电池采样值和分压系数，得到实际电池电压。\n为什么采到电压后还不能直接使用？ 播放音乐或者蓝牙发射时，电池电压会短暂下降，负载变小后又会回升，所以一次采样不能直接拿来估算电量。\n结合耳机电压变化缓慢、但采样又容易受负载影响的特点，我更推荐用递推平均的方式更新电压：每来一个新采样值，只让平均电压向它靠近一小步。平均电压变化达到条件后，才更新稳定电压。下降条件更容易触发，上升条件更严格，可以减少电压回弹的影响。\n参考电量为什么不直接给手机？ 稳定电压通过“电压—电量对照表”换算成参考电量。它只用来判断当前显示偏高还是偏低，不是手机最终看到的电量。\n如果每次都用参考电量覆盖显示电量，负载变化仍然会造成百分比跳变。\n显示电量是怎么变化的？ 电量管理任务大约每 10 秒运行一次，并根据当前状态选择更新方式。\n放电时，参考电量低于显示电量，软件就让显示电量逐级下降，不会一次跳到参考值；电压短暂回升时，也不会把电量加回来。\n充电时，电压回升很快，不能直接用它增加百分比。软件根据充电电流和时间估算充入容量，再换算成显示电量；底层确认充满后，才显示 100%。\n温度决定当前能不能充电、使用多大的充电电流。低电提醒主要看显示电量，低压关机最终看实际电压。\n为什么内部电量和手机显示还可能不同？ 显示电量发生变化后会被保存，耳机下次开机可以从上次的结果继续计算。耳机内部按 1% 维护电量，当前 App 路径按 5% 一档显示。\n例如内部电量是 59%，手机仍然显示 60%；内部电量下降到 55% 后，手机才显示 55%。\n左右耳电量最后由谁上报？ 左右耳各自计算本耳电量，再通过 TWS 交换耳机和充电盒电量。主耳汇总左耳、右耳和充电盒数据后，统一上报给手机系统和配套 App。\n现在再看这条链路，重点就是分清三份数据：实际电压反映电池状态，参考电量判断变化方向，显示电量负责最终呈现。\n原文：CSDN · 【中科蓝讯】耳机电量是怎样更新到手机的？ ","permalink":"https://btio.cn/posts/2026/08/17/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AF%E8%80%B3%E6%9C%BA%E7%94%B5%E9%87%8F%E6%98%AF%E6%80%8E%E6%A0%B7%E6%9B%B4%E6%96%B0%E5%88%B0%E6%89%8B%E6%9C%BA%E7%9A%84/","summary":"\u003cp\u003e从功能表面看，耳机电量更新并不复杂：ADC 采到电池电压，再查表换算成百分比，手机就能显示。\u003c/p\u003e\n\u003cp\u003e但在实际项目中，电量更新并不是采样后直接查表：采样值要先经过滤波，查表得到的也只是参考电量，手机最终显示的还要由软件继续维护。先把实际电压、参考电量和显示电量这三个值分清楚，整条链路就容易理解了。\u003c/p\u003e\n\u003cp\u003e先看完整链路：前 3 步把采样值处理成参考电量，后 3 步由软件维护显示值、完成双耳同步，最后上报手机。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"TWS 耳机电量更新六步流程\" loading=\"lazy\" src=\"/images/csdn/163825929/%E7%94%B5%E9%87%8F%E6%9B%B4%E6%96%B0%E5%85%AD%E6%AD%A5%E6%B5%81%E7%A8%8B.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"电池电压是怎么采出来的\"\u003e电池电压是怎么采出来的？\u003c/h2\u003e\n\u003cp\u003e顺着中科蓝讯 SDK 的调用关系看，系统会在 5ms 任务中检查 ADC 转换状态。转换完成后，读取电池通道和内部参考通道的 ADC 值。内部参考电压的已知值用于校准换算比例，再结合电池采样值和分压系数，得到实际电池电压。\u003c/p\u003e\n\u003ch2 id=\"为什么采到电压后还不能直接使用\"\u003e为什么采到电压后还不能直接使用？\u003c/h2\u003e\n\u003cp\u003e播放音乐或者蓝牙发射时，电池电压会短暂下降，负载变小后又会回升，所以一次采样不能直接拿来估算电量。\u003c/p\u003e\n\u003cp\u003e结合耳机电压变化缓慢、但采样又容易受负载影响的特点，我更推荐用递推平均的方式更新电压：每来一个新采样值，只让平均电压向它靠近一小步。平均电压变化达到条件后，才更新稳定电压。下降条件更容易触发，上升条件更严格，可以减少电压回弹的影响。\u003c/p\u003e\n\u003ch2 id=\"参考电量为什么不直接给手机\"\u003e参考电量为什么不直接给手机？\u003c/h2\u003e\n\u003cp\u003e稳定电压通过“电压—电量对照表”换算成参考电量。它只用来判断当前显示偏高还是偏低，不是手机最终看到的电量。\u003c/p\u003e\n\u003cp\u003e如果每次都用参考电量覆盖显示电量，负载变化仍然会造成百分比跳变。\u003c/p\u003e\n\u003ch2 id=\"显示电量是怎么变化的\"\u003e显示电量是怎么变化的？\u003c/h2\u003e\n\u003cp\u003e电量管理任务大约每 10 秒运行一次，并根据当前状态选择更新方式。\u003c/p\u003e\n\u003cp\u003e放电时，参考电量低于显示电量，软件就让显示电量逐级下降，不会一次跳到参考值；电压短暂回升时，也不会把电量加回来。\u003c/p\u003e\n\u003cp\u003e充电时，电压回升很快，不能直接用它增加百分比。软件根据充电电流和时间估算充入容量，再换算成显示电量；底层确认充满后，才显示 100%。\u003c/p\u003e\n\u003cp\u003e温度决定当前能不能充电、使用多大的充电电流。低电提醒主要看显示电量，低压关机最终看实际电压。\u003c/p\u003e\n\u003ch2 id=\"为什么内部电量和手机显示还可能不同\"\u003e为什么内部电量和手机显示还可能不同？\u003c/h2\u003e\n\u003cp\u003e显示电量发生变化后会被保存，耳机下次开机可以从上次的结果继续计算。耳机内部按 1% 维护电量，当前 App 路径按 5% 一档显示。\u003c/p\u003e\n\u003cp\u003e例如内部电量是 59%，手机仍然显示 60%；内部电量下降到 55% 后，手机才显示 55%。\u003c/p\u003e\n\u003ch2 id=\"左右耳电量最后由谁上报\"\u003e左右耳电量最后由谁上报？\u003c/h2\u003e\n\u003cp\u003e左右耳各自计算本耳电量，再通过 TWS 交换耳机和充电盒电量。主耳汇总左耳、右耳和充电盒数据后，统一上报给手机系统和配套 App。\u003c/p\u003e\n\u003cp\u003e现在再看这条链路，重点就是分清三份数据：\u003cstrong\u003e实际电压\u003c/strong\u003e反映电池状态，\u003cstrong\u003e参考电量\u003c/strong\u003e判断变化方向，\u003cstrong\u003e显示电量\u003c/strong\u003e负责最终呈现。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原文：\u003ca href=\"https://blog.csdn.net/qq_56070244/article/details/163825929\"\u003eCSDN · 【中科蓝讯】耳机电量是怎样更新到手机的？\u003c/a\u003e\n\u003c/p\u003e","title":"【中科蓝讯】耳机电量是怎样更新到手机的"},{"content":"本文先整理电压采样的方案设计思路，再复盘下面五类电量与充电问题：\n电压稳定方案的设计思路 电量曲线 温度采样 上报精度 电量管控 状态刷新 一、电压稳定方案的设计思路 电量计算不会直接使用一次 ADC 采样结果。播放音乐、蓝牙发射和负载切换都会造成短时波动，因此在进入电量估算前，需要先得到一份相对稳定的电压。\n这里先分清三个值：\n数据 含义 新采样电压 ADC 本次换算得到的电压，每次都可能变化 平均电压 经过滤波后缓慢上升或下降的中间值 稳定电压 达到更新条件后才改变，提供给后续电量算法使用 稳定电压不是手机显示的百分比，也不是永远固定不变。它只是在平均电压没有达到更新条件时保持原值。\n方案一：去极值后取平均值\n先连续采集一组电压，去掉其中几个最大值和最小值，再对剩余数据求平均。偶发的异常高值和异常低值不会直接影响结果，适合 ADC 容易出现尖峰干扰的场景。\n这种方案的优点是抗单点异常能力强，思路也比较直观。缺点是需要保存一组采样数据，还要完成极值查找或排序，会占用更多内存和计算资源。采样窗口越长，结果越稳定，但输出延迟也越大，真实电压发生变化时不容易及时跟上。\n方案二：递推平均加更新阈值\n递推平均不需要保存整组数据。每得到一个有效采样值，就让原来的平均电压向新采样值移动一小步。新采样值低于旧平均值时向下移动，高于旧平均值时向上移动，每次移动的大小由两者差值和递推系数决定。\n这种方案的优点是内存和计算开销小，适合持续采样，也可以通过调整递推系数改变响应速度。缺点是它不会自动剔除异常值，一个突发尖峰会影响后续一段时间；系数过小，电压变化跟得慢，系数过大，又容易把短时波动带进结果。\n以递推系数 1/32 为例：\n当前平均电压和稳定电压都是 4000mV。 本次新采样电压为 3800mV，与平均电压相差 200mV。 本次只向下移动差值的 1/32，也就是 6.25mV，新的平均电压约为 3993.75mV。 新的平均电压再与稳定电压比较。两者向下相差约 6.25mV，达到当前项目的下降更新条件，稳定电压才更新。 如果旧平均电压低于新采样电压，处理方向相反：旧平均电压加上两者差值的 1/32，得到新的平均电压。无论差值大小，每次采样都会参与递推；更新阈值只负责决定稳定电压是否改变。\n当前项目在平均电压下降约 5mV 时允许更新稳定电压，向上则需要变化约 10mV。下降阈值更小，可以及时反映放电趋势；上升阈值更大，可以减少负载解除后电压回弹带来的影响。\n对于 TWS 耳机这种电压变化缓慢、需要持续采样的设备，递推平均占用资源少，也方便调整响应速度，当前 SDK 采用的就是第二种方案。如果还会出现明显的异常采样，可以先剔除异常值，再进入递推平均。\n二、电量曲线 问题现象\n耳机从满电持续播放音乐时，有些 10% 很快掉完，有些区间长时间不变化；左右耳在同一电量区间的续航也可能相差较大。总续航接近要求，但百分比曲线不均匀。\n问题原因\n这次问题的直接原因是软件仍使用旧电池的电压—电量表，与当前电芯的充放电曲线不匹配，导致各百分比区间对应的实际续航不均匀。左右耳的主从角色还会带来功耗差异，因此双耳验证时也要把主从切换考虑进去。\n处理结果\n根据当前电芯的实测充放电数据更新电量表，同时调整电量相关的主从切换逻辑。修改后分别在 ANC 开启和关闭场景下完成整轮放电验证，各 10% 区间的续航恢复到要求范围内。\n三、温度采样 问题现象\n断开 NTC 检测电阻后，系统没有识别到温度检测异常，充电也没有被禁止。\n问题原因\nNTC 通过分压电路接到 ADC，正常连接时会得到有效采样值，断路或短路时则会落到异常范围。原来的流程中，部分场景没有及时刷新 NTC 采样值，检测电源的初始化也不完整，断开 NTC 后仍可能读到上一次的正常值，因此充电保护没有触发。\n处理结果\n修正 NTC 的 ADC 刷新和检测电源初始化流程，并把断路、短路等边界值直接按异常处理。修改后断开 NTC，系统能够识别异常并禁止充电。\n四、上报精度 问题现象\n同一副耳机在 Google 设备页显示 95%，手机下拉状态栏却显示 100%；重新连接后，两个位置的显示结果还可能发生变化。\n问题原因\nGoogle 设备页和状态栏分别使用 GFPS 与 HFP 的电量上报。首次连接和重新连接时，HFP 协商出的电量精度可能不同，再加上两条链路使用了不同的取整规则，同一个 95% 最终可能显示成 95% 或 100%。\n处理结果\n将 GFPS 与 HFP 统一为 10% 精度，App 继续使用 5% 精度，同时整理首次连接和重连时的上报流程。修改后，Google 设备页与手机状态栏的电量保持一致。\n五、电量管控 问题现象\n产品开启电量管控，并把耳机充电上限设为 80%。耳机低电入盒并持续充电两小时后，电量仍然达到满电，没有停在设置的上限。\n问题原因\n电量管控虽然设置了充电上限，但达到 80% 后没有正确执行停充动作，充电流程仍然继续。\n处理结果\n在周期电量管理中补充上限检查：电量管控开启、耳机正在充电且当前电量达到上限时，关闭充电电流并更新管控状态。修改后持续充电两小时，耳机电量保持在 80%。\n六、状态刷新 问题现象\n充电盒无电时，将双耳放入盒内并插入充电线，App 正常显示充电图标；拔掉充电线后，图标却一直没有消失。\n问题原因\n充电状态放在盒子电量字段的最高位。拔线后，机盒没有在关机前发送清除充电标志的新状态包，耳机只能继续使用缓存中的旧状态，App 因此一直显示充电。\n处理结果\n机盒在拔线后、关机前补发一次最新电量状态，并清除充电标志。耳机收到新数据后更新缓存并通知 App，充电图标能够正常消失。\n","permalink":"https://btio.cn/posts/2026/08/17/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AF%E8%80%B3%E6%9C%BA%E7%94%B5%E9%87%8F%E4%B8%8E%E5%85%85%E7%94%B5%E9%97%AE%E9%A2%98%E5%A4%8D%E7%9B%98/","summary":"\u003cp\u003e本文先整理电压采样的方案设计思路，再复盘下面五类电量与充电问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#section-1\"\u003e电压稳定方案的设计思路\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-2\"\u003e电量曲线\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-3\"\u003e温度采样\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-4\"\u003e上报精度\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-5\"\u003e电量管控\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-6\"\u003e状态刷新\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"section-1\"\u003e一、电压稳定方案的设计思路\u003c/h2\u003e\n\u003cp\u003e电量计算不会直接使用一次 ADC 采样结果。播放音乐、蓝牙发射和负载切换都会造成短时波动，因此在进入电量估算前，需要先得到一份相对稳定的电压。\u003c/p\u003e\n\u003cp\u003e这里先分清三个值：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e数据\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e含义\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e新采样电压\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eADC 本次换算得到的电压，每次都可能变化\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e平均电压\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e经过滤波后缓慢上升或下降的中间值\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e稳定电压\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e达到更新条件后才改变，提供给后续电量算法使用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e稳定电压不是手机显示的百分比，也不是永远固定不变。它只是在平均电压没有达到更新条件时保持原值。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e方案一：去极值后取平均值\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先连续采集一组电压，去掉其中几个最大值和最小值，再对剩余数据求平均。偶发的异常高值和异常低值不会直接影响结果，适合 ADC 容易出现尖峰干扰的场景。\u003c/p\u003e\n\u003cp\u003e这种方案的优点是抗单点异常能力强，思路也比较直观。缺点是需要保存一组采样数据，还要完成极值查找或排序，会占用更多内存和计算资源。采样窗口越长，结果越稳定，但输出延迟也越大，真实电压发生变化时不容易及时跟上。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e方案二：递推平均加更新阈值\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e递推平均不需要保存整组数据。每得到一个有效采样值，就让原来的平均电压向新采样值移动一小步。新采样值低于旧平均值时向下移动，高于旧平均值时向上移动，每次移动的大小由两者差值和递推系数决定。\u003c/p\u003e\n\u003cp\u003e这种方案的优点是内存和计算开销小，适合持续采样，也可以通过调整递推系数改变响应速度。缺点是它不会自动剔除异常值，一个突发尖峰会影响后续一段时间；系数过小，电压变化跟得慢，系数过大，又容易把短时波动带进结果。\u003c/p\u003e\n\u003cp\u003e以递推系数 \u003ccode\u003e1/32\u003c/code\u003e 为例：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e当前平均电压和稳定电压都是 \u003ccode\u003e4000mV\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e本次新采样电压为 \u003ccode\u003e3800mV\u003c/code\u003e，与平均电压相差 \u003ccode\u003e200mV\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e本次只向下移动差值的 \u003ccode\u003e1/32\u003c/code\u003e，也就是 \u003ccode\u003e6.25mV\u003c/code\u003e，新的平均电压约为 \u003ccode\u003e3993.75mV\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e新的平均电压再与稳定电压比较。两者向下相差约 \u003ccode\u003e6.25mV\u003c/code\u003e，达到当前项目的下降更新条件，稳定电压才更新。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果旧平均电压低于新采样电压，处理方向相反：旧平均电压加上两者差值的 \u003ccode\u003e1/32\u003c/code\u003e，得到新的平均电压。无论差值大小，每次采样都会参与递推；更新阈值只负责决定稳定电压是否改变。\u003c/p\u003e\n\u003cp\u003e当前项目在平均电压下降约 \u003ccode\u003e5mV\u003c/code\u003e 时允许更新稳定电压，向上则需要变化约 \u003ccode\u003e10mV\u003c/code\u003e。下降阈值更小，可以及时反映放电趋势；上升阈值更大，可以减少负载解除后电压回弹带来的影响。\u003c/p\u003e\n\u003cp\u003e对于 TWS 耳机这种电压变化缓慢、需要持续采样的设备，递推平均占用资源少，也方便调整响应速度，当前 SDK 采用的就是第二种方案。如果还会出现明显的异常采样，可以先剔除异常值，再进入递推平均。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、电量曲线\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机从满电持续播放音乐时，有些 \u003ccode\u003e10%\u003c/code\u003e 很快掉完，有些区间长时间不变化；左右耳在同一电量区间的续航也可能相差较大。总续航接近要求，但百分比曲线不均匀。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题原因\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这次问题的直接原因是软件仍使用旧电池的电压—电量表，与当前电芯的充放电曲线不匹配，导致各百分比区间对应的实际续航不均匀。左右耳的主从角色还会带来功耗差异，因此双耳验证时也要把主从切换考虑进去。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e处理结果\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e根据当前电芯的实测充放电数据更新电量表，同时调整电量相关的主从切换逻辑。修改后分别在 ANC 开启和关闭场景下完成整轮放电验证，各 \u003ccode\u003e10%\u003c/code\u003e 区间的续航恢复到要求范围内。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、温度采样\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e","title":"【中科蓝讯】耳机电量与充电问题复盘"},{"content":"本文围绕下面四类问题，整理对应的现象、问题原因和处理结果：\n快速开关盖后异常断连 左右耳回复不一致，盒子灯效频闪 耳机已经进入配对，盒子却不显示配对灯效 配对超时后，盒子显示了错误灯效 一、快速开关盖后异常断连 问题现象\n快速开关盖时，耳机低概率断开。日志中没有完整的关盖命令，耳机却进入了充电状态。\n问题原因\n快速开关盖时，充电盒重新拉起供电。电平持续时间超过原来的防抖阈值，耳机提前判断为稳定入盒。状态切换由充电检测触发，不是串口丢包。\n处理结果\n将充电电平持续检测时间从约 600 ms 延长到 1 s，避免短暂电平触发充电模式。连续快速开关盖验证后，未再出现异常断连。\n二、左右耳回复不一致，盒子灯效频闪 问题现象\n耳机进入配对状态并放入盒内后，充电盒白灯闪烁不均匀，部分场景出现频闪或亮度异常。\n问题原因\n左右耳返回给充电盒的配对状态不一致。盒子先收到一只耳机的配对状态，又收到另一只耳机的非配对状态，灯效状态被反复切换。\n左耳回复：配对中 右耳回复：非配对 ↓ 盒子灯效在两个状态之间反复切换 处理结果\n由从耳同步主耳的配对状态，确保左右耳向充电盒返回一致的数据。双耳状态一致后，配对灯效恢复正常。\n三、耳机进入配对，盒子却不显示配对灯效 问题现象\n耳机已经连接一台手机，再进入配对模式并放入盒内。充电盒只显示耳机电量，没有显示配对灯效。\n问题原因\n耳机同时存在“已连接”和“可发现可连接”两个状态。原来的回复逻辑优先返回已连接状态，覆盖了正在配对的状态，盒子因此没有进入配对灯效。\n/* 修改前 */ if (phone_connected()) { report_connected(); } else if (pairing()) { report_pairing(); } /* 修改后 */ if (pairing()) { report_pairing(); } else if (phone_connected()) { report_connected(); } 处理结果\n回复状态时优先判断配对状态，再判断手机连接状态。修改后，耳机在已连接状态下重新进入配对，盒子能够正常显示配对灯效。\n四、配对超时后，盒子显示了错误灯效 问题现象\n耳机在配对状态下入盒，配对超时后，充电盒显示了配对成功灯效，而不是结束灯效。\n问题原因\n耳机在配对超时后返回无效状态，充电盒却把这个状态按配对成功处理。数据传输正常，但两端对同一状态的含义没有统一。\n处理结果\n统一耳机与充电盒对回复状态的定义。收到无效状态时直接结束灯效，不再进入配对成功灯效。\n这四类问题对应的检查入口是：充电电平、左右耳回复是否一致、状态判断优先级、两端状态定义。\n","permalink":"https://btio.cn/posts/2026/08/16/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AF%E6%9C%BA%E7%9B%92%E9%80%9A%E4%BF%A1%E9%97%AE%E9%A2%98%E5%8D%95%E5%A4%8D%E7%9B%98/","summary":"\u003cp\u003e本文围绕下面四类问题，整理对应的现象、问题原因和处理结果：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#section-1\"\u003e快速开关盖后异常断连\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-2\"\u003e左右耳回复不一致，盒子灯效频闪\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-3\"\u003e耳机已经进入配对，盒子却不显示配对灯效\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-4\"\u003e配对超时后，盒子显示了错误灯效\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"section-1\"\u003e一、快速开关盖后异常断连\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e快速开关盖时，耳机低概率断开。日志中没有完整的关盖命令，耳机却进入了充电状态。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题原因\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e快速开关盖时，充电盒重新拉起供电。电平持续时间超过原来的防抖阈值，耳机提前判断为稳定入盒。状态切换由充电检测触发，不是串口丢包。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e处理结果\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e将充电电平持续检测时间从约 \u003ccode\u003e600 ms\u003c/code\u003e 延长到 \u003ccode\u003e1 s\u003c/code\u003e，避免短暂电平触发充电模式。连续快速开关盖验证后，未再出现异常断连。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、左右耳回复不一致，盒子灯效频闪\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机进入配对状态并放入盒内后，充电盒白灯闪烁不均匀，部分场景出现频闪或亮度异常。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题原因\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e左右耳返回给充电盒的配对状态不一致。盒子先收到一只耳机的配对状态，又收到另一只耳机的非配对状态，灯效状态被反复切换。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e左耳回复：配对中\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e右耳回复：非配对\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e盒子灯效在两个状态之间反复切换\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e处理结果\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e由从耳同步主耳的配对状态，确保左右耳向充电盒返回一致的数据。双耳状态一致后，配对灯效恢复正常。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、耳机进入配对，盒子却不显示配对灯效\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机已经连接一台手机，再进入配对模式并放入盒内。充电盒只显示耳机电量，没有显示配对灯效。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题原因\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机同时存在“已连接”和“可发现可连接”两个状态。原来的回复逻辑优先返回已连接状态，覆盖了正在配对的状态，盒子因此没有进入配对灯效。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e/* 修改前 */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003ephone_connected\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ereport_connected\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003epairing\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ereport_pairing\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e/* 修改后 */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003epairing\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ereport_pairing\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003ephone_connected\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ereport_connected\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e处理结果\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e回复状态时优先判断配对状态，再判断手机连接状态。修改后，耳机在已连接状态下重新进入配对，盒子能够正常显示配对灯效。\u003c/p\u003e\n\u003ch2 id=\"section-4\"\u003e四、配对超时后，盒子显示了错误灯效\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机在配对状态下入盒，配对超时后，充电盒显示了配对成功灯效，而不是结束灯效。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题原因\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机在配对超时后返回无效状态，充电盒却把这个状态按配对成功处理。数据传输正常，但两端对同一状态的含义没有统一。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e处理结果\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e统一耳机与充电盒对回复状态的定义。收到无效状态时直接结束灯效，不再进入配对成功灯效。\u003c/p\u003e\n\u003cp\u003e这四类问题对应的检查入口是：\u003cstrong\u003e充电电平、左右耳回复是否一致、状态判断优先级、两端状态定义。\u003c/strong\u003e\u003c/p\u003e","title":"【中科蓝讯】机盒通信问题单复盘"},{"content":"机盒通信的主链路是：UART 收包、协议解析、校验与寻址、命令分发、业务执行、应答返回。这篇记录我负责该模块时使用的应用层调用链。\n一、UART 收包与协议解析 %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;充电盒发送数据\u0026#34;] N1[\u0026#34;UART 中断接收字节\u0026lt;br/\u0026gt;并写入环形缓冲区\u0026#34;] N2[\u0026#34;协议状态机\u0026lt;br/\u0026gt;拼接完整数据包\u0026#34;] N3[\u0026#34;应用任务\u0026lt;br/\u0026gt;从缓冲区取包并校验\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; N2 N2 --\u0026gt; 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 中断侧负责收字节和快速识别数据包：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 中断与接收回调\u0026#34;] direction LR R0N0[\u0026#34;uart1_isr()\u0026#34;] R0N1[\u0026#34;uart1_rx_done_cb()\u0026#34;] R0N0 --\u0026gt; R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 解析与接收处理\u0026#34;] direction LR R1N0[\u0026#34;bsp_uart_packet_parse()\u0026#34;] R1N1[\u0026#34;charge_box_packet_recv()\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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) 再从缓冲区取出字节，重新组包、校验并处理命令。\n协议包包含以下字段：\n字段 作用 帧头 判断包起点和数据方向 校验值 检查传输数据 左右耳方向 指定左耳、右耳或双耳 命令 区分开盖、关盖、电量查询、配对等操作 数据长度 指定有效数据长度 数据区 携带电量、版本和配对信息 帧头、方向或长度无效时，解析器丢弃当前包并重新等待帧头。数据包接收完整后，再计算校验值。\n二、充电与非充电两条处理路径 %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;充电流程：① 进入处理\u0026#34;] direction LR R0N0[\u0026#34;charge_box_charge_on_process()\u0026#34;] R0N1[\u0026#34;charge_box_ssw_process(1)\u0026#34;] R0N0 --\u0026gt; R0N1 end class R0N0,R0N1 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;充电流程：② 校验并分发\u0026#34;] direction LR R1N0[\u0026#34;charge_box_analysis_packet_for_charge()\u0026#34;] R1N1[\u0026#34;ux_box_packet_proc_charge()\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 blue; style R1 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b R0 --\u0026gt; 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; %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;非充电流程：① 进入处理\u0026#34;] direction LR R0N0[\u0026#34;charge_box_process()\u0026lt;br/\u0026gt;或蓝牙流程 / 低功耗流程\u0026#34;] R0N1[\u0026#34;charge_box_ssw_process(0)\u0026#34;] R0N0 --\u0026gt; R0N1 end class R0N0,R0N1 green; style R0 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a subgraph R1[\u0026#34;非充电流程：② 校验并分发\u0026#34;] direction LR R1N0[\u0026#34;charge_box_analysis_packet()\u0026#34;] R1N1[\u0026#34;ux_box_packet_proc()\u0026#34;] R1N2[\u0026#34;ux_box_commu_pkt_proc()\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 两条路径共用 UART 缓冲区、协议解析和校验，区别在有效数据包进入哪个业务分发函数。\n两个分发入口都会检查左右耳方向。命令指定左耳时右耳不处理，指定右耳时左耳不处理，指定双耳时两边都可以处理。\n三、主要命令落到哪些业务 机盒命令 耳机侧处理 开盖 更新盒子电量和版本、切换开盖状态、取消关盖退出计时、发起回连 关盖 返回电量、更新关盖状态、停止相关业务、准备退出蓝牙 电量查询 更新盒子电量，返回耳机电量和连接状态 手机配对 返回本机地址和当前配对状态 清除配对 清理手机连接记录并返回执行结果 TWS 组队 交换左右耳信息，更新对耳关系 关机或复位 先返回应答，再执行关机或复位 主从切换、手机回连和灯效不是机盒协议解析本身，而是开关盖、入盒等命令触发的后续业务。\n四、以开盖命令看完整链路 %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 接收并更新盒状态\u0026#34;] direction LR R0N0[\u0026#34;收到开盖数据包\u0026#34;] R0N1[\u0026#34;校验数据\u0026lt;br/\u0026gt;并确认目标耳机\u0026#34;] R0N2[\u0026#34;读取盒子电量和版本\u0026#34;] R0N3[\u0026#34;更新开盖、入盒状态\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 --\u0026gt; R0N3 end class R0N0,R0N1,R0N2,R0N3 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 恢复业务并应答\u0026#34;] direction LR R1N0[\u0026#34;清除未执行的关盖退出计时\u0026#34;] R1N1[\u0026#34;恢复 TWS 或手机回连\u0026#34;] R1N2[\u0026#34;填入耳机电量\u0026lt;br/\u0026gt;配对和 TWS 状态\u0026#34;] R1N3[\u0026#34;重新计算校验\u0026lt;br/\u0026gt;并返回应答\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 --\u0026gt; R1N3 end class R1N0,R1N1,R1N2,R1N3 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 应答包需要重新设置发送帧头、左右耳方向、数据长度和数据区，并在协议规定的时间内发出。关机、复位等命令必须先应答，再改变系统状态。\n机盒通信的核心链路可以固定为：收包 → 组帧 → 校验 → 寻址 → 分发 → 执行业务 → 应答。\n","permalink":"https://btio.cn/posts/2026/08/15/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA%E6%9C%BA%E7%9B%92%E9%80%9A%E4%BF%A1%E9%93%BE%E8%B7%AF/","summary":"\u003cp\u003e机盒通信的主链路是：UART 收包、协议解析、校验与寻址、命令分发、业务执行、应答返回。这篇记录我负责该模块时使用的应用层调用链。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、UART 收包与协议解析\u003c/h2\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart LR\n    N0[\u0026#34;充电盒发送数据\u0026#34;]\n    N1[\u0026#34;UART 中断接收字节\u0026lt;br/\u0026gt;并写入环形缓冲区\u0026#34;]\n    N2[\u0026#34;协议状态机\u0026lt;br/\u0026gt;拼接完整数据包\u0026#34;]\n    N3[\u0026#34;应用任务\u0026lt;br/\u0026gt;从缓冲区取包并校验\u0026#34;]\n    N0 --\u0026gt; N1\n    N1 --\u0026gt; N2\n    N2 --\u0026gt; N3\n    class N0,N1,N2,N3 blue;\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003eUART 中断侧负责收字节和快速识别数据包：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 中断与接收回调\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;uart1_isr()\u0026#34;]\n        R0N1[\u0026#34;uart1_rx_done_cb()\u0026#34;]\n        R0N0 --\u0026gt; R0N1\n    end\n    class R0N0,R0N1 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 解析与接收处理\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;bsp_uart_packet_parse()\u0026#34;]\n        R1N1[\u0026#34;charge_box_packet_recv()\u0026#34;]\n        R1N0 --\u0026gt; R1N1\n    end\n    class R1N0,R1N1 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e中断将字节写入环形缓冲区，快速解析一次以记录收包时间及开盖、心跳等标志。\u003ccode\u003echarge_box_ssw_process(charge_sta)\u003c/code\u003e 再从缓冲区取出字节，重新组包、校验并处理命令。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机机盒通信链路"},{"content":"这篇梳理音乐音量在应用层的处理：手机下发的音量怎样换算为本地等级，左右耳怎样同时生效，以及耳机按键调节后怎样同步手机音量条。通话音量只作区分，不展开 HFP 细节。\n一、手机调节音量后的主流程 一次音量调节要对应下面四处状态：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;手机音量条\u0026#34;] N1[\u0026#34;耳机内部音量级\u0026#34;] N2[\u0026#34;左右耳同步状态\u0026#34;] N3[\u0026#34;DAC 实际输出音量\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; N2 N2 --\u0026gt; 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 使用的数字音量。\n因此，手机显示 50% 不等于把数值 50 直接写入 DAC，而是先换算、再查表。\n这里的“绝对音量”指手机音量条和耳机媒体音量相互对应：任一侧调节，另一侧都要更新。\n手机调节音乐音量后，协议栈将新的媒体音量交给应用层，主线如下：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 接收与换算\u0026#34;] direction LR R0N0[\u0026#34;收到手机设置的媒体音量\u0026#34;] R0N1[\u0026#34;把 0～127\u0026lt;br/\u0026gt;换算为本地音量级\u0026#34;] R0N2[\u0026#34;发起 TWS 定时音量请求\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 定时执行与保存\u0026#34;] direction LR R1N0[\u0026#34;左右耳在约定时间设置 DAC\u0026#34;] R1N1[\u0026#34;更新当前音量\u0026lt;br/\u0026gt;后续再延时保存\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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，而是先发起定时请求；到达定时点后，左右耳再一起设置实际音量。\n非双耳场景只设置本机；双耳连接时，需要两边使用同一个目标值，并在同一时刻生效。\n二、为什么只同步音量值还不够 主耳和从耳最终即使都是音量 10，只要一侧早几十毫秒生效，听感仍会短暂偏向另一侧。双耳同步既要同步目标值，也要同步执行时间。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;主耳收到目标音量\u0026#34;] N1[\u0026#34;把目标和定时信息\u0026lt;br/\u0026gt;交给 TWS\u0026#34;] N2[\u0026#34;左右耳等待\u0026lt;br/\u0026gt;同一个执行点\u0026#34;] N3[\u0026#34;同时更新实际输出音量\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; N2 N2 --\u0026gt; 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; 手机上滑动调节时，两边的起点、目标值和步进也要一致，否则中间过程仍可能一边先变大或先静音。\n排查不同步时，除了最终音量值，还要对比两边收到目标值、进入定时回调和写入 DAC 的时间。\n三、耳机按键调节为什么还要通知手机 %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 识别与计算\u0026#34;] direction LR R0N0[\u0026#34;耳机识别音量加减操作\u0026#34;] R0N1[\u0026#34;判断当前是音乐还是通话\u0026#34;] R0N2[\u0026#34;计算新的本地音量级\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步与上报\u0026#34;] direction LR R1N0[\u0026#34;同步对耳并设置实际音量\u0026#34;] R1N1[\u0026#34;启用绝对音量时\u0026lt;br/\u0026gt;通知手机更新音量条\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 耳机只改本地音量而不通知手机时，手机仍会显示旧音量；下一次下发也可能覆盖耳机当前值。\n音乐主要使用 A2DP/AVRCP 音量，通话使用独立的 HFP 音量。SCO 建立后切到通话音量，结束后恢复音乐音量；排查通话前后突变时，先确认当前取的是哪一份值。\n四、出现异常时怎么顺着链路排查 1. 手机音量条变化，耳机声音没有变化 依次确认：手机音量事件是否到达应用层、协议值是否换算正确、定时回调是否执行、DAC 音量是否写入。\n2. 左右耳声音短暂不一致 先比较两边的目标值，再比较进入定时回调和设置 DAC 的时间，不要只看最终保存的音量。\n3. 耳机按键已经调节，手机音量条没有变化 先确认是否启用绝对音量，再看本地变化后有没有走到手机通知链路。声音变化只说明本地设置成功，不代表手机已同步。\n4. 快速滑动时声音突然跳变 记录手机下发和耳机实际执行的音量序列，先看中间值是否丢失。请求过密时可保留最新目标值并平滑过渡，但左右耳的平滑状态也要一致。\n原文：CSDN · 【中科蓝讯】TWS音量是怎样同步的？ ","permalink":"https://btio.cn/posts/2026/08/14/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws%E9%9F%B3%E9%87%8F%E6%98%AF%E6%80%8E%E6%A0%B7%E5%90%8C%E6%AD%A5%E7%9A%84/","summary":"\u003cp\u003e这篇梳理音乐音量在应用层的处理：手机下发的音量怎样换算为本地等级，左右耳怎样同时生效，以及耳机按键调节后怎样同步手机音量条。通话音量只作区分，不展开 HFP 细节。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、手机调节音量后的主流程\u003c/h2\u003e\n\u003cp\u003e一次音量调节要对应下面四处状态：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart LR\n    N0[\u0026#34;手机音量条\u0026#34;]\n    N1[\u0026#34;耳机内部音量级\u0026#34;]\n    N2[\u0026#34;左右耳同步状态\u0026#34;]\n    N3[\u0026#34;DAC 实际输出音量\u0026#34;]\n    N0 --\u0026gt; N1\n    N1 --\u0026gt; N2\n    N2 --\u0026gt; N3\n    class N0,N1,N2,N3 blue;\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e手机和耳机的音量范围并不相同。AVRCP 绝对音量是 0～127，SDK 内部可配置为 16 级或 32 级，最后还要通过音量表转换为 DAC 使用的数字音量。\u003c/p\u003e\n\u003cp\u003e因此，手机显示 50% 不等于把数值 50 直接写入 DAC，而是先换算、再查表。\u003c/p\u003e\n\u003cp\u003e这里的“绝对音量”指手机音量条和耳机媒体音量相互对应：任一侧调节，另一侧都要更新。\u003c/p\u003e\n\u003cp\u003e手机调节音乐音量后，协议栈将新的媒体音量交给应用层，主线如下：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 接收与换算\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;收到手机设置的媒体音量\u0026#34;]\n        R0N1[\u0026#34;把 0～127\u0026lt;br/\u0026gt;换算为本地音量级\u0026#34;]\n        R0N2[\u0026#34;发起 TWS 定时音量请求\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 定时执行与保存\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;左右耳在约定时间设置 DAC\u0026#34;]\n        R1N1[\u0026#34;更新当前音量\u0026lt;br/\u0026gt;后续再延时保存\u0026#34;]\n        R1N0 --\u0026gt; R1N1\n    end\n    class R1N0,R1N1 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003eTWS 路径不会在当前回调里直接设置 DAC，而是先发起定时请求；到达定时点后，左右耳再一起设置实际音量。\u003c/p\u003e","title":"【中科蓝讯】TWS音量是怎样同步的"},{"content":"写在开始 这篇文章整理项目中比较有代表性的音量问题单。只保留已经写明原因和处理方向的内容，并按音量控制链路归为三类。\n排查音量问题时，可以先确认四个结果：\n1. 收到的目标音量是否正确 ↓ 2. 左右耳保存的音量是否一致 ↓ 3. 两耳应用音量的时间是否一致 ↓ 4. 实际写入硬件的音量是否正确 本文围绕下面三类问题，整理对应的现象和排查思路：\n回连后的初始音量不对 ； 快速调节时双耳音量不同步 ； 出盒恢复时单耳突然变大 。 一、回连后的初始音量不对 问题现象 1\n耳机重新组队并连接手机后，预期使用耳机的默认音量，实际却变成手机之前保存的最大音量。\n排查思路 1\n按时间确认三个值：重新组队后恢复的默认音量、手机首次下发的历史音量，以及最终实际生效的音量。若默认值恢复正确，但最终值变成手机历史值，说明问题出在手机下发后的覆盖。\n耳机重新组队后已恢复到默认音量，但手机连接时下发的历史音量又覆盖了本地值。如果要求重新组队后采用耳机默认音量，可以在首次连接的短暂保护窗口内忽略手机下发的音量，使用耳机的默认音量，同时将默认音量写回给 App。\n问题现象 2\n一拖二场景下，设备 A 已连接耳机且音量调到最大；设备 B 的音量较小，在播放音乐时抢连耳机。抢连成功后，音乐开始时仍按设备 A 的最大音量播放，随后才突然降到设备 B 的低音量。\n排查思路 2\n检查抢连后耳机采用的是哪台设备的音量，以及 A2DP 开始播放前硬件音量是否已经更新。\n已有问题中，设备 B 抢连成功后，硬件音量设置晚于音乐开始，导致开始播放时仍保留设备 A 的最大音量。处理时要在 A2DP 出声前，按设备 B 的音量设置好硬件音量。\n二、快速调节时双耳音量不同步 问题现象 1\n长按手机音量键或快速滑动音量条时，声音会短暂偏向一边；从最大音量降到静音时，一只耳机比另一只更晚无声。反复在最大和最小音量之间切换时，还可能出现音量跳变。\n排查思路 1\n先看手机下发音量后，主从耳是不是在 alarm 到点前就各自更新了本地媒体音量；再对比 alarm 回调里两边实际采用的目标值。\n已有问题中，主从耳在 alarm 到点前更新本地媒体音量的路径不同。快速滑动时，虽然两边在同一时刻进入 alarm 回调，但取到的音量基准已经不一致，所以会出现一边先变大、先变小或更晚静音。\n处理时，不在收到手机音量后立即更新本地媒体音量，而是把换算后的耳机音量等级编码进 alarm 音量包。alarm 到点后，左右耳从同一个包中取出目标值，再统一更新本地状态和实际输出。\n目标值同步后，再由应用层的平滑函数处理听感。它不直接把目标值写到实际输出，而是让当前实际值按定时步进逐步逼近目标值，直到到达目标值。\n这部分平滑方案由我主导设计，已经在实际产品中使用。以目前的测试体验来看，手机快速滑动音量时，双耳变化更连贯，听感也更自然。验证时要同时确认两边目标值、最终音量和变化过程是否一致。\n下图记录了当前实际值逐步逼近目标值的过程：\n三、出盒恢复时单耳突然变大 问题现象 1\n音乐播放中，一只耳机入盒后再次取出，刚恢复声音时会突然偏大，随后才与另一只耳机一致。\n排查思路 1\n先看出盒后的第一段声音，此时音量寄存器是否已经写入安全值，再看后续同步值是否正常。若只在启动瞬间偏大，问题通常出在硬件初始化，而不是两耳保存的音量不同。\n已有问题中，DAC 初始化时音量寄存器默认写成最大值，而且没有经过淡入。处理方向是在 DAC 初始化阶段先把音量设为 0，再由后续音量流程恢复到目标值，避免硬件通路刚打开时直接输出最大音量。\n四、最后总结 音量异常可以先按下面三类判断：\n恢复错误：查本地记忆、手机下发和最终采用值 快速调节异常：查两耳目标值、定时点和硬件设置时间 启动偏大：查 DAC 初始值和淡入过程 先把手机显示、耳机内部值和实际听感分开，再对照左右耳的执行时间，通常就能判断问题是在恢复策略、同步时序，还是最后的硬件音量设置。\n如果你也遇到类似的音量问题，欢迎一起交流排查思路。\n","permalink":"https://btio.cn/posts/2026/08/14/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA%E9%9F%B3%E9%87%8F%E9%97%AE%E9%A2%98%E5%8D%95%E5%A4%8D%E7%9B%98/","summary":"\u003ch2 id=\"preface\"\u003e写在开始\u003c/h2\u003e\n\u003cp\u003e这篇文章整理项目中比较有代表性的音量问题单。只保留已经写明原因和处理方向的内容，并按音量控制链路归为三类。\u003c/p\u003e\n\u003cp\u003e排查音量问题时，可以先确认四个结果：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e1. 收到的目标音量是否正确\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e2. 左右耳保存的音量是否一致\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e3. 两耳应用音量的时间是否一致\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e4. 实际写入硬件的音量是否正确\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e本文围绕下面三类问题，整理对应的现象和排查思路：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#section-1\"\u003e回连后的初始音量不对\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-2\"\u003e快速调节时双耳音量不同步\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-3\"\u003e出盒恢复时单耳突然变大\u003c/a\u003e\n。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"section-1\"\u003e一、回连后的初始音量不对\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机重新组队并连接手机后，预期使用耳机的默认音量，实际却变成手机之前保存的最大音量。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e按时间确认三个值：重新组队后恢复的默认音量、手机首次下发的历史音量，以及最终实际生效的音量。若默认值恢复正确，但最终值变成手机历史值，说明问题出在手机下发后的覆盖。\u003c/p\u003e\n\u003cp\u003e耳机重新组队后已恢复到默认音量，但手机连接时下发的历史音量又覆盖了本地值。如果要求重新组队后采用耳机默认音量，可以在首次连接的短暂保护窗口内忽略手机下发的音量，使用耳机的默认音量，同时将默认音量写回给 App。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e一拖二场景下，设备 A 已连接耳机且音量调到最大；设备 B 的音量较小，在播放音乐时抢连耳机。抢连成功后，音乐开始时仍按设备 A 的最大音量播放，随后才突然降到设备 B 的低音量。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e检查抢连后耳机采用的是哪台设备的音量，以及 A2DP 开始播放前硬件音量是否已经更新。\u003c/p\u003e\n\u003cp\u003e已有问题中，设备 B 抢连成功后，硬件音量设置晚于音乐开始，导致开始播放时仍保留设备 A 的最大音量。处理时要在 A2DP 出声前，按设备 B 的音量设置好硬件音量。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、快速调节时双耳音量不同步\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e长按手机音量键或快速滑动音量条时，声音会短暂偏向一边；从最大音量降到静音时，一只耳机比另一只更晚无声。反复在最大和最小音量之间切换时，还可能出现音量跳变。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先看手机下发音量后，主从耳是不是在 alarm 到点前就各自更新了本地媒体音量；再对比 alarm 回调里两边实际采用的目标值。\u003c/p\u003e\n\u003cp\u003e已有问题中，主从耳在 alarm 到点前更新本地媒体音量的路径不同。快速滑动时，虽然两边在同一时刻进入 alarm 回调，但取到的音量基准已经不一致，所以会出现一边先变大、先变小或更晚静音。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机音量问题单复盘"},{"content":"最开始看原厂 SDK 时，我觉得它的切换很简单：确定关闭、降噪或通透，再调用模式接口即可。\n随着 App 控制、按键配置、单双耳记忆和出入盒切换逐渐加进来，我才发现应用层维护的并不只是一个 ANC 模式。提示音何时播放、左右耳怎样同步、状态什么时候保存和上报，也都在这条链路里。\n一、先把 ANC 的几条链路放在一起 ANC 模式可能因为三类事件发生变化：\nApp 下发一个明确的目标模式； 用户通过耳机按键切换模式； 耳机出入盒或 TWS 连接状态变化，需要重新选择单双耳记忆。 把三个入口放在一起，我目前是这样理解的：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 确定目标模式\u0026#34;] direction LR R0N0[\u0026#34;收到 ANC 相关事件\u0026lt;br/\u0026gt;App 指令、按键事件\u0026lt;br/\u0026gt;出入盒或 TWS 状态变化\u0026#34;] R0N1[\u0026#34;确定当前使用场景\u0026lt;br/\u0026gt;单耳、双耳\u0026lt;br/\u0026gt;或者双耳都在盒内\u0026#34;] R0N2[\u0026#34;得到本次目标模式\u0026lt;br/\u0026gt;关闭、通透、不同等级的降噪\u0026lt;br/\u0026gt;或自适应降噪\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步并应用\u0026#34;] direction LR R1N0[\u0026#34;更新并同步状态\u0026lt;br/\u0026gt;选择或更新单耳、双耳记忆\u0026lt;br/\u0026gt;必要时通知对耳\u0026#34;] R1N1[\u0026#34;处理提示音并应用模式\u0026lt;br/\u0026gt;按触发来源处理提示音\u0026lt;br/\u0026gt;再转换成 SDK 模式\u0026#34;] R1N2[\u0026#34;保存并更新 App\u0026lt;br/\u0026gt;写入记忆；按键或状态变化时\u0026lt;br/\u0026gt;更新 App 显示\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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、按键和状态变化的处理顺序并不完全相同，后面再分别展开。\n二、应用层同时维护了哪些 ANC 状态 结合当前代码看，我会把 ANC 状态分成四层：\n状态 记录的内容 单耳记忆 单耳场景使用的模式和降噪等级 双耳记忆 双耳共同使用时的模式和降噪等级 当前运行状态 ANC 硬件此时真正运行的模式 App 显示状态 根据当前使用场景返回给 App 的模式 这几层状态大多数时候相同，但不能混为一个变量。以当前实现为例，恢复出厂后的双耳记忆是强降噪，单耳记忆是通透。双耳都在盒外并完成 TWS 连接时使用双耳记忆，只有一只耳机使用时恢复单耳记忆。\n记忆状态也不等于实时状态。双耳都在盒内时，ANC 硬件和 App 显示为关闭，但两份记忆仍然保留，出盒后再按当前场景恢复。每份记忆还会保存降噪等级，因此从通透切回降噪时，可以恢复之前的强、中、低或自适应降噪。\n三、App 下发和按键切换走的是两条路径 App 和按键最后都会改变 ANC，但处理方式不一样。\nApp 下发路径\nApp 已经给出了目标模式。耳机收到指令后，只需要判断这次修改的是单耳记忆还是双耳记忆。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 接收与更新\u0026#34;] direction LR R0N0[\u0026#34;App 下发目标模式和等级\u0026#34;] R0N1[\u0026#34;判断当前是单耳\u0026lt;br/\u0026gt;还是双耳场景\u0026#34;] R0N2[\u0026#34;更新对应的 ANC 记忆\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步与生效\u0026#34;] direction LR R1N0[\u0026#34;把同一状态同步给对耳\u0026#34;] R1N1[\u0026#34;回复当前 App\u0026lt;br/\u0026gt;双 App 场景下更新另一端\u0026#34;] R1N2[\u0026#34;经过防抖后\u0026lt;br/\u0026gt;播放提示音并应用实际模式\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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”的标记。这样提示音结束后只应用实际模式，不会重复修改记忆和上报同一次选择。\n按键切换路径\n按键路径需要先读取用户在 App 中配置的切换组合，当前代码支持：\n关闭、降噪和通透循环； 关闭和降噪切换； 关闭和通透切换； 降噪和通透切换。 如果下一步进入降噪，还会读取当前场景保存的降噪等级，选择对应提示音。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 确定切换目标\u0026#34;] direction LR R0N0[\u0026#34;识别按键配置和当前运行模式\u0026#34;] R0N1[\u0026#34;计算下一个模式\u0026#34;] R0N2[\u0026#34;选择对应的 ANC 提示音\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步与生效\u0026#34;] direction LR R1N0[\u0026#34;由 TWS 主耳协调双耳播放\u0026#34;] R1N1[\u0026#34;提示音结束后\u0026lt;br/\u0026gt;更新单双耳记忆\u0026#34;] R1N2[\u0026#34;应用实际模式\u0026lt;br/\u0026gt;保存并上报 App\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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 路径已经更新过状态，回调只应用实际模式；按键路径还要更新记忆、保存并上报 App。从耳发起按键时，提示音请求会交给主耳协调，最终由主耳完成上报。\n四、单双耳记忆怎样跟随出入盒和 TWS 变化 当前使用哪份 ANC 记忆，主要看 TWS 是否连接，以及两只耳机是否都在盒外。\nTWS 状态 出入盒状态 当前处理 未连接 当前耳机在盒外 使用单耳记忆 已连接 一只在盒内，一只在盒外 盒外耳机使用单耳记忆 已连接 两只都在盒外 两边使用双耳记忆 已连接 两只都在盒内 当前 ANC 关闭，保留两份记忆 假设单耳记忆为通透，双耳记忆为降噪，一次快速出盒可能经历下面几个状态：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 仍按单耳记忆处理\u0026#34;] direction LR R0N0[\u0026#34;一只耳机在盒外\u0026lt;br/\u0026gt;另一只在盒内\u0026#34;] R0N1[\u0026#34;使用单耳记忆：通透\u0026#34;] R0N2[\u0026#34;另一只耳机出盒\u0026lt;br/\u0026gt;但 TWS 尚未连接\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步后重新判断\u0026#34;] direction LR R1N0[\u0026#34;暂时仍按单耳记忆处理\u0026#34;] R1N1[\u0026#34;TWS 连接\u0026lt;br/\u0026gt;并同步完对耳状态\u0026#34;] R1N2[\u0026#34;重新判断为双耳场景\u0026lt;br/\u0026gt;恢复双耳记忆：降噪\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 end class R1N0,R1N1,R1N2 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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 可能还没有回连，如果只判断一次，后面就可能一直停留在单耳模式。因此，本耳出盒、对耳出入盒、TWS 断开和重连时，都要重新判断当前应该使用哪份记忆。尤其是 TWS 完成连接后，需要再补一次 ANC 判断，处理快速出盒时来不及切换的问题。\nTWS 连接时还会同步两份 ANC 记忆。主耳把单耳、双耳模式和对应等级发给从耳，从耳更新并保存，保证重新组队后的状态一致。\n五、提示音和 App 防抖也是切换链路的一部分 在 App 和按键路径中，提示音结束回调还会把提示音资源转换成 ANC 模式，再调用 ANC 接口。因此，提示音的来源和播放顺序也会影响最终结果。双耳连接时，从耳的提示音请求会交给主耳协调，保证两边同步播放。\nApp 快速连续切换时，当前代码做了两层处理：\nApp 防抖：每条指令仍然更新最新记忆并同步对耳，但中间操作不逐个播放提示音；停止操作 1 秒后，再执行最后一次选择。 队列清理：新的 ANC 提示音入队前，先清理队列中等待播放的旧 ANC 提示音。 前者合并连续操作，后者清掉已经进入提示音模块的旧请求，目的都是让最终模式尽快与 App 的最后一次选择保持一致。\n六、最后落到硬件、保存和 App 显示 应用层确定目标后，还要转换成 SDK 使用的模式。原厂 SDK 最基本的是关闭、降噪和通透；当前实现增加了中、低和自适应降噪，它们仍属于降噪模式，只是加载的参数不同。\n进入 ANC 接口后，先检查功能是否使能、本耳是否在盒内，以及是否需要重复设置。随后根据目标模式停止 ANC，或者加载降噪、通透参数并启动 ANC。盒内耳机会跳过硬件操作，但记忆仍可保留，等待出盒后恢复。\n最后，单耳和双耳记忆写入 Flash，App 则根据当前场景显示关闭、单耳记忆或双耳记忆。主动上报由主耳完成，避免左右耳重复通知。\n回头看这条链路，我觉得核心就是把六个环节对应起来：触发来源、单双耳场景、目标记忆、提示音、硬件模式和 App 状态。如果模式链路已经正确，实际降噪效果仍有差异，再继续检查声学参数、校准值和算法状态。\n原文：CSDN · 【中科蓝讯】ANC 模式是怎样切换的 ","permalink":"https://btio.cn/posts/2026/08/12/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFanc-%E6%A8%A1%E5%BC%8F%E6%98%AF%E6%80%8E%E6%A0%B7%E5%88%87%E6%8D%A2%E7%9A%84/","summary":"\u003cp\u003e最开始看原厂 SDK 时，我觉得它的切换很简单：确定关闭、降噪或通透，再调用模式接口即可。\u003c/p\u003e\n\u003cp\u003e随着 App 控制、按键配置、单双耳记忆和出入盒切换逐渐加进来，我才发现应用层维护的并不只是一个 ANC 模式。提示音何时播放、左右耳怎样同步、状态什么时候保存和上报，也都在这条链路里。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、先把 ANC 的几条链路放在一起\u003c/h2\u003e\n\u003cp\u003eANC 模式可能因为三类事件发生变化：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eApp 下发一个明确的目标模式；\u003c/li\u003e\n\u003cli\u003e用户通过耳机按键切换模式；\u003c/li\u003e\n\u003cli\u003e耳机出入盒或 TWS 连接状态变化，需要重新选择单双耳记忆。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e把三个入口放在一起，我目前是这样理解的：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 确定目标模式\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;收到 ANC 相关事件\u0026lt;br/\u0026gt;App 指令、按键事件\u0026lt;br/\u0026gt;出入盒或 TWS 状态变化\u0026#34;]\n        R0N1[\u0026#34;确定当前使用场景\u0026lt;br/\u0026gt;单耳、双耳\u0026lt;br/\u0026gt;或者双耳都在盒内\u0026#34;]\n        R0N2[\u0026#34;得到本次目标模式\u0026lt;br/\u0026gt;关闭、通透、不同等级的降噪\u0026lt;br/\u0026gt;或自适应降噪\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 同步并应用\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;更新并同步状态\u0026lt;br/\u0026gt;选择或更新单耳、双耳记忆\u0026lt;br/\u0026gt;必要时通知对耳\u0026#34;]\n        R1N1[\u0026#34;处理提示音并应用模式\u0026lt;br/\u0026gt;按触发来源处理提示音\u0026lt;br/\u0026gt;再转换成 SDK 模式\u0026#34;]\n        R1N2[\u0026#34;保存并更新 App\u0026lt;br/\u0026gt;写入记忆；按键或状态变化时\u0026lt;br/\u0026gt;更新 App 显示\u0026#34;]\n        R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2\n    end\n    class R1N0,R1N1,R1N2 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e这是一条方便理解的主线。实际代码里，App、按键和状态变化的处理顺序并不完全相同，后面再分别展开。\u003c/p\u003e","title":"【中科蓝讯】ANC 模式是怎样切换的"},{"content":"写在开始 这篇文章整理项目中比较有代表性的 ANC 问题单，并按问题现象归为五类。\n有些问题标题虽然带有 ANC，但最后确认是电量、功耗或连接问题，这里不展开。本文只保留能够放回 ANC 控制链路解释的问题，没有明确结论的地方也不补充猜测。\n先把 ANC 控制流程简化成四步：\n1. 收到切换请求 ↓ 2. 确定并同步目标模式 ↓ 3. 加载参数，更新实际效果 ↓ 4. 保存状态并上报 App 本文围绕下面五类问题，整理对应的问题现象和排查思路：\n实际模式正常，App 显示却不对 ； 单耳和双耳切换后，ANC 状态混乱 ； 按键或 App 操作后，ANC 没有切换 ； 模式切换成功，降噪效果却不对 ； 切换 ANC 时出现杂音、短暂无声或突变 。 一、实际模式正常，App 显示却不对 问题现象 1\n从耳长按切换 ANC 后，耳机实际已经切换，App 仍显示原来的模式；\n排查思路 1\n先确认三个结果：从耳是否识别到长按、左右耳实际模式是否变化、主耳是否向 App 上报了新状态。\n如果从耳和两耳实际模式都正常，重点查主耳是否收到新的目标模式、更新本地状态并上报 App。问题单中，从耳已经完成 ANC 切换，但主耳没有更新状态并发送新模式，因此问题出在主耳的状态同步和上报链路。\n问题现象 2\n耳机实际是高降噪，App 却显示低降噪；重新进入 App 后，等级还会发生跳变。\n排查思路 2\n对照耳机内部等级、协议值和 App 显示值是否一致。问题单中，耳机使用 1 表示低降噪、3 表示高降噪，App 却映射成1是高降噪，3是低降噪，导致高低等级颠倒。这是状态映射错误，不是 ANC 参数没有生效。\n二、单耳和双耳切换后，ANC 状态混乱 问题现象 1\n开盖后迅速取出一只耳机，两耳随后已经连接，ANC 却仍保持单耳模式，没有恢复双耳记忆状态。\n排查思路 1\n重点看出盒事件和 TWS 回连的先后顺序。问题单中，耳机开盖后快速出盒，出盒事件先触发了 ANC 评估；由于此时 TWS 还没有建立连接，系统按单耳状态选择了模式。随后两耳虽然完成了 TWS 连接，但没有重新评估 ANC，最终继续使用单耳记忆模式。\n处理时要在 TWS 建立连接并完成状态同步后，重新评估 ANC 模式。如果重新评估出的模式与当前模式不一致，就更新 ANC 状态，并将新状态重新上报给 App。\n问题现象 2\nApp 切换 ANC 后，快速将一只耳机入盒，之后单耳使用时，左右耳保存的 ANC 模式不一致。\n排查思路 2\n先查两只耳机开机时从 Flash 恢复的 anc_local 是否一致。如果开机时就不一致，说明问题发生在上一次 ANC 状态同步或保存阶段。重点检查 App 下发或单耳切换后，TWS 是否把状态同步给对耳，以及两边是否都完成了状态更新和保存。\n问题单中已经确认，App 切换 ANC 后快速将一只耳机入盒，App 下发的状态没有及时同步给从耳，导致两只耳机后续保存了不同的单耳 ANC 状态。处理时要确保 TWS 同步成功、对耳完成更新后，再进入后续保存流程；如果开机时两边的值一致，运行后才出现差异，再检查本次 TWS 同步和模式更新。\n三、按键或 App 操作后，ANC 没有切换 问题现象 1\n耳机恢复出厂后，长按可以识别，但无法切换 ANC。\n排查思路 1\n先看长按后有没有产生 ANC 切换请求。问题单中，恢复出厂后按键表没有同步，长按没有正确对应 ANC 功能。\n可以分两种情况：\n没有产生 ANC 切换请求：查按键识别、按键表和 App 按键配置； 已经产生正确请求：再查目标模式和 ANC 更新流程。 四、模式切换成功，降噪效果却不对 问题现象 1\nApp 可以切换低、中、高降噪，但实际效果顺序不对，例如中降噪反而比高降噪更强。\n排查思路 1\n先在相同噪声环境下分别测试低、中、高降噪，再检查每个等级实际加载的参数是否对应。问题单中是等级和参数对应错误，导致中降噪加载了更强的参数。修正对应关系后，低、中、高的效果恢复为逐级增强。\n问题现象 2\n左右耳都显示同一个模式，但降噪强度明显不同。\n排查思路 2\n模式一致只说明控制状态相同，还要继续比较左右耳加载的参数、麦克风校准值和声学一致性。问题单最终通过声学参数校准处理；如果控制日志一致，问题通常不在应用层状态机。\n问题现象 3\n通透模式持续有进气声，或者用手遮住耳机后出现啸叫。\n排查思路 3\n优先检查声学参数、麦克风通路和防啸叫能力。问题单中分别通过更新声学参数、加入防啸叫算法改善；如果启用算法受内存限制，还要确认当前资源是否足够。\n五、切换 ANC 时出现杂音、短暂无声或突变 问题现象 1\n切换到降噪后出现明显杂音或底噪。\n排查思路 1\n先区分杂音只出现在开启瞬间，还是会持续存在。瞬间杂音可先检查 ANC 淡入，持续底噪则继续查声学参数和麦克风通路。\n问题现象 2\n通话中从降噪切到通透，一只耳机短暂无声。\n排查思路 2\n通话中切换 ANC 会重新配置音频链路，两耳更新时机不一致时，可能出现单耳短暂无声。分别记录两耳收到目标模式、开始更新参数和恢复通话音频的时间，判断是单耳更新失败，还是两耳时间差过大。问题单的处理方向是加强 TWS 同步。\n问题现象 3\n在高、中、低降噪之间切换时，会短暂听到类似通透的效果。\n排查思路 3\n这是滤波器切换过程中的短暂听感，可通过调整淡入淡出步长改善。验证时要同时关注切换时长、突变感和听感平滑度。\n六、最后总结 ANC 问题看起来都是“降噪不对”，排查时可以先分三层：\n请求层：按键或 App 是否给出了正确目标模式 状态层：左右耳、保存值和 App 显示是否一致 效果层：参数、校准和音频通路是否真正更新 按键不能切换时先查请求；单耳、双耳或 App 显示混乱时查状态同步；模式正确但听感不对时，再进入参数、校准和算法。先分层，再看日志，通常比一开始就在 ANC 底层里找原因更清楚。\n","permalink":"https://btio.cn/posts/2026/08/12/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA-anc-%E9%97%AE%E9%A2%98%E5%8D%95%E5%A4%8D%E7%9B%98/","summary":"\u003ch2 id=\"preface\"\u003e写在开始\u003c/h2\u003e\n\u003cp\u003e这篇文章整理项目中比较有代表性的 ANC 问题单，并按问题现象归为五类。\u003c/p\u003e\n\u003cp\u003e有些问题标题虽然带有 ANC，但最后确认是电量、功耗或连接问题，这里不展开。本文只保留能够放回 ANC 控制链路解释的问题，没有明确结论的地方也不补充猜测。\u003c/p\u003e\n\u003cp\u003e先把 ANC 控制流程简化成四步：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e1. 收到切换请求\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e2. 确定并同步目标模式\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e3. 加载参数，更新实际效果\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e4. 保存状态并上报 App\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e本文围绕下面五类问题，整理对应的问题现象和排查思路：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#section-1\"\u003e实际模式正常，App 显示却不对\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-2\"\u003e单耳和双耳切换后，ANC 状态混乱\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-3\"\u003e按键或 App 操作后，ANC 没有切换\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-4\"\u003e模式切换成功，降噪效果却不对\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-5\"\u003e切换 ANC 时出现杂音、短暂无声或突变\u003c/a\u003e\n。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"section-1\"\u003e一、实际模式正常，App 显示却不对\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e从耳长按切换 ANC 后，耳机实际已经切换，App 仍显示原来的模式；\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先确认三个结果：从耳是否识别到长按、左右耳实际模式是否变化、主耳是否向 App 上报了新状态。\u003c/p\u003e\n\u003cp\u003e如果从耳和两耳实际模式都正常，重点查主耳是否收到新的目标模式、更新本地状态并上报 App。问题单中，从耳已经完成 ANC 切换，但主耳没有更新状态并发送新模式，因此问题出在主耳的状态同步和上报链路。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机实际是高降噪，App 却显示低降噪；重新进入 App 后，等级还会发生跳变。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e对照耳机内部等级、协议值和 App 显示值是否一致。问题单中，耳机使用 \u003ccode\u003e1\u003c/code\u003e 表示低降噪、\u003ccode\u003e3\u003c/code\u003e 表示高降噪，App 却映射成1是高降噪，3是低降噪，导致高低等级颠倒。这是状态映射错误，不是 ANC 参数没有生效。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机 ANC 问题单复盘"},{"content":"一、本文要解决哪些提示音问题 这篇文章记录的是我对 TWS 耳机提示音问题的整理和理解。我结合问题单、原厂 SDK 以及实际修改点，把零散的问题按现象和根因重新归类，并从业务请求、队列、播放到状态恢复，整理出一套排查思路。\n具体问题的修改结论，以问题单和现有代码记录为准；没有明确依据的地方，我不额外延伸。理解提示音问题，可以先把流程分成四步：\n1. 业务事件发生 ↓ 2. 提示音请求进入队列 ↓ 3. 提示音开始播放 ↓ 4. 播放结束，恢复音频状态 有些提示音只是给用户提供声音反馈，有些提示音还和业务回调绑定在一起。原厂 SDK 中，ANC 和音乐/游戏模式会在提示音播放完成后更新；接听、拒接和回拨也会在提示音相关回调中触发。\n所以排查时不能只看“有没有声音”，还要看对应的业务回调是否按预期执行。遇到问题时，先分清是“声音没有正常播放”，还是“提示音对应的业务动作没有执行”：前者检查资源、音量和播放链路，后者检查业务请求和回调。\n本文主要围绕以下 7 类问题展开，后文会分别说明问题现象、原因和排查思路：\n业务已经发生，提示音却没响或播错 ； 提示音太大、太小，或换资源后异常 ； 连续或快速操作后，提示音消失、滞后或重复 ； 提示音响错耳朵 ； 双耳提示音不同步，或音乐恢复不同步 ； 提示音响完，音乐却出现杂音或无声 ； 来电和双连接时，响铃状态混乱 。 二、业务已经发生，提示音却没响或播错 问题现象 1\n电量已经低于 10%，却没有低电提示音。\n排查思路 1\n先确认低电条件是否真的触发。比如电量从 11% 直接变成 9% 时，可能跳过“等于 10%”的判断，后面就不会提交低电提示音。确认条件后，再检查代码是否走到提示音调用位置。\n问题现象 2\n来电双击已经接听，却播放成三击提示音。\n排查思路 2\n这个问题说明提示音请求已经提交，但事件和提示音没有正确对应。重点检查双击事件最终传入的提示音资源编号，确认是否误用了三击提示音。\n这类问题可以按“业务条件、提示音调用、资源编号”的顺序检查。前三步没有问题后，再进入队列和播放流程。\n三、提示音太大、太小，或换资源后异常 问题现象 1\n通过 App 的“查找耳机”功能播放正常，但在手机设置中的 Google“查找设备”页面，点击“让耳机响铃”时声音偏小。\n排查思路 1\n这个问题中，Google“查找设备”使用了手机提示音的音量，没有和 App 的“查找耳机”使用同一套音量设置。排查时要比较两个入口实际使用的资源和音量来源。\n问题现象 2\n音乐播放中进入配对，提示音响起时音乐短暂无声。\n排查思路 2\n问题单 246 中，配对提示音使用 MP3 时，原厂 SDK 会先停止当前音乐，提示音播放结束后再恢复音乐。停止和恢复之间存在一段时间，因此出现了约 2 秒无声。改成 WAV 后，提示音没有走 MP3 分支中的音乐停止和恢复流程，所以这个场景恢复正常。\n这里的结论针对当前 SDK 的播放实现，并不是说 MP3 格式本身一定会打断音乐。遇到类似问题时，要重点检查提示音格式对应的播放分支，以及它是否会暂停或停止背景音乐。\n问题现象 3\n替换提示音文件后，耳机仍然播放旧声音。\n排查思路 3\n先确认新资源是否成功编译，再确认包含新资源的固件是否真正烧录或升级到设备。前两步没有问题后，再检查提示音编号和资源文件的映射关系。\n四、连续或快速操作后，提示音消失、滞后或重复 问题现象 1\n快速切换降噪和通透后，提示音越来越滞后，几秒后状态又被切回。\n排查思路 1\n快速切换 ANC 时，旧提示音可能还没有播放，新请求又进入队列。由于 ANC 状态在提示音结束回调中更新，旧提示音播放完成后可能再次执行旧状态，于是出现提示音滞后或状态回跳。处理时可以在入队前过滤已经过期的同类请求。\n问题现象 2\n反复接听、挂断几次后，双击和三击仍有效，但没有提示音。\n排查思路 2\n重点检查队列长度和入队结果。已有问题确认过队列计数只增加、没有正确减少，几次操作后队列被认为已满，后续请求就无法进入。\n排查时记录请求、入队、出队、开始播放和结束回调，确认旧请求是否还在队列，以及计数是否成对变化。ANC 这类只需要保留最新状态的提示音，可以过滤旧请求；连接、断连等提示音是否能清理，要按业务设计确认。\n五、提示音响错耳朵 问题现象\n只双击左耳，左右耳却同时播放触控反馈音。\n排查思路\n先确认这段提示音是否需要两耳播放。如果不需要，就调用本地播放接口，例如 bsp_res_play_local()，只让操作耳播放，不通知另一只耳机，也不做两耳同步。触控反馈通常就属于这种情况。\n只有连接、断连和共同模式变化等需要两耳反馈的提示音，才使用双耳播放和同步流程。出现两边同时响时，先检查播放接口和播放范围是否用错，再检查双耳同步时序。\n六、双耳提示音不同步 问题现象 1\n回连或来电时，两只耳机都有声音，但一只耳明显先响。\n排查思路 1\n先通过日志分别记录左右耳提示音实际开始播放的时间，确认两边相差多少。如果时间差比较大，先检查当前使用的同步方式，确认是否已经使用 alarm 级同步。\n如果已经使用 alarm 级同步，但仍然偶尔出现明显延迟，就继续检查蓝牙任务、算法或其他任务是否阻塞了提示音准备流程。线程过于繁忙时，一侧可能准备或启动得更晚，导致播放滞后。\n问题现象 2\n提示音播放后，音乐也是一只耳先恢复。\n排查思路 2\n这不一定是某一只耳机没有重新打开音乐通路，更可能是两只耳机恢复音乐的时间不同。提示音播放前，耳机要先让音乐暂时让路；提示音结束后，再恢复音乐。如果一只耳机先完成这套流程，就会出现一只耳先恢复的现象。\n排查时记录两耳进入音频暂避、开始播放、结束播放和恢复音乐的时间。如果两边都执行了恢复动作，但时间差比较大，就继续检查提示音同步和两耳结束确认。问题单最终确认是提示音不同步导致两耳音频暂避时间不一致，最后音乐恢复不一致，处理方向也是优化提示音同步流程，修复后测试通过。\n七、提示音响完，音乐却出现杂音或无声 问题现象 1\n连接提示音结束时出现“啪”声。\n排查思路 1\n重点检查提示音尾部、缓存，以及音频通路关闭的时机。提示音结束时直接关闭通路，可能产生杂音。\n问题现象 2\n停止查找耳机后，后续音乐一直无声。\n排查思路 2\n重点检查 DAC（音频输出）、静音和音乐通路是否恢复。已有问题确认过，查找结束后 DAC 没有恢复，会导致后续音乐无声。\n问题现象 3\n音乐暂停、恢复后，其中一只耳保持无声。\n排查思路 3\n检查两耳的淡入淡出状态和音频通路是否一致。如果结束后音量不对，还要确认提示音播放期间是否阻塞了正常的音量消息。\n原厂 SDK 播放提示音前会保存播放状态、音量、静音和 DAC 状态，结束后再恢复。因此，这类问题要重点检查提示音结束后的恢复过程。\n八、来电和双连接时，响铃状态混乱 问题现象 1\n电话接通后，耳机仍在响铃，随后单耳或双耳无声。\n排查思路 1\n按时间顺序检查来电、SCO（通话音频链路）建立、开始响铃、接听、退出响铃和通话音频打开。重点确认 ring（响铃状态）是否退出，以及两耳通话通路是否都打开。\n问题现象 2\n两台手机先后来电时，手机铃声和耳机本地铃声混在一起。\n排查思路 2\n分别记录两台手机的来电和 SCO 状态，再确认当前应该播放手机铃声还是耳机本地铃声。这个问题不能只看提示音资源，还要看双连接时的响铃状态切换。\n问题现象 3\n手机没有及时建立铃声通路时，耳机播放本地铃声。\n排查思路 3\n手机没有及时建立 SCO 时，耳机回退播放本地铃声可能是设计行为。确认属于正常回退后，不要直接按“提示音播错”处理。\n九、最后总结 把提示音问题放回完整链路看，排查顺序可以归纳为：先确认业务请求和资源，再看队列与播放范围；双耳场景重点看同步；提示音结束后，再确认音乐、音频通路和通话状态是否恢复。\n","permalink":"https://btio.cn/posts/2026/08/11/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA%E6%8F%90%E7%A4%BA%E9%9F%B3%E9%97%AE%E9%A2%98%E5%8D%95%E5%A4%8D%E7%9B%98/","summary":"\u003ch2 id=\"section-1\"\u003e一、本文要解决哪些提示音问题\u003c/h2\u003e\n\u003cp\u003e这篇文章记录的是我对 TWS 耳机提示音问题的整理和理解。我结合问题单、原厂 SDK 以及实际修改点，把零散的问题按现象和根因重新归类，并从业务请求、队列、播放到状态恢复，整理出一套排查思路。\u003c/p\u003e\n\u003cp\u003e具体问题的修改结论，以问题单和现有代码记录为准；没有明确依据的地方，我不额外延伸。理解提示音问题，可以先把流程分成四步：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e1. 业务事件发生\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e2. 提示音请求进入队列\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e3. 提示音开始播放\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e4. 播放结束，恢复音频状态\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e有些提示音只是给用户提供声音反馈，有些提示音还和业务回调绑定在一起。原厂 SDK 中，ANC 和音乐/游戏模式会在提示音播放完成后更新；接听、拒接和回拨也会在提示音相关回调中触发。\u003c/p\u003e\n\u003cp\u003e所以排查时不能只看“有没有声音”，还要看对应的业务回调是否按预期执行。遇到问题时，先分清是“声音没有正常播放”，还是“提示音对应的业务动作没有执行”：前者检查资源、音量和播放链路，后者检查业务请求和回调。\u003c/p\u003e\n\u003cp\u003e本文主要围绕以下 7 类问题展开，后文会分别说明问题现象、原因和排查思路：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#section-2\"\u003e业务已经发生，提示音却没响或播错\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-3\"\u003e提示音太大、太小，或换资源后异常\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-4\"\u003e连续或快速操作后，提示音消失、滞后或重复\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-5\"\u003e提示音响错耳朵\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-6\"\u003e双耳提示音不同步，或音乐恢复不同步\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-7\"\u003e提示音响完，音乐却出现杂音或无声\u003c/a\u003e\n；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#section-8\"\u003e来电和双连接时，响铃状态混乱\u003c/a\u003e\n。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"section-2\"\u003e二、业务已经发生，提示音却没响或播错\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e电量已经低于 10%，却没有低电提示音。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先确认低电条件是否真的触发。比如电量从 11% 直接变成 9% 时，可能跳过“等于 10%”的判断，后面就不会提交低电提示音。确认条件后，再检查代码是否走到提示音调用位置。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e来电双击已经接听，却播放成三击提示音。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路 2\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这个问题说明提示音请求已经提交，但事件和提示音没有正确对应。重点检查双击事件最终传入的提示音资源编号，确认是否误用了三击提示音。\u003c/p\u003e\n\u003cp\u003e这类问题可以按“业务条件、提示音调用、资源编号”的顺序检查。前三步没有问题后，再进入队列和播放流程。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、提示音太大、太小，或换资源后异常\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e问题现象 1\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e通过 App 的“查找耳机”功能播放正常，但在手机设置中的 Google“查找设备”页面，点击“让耳机响铃”时声音偏小。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机提示音问题单复盘"},{"content":"在 TWS 耳机里，开机、蓝牙连接、低电量和降噪模式切换，通常都会伴随一段提示音。\n从表面看，提示音像是准备一个音频文件，需要时调用播放函数即可。实际上，一段提示音从代码触发到左右耳真正出声，中间还要经过资源查找、播放排队、双耳同步和音频恢复。\n本文以中科蓝讯 BT891x 原厂 SDK 的实现为例，重点分析提示音播放的核心流程。不同 SDK 的接口名称可能不一样，但整体思路通常可以互相参考。\n一、提示音播放的完整链路 一段提示音的完整流程，可以先简化成下面五步：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 准备播放请求\u0026#34;] direction LR R0N0[\u0026#34;业务触发\u0026#34;] R0N1[\u0026#34;找到对应的提示音资源\u0026#34;] R0N2[\u0026#34;把播放请求加入队列\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 同步与播放\u0026#34;] direction LR R1N0[\u0026#34;确定哪只耳机播放\u0026lt;br/\u0026gt;并完成双耳同步\u0026#34;] R1N1[\u0026#34;解码播放\u0026lt;br/\u0026gt;结束后恢复原来的音频状态\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 比如手机连接耳机成功以后，蓝牙业务只需要提出“播放连接成功提示音”这个请求。至于提示音文件放在哪里、左右耳什么时候开始播放、当前音乐要不要降低音量，都会继续交给提示音模块处理。\n后面的内容就按这五步展开。\n二、更换提示音后，为什么需要重新编译 更换提示音后，不能只把一个新的 MP3 或 WAV 文件复制到工程里。耳机固件不像电脑一样，能按文件路径临时打开音频；它只能读取已经写入 Flash 资源区的数据。\n在原厂工程中，提示音素材先放在专门的资源目录。编译时，构建脚本会根据语言和功能配置，选择需要的素材，打包成固件可以读取的二进制资源，再随最终固件一起烧录。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;原始提示音文件\u0026#34;] N1[\u0026#34;固件中的二进制资源\u0026#34;] N2[\u0026#34;对应资源的地址和长度\u0026#34;] N0 --\u0026gt;|\u0026#34;编译时选择和打包\u0026#34;| N1 N1 --\u0026gt;|\u0026#34;运行时按编号查找\u0026#34;| 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; 这里的“重新编译”，重点是让新的提示音重新打包进固件资源区，不等同于音频格式转换。即使新素材本身已经是 SDK 支持的格式，也仍然需要经过这一步。\n所以，排查“文件换了，声音却没变”时，先确认三件事：新文件有没有被构建脚本选中、重新编译后的固件有没有更新资源区、当前烧录或升级方式有没有把资源区写进去。\n三、业务触发后，提示音怎样找到并排队播放 业务代码一般不直接使用提示音文件名，而是提交一个有明确含义的提示音编号，例如蓝牙连接成功、断开、低电量、左声道或降噪模式。\n原厂 SDK 中，这类编号使用 RES_IDX_xxx 表示。以连接成功为例，业务提交的是：\nbsp_res_play(RES_IDX_CONNECTED); 这行代码只表达两件事：要播放“连接成功”这类提示音，并且它需要按 TWS 规则播放。提示音模块再根据编号，从资源表中找到已经打包的资源；如果产品支持中英文切换，同一个编号还能对应不同语言的提示音。\n找到资源后，提示音也不会马上出声，而是先进入提示音队列。这样当前提示音还没放完时，后来的请求可以等待，不会卡住蓝牙或按键业务。\n队列也解释了一个常见现象：快速切换降噪和通透模式后，功能已经是最新状态，耳机却还在播报旧状态。因为旧请求可能仍排在队列里，还没有来得及播放或清理。\n所以，调用播放接口只是提交请求，不代表耳机已经出声。连续操作后声音滞后或重复时，重点看旧请求是否还留在队列里。\n四、双耳提示音：谁播放，什么时候播放 TWS 耳机有两只，提示音却不一定每次都要双耳播放。原厂 SDK 大致分为三种情况：\n播放范围 适合的场景 双耳播放 蓝牙连接、断连、模式切换等双耳共同状态 当前耳播放 当前耳低电量、当前耳触控反馈 对耳播放 当前耳不响、由另一只耳机发声，例如指定左右声道 蓝牙连接属于两只耳机共享的状态，所以一般双耳播放。低电量则不同，左右耳的剩余电量可能不一样，因此由发生低电的当前耳提醒更合理。\n确定了播放范围之后，双耳播放还要解决“什么时候开始”。左右耳各自收到请求后立即启动，容易因为消息传输和任务调度不同，听到一前一后的两声。原厂 SDK 的基本思路是由主耳协调：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 双耳准备\u0026#34;] direction LR R0N0[\u0026#34;主耳收到播放请求\u0026#34;] R0N1[\u0026#34;把同一段提示音\u0026lt;br/\u0026gt;通知给对耳\u0026#34;] R0N2[\u0026#34;左右耳提前准备好\u0026lt;br/\u0026gt;资源和播放环境\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 定时启动\u0026#34;] direction LR R1N0[\u0026#34;约定同一个 TWS 时间点\u0026#34;] R1N1[\u0026#34;时间到达后\u0026lt;br/\u0026gt;两边一起启动\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 所以“双耳都有这段资源”只能保证两边可以播放，不能保证两边同时播放。响错耳朵时先检查播放范围；双耳一前一后时，再检查请求分发和同步时刻。\n五、MP3 和 WAV 提示音怎样接入音频链 日常接触到的提示音素材，MP3 和 WAV 会更常见。它们都是外部音频文件，由对应的解码流程读取并输出声音。\nMP3 是压缩音频。这版原厂工程中，连接、断连等语音提示多使用 MP3。WAV 可以先理解为波形音频文件，使用时要重点确认素材参数是否与当前 SDK 的解码能力匹配。具体选哪一种，还要结合固件空间、提示音长度和真机听感决定。\n格式不同，后面的解码流程会有差异，但它们接入音频系统时要解决的问题是一样的：耳机可能已经在播放音乐，提示音不能只顾自己出声。\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 播放前与播放中\u0026#34;] direction LR R0N0[\u0026#34;保存当前音频状态\u0026#34;] R0N1[\u0026#34;暂停或降低背景音乐\u0026#34;] R0N2[\u0026#34;解码并输出提示音\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 播放结束\u0026#34;] direction LR R1N0[\u0026#34;提示音结束\u0026#34;] R1N1[\u0026#34;恢复原来的音量和音频业务\u0026#34;] R1N0 --\u0026gt; R1N1 end class R1N0,R1N1 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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; 因此，提示音播放完成不只是“这一段声音播完了”，还包括原来的音乐、音量和输出状态都回到正确状态。理解这一点，也就抓住了提示音音频链的最后一环。\n原文：CSDN · TWS 耳机提示音是怎样播放出来的 ","permalink":"https://btio.cn/posts/2026/08/10/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA%E6%8F%90%E7%A4%BA%E9%9F%B3%E6%98%AF%E6%80%8E%E6%A0%B7%E6%92%AD%E6%94%BE%E5%87%BA%E6%9D%A5%E7%9A%84/","summary":"\u003cp\u003e在 TWS 耳机里，开机、蓝牙连接、低电量和降噪模式切换，通常都会伴随一段提示音。\u003c/p\u003e\n\u003cp\u003e从表面看，提示音像是准备一个音频文件，需要时调用播放函数即可。实际上，一段提示音从代码触发到左右耳真正出声，中间还要经过资源查找、播放排队、双耳同步和音频恢复。\u003c/p\u003e\n\u003cp\u003e本文以中科蓝讯 BT891x 原厂 SDK 的实现为例，重点分析提示音播放的核心流程。不同 SDK 的接口名称可能不一样，但整体思路通常可以互相参考。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、提示音播放的完整链路\u003c/h2\u003e\n\u003cp\u003e一段提示音的完整流程，可以先简化成下面五步：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 准备播放请求\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;业务触发\u0026#34;]\n        R0N1[\u0026#34;找到对应的提示音资源\u0026#34;]\n        R0N2[\u0026#34;把播放请求加入队列\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 同步与播放\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;确定哪只耳机播放\u0026lt;br/\u0026gt;并完成双耳同步\u0026#34;]\n        R1N1[\u0026#34;解码播放\u0026lt;br/\u0026gt;结束后恢复原来的音频状态\u0026#34;]\n        R1N0 --\u0026gt; R1N1\n    end\n    class R1N0,R1N1 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e比如手机连接耳机成功以后，蓝牙业务只需要提出“播放连接成功提示音”这个请求。至于提示音文件放在哪里、左右耳什么时候开始播放、当前音乐要不要降低音量，都会继续交给提示音模块处理。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机提示音是怎样播放出来的"},{"content":"一、写在开始 我参与的是一款基于中科蓝讯 BT8910 的 TWS 耳机品牌项目，目前这个项目已经完成。\n项目结束后，我想把开发过程中接触到的 Jira 问题单重新梳理一遍。其中既有我参与处理的内容，也有同事负责的模块。\n现在查一个函数、一个配置项并不难，真正值得记录的是：面对一个问题现象时，应该先判断哪一层，为什么往这个方向查，以及怎么通过日志一步步缩小范围。\n所以这次复盘不是给出一套标准答案，而是站在我目前对应用层的理解上，把问题现象、判断过程和排查思路整理下来，供遇到类似问题的同学参考。\n整理过程中我还发现，很多反馈看起来不一样，根因却有重复。按根因归类后，下次遇到类似现象时，就可以先判断问题属于哪个环节，再沿对应链路往下查。\n后续我会继续按模块整理项目中的问题单，包括按键、提示音、机盒通信、ANC、音量控制、功耗管理、出入盒、双耳同步、蓝牙问题和连接问题等内容。这一篇先从按键开始，归纳其中比较常见的 4 类问题。\n阅读提示\n全文约 2 300 字，预计 10 分钟读完。\n本文包含 4 类按键问题：完全无响应、场景功能错误、误触，以及复杂手势互相干扰。重点不是讲基础原理，而是遇到类似反馈时，应该先查哪里、再往哪里查。\n本文只讨论按键链路本身。如果按键已经触发正确功能，只是提示音没有播放或播放不对，就归到后续的提示音问题中。\n二、所有手势都没有反应 客户反馈\n其中一只耳机的触控按键完全无效，双击、三击、长按都没有反应。\n排查思路\n第一步：确认触控输入\n先看这只耳机的触控按键是否有按下、松开的日志。\n没有日志：先查这个触控按键的输入本身，例如触控是否启用、耳机是否在盒内、是否正处于禁键时间、触控阈值或通道是否正常。\n有日志：说明这个触控按键已经有输入，继续往下查。\n第二步：确认手势识别\n再看目标手势有没有识别出来。\n没有识别出来：重点查双击间隔、长按时间、松手后状态是否复位，以及组合手势是否把普通手势挡住了。\n识别出来：说明手势已经产生，继续看功能。\n第三步：确认功能消息\n最后看手势有没有转换成正确的功能消息。\n没有、为空或错误：查按键定义、场景切换和配置更新。\n有正确消息：再确认功能处理是否执行；如果功能已经执行，后面只是没有提示音，就属于提示音问题，不再按按键问题处理。\n判断与处理\n“完全无效”不一定是触控硬件问题，也可能是手势已经产生、但后面没有正确链接到功能。处理时不要一开始就调灵敏度或改功能定义，先通过日志确认问题停在哪一层。\n三、同一手势，功能却错了 客户反馈\n听歌时双击正常，来电时双击却不能接听或挂断。 长按在听歌时能切换功能，到响铃或通话时却失效。 App 改完触控后，耳机执行的功能不对。 排查思路\n第一步：固定一个场景复现\n只测听歌、只测来电响铃，或只测通话，避免混在一起判断。\n第二步：确认手势已经识别\n如果手势都没出来，回到第一类问题排查。\n第三步：核对当前场景的功能\n手势正常后，直接看耳机最后想执行的是什么功能。\n同样是“双击”：听歌时可能是切歌，来电时应该是接听或拒接，通话中又可能是其他操作。 来电时双击却去切歌：说明双击识别没有问题，只是耳机还在按“听歌时的规则”处理。 只有通话异常：重点检查进入通话后，双击和长按是否已经改成通话时该做的功能。 App 改完才异常：重点检查新设置是否把听歌、来电或通话时的功能改错了。 判断与处理\n听歌、来电响铃、通话时都可以识别同一个“双击”或“长按”，但后续要执行的功能并不相同。\n这类问题通常是耳机没有按当前使用状态执行对应功能。处理重点是把每种状态下的功能对应关系确认清楚。\n四、不该响应时仍然响应 客户反馈\n耳机放在盒内还能切歌；刚出盒或拿取耳机时，容易误触切换功能。\n排查思路\n第一步：确认误触发生的时机\n只发生在入盒、出盒和拿取时，还是正常佩戴时也频繁出现。\n第二步：确认当前是否允许响应按键\n查看触控输入到来时，耳机是否处于允许响应按键的状态。\n第三步：区分入盒误触和正常佩戴误触\n只在出盒、拿取时误触：优先查是否缺少短暂禁键保护。\n正常佩戴时也频繁误触：再查触控灵敏度和触控区域。\n判断与处理\n这类问题不一定是触控太灵敏，更常见的是系统没有先判断“当前允不允许响应”。入盒后应禁用按键，出盒后可以保留一小段禁键时间；确认使用条件没有问题后，再考虑调灵敏度。\n五、复杂手势互相干扰 客户反馈\n单只耳机长按触控按键，有时执行成了另一个功能。 左右耳同时长按各自的触控按键时，可能只响应一只耳机，也可能同一个功能执行两次。 有时操作结束后，下一次按键才开始不正常。 这类反馈的共同点是：触控并不是完全没识别到，而是耳机把这次操作理解错了。\n排查思路\n第一步：把操作还原完整\n按的是左耳还是右耳的触控按键、按了多久、另一只耳机有没有一起按，都要记录下来。不要只记录“长按异常”，因为少一个动作，结果可能就不同。\n第二步：把手势拆开验证\n先只测一只耳机长按触控按键，再测左右耳同时长按，最后测连续操作，确认是哪一种操作开始出现异常。\n第三步：对照预期和实际结果\n例如预期是“左右耳同时长按，只执行一次切换功能”，实际却执行了两次，就重点检查两只耳机是否已经知道对方也在按。\n第四步：再测一次后续操作\n若第一次正常、第二次异常，重点检查耳机是否还记着上一次按下的信息，或对耳的按下状态没有及时清掉。\n判断与处理\n长按、连续按和双耳同时按，都不是一个瞬间就能判断完的动作。耳机需要结合按下时长、前后两次操作，以及另一只耳机的状态再做判断。\n处理时先把操作顺序和实际结果写出来，再检查操作结束后相关状态是否清掉，通常比直接改判断条件更容易定位。\n六、写在最后 后续排查按键问题时，可以先判断它属于哪一类：完全无响应、场景功能不对、不该响应时误触，还是复杂手势互相干扰。\n先分清类别，再往对应链路查，通常比一开始就调触控参数更有效。\n原文：CSDN · 【中科蓝讯】TWS 按键问题单复盘 ","permalink":"https://btio.cn/posts/2026/08/07/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E6%8C%89%E9%94%AE%E9%97%AE%E9%A2%98%E5%8D%95%E5%A4%8D%E7%9B%98/","summary":"\u003ch2 id=\"section-1\"\u003e一、写在开始\u003c/h2\u003e\n\u003cp\u003e我参与的是一款基于中科蓝讯 BT8910 的 TWS 耳机品牌项目，目前这个项目已经完成。\u003c/p\u003e\n\u003cp\u003e项目结束后，我想把开发过程中接触到的 Jira 问题单重新梳理一遍。其中既有我参与处理的内容，也有同事负责的模块。\u003c/p\u003e\n\u003cp\u003e现在查一个函数、一个配置项并不难，真正值得记录的是：面对一个问题现象时，应该先判断哪一层，为什么往这个方向查，以及怎么通过日志一步步缩小范围。\u003c/p\u003e\n\u003cp\u003e所以这次复盘不是给出一套标准答案，而是站在我目前对应用层的理解上，把问题现象、判断过程和排查思路整理下来，供遇到类似问题的同学参考。\u003c/p\u003e\n\u003cp\u003e整理过程中我还发现，很多反馈看起来不一样，根因却有重复。按根因归类后，下次遇到类似现象时，就可以先判断问题属于哪个环节，再沿对应链路往下查。\u003c/p\u003e\n\u003cp\u003e后续我会继续按模块整理项目中的问题单，包括按键、提示音、机盒通信、ANC、音量控制、功耗管理、出入盒、双耳同步、蓝牙问题和连接问题等内容。这一篇先从按键开始，归纳其中比较常见的 4 类问题。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e阅读提示\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e全文约 2 300 字，预计 10 分钟读完。\u003c/p\u003e\n\u003cp\u003e本文包含 4 类按键问题：完全无响应、场景功能错误、误触，以及复杂手势互相干扰。重点不是讲基础原理，而是遇到类似反馈时，应该先查哪里、再往哪里查。\u003c/p\u003e\n\u003cp\u003e本文只讨论按键链路本身。如果按键已经触发正确功能，只是提示音没有播放或播放不对，就归到后续的提示音问题中。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、所有手势都没有反应\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e客户反馈\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e其中一只耳机的触控按键完全无效，双击、三击、长按都没有反应。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一步：确认触控输入\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先看这只耳机的触控按键是否有按下、松开的日志。\u003c/p\u003e\n\u003cp\u003e没有日志：先查这个触控按键的输入本身，例如触控是否启用、耳机是否在盒内、是否正处于禁键时间、触控阈值或通道是否正常。\u003c/p\u003e\n\u003cp\u003e有日志：说明这个触控按键已经有输入，继续往下查。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第二步：确认手势识别\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e再看目标手势有没有识别出来。\u003c/p\u003e\n\u003cp\u003e没有识别出来：重点查双击间隔、长按时间、松手后状态是否复位，以及组合手势是否把普通手势挡住了。\u003c/p\u003e\n\u003cp\u003e识别出来：说明手势已经产生，继续看功能。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第三步：确认功能消息\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e最后看手势有没有转换成正确的功能消息。\u003c/p\u003e\n\u003cp\u003e没有、为空或错误：查按键定义、场景切换和配置更新。\u003c/p\u003e\n\u003cp\u003e有正确消息：再确认功能处理是否执行；如果功能已经执行，后面只是没有提示音，就属于提示音问题，不再按按键问题处理。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e判断与处理\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e“完全无效”不一定是触控硬件问题，也可能是手势已经产生、但后面没有正确链接到功能。处理时不要一开始就调灵敏度或改功能定义，先通过日志确认问题停在哪一层。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、同一手势，功能却错了\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e客户反馈\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e听歌时双击正常，来电时双击却不能接听或挂断。\u003c/li\u003e\n\u003cli\u003e长按在听歌时能切换功能，到响铃或通话时却失效。\u003c/li\u003e\n\u003cli\u003eApp 改完触控后，耳机执行的功能不对。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一步：固定一个场景复现\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e只测听歌、只测来电响铃，或只测通话，避免混在一起判断。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第二步：确认手势已经识别\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e如果手势都没出来，回到第一类问题排查。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第三步：核对当前场景的功能\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e手势正常后，直接看耳机最后想执行的是什么功能。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e同样是“双击”：听歌时可能是切歌，来电时应该是接听或拒接，通话中又可能是其他操作。\u003c/li\u003e\n\u003cli\u003e来电时双击却去切歌：说明双击识别没有问题，只是耳机还在按“听歌时的规则”处理。\u003c/li\u003e\n\u003cli\u003e只有通话异常：重点检查进入通话后，双击和长按是否已经改成通话时该做的功能。\u003c/li\u003e\n\u003cli\u003eApp 改完才异常：重点检查新设置是否把听歌、来电或通话时的功能改错了。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e判断与处理\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e听歌、来电响铃、通话时都可以识别同一个“双击”或“长按”，但后续要执行的功能并不相同。\u003c/p\u003e\n\u003cp\u003e这类问题通常是耳机没有按当前使用状态执行对应功能。处理重点是把每种状态下的功能对应关系确认清楚。\u003c/p\u003e\n\u003ch2 id=\"section-4\"\u003e四、不该响应时仍然响应\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e客户反馈\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e耳机放在盒内还能切歌；刚出盒或拿取耳机时，容易误触切换功能。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查思路\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一步：确认误触发生的时机\u003c/strong\u003e\u003c/p\u003e","title":"【中科蓝讯】TWS 按键问题单复盘"},{"content":"上一篇已经把 SDK 的几个核心概念过了一遍，包括模式状态机、process/message/display 和消息分层。\n这一篇就拿按键功能做一次具体分析，从一个按键事件出发，一直追到最终执行的位置。\n以我接触到的 TWS 项目来看，按键是应用层里相对好理解、也比较容易上手的一部分：改单击功能、加长按、加多击，或者调整不同模式下的按键行为。这些需求在 SDK 里都对应到同一套机制，弄懂之后，定位修改位置会更快。\n一、先跑一遍完整链路 一个按键从手指按下到功能执行，在 SDK 里走了这几步：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB subgraph R0[\u0026#34;① 输入与识别\u0026#34;] direction LR R0N0[\u0026#34;手指按下\u0026lt;br/\u0026gt;用户触发按键\u0026#34;] R0N1[\u0026#34;硬件扫描\u0026lt;br/\u0026gt;读取按键编号\u0026#34;] R0N2[\u0026#34;消抖 / 识别\u0026lt;br/\u0026gt;判断短按、长按、多击\u0026#34;] R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2 end class R0N0,R0N1,R0N2 blue; style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b subgraph R1[\u0026#34;② 消息与业务\u0026#34;] direction LR R1N0[\u0026#34;查按键表\u0026lt;br/\u0026gt;找到对应消息\u0026#34;] R1N1[\u0026#34;消息入队\u0026lt;br/\u0026gt;放入消息队列\u0026#34;] R1N2[\u0026#34;模式分发\u0026lt;br/\u0026gt;按当前模式处理\u0026#34;] R1N3[\u0026#34;功能执行\u0026lt;br/\u0026gt;播放、切歌、接听等\u0026#34;] R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 --\u0026gt; R1N3 end class R1N0,R1N1,R1N2,R1N3 green; style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a R0 --\u0026gt; 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 步开始，软件才把按键行为转换成功能。\n理解这条链路后，改按键时就能快速判断：是改按键表的映射，还是改消息处理逻辑，还是两处都要改。\n先看最核心的一段代码——bsp_key_scan()。它不是只做“扫描”，而是把扫描、手势识别、查按键表和消息入队串了起来：\nu8 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; } 先不用记住每个函数的细节，先区分下面三个结果分别代表什么。\nkey_uid：按的是哪个物理按键。对单个 TWS 耳机来说，通常就是 TKEY_EN 对应的唯一按键编号；没有触控时就是“没有按键”。 key_evt：这个按键最后被识别成什么操作，例如单击、双击或长按。 key_msg：这个操作在当前状态下要做什么，例如听歌时双击切歌、来电时双击接听。它是查按键表后才得到的，随后才进入消息队列。 所以，bsp_key_scan_do() 只产生第一个结果；消息不是它产生的，而是在 bsp_key_scan() 里识别出手势、查完按键表后才得到。\n后面几步——消息出队、模式分发、功能执行——在前一篇已经讲过。\n对应的是 func_bt() 循环里的 func_bt_message(msg_dequeue()) → func_bt_message_do()；没有处理的消息会落到 func_message()。\n二、硬件扫描：按键从哪来 对这类 TWS 耳机来说，单只耳机通常只启用一个 TKEY_EN 触控按键。负责读取它的函数叫 bsp_key_scan_do()。\nu8 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 之后，做两件事：消抖，然后判断是短按、长按、还是多击。这部分代码在库里面，应用层不需要改，但需要知道它产出了什么。\n产出的东西叫 key_evt，一个 16 位的值，编码了两层信息：\nkey_evt: ┌─────────────────┬──────────────────┐ │ 高4位: 事件类型 │ 低8位: 按键编号 │ │ (KEY_EVT) │ (KEY_ID) │ └─────────────────┴──────────────────┘ 事件类型有这些：\n事件 含义 例子 KEY_SHORT_UP 短按抬起 单击播放/暂停 KEY_LONG 长按（按下达到阈值） 长按拒接来电 KEY_HOLD 按住不放（持续触发） 按住快进 KEY_LONG_UP 长按后抬起 长按松开执行关机 KEY_DOUBLE 双击 双击切下一曲 KEY_THREE 三击 三击回拨最后一个号码 KEY_FOUR 四击 预留 KEY_FIVE 五击 预留 消抖和时长阈值由 key_shake_table 控制：\nconst 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 秒 }; 这几个值一般不需要改。如果客户反馈“长按要按很久才有反应”，先确认长按事件是不是出现得太晚。\n确认是后，就把 KEY_LONG_TIMES 适当调小。它会赋给 long_cnt，不需要先去改定时器或状态机。\n如果长按功能一定要等松手才执行，或者日志里已经有长按事件、功能却迟迟没有反应，问题就不在这个参数，要继续看按键表和后面的功能处理。\n四、按键表：把手势翻译成消息 拿到 key_evt（哪个按键 + 什么事件）之后，下一步是把它翻译成消息号。这个翻译靠的就是按键表。\n按键表的结构很直观——一个二维数组，行是按键编号，列是事件类型，每个格子里填的是对应的消息号。\n为了把这张二维表讲清楚，这里暂时不拿单触控的 TWS 耳机举例，而是用一副有三个实体按键的有线耳机说明。\nKEY_1 是多功能键，KEY_2 是上一首键，KEY_3 是下一首键。单触控 TWS 通常只会用到其中一个按键编号，反而不容易看出“行”和“列”的关系。\n// 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, // 五击 }, }; 把这张表读出来就是：\nKEY_1（多功能键）：单击 → 播放暂停，长按 → Siri，按住 → 关机，双击 → 切换模式，三击 → 回拨 KEY_2（上一首键）：单击 → 上一曲，长按 → 快退开始，长按抬起 → 快退结束，双击 → 音量减 KEY_3（下一首键）：单击 → 下一曲，长按 → 快进开始，长按抬起 → 快进结束，双击 → 音量加 如果只是修改按键功能，通常只需要改这张表，不需要动后面的处理逻辑，把对应位置的消息号换掉就可以了。\n比如想把 KEY_1 的长按从 Siri 改成 ANC 切换，把 MSG_SWITCH_SIRI 换成 MSG_CHANGE_ANC 就可以了。\n消息的处理逻辑 SDK 已经写好，改的是“什么按键触发什么消息”这一层。\n在单触控 TWS 中，原理完全一样，通常只需要关心 KEY_1 这一行：同一个 TKEY_EN 的单击、双击、长按分别对应什么功能。\n五、不同场景，按键表不同 同一个 KEY_1 在不同使用状态下，拿到的表可以不同：\n使用状态 KEY_1 单击的常见结果 听歌 播放/暂停 来电响铃 接听或拒接 通话中 挂断或执行通话控制 这就是“同一颗触控按键不同行为”的原因：进入来电或通话后，系统会切到对应的按键表；回到听歌状态后，再恢复音乐表。\n通话表里有些消息不会直接写成“接听”或“挂断”，而是先交给通话处理，再由当前状态决定最终动作。\n如果听歌正常、只有来电或通话时按键不对，先检查当前是不是取到了正确的通话按键表，而不是先调触控灵敏度。\n六、新按键功能怎么加 以单触控 TWS 为例：想让 KEY_1 的三击触发 ANC 切换。\n遇到新增按键功能时，我一般先确认三个地方：\n手势能不能被识别？ 三击属于多击，先确认当前配置支持三击；只支持单击和长按时，表里填了三击功能也不会生效。 要改表里的哪个格子？ 这里就是音乐表中 KEY_1 的“三击”列，把原来的消息改成 ANC 切换消息。 这个消息有没有人处理？ 如果 ANC 切换功能本身已经存在，通常不用新写功能处理，只要把消息映射过去。 还有一个容易忽略的情况：前面改的是代码里的默认按键表。\n有些项目会在烧录时，通过 Downloader 配置重新指定 KEY_1 的单击、双击、三击和长按功能。\n所以遇到“代码明明改了，烧录后还是旧功能”时，不要马上怀疑表没改对。\n先确认烧录工具有没有启用按键自定义。启用后，工具里的配置会覆盖代码默认值，排查时也要把这一层考虑进去。\n七、写在最后 以后改按键时，先按这个顺序判断：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR N0[\u0026#34;先看手势\u0026lt;br/\u0026gt;有没有识别出来\u0026#34;] N1[\u0026#34;再看当前场景\u0026lt;br/\u0026gt;取到的按键表是否正确\u0026#34;] N2[\u0026#34;代码改了不生效\u0026lt;br/\u0026gt;再查 Downloader 是否覆盖默认配置\u0026#34;] N0 --\u0026gt; N1 N1 --\u0026gt; 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; 大多数按键问题都可以先按这个顺序排查，不要一开始就去调触控灵敏度或改底层扫描。\n原文：CSDN · 【中科蓝讯】TWS 耳机按键处理流程 ","permalink":"https://btio.cn/posts/2026/08/07/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFtws-%E8%80%B3%E6%9C%BA%E6%8C%89%E9%94%AE%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B/","summary":"\u003cp\u003e上一篇已经把 SDK 的几个核心概念过了一遍，包括模式状态机、process/message/display 和消息分层。\u003c/p\u003e\n\u003cp\u003e这一篇就拿按键功能做一次具体分析，从一个按键事件出发，一直追到最终执行的位置。\u003c/p\u003e\n\u003cp\u003e以我接触到的 TWS 项目来看，按键是应用层里相对好理解、也比较容易上手的一部分：改单击功能、加长按、加多击，或者调整不同模式下的按键行为。这些需求在 SDK 里都对应到同一套机制，弄懂之后，定位修改位置会更快。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、先跑一遍完整链路\u003c/h2\u003e\n\u003cp\u003e一个按键从手指按下到功能执行，在 SDK 里走了这几步：\u003c/p\u003e\n\u003cdiv class=\"mermaid-diagram\" role=\"group\" aria-label=\"流程图\"\u003e\n  \u003cpre class=\"mermaid\"\u003e%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%%\nflowchart TB\n    subgraph R0[\u0026#34;① 输入与识别\u0026#34;]\n        direction LR\n        R0N0[\u0026#34;手指按下\u0026lt;br/\u0026gt;用户触发按键\u0026#34;]\n        R0N1[\u0026#34;硬件扫描\u0026lt;br/\u0026gt;读取按键编号\u0026#34;]\n        R0N2[\u0026#34;消抖 / 识别\u0026lt;br/\u0026gt;判断短按、长按、多击\u0026#34;]\n        R0N0 --\u0026gt; R0N1 --\u0026gt; R0N2\n    end\n    class R0N0,R0N1,R0N2 blue;\n    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b\n    subgraph R1[\u0026#34;② 消息与业务\u0026#34;]\n        direction LR\n        R1N0[\u0026#34;查按键表\u0026lt;br/\u0026gt;找到对应消息\u0026#34;]\n        R1N1[\u0026#34;消息入队\u0026lt;br/\u0026gt;放入消息队列\u0026#34;]\n        R1N2[\u0026#34;模式分发\u0026lt;br/\u0026gt;按当前模式处理\u0026#34;]\n        R1N3[\u0026#34;功能执行\u0026lt;br/\u0026gt;播放、切歌、接听等\u0026#34;]\n        R1N0 --\u0026gt; R1N1 --\u0026gt; R1N2 --\u0026gt; R1N3\n    end\n    class R1N0,R1N1,R1N2,R1N3 green;\n    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a\n    R0 --\u0026gt; R1\n    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;\n    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;\n    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;\u003c/pre\u003e\n\u003c/div\u003e\u003cp\u003e前两步主要回答“这个按键物理上发生了什么”。从第 3 步开始，软件才把按键行为转换成功能。\u003c/p\u003e","title":"【中科蓝讯】TWS 耳机按键处理流程"},{"content":"前两篇记录了环境搭建和 com/bank 内存的事。这篇主要想讲一下中科蓝汛的 SDK——不讲具体细节，也不列目录结构，每个项目的目录可能都不太一样，列了也没太大意义。主要是聊一下我对这套 SDK 几个核心概念的理解。以后不管拿到哪个版本、做什么功能，这几个概念应该都不会变。\n一、SDK 就是一个\u0026quot;模式状态机\u0026quot; 这是做完项目之后最大的感受。一颗 TWS 耳机芯片，任何时候只处于一个\u0026quot;模式\u0026quot;——蓝牙模式、充电模式、IDLE 待机、关机。切模式就是改一个变量 func_cb.sta。\n// 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; // 退出，切下一个 } } 虽然每个模式做的事情不一样，但内部结构都有共同点。拿蓝牙模式看：\nvoid 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 这个三段式。\n上面这张图把两层循环串起来了——外层 func_run 负责\u0026quot;当前在哪个模式\u0026quot;，内层 func_bt 负责\u0026quot;这个模式里每轮做什么\u0026quot;。那么 process、message、display 分别干了什么？以蓝牙模式为例拆开看看。\nprocess——每轮扫一眼的事\nvoid func_bt_process(void) { func_process(); // 公共事件：充电检测、低电检测、关机检测、设备插拔 // 根据蓝牙状态，临时切换按键表 if (来电) { 切到通话按键表 → 跑响铃逻辑 → 切回音乐按键表 } else if (OTA升级) { 跑升级逻辑 } else if (通话中) { 切到通话按键表 → 跑通话逻辑 → 切回音乐按键表 } // 该休眠了？该关机了？扫一眼，没事就退出 } process 不像 message 那样来了事件就处理，而是每 10ms 左右快速扫一眼——充电插入了没？电还够不够？该休眠了没？有情况就处理，没情况直接退出。大部分时候其实什么都没做，就是进去逛一圈又出来了。\nmessage——响应\u0026quot;一次性事件\u0026quot;\nvoid func_bt_message_do(u16 msg) { switch (msg) { case 播放暂停: 调蓝牙播放暂停; break; case 上一曲: 切上一首; break; case 下一曲: 切下一首; break; case 接听: 接电话; break; case 挂断: 挂电话; break; // ... } } 按键、系统事件、TWS 同步指令，最终都变成一个消息号，在这里 switch 分发。message 和 process 一个是\u0026quot;来事了就干\u0026quot;，一个是\u0026quot;逛一圈看看有没有事\u0026quot;——有人按了键，消息立刻丢过来，message 马上处理；但电池还剩多少、该不该休眠，这种事没人通知你，得靠 process 自己隔一会扫一眼。\ndisplay——管灯怎么闪\ndisplay 干了件最简单的事——根据当前状态（连上了、播放中、通话中、没电了\u0026hellip;）决定状态灯怎么闪。跟硬件绑得比较紧，不同板子 LED 接线不一样，所以单独放在 projects/display/ 下面，不和通用逻辑搅在一起。\n三个子函数拆完，回到循环里看就很清楚了——每轮先扫一眼状态，再处理积压的消息，最后刷一下灯，完事。下一轮再来一遍。\n二、三个问题，定位任何功能 有了\u0026quot;模式状态机\u0026quot;这个前提，后面不管做什么功能，我习惯先问自己三个问题：\n第一个问题：这个功能发生在哪个模式？\n是在蓝牙音乐模式下？通话模式下？充电模式下？还是所有模式都通用的？这个问题决定了代码加在 func_bt.c 还是 func.c。\n拿音量加减来说——它在蓝牙和非蓝牙模式下的逻辑不一样（蓝牙模式要走 A2DP 音量同步，非蓝牙模式直接调系统音量），所以 func_volume_up() 里有一个 if (func_cb.sta == FUNC_BT) 的分支。但它又确实是所有模式都能触发的，所以放在 func.c 的 func_message() 里作为公共消息处理。\n第二个问题：是什么触发的？按键？系统事件？还是轮询状态变化？\n三种触发方式对应三个入口：\n触发方式 入口 例子 按键 func_message() 或 func_bt_message() 单击播放/暂停 系统事件 func_message() 里的 EVT_xxx 消息 手机端调音量、来电、TWS 同步 状态轮询 func_process() 或 func_bt_process() 低电检测、入盒检测、充电状态变化 表格看着三类触发源，但其实真正的入口就两个：按键和系统事件最终都进了 func_message()，状态轮询走 func_process()。之前遇到问题到处找源头，现在知道了——顺着这两个入口往回追就行。\n消息本身也是分层的，但这个分层有意思——不是\u0026quot;先判断再分发\u0026quot;，而是模式专属先处理，处理不了的才往下落。比如 func_bt_message_do() 先处理播放/暂停/上下曲这些蓝牙消息，它不认识的消息走 default 落到 func_message() 处理公共逻辑（音量、关机、EQ 切换）。加功能的时候想清楚是\u0026quot;蓝牙专属\u0026quot;还是\u0026quot;所有模式通用\u0026quot;，就知道放哪一层了。\n第三个问题：受哪个宏控制？编译期的还是运行时的？\n这个在上一篇 config.h 里提过，但值得再强调一次——因为踩过坑。有些宏决定了\u0026quot;这段代码有没有编译进去\u0026quot;（比如 FUNC_BT_EN），关了就真没了。有些宏只是默认值，运行时会被 Downloader Setting（xcfg_cb）覆盖。\n改完宏不确定有没有生效的时候，有个简单的验证办法——在对应的 #if 代码块里随便敲几个乱码字符，如果编译过了，说明这段代码根本没参与编译，也就是宏没开。不用烧录就能确认，比在那猜快得多。\n三、拿到一个新 SDK，从哪里看？ 不讲步骤了，就是自己的一点习惯：\n先点开 main.c，看 main() 里调了什么——基本上就是 sys_init() + func_run()，确认入口。\n然后点开 func_run()，看 switch 里有哪些 case——就知道这个 SDK 支持哪些模式。\n然后随便挑一个模式（蓝牙最好，最常用），点进去看它的 enter → loop → process → message → display → exit。看完一个模式，其他模式的结构就全知道了。\n最后扫一眼 config.h，看有哪些功能宏——知道哪些功能可以开关、哪些参数可以调。\n读完这四个文件（main.c、func.c、func_bt.c、config.h），SDK 的骨架就清楚了。剩下的模块代码是用到的时候再翻，不需要提前全读一遍。\n四、回顾一下 三篇连起来，从零到能改功能的路大概是这样的：\n环境跑通，能把代码烧进去（第一篇） 知道代码放在芯片的什么地方跑——com 区和 bank 区（第二篇） 脑子里有张 SDK 的地图——模式状态机、怎么定位功能、从哪开始看（这篇） 后面再往里加功能，不管是改按键、加 ANC 切换、做入盒检测，都是在这个框架里填东西。框架是原厂搭好的，应用层要做的，是在正确的地方加正确的逻辑。\n原文：CSDN · 【中科蓝讯】聊聊我对这套SDK的理解 ","permalink":"https://btio.cn/posts/2026/08/06/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AF%E8%81%8A%E8%81%8A%E6%88%91%E5%AF%B9%E8%BF%99%E5%A5%97sdk%E7%9A%84%E7%90%86%E8%A7%A3/","summary":"\u003cp\u003e前两篇记录了环境搭建和 com/bank 内存的事。这篇主要想讲一下中科蓝汛的 SDK——不讲具体细节，也不列目录结构，每个项目的目录可能都不太一样，列了也没太大意义。主要是聊一下我对这套 SDK 几个核心概念的理解。以后不管拿到哪个版本、做什么功能，这几个概念应该都不会变。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、SDK 就是一个\u0026quot;模式状态机\u0026quot;\u003c/h2\u003e\n\u003cp\u003e这是做完项目之后最大的感受。一颗 TWS 耳机芯片，任何时候只处于一个\u0026quot;模式\u0026quot;——蓝牙模式、充电模式、IDLE 待机、关机。切模式就是改一个变量 \u003ccode\u003efunc_cb.sta\u003c/code\u003e。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// func_run() —— 整个 SDK 的主循环就长这样\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003efunc_enter\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efunc_cb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esta\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nl\"\u003eFUNC_BT\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e      \u003cspan class=\"nf\"\u003efunc_bt\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e      \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 蓝牙\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nl\"\u003eFUNC_CHARGE\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e  \u003cspan class=\"nf\"\u003efunc_charge\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e  \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 充电\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nl\"\u003eFUNC_IDLE\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e    \u003cspan class=\"nf\"\u003efunc_idle\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e    \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 待机\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nl\"\u003eFUNC_PWROFF\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e  \u003cspan class=\"nf\"\u003efunc_pwroff\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e  \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 关机\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e           \u003cspan class=\"nf\"\u003efunc_exit\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e    \u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 退出，切下一个\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e虽然每个模式做的事情不一样，但内部结构都有共同点。拿蓝牙模式看：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003efunc_bt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003efunc_bt_enter\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e                    \u003cspan class=\"c1\"\u003e// 进来先初始化\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efunc_cb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esta\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eFUNC_BT\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 没切模式就一直在这转\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nf\"\u003efunc_bt_process\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e              \u003cspan class=\"c1\"\u003e// 轮询事件\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nf\"\u003efunc_bt_message\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003emsg_dequeue\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 处理消息\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nf\"\u003efunc_bt_display\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e              \u003cspan class=\"c1\"\u003e// 刷新显示\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003efunc_bt_exit\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e                     \u003cspan class=\"c1\"\u003e// 走之前收尾\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e就是 enter → while → process/message/display → exit。再看 idle 模式，几乎一模一样——\u003ccode\u003efunc_idle_enter()\u003c/code\u003e → \u003ccode\u003ewhile(func_cb.sta == FUNC_IDLE)\u003c/code\u003e → \u003ccode\u003eprocess/message/display\u003c/code\u003e → 退出。充电和关机模式虽然内部细节不同，但也是 enter → 做事 → exit 这个三段式。\u003c/p\u003e","title":"【中科蓝讯】聊聊我对这套SDK的理解"},{"content":"刚接触中科蓝讯 SDK 时，我经常看到函数前面有 AT(.com_text.xxx)。一开始我只知道照着原有代码写，却不明白它为什么在这里。后来看了资料和工程里的写法，我才认识到：SDK 把代码分区域存放，不同区域的运行方式不一样。\n这篇只是我目前从应用侧整理的一点理解，重点放在看到 AT(...) 时怎么读。\n一、com 区和 bank 区有什么区别 按照官方资料的说明，芯片上电后，一部分代码会从 Flash 搬到 RAM 里常驻下来，这部分就是 com 区；剩下的代码继续待在 Flash，用到时才被加载到 bank 运行区。\n区域 简单理解 适合放什么 com 区 上电后会加载到 RAM，并在运行期间常驻 中断函数、实时性要求高的 bank 区 代码存在 Flash，运行时按需加载到 bank 运行区 大多数普通应用逻辑 com 区的空间有限，所以不要因为更快，就把普通功能都放进去。\n二、真正要注意的是中断和它调用的函数 中断函数和它调到的关键函数，通常需要放在 com 区。因为中断来了要马上处理，不能等到需要时再去加载代码。\n在工程里看定时和一些中断相关代码时，我能看到类似下面的写法：\nAT(.com_text.user) void user_timer_isr(void) { // 中断中的处理 } 实际用的时候，我是先看同模块已有代码的写法，不凭空猜。但照抄还不够——踩过两次坑才知道，光盯着函数本身的 AT，容易漏掉更深层的依赖。\n第一次是在中断里调了一个普通函数，我给那个函数加了 AT(.com_text.xxx)，以为这就完事了。结果还是偶发异常。排查后发现，这个函数内部又调了另一个普通函数，那个子函数没加 AT。中断触发时 CPU 跳到 com 区，执行到嵌套的子函数时，它的代码还在 bank 区没加载进来，一样挂。这件事的风险在于，被调函数可能还调了其他函数，嵌套几层之后很容易漏掉。所以给函数加 AT 时，得顺着调用链往下检查，确保整条执行路径上的函数都在 com 区。如果调用深度比较大，更稳妥的做法是通过消息转发把逻辑放到其他线程执行，避免在 com 区里越套越深。\n第二次是 printf 的格式字符串。我之前在 UART 中断里加 printf 调试，跑起来后出现了偶发性死机。排查时对比了同文件里其他中断函数的写法，才发现人家的格式字符串前面都带了 AT(.com_rodata...)，就我新加的这一句没写。补上之后死机不再复现。回头想这件事，原因其实就回到 com 区和 bank 区的区别上：函数本身已经放在 com 区，但格式字符串如果不加 AT(.com_rodata...)，就会被分配到 bank 区的只读段。中断触发时 CPU 直接跳过来执行，不会等 Flash 按需加载——字符串还没到位，访问就异常了。所以中断里加打印，不能只看 printf 调用本身，格式字符串的段属性也要和中断函数保持一致，都在 com 区才安全。\n三、总结 回到一开始的问题：看到 AT(.com_text.xxx) 该怎么读？对我来说，它表达的就一件事——com 区执行路径上的所有东西，函数也好、字符串也好、调用的子函数也好，都得在 com 区，漏一个就可能偶发异常。\n平时写普通应用逻辑，沿用什么都不加就行，SDK 默认放在 bank 区，不用每行代码都去想该放哪个区。但一旦涉及中断、定时回调这类对时机敏感的场景，加 AT 之前我会先想两件事：一是这个函数内部调了谁，整条调用链上有没有会漏掉的；二是这件事一定要在 com 区做吗，如果调用链太深，放消息转发到其他线程反而是更安全的选择。改完如果出现偶发性问题，代码放置位置就是一个排查方向。\n现在再看 AT(.com_text.timer)，我不会把它当成一串无意义的标记了——它是在提醒我：这段代码跑起来的时候，Flash 可能帮不上忙。\n原文：CSDN · 【中科蓝讯】从两次偶发死机，理解 com 区和 bank 区 ","permalink":"https://btio.cn/posts/2026/08/06/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AF%E4%BB%8E%E4%B8%A4%E6%AC%A1%E5%81%B6%E5%8F%91%E6%AD%BB%E6%9C%BA%E7%90%86%E8%A7%A3-com-%E5%8C%BA%E5%92%8C-bank-%E5%8C%BA/","summary":"\u003cp\u003e刚接触中科蓝讯 SDK 时，我经常看到函数前面有 \u003ccode\u003eAT(.com_text.xxx)\u003c/code\u003e。一开始我只知道照着原有代码写，却不明白它为什么在这里。后来看了资料和工程里的写法，我才认识到：SDK 把代码分区域存放，不同区域的运行方式不一样。\u003c/p\u003e\n\u003cp\u003e这篇只是我目前从应用侧整理的一点理解，重点放在看到 \u003ccode\u003eAT(...)\u003c/code\u003e 时怎么读。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、com 区和 bank 区有什么区别\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"com 区和 bank 区的存放与运行示意\" loading=\"lazy\" src=\"/images/csdn/163511359/image.png\"\u003e\u003c/p\u003e\n\u003cp\u003e按照官方资料的说明，芯片上电后，一部分代码会从 Flash 搬到 RAM 里常驻下来，这部分就是 \u003ccode\u003ecom\u003c/code\u003e 区；剩下的代码继续待在 Flash，用到时才被加载到 \u003ccode\u003ebank\u003c/code\u003e 运行区。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e区域\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e简单理解\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e适合放什么\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ecom\u003c/code\u003e 区\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e上电后会加载到 RAM，并在运行期间常驻\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e中断函数、实时性要求高的\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ebank\u003c/code\u003e 区\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e代码存在 Flash，运行时按需加载到 bank 运行区\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e大多数普通应用逻辑\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003ecom\u003c/code\u003e 区的空间有限，所以不要因为更快，就把普通功能都放进去。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、真正要注意的是中断和它调用的函数\u003c/h2\u003e\n\u003cp\u003e中断函数和它调到的关键函数，通常需要放在 \u003ccode\u003ecom\u003c/code\u003e 区。因为中断来了要马上处理，不能等到需要时再去加载代码。\u003c/p\u003e\n\u003cp\u003e在工程里看定时和一些中断相关代码时，我能看到类似下面的写法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nf\"\u003eAT\u003c/span\u003e\u003cspan class=\"p\"\u003e(.\u003c/span\u003e\u003cspan class=\"n\"\u003ecom_text\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003euser_timer_isr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 中断中的处理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e实际用的时候，我是先看同模块已有代码的写法，不凭空猜。但照抄还不够——踩过两次坑才知道，光盯着函数本身的 \u003ccode\u003eAT\u003c/code\u003e，容易漏掉更深层的依赖。\u003c/p\u003e\n\u003cp\u003e第一次是在中断里调了一个普通函数，我给那个函数加了 \u003ccode\u003eAT(.com_text.xxx)\u003c/code\u003e，以为这就完事了。结果还是偶发异常。排查后发现，这个函数内部又调了另一个普通函数，那个子函数没加 \u003ccode\u003eAT\u003c/code\u003e。中断触发时 CPU 跳到 com 区，执行到嵌套的子函数时，它的代码还在 bank 区没加载进来，一样挂。这件事的风险在于，被调函数可能还调了其他函数，嵌套几层之后很容易漏掉。所以给函数加 \u003ccode\u003eAT\u003c/code\u003e 时，得顺着调用链往下检查，确保整条执行路径上的函数都在 com 区。如果调用深度比较大，更稳妥的做法是通过消息转发把逻辑放到其他线程执行，避免在 com 区里越套越深。\u003c/p\u003e","title":"【中科蓝讯】从两次偶发死机，理解 com 区和 bank 区"},{"content":"以我最近实际接触的中科蓝讯 BT891x 项目为例，刚开始最先要做的就是把开发环境搭起来：准备资料和工具、打开工程、编译生成 app.dcf，再用 Downloader 完成后续烧录。\n不同 SDK 版本和开发板的目录、工具版本可能不一样，最终以当前资料包和工程说明为准。\n一、准备资料和工具 如果手上的 SDK 对应特定的 TWS 开发板，原理图和相关硬件资料也要一并保存好，后面核对硬件配置、下载和外设问题时都会用到。\n刚开始做 BT891x 时，主要先准备下面四项资料和工具：\n内容 先拿它做什么 BT891x 固件 SDK 后续的功能开发主要就在这里进行 CodeBlocks 开发环境 打开 .cbp 工程并发起编译 RV32 Toolchain 供 CodeBlocks 调用的 RISC-V 编译工具链 Downloader 读取 app.dcf，进行配置和烧录处理 如果当前使用的是串口下载方式，还要保留蓝讯提供的串口驱动。资料包里的驱动通常以 CP210x 驱动包的形式提供，用于让电脑识别 CP2102 或 XLink 模块对应的 COM 口。\nCodeBlocks 和 RV32 Toolchain 都是双击安装程序后，按提示一路选择 Next、默认路径安装即可。CodeBlocks 用来打开 .cbp 工程并编译，RV32 Toolchain 是真正执行编译的工具。\n如果使用串口下载，按当前下载器对应的驱动安装即可。下载器和设备接入电脑后，系统能够识别出 COM 口，Downloader 才能继续扫描设备。\nDownloader 不需要安装，解压后直接运行 Downloader.exe 即可。\n二、编译与烧录 环境装完后，我不会马上改功能，而是先在 CodeBlocks 中打开 SDK 里的 .cbp 工程并编译一次。\n第一次打开工程时，可以在 Settings -\u0026gt; Compiler 中确认当前编译器为 RISCV32。如果列表里没有这个选项，通常是 RV32 Toolchain 没有安装成功，或者版本和 SDK 不匹配。确认后直接编译，编译日志没有报错就说明环境基本跑通了。\n编译通过后，到当前工程目录下的 Output\\bin 查看构建产物。后面 Downloader 会使用其中的 app.dcf。\n接入下载器和设备后，打开 Downloader，先让 DownFile 指向当前工程刚生成的：\nOutput\\bin\\app.dcf 然后在“配置”中勾选当前工程对应的配置，打开串口并确认 Downloader 已经识别到 COM 口。状态显示串口正常打开后，再点击“开始”烧录。\n三、CP2102 和 XLink 两种串口模块 下载时，Downloader 是电脑端的软件；它需要通过串口模块和耳机通信。以我看到的使用说明来说，常见的是 CP2102 和 XLink 两种串口模块方案。\nCP2102 是常见的 USB 转串口模块。如果接到耳机的单线通信 IO 上，需要在 TX 和 RX 之间增加 200 Ω 电阻，把原本的双线串口方式转换成单线通信方式。\nXLink 可以理解为蓝讯提供的成品串口模块。它和 CP2102 的用途相同，但模块内部已经在 TX 和 RX 之间集成了 200 Ω 电阻，使用时不需要再自己额外加这一颗电阻。\n不管用哪种模块，电脑端都是通过 Downloader 选择 app.dcf、打开对应 COM 口后再发起烧录。\n四、写在最后 前面这套流程里，CodeBlocks 主要负责打开工程和编译，Downloader 负责烧录。日常浏览、搜索和修改代码时，我个人更习惯用 VS Code。它的插件生态比较丰富，代码搜索、跳转和 C/C++ 语法提示用起来也更顺手。\n我会直接用 VS Code 打开 BT891x SDK 根目录，后续改代码主要在这里完成；需要生成固件时，再回到 CodeBlocks 打开 .cbp 工程编译，然后用 Downloader 烧录。\n原文：CSDN · 【中科蓝讯】SDK 环境搭建与烧录 ","permalink":"https://btio.cn/posts/2026/08/05/%E4%B8%AD%E7%A7%91%E8%93%9D%E8%AE%AFsdk-%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%E4%B8%8E%E7%83%A7%E5%BD%95/","summary":"\u003cp\u003e以我最近实际接触的中科蓝讯 BT891x 项目为例，刚开始最先要做的就是把开发环境搭起来：准备资料和工具、打开工程、编译生成 \u003ccode\u003eapp.dcf\u003c/code\u003e，再用 Downloader 完成后续烧录。\u003c/p\u003e\n\u003cp\u003e不同 SDK 版本和开发板的目录、工具版本可能不一样，最终以当前资料包和工程说明为准。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、准备资料和工具\u003c/h2\u003e\n\u003cp\u003e如果手上的 SDK 对应特定的 TWS 开发板，原理图和相关硬件资料也要一并保存好，后面核对硬件配置、下载和外设问题时都会用到。\u003c/p\u003e\n\u003cp\u003e刚开始做 BT891x 时，主要先准备下面四项资料和工具：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e内容\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e先拿它做什么\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eBT891x 固件 SDK\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e后续的功能开发主要就在这里进行\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eCodeBlocks 开发环境\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e打开 \u003ccode\u003e.cbp\u003c/code\u003e 工程并发起编译\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRV32 Toolchain\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e供 CodeBlocks 调用的 RISC-V 编译工具链\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eDownloader\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e读取 \u003ccode\u003eapp.dcf\u003c/code\u003e，进行配置和烧录处理\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e如果当前使用的是串口下载方式，还要保留蓝讯提供的串口驱动。资料包里的驱动通常以 CP210x 驱动包的形式提供，用于让电脑识别 CP2102 或 XLink 模块对应的 COM 口。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"BT891x 工具目录与安装、编译、烧录流程\" loading=\"lazy\" src=\"/images/csdn/163496844/2e1ff8f206554d969a4660b56c737383.png\"\u003e\u003c/p\u003e\n\u003cp\u003eCodeBlocks 和 RV32 Toolchain 都是双击安装程序后，按提示一路选择 \u003ccode\u003eNext\u003c/code\u003e、默认路径安装即可。CodeBlocks 用来打开 \u003ccode\u003e.cbp\u003c/code\u003e 工程并编译，RV32 Toolchain 是真正执行编译的工具。\u003c/p\u003e\n\u003cp\u003e如果使用串口下载，按当前下载器对应的驱动安装即可。下载器和设备接入电脑后，系统能够识别出 COM 口，Downloader 才能继续扫描设备。\u003c/p\u003e\n\u003cp\u003eDownloader 不需要安装，解压后直接运行 \u003ccode\u003eDownloader.exe\u003c/code\u003e 即可。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、编译与烧录\u003c/h2\u003e\n\u003cp\u003e环境装完后，我不会马上改功能，而是先在 CodeBlocks 中打开 SDK 里的 \u003ccode\u003e.cbp\u003c/code\u003e 工程并编译一次。\u003c/p\u003e","title":"【中科蓝讯】SDK 环境搭建与烧录"},{"content":"做 TWS 耳机时，经常会同时看到经典蓝牙和 BLE。\n手机已经通过蓝牙连接耳机，可以播放音乐、接打电话，为什么配套 App 还要再连接一次 BLE？我刚开始接触时，也没有认真区分这两部分，只知道它们都叫蓝牙。\n后来做 App 交互功能多了，我才慢慢理解：在目前常见的 TWS 产品里，它们处理的事情并不一样。经典蓝牙主要负责音乐、通话和媒体控制，BLE 更像一条给 App 使用的数据通道。\n一、经典蓝牙主要负责耳机的基本功能 平时用耳机听歌、调节音量和接打电话，主要涉及经典蓝牙中的几个 Profile：\nProfile 在耳机中的主要作用 A2DP 传输音乐音频 AVRCP 播放、暂停、切歌和音量控制 HFP 来电、接听、挂断等通话控制 通话时的语音通常还会通过 SCO/eSCO 链路传输。\n这些功能决定了一副耳机能不能正常听歌和通话。以我目前接触到的应用开发来说，平时主要处理 SDK 上报的 A2DP、AVRCP 和 HFP 事件，再根据当前状态执行对应的产品逻辑。\n二、BLE 主要负责耳机与 App 交互 配套 App 需要做的事情和播放音乐不太一样。它传输的通常不是连续音频，而是一些控制命令和状态数据。\n我做过的项目里，BLE 主要用于这些功能：\n修改 EQ； 切换 ANC 模式； 读取耳机电量； OTA 升级； 查找耳机。 例如用户在 App 上切换 ANC，App 会通过 BLE 把命令发给耳机；耳机执行以后，还可能需要把最新状态返回给 App。读取电量也是类似的过程，只是数据方向和触发时机可能不同。\n我接触过 GATT 的 Service、Characteristic、读写和通知，不过实际开发时，大部分数据收发还是通过 SDK 封装好的接口完成。刚开始看这些名词时，我觉得它们比较抽象。结合 TWS 耳机的功能，可以先这样理解：\nService 是一组相关功能的集合，例如电量服务或设备控制服务； Characteristic 是服务下面具体的数据或控制接口，例如左耳电量或 ANC 模式； Read、Write 和 Notify 是这些接口支持的不同操作方式。 可以先看下面这个例子：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart TB ROOT[\u0026#34;耳机 GATT\u0026#34;] BAT[\u0026#34;电量服务 Service\u0026#34;] CTRL[\u0026#34;控制服务 Service\u0026#34;] BATCHARS[\u0026#34;左耳电量 Characteristic\u0026lt;br/\u0026gt;Read：App 主动查询\u0026lt;br/\u0026gt;Notify：耳机主动上报变化\u0026lt;br/\u0026gt;\u0026lt;br/\u0026gt;右耳电量 Characteristic\u0026lt;br/\u0026gt;充电盒电量 Characteristic\u0026#34;] CTRLCHARS[\u0026#34;ANC 模式 Characteristic\u0026lt;br/\u0026gt;Write：App 发送切换命令\u0026lt;br/\u0026gt;Read：App 查询当前模式\u0026lt;br/\u0026gt;Notify：耳机通知模式变化\u0026lt;br/\u0026gt;\u0026lt;br/\u0026gt;音量 Characteristic\u0026#34;] ROOT --- BAT \u0026amp; CTRL BAT --- BATCHARS CTRL --- CTRLCHARS class ROOT,BAT,BATCHARS blue; class CTRL,CTRLCHARS green; 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 想让耳机打开 ANC，通常会找到“控制服务”下面的“ANC 模式 Characteristic”，然后向它执行 Write 操作。耳机收到命令后切换模式，如果项目支持状态通知，还可以通过 Notify 把最新模式告诉 App。\nApp 想查看左耳电量时，可以对“左耳电量 Characteristic”执行 Read；如果 App 订阅了 Notify，耳机电量发生变化后，也可以主动把新电量通知给 App。\n因此，Characteristic 可以先理解为 Service 下面的具体数据项或功能接口。它到底支持 Read、Write 还是 Notify，要看项目定义，不能把所有功能都套成同一种方式。\n三、两种连接可以同时存在，但状态要分开看 一部手机可以通过经典蓝牙连接耳机，同时再通过 BLE 与耳机建立数据连接。它们都使用蓝牙，但连接过程和业务状态并不是同一个东西。\n可以把常见的关系简单理解为：\n%%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: true, \u0026#34;wrappingWidth\u0026#34;: 1000, \u0026#34;rankSpacing\u0026#34;: 30}, \u0026#34;themeVariables\u0026#34;: {\u0026#34;lineColor\u0026#34;: \u0026#34;#71829a\u0026#34;}}}%% flowchart LR PHONE[\u0026#34;手机\u0026#34;] BT[\u0026#34;经典蓝牙\u0026#34;] BLE[\u0026#34;BLE\u0026#34;] PROFILES[\u0026#34;A2DP：音乐\u0026lt;br/\u0026gt;AVRCP：播放控制和音量\u0026lt;br/\u0026gt;HFP：通话\u0026#34;] GATT[\u0026#34;GATT：App 功能\u0026lt;br/\u0026gt;EQ、ANC、电量、OTA\u0026lt;br/\u0026gt;查找耳机等\u0026#34;] PHONE --- BT \u0026amp; BLE BT --- PROFILES BLE --- GATT class PHONE,BT,PROFILES blue; class BLE,GATT green; 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; 因此，经典蓝牙连接成功，不代表 BLE 一定已经连接；App 连上 BLE，也不能说明 A2DP 或 HFP 已经可用。\n我实际遇到过经典蓝牙已经连接，但 App 扫描不到 BLE 的情况。遇到这种问题，如果只看到手机系统里显示“已连接”，很容易误以为整个蓝牙都没有问题。\n后来再看这类现象，我会先把两条路径拆开：经典蓝牙是否连接是一件事，BLE 是否正在广播、App 是否能够扫描并建立连接是另一件事。至于具体原因，还需要结合当时的广播状态、App 扫描条件、连接记录和日志继续确认，不能只根据一个现象下结论。\n四、应用开发时为什么要分清它们 区分经典蓝牙和 BLE，不只是为了记住几个名词，更直接的作用是避免排查错方向。\n音乐没有声音时，我会先关注 A2DP 状态、音频流事件和应用侧音频资源；App 控制不了 ANC 时，则应该先检查 BLE 连接、命令收发和状态返回。两种现象都可能被描述成“蓝牙连接有问题”，但需要检查的地方并不一样。\n应用代码里也不能只保留一个笼统的“蓝牙已连接”状态。至少需要知道当前说的是经典蓝牙、某个 Profile，还是 BLE 数据连接，否则不同业务很容易互相影响。\n五、补充说明 这篇整理的是我目前接触较多的传统 TWS 产品：经典蓝牙负责音乐和通话，BLE 负责配套 App 的数据交互。\n现在蓝牙还有 LE Audio，可以在低功耗蓝牙上承载音频。它和这里提到的 BLE App 控制不是一回事，涉及的协议和产品支持情况也更多，这篇先不展开。\n对我目前的工作来说，先分清经典蓝牙和 BLE 各自在产品中负责什么，再去看 A2DP、HFP、GATT 和具体业务流程，会更容易把代码和实际功能对应起来。\n原文：CSDN · 一副 TWS 耳机，为什么既要用经典蓝牙，又要用 BLE？ ","permalink":"https://btio.cn/posts/2026/08/04/%E4%B8%80%E5%89%AF-tws-%E8%80%B3%E6%9C%BA%E4%B8%BA%E4%BB%80%E4%B9%88%E6%97%A2%E8%A6%81%E7%94%A8%E7%BB%8F%E5%85%B8%E8%93%9D%E7%89%99%E5%8F%88%E8%A6%81%E7%94%A8-ble/","summary":"\u003cp\u003e做 TWS 耳机时，经常会同时看到经典蓝牙和 BLE。\u003c/p\u003e\n\u003cp\u003e手机已经通过蓝牙连接耳机，可以播放音乐、接打电话，为什么配套 App 还要再连接一次 BLE？我刚开始接触时，也没有认真区分这两部分，只知道它们都叫蓝牙。\u003c/p\u003e\n\u003cp\u003e后来做 App 交互功能多了，我才慢慢理解：在目前常见的 TWS 产品里，它们处理的事情并不一样。经典蓝牙主要负责音乐、通话和媒体控制，BLE 更像一条给 App 使用的数据通道。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、经典蓝牙主要负责耳机的基本功能\u003c/h2\u003e\n\u003cp\u003e平时用耳机听歌、调节音量和接打电话，主要涉及经典蓝牙中的几个 Profile：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eProfile\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e在耳机中的主要作用\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eA2DP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e传输音乐音频\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAVRCP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e播放、暂停、切歌和音量控制\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eHFP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e来电、接听、挂断等通话控制\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e通话时的语音通常还会通过 SCO/eSCO 链路传输。\u003c/p\u003e\n\u003cp\u003e这些功能决定了一副耳机能不能正常听歌和通话。以我目前接触到的应用开发来说，平时主要处理 SDK 上报的 A2DP、AVRCP 和 HFP 事件，再根据当前状态执行对应的产品逻辑。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、BLE 主要负责耳机与 App 交互\u003c/h2\u003e\n\u003cp\u003e配套 App 需要做的事情和播放音乐不太一样。它传输的通常不是连续音频，而是一些控制命令和状态数据。\u003c/p\u003e\n\u003cp\u003e我做过的项目里，BLE 主要用于这些功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e修改 EQ；\u003c/li\u003e\n\u003cli\u003e切换 ANC 模式；\u003c/li\u003e\n\u003cli\u003e读取耳机电量；\u003c/li\u003e\n\u003cli\u003eOTA 升级；\u003c/li\u003e\n\u003cli\u003e查找耳机。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e例如用户在 App 上切换 ANC，App 会通过 BLE 把命令发给耳机；耳机执行以后，还可能需要把最新状态返回给 App。读取电量也是类似的过程，只是数据方向和触发时机可能不同。\u003c/p\u003e\n\u003cp\u003e我接触过 GATT 的 Service、Characteristic、读写和通知，不过实际开发时，大部分数据收发还是通过 SDK 封装好的接口完成。刚开始看这些名词时，我觉得它们比较抽象。结合 TWS 耳机的功能，可以先这样理解：\u003c/p\u003e","title":"一副 TWS 耳机，为什么既要用经典蓝牙，又要用 BLE"},{"content":"TWS 应用层的大部分工作，最后还是要落到 C 代码上。这篇整理开发中经常用到、也容易混淆的内容，方便需要时快速回顾。\n一、数据类型与类型转换 嵌入式代码经常使用 uint8_t、uint16_t 和 uint32_t，因为它们的位宽是明确的。\nint 占多少字节，由芯片架构和编译器决定。在常见的 32 位平台上，int 通常占 4 字节；在部分嵌入式平台上，也可能只占 2 字节。可以使用 sizeof 查看当前平台上的实际大小：\nprintf(\u0026#34;int size: %u\\n\u0026#34;, (unsigned int)sizeof(int)); sizeof 返回的是 size_t 类型。这里先把结果转换成 unsigned int，再使用 %u 输出，适合常见的嵌入式 printf。\n普通计算和循环计数可以使用 int。通信协议、Flash 数据、文件格式和寄存器字段需要明确长度时，优先使用固定位宽类型。\n类型转换主要注意下面几种情况：\n情况 需要注意什么 大类型转小类型 高位可能被截断 有符号转无符号 负数会变成很大的正数 无符号与有符号比较 有符号数可能先被转成无符号数 指针类型转换 只改变读取内存的方式，不会改变原来的数据 注意：有符号数和无符号数比较前，尽量把两边统一成合适的类型。进行指针类型转换时，要确认原内存长度不小于目标类型的大小。例如原数据只有 1 字节，转换成 uint32_t * 后却按照 4 字节读取，就会读到不属于它的内存。\n二、指针、数组与生命周期 指针保存的是地址。使用前主要检查四件事：\n指针是否为空。 指向的内存是否仍然有效。 访问长度是否越界。 指针指向的数据类型是否正确。 void update_state(uint8_t *state) { if (state == NULL) { return; } *state = 1; } 数组传入函数后通常会退化成指针，函数内部无法再通过 sizeof 得到原数组长度。因此传递缓冲区时，最好同时传入长度。\nvoid parse_data(const uint8_t *data, uint16_t len); 也不要返回局部变量的地址。函数退出后，局部变量的生命周期已经结束，原来的地址不能继续使用。\n三、几个常见关键字 关键字 主要作用 static 限制跨文件可见性，或让局部变量保留上次的值 const 限制通过当前对象或指针修改数据 volatile 要求编译器每次实际访问内存，但不保证原子性和线程安全 extern 声明符号定义在其他文件中，由链接器查找 const 放在不同位置，含义也不同：\n写法 能否通过指针修改数据 指针能否改指向 常见叫法 const uint8_t *p1 不能 可以 常量指针 uint8_t * const p2 可以 不能 指针常量 const uint8_t * const p3 不能 不能 指向常量的指针常量 简单记忆：const 在 * 前面，限制指向的数据；const 在 * 后面。\nvolatile 常用于可能被中断、硬件或其他执行上下文改变的数据。它解决的是编译器优化问题，不能代替锁、临界区或原子操作。\n四、内存区域、结构体与大小端 区域 常见内容 主要特点 栈 局部变量、函数调用信息 自动分配和释放，空间有限 堆 动态申请的内存 使用灵活，但要防止泄漏和碎片 全局区 全局变量、静态变量 生命周期贯穿程序运行过程 只读区 常量、只读数据 通常不允许修改 结构体大小不能只把各成员占用的字节数直接相加，这个问题我之前踩过坑。例如下面的成员看起来一共只有 5 字节：\ntypedef struct { uint8_t type; // 1 字节 uint32_t value; // 4 字节 } packet_t; printf(\u0026#34;size: %u\\n\u0026#34;, (unsigned int)sizeof(packet_t)); 但在部分平台上，sizeof(packet_t) 得到的可能是 8，而不是 5。编译器可能会在 type 后面自动填充 3 个字节，使 value 从合适的地址开始存放。\n因此，通信数据不能想当然地直接转换成结构体。原始数据可能只有 5 字节，但程序却按照 8 字节的结构体读取，字段位置也可能对不上。处理协议数据时，按字段的位置和长度分别解析会更稳妥。\n例如，协议规定一包数据固定为 5 字节：\n字节位置 含义 第 0 字节 类型 type 第 1～4 字节 数值 value，小端格式 收到的数据如下：\nuint8_t data[5] = { 0x01, // type 0x78, 0x56, 0x34, 0x12 // value }; 不建议直接把它强制转换成结构体：\npacket_t *packet = (packet_t *)data; // 不推荐 可以先检查长度，再按照字段位置分别读取：\nuint8_t type; uint32_t value; if (sizeof(data) \u0026gt;= 5) { type = data[0]; value = (uint32_t)data[1] | ((uint32_t)data[2] \u0026lt;\u0026lt; 8) | ((uint32_t)data[3] \u0026lt;\u0026lt; 16) | ((uint32_t)data[4] \u0026lt;\u0026lt; 24); } 解析后，type 等于 0x01，value 等于 0x12345678。这种写法不受结构体填充影响，也明确处理了协议规定的小端顺序。\n大小端决定多字节数据在内存中的排列顺序。发送和接收 uint16_t、uint32_t 时，要按照协议规定转换，不能直接假设双方顺序相同。\n写在最后 这篇主要整理了 TWS 应用开发中经常接触，也比较容易混淆的几块 C 语言知识。\n实际开发中，很多问题并不是语法不会写，而是忽略了数据类型、长度、地址、内存对齐和生命周期等细节。把这些基础问题弄清楚，在阅读代码和排查问题时会更有帮助。后续遇到值得记录的内容，我也会继续整理。\n原文：CSDN · 做耳机开发，需要掌握哪些 C 语言知识？ ","permalink":"https://btio.cn/posts/2026/08/03/%E5%81%9A%E8%80%B3%E6%9C%BA%E5%BC%80%E5%8F%91%E9%9C%80%E8%A6%81%E6%8E%8C%E6%8F%A1%E5%93%AA%E4%BA%9B-c-%E8%AF%AD%E8%A8%80%E7%9F%A5%E8%AF%86/","summary":"\u003cp\u003eTWS 应用层的大部分工作，最后还是要落到 C 代码上。这篇整理开发中经常用到、也容易混淆的内容，方便需要时快速回顾。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、数据类型与类型转换\u003c/h2\u003e\n\u003cp\u003e嵌入式代码经常使用 \u003ccode\u003euint8_t\u003c/code\u003e、\u003ccode\u003euint16_t\u003c/code\u003e 和 \u003ccode\u003euint32_t\u003c/code\u003e，因为它们的位宽是明确的。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eint\u003c/code\u003e 占多少字节，由芯片架构和编译器决定。在常见的 32 位平台上，\u003ccode\u003eint\u003c/code\u003e 通常占 4 字节；在部分嵌入式平台上，也可能只占 2 字节。可以使用 \u003ccode\u003esizeof\u003c/code\u003e 查看当前平台上的实际大小：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;int size: %u\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"k\"\u003esizeof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003esizeof\u003c/code\u003e 返回的是 \u003ccode\u003esize_t\u003c/code\u003e 类型。这里先把结果转换成 \u003ccode\u003eunsigned int\u003c/code\u003e，再使用 \u003ccode\u003e%u\u003c/code\u003e 输出，适合常见的嵌入式 \u003ccode\u003eprintf\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e普通计算和循环计数可以使用 \u003ccode\u003eint\u003c/code\u003e。通信协议、Flash 数据、文件格式和寄存器字段需要明确长度时，优先使用固定位宽类型。\u003c/p\u003e\n\u003cp\u003e类型转换主要注意下面几种情况：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e情况\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e需要注意什么\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e大类型转小类型\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e高位可能被截断\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e有符号转无符号\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e负数会变成很大的正数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e无符号与有符号比较\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e有符号数可能先被转成无符号数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e指针类型转换\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e只改变读取内存的方式，不会改变原来的数据\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注意：有符号数和无符号数比较前，尽量把两边统一成合适的类型。进行指针类型转换时，要确认原内存长度不小于目标类型的大小。例如原数据只有 1 字节，转换成 \u003ccode\u003euint32_t *\u003c/code\u003e 后却按照 4 字节读取，就会读到不属于它的内存。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"section-2\"\u003e二、指针、数组与生命周期\u003c/h2\u003e\n\u003cp\u003e指针保存的是地址。使用前主要检查四件事：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e指针是否为空。\u003c/li\u003e\n\u003cli\u003e指向的内存是否仍然有效。\u003c/li\u003e\n\u003cli\u003e访问长度是否越界。\u003c/li\u003e\n\u003cli\u003e指针指向的数据类型是否正确。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eupdate_state\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003euint8_t\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e数组传入函数后通常会退化成指针，函数内部无法再通过 \u003ccode\u003esizeof\u003c/code\u003e 得到原数组长度。因此传递缓冲区时，最好同时传入长度。\u003c/p\u003e","title":"做耳机开发，需要掌握哪些 C 语言知识"},{"content":"刚接触 TWS 应用开发时，我一直有一个疑问：蓝牙协议栈大部分已经由芯片原厂封装好了，平时做功能主要是调用 SDK 接口、处理事件和编写产品逻辑，还有没有必要专门学习协议栈？\n刚入行的时候，我也尝试过从头学习蓝牙协议。当时下载了一份 Bluetooth Core Specification 6.0，打开后看到整整 3815 页，心里压力一下就上来了。里面有大量英文术语、状态、命令和数据包格式，我硬着头皮看了几天，还是很难把规范里的内容和手上的代码对应起来，最后没有坚持下去。\n工作两年左右，我对协议栈内部仍然谈不上有多深入的理解，平时接触最多的还是应用层代码。所以我现在的看法是：**需要了解一些，但没必要一开始钻得特别深。**这篇文章只是记录我目前在项目中用到的一些认识。\n一、我目前主要关注什么 以我目前的工作内容来说，HCI、L2CAP 以及各个 Profile 的内部状态机，我其实没有深入研究。日常开发更常见的是调用 SDK 接口、接收事件，再根据产品状态处理业务。\n为了不把不同问题混在一起，我会先记住几个和工作直接相关的区别：\nACL 连接和 Profile 连接不是一回事。 A2DP 负责音乐，AVRCP 负责播放控制和音量同步。 HFP 负责通话控制，SCO/eSCO 承载通话语音。 SPP 和 BLE GATT 常用于 App 数据通信。 接口调用成功，不一定代表异步操作已经完成，还要等待后续事件。 为了更直观地区分这些连接和业务，可参考蓝牙连接关系简化图：\n图中把 GATT 单独放在 BLE 连接下。严格来说，协议内部的承载关系还要更细，这里只保留我在应用开发中经常接触到的部分。\n我目前也就是先把这些常见概念分清。这样遇到异常时，至少知道先看哪一类状态，不至于一上来就把所有现象都归结成“蓝牙有问题”。\n二、实际项目里，我能接触到什么 以我使用的中科蓝讯 bt8910 SDK 为例，工程里可以看到 A2DP、HFP、SPP 和 TWS 相关的配置及适配代码，但真正的核心实现主要由预编译的 libbtstack.a 提供。\n应用层这边，A2DP 和 HFP 主要是调整部分参数、接收事件和处理业务回调；SPP 的 SDP Record 和数据收发回调可以看到并修改；GATT 可以添加 Service、Characteristic 和读写回调；TWS 这边则能做一些业务状态同步和主从切换处理。\n所以按我目前的理解，这套 SDK 并没有把完整的 Profile 源码全部开放出来，而是提供配置、API 和回调，让应用层在上面实现产品功能。\n实际排查时，我会先检查自己能看到的应用代码。如果接口已经调用，但一直没有后续事件，就把日志、版本和复现步骤整理出来，再请更熟悉协议栈的同事或芯片原厂继续确认。\n三、不能只看手机上的“已连接” 我目前觉得最实用的一点，是不要把手机显示的“已连接”当成唯一状态。软件内部可能还要分别判断基础链路、A2DP 或 HFP、音频流、SCO、应用状态机以及 TWS 对耳状态。\n这些模块内部怎样交互，我目前没有研究得很深。但先知道“连接成功”和“业务可用”不是一回事，对应用侧排查已经有帮助。\n四、拿“连上但没声音”举个例子 如果手机显示耳机已经连接，但播放音乐没有声音，我会先从应用侧做几项比较基础的检查：\n耳机是否处于入盒、通话、升级等不允许播放音乐的状态。 应用收到的是基础连接事件，还是已经收到 A2DP 连接和音频流启动事件。 收到事件以后，应用是否正确打开了音频资源，是否被提示音、ANC 或其他状态拦截。 单耳是否正常，问题是否只出现在双耳、特定手机或特定固件版本上。 如果音频流事件已经出现，但应用没有打开对应资源，我会优先检查应用逻辑；如果一直没有收到预期的 A2DP 事件，就只能结合更多日志或原厂信息继续确认。这只是应用侧的初步排除，还算不上完整的问题定位。\n五、写在最后 以我目前接触到的内容来看，先把 SDK 接口用对、把应用状态理清，再了解与当前业务相关的 Profile 和事件流程，就已经比较实用。\n协议栈更深层的原理，我目前仍在边做项目边补充。对现在的我来说，先弄懂用得到的部分，比试图一次读完整套规范更实际。\n原文：CSDN · 做 TWS 应用开发，到底需不需要懂协议栈？ ","permalink":"https://btio.cn/posts/2026/08/03/%E5%81%9A-tws-%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E5%88%B0%E5%BA%95%E9%9C%80%E4%B8%8D%E9%9C%80%E8%A6%81%E6%87%82%E5%8D%8F%E8%AE%AE%E6%A0%88/","summary":"\u003cp\u003e刚接触 TWS 应用开发时，我一直有一个疑问：蓝牙协议栈大部分已经由芯片原厂封装好了，平时做功能主要是调用 SDK 接口、处理事件和编写产品逻辑，还有没有必要专门学习协议栈？\u003c/p\u003e\n\u003cp\u003e刚入行的时候，我也尝试过从头学习蓝牙协议。当时下载了一份 Bluetooth Core Specification 6.0，打开后看到整整 3815 页，心里压力一下就上来了。里面有大量英文术语、状态、命令和数据包格式，我硬着头皮看了几天，还是很难把规范里的内容和手上的代码对应起来，最后没有坚持下去。\u003c/p\u003e\n\u003cp\u003e工作两年左右，我对协议栈内部仍然谈不上有多深入的理解，平时接触最多的还是应用层代码。所以我现在的看法是：**需要了解一些，但没必要一开始钻得特别深。**这篇文章只是记录我目前在项目中用到的一些认识。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、我目前主要关注什么\u003c/h2\u003e\n\u003cp\u003e以我目前的工作内容来说，HCI、L2CAP 以及各个 Profile 的内部状态机，我其实没有深入研究。日常开发更常见的是调用 SDK 接口、接收事件，再根据产品状态处理业务。\u003c/p\u003e\n\u003cp\u003e为了不把不同问题混在一起，我会先记住几个和工作直接相关的区别：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eACL 连接和 Profile 连接不是一回事。\u003c/li\u003e\n\u003cli\u003eA2DP 负责音乐，AVRCP 负责播放控制和音量同步。\u003c/li\u003e\n\u003cli\u003eHFP 负责通话控制，SCO/eSCO 承载通话语音。\u003c/li\u003e\n\u003cli\u003eSPP 和 BLE GATT 常用于 App 数据通信。\u003c/li\u003e\n\u003cli\u003e接口调用成功，不一定代表异步操作已经完成，还要等待后续事件。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e为了更直观地区分这些连接和业务，可参考蓝牙连接关系简化图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"手机与 TWS 耳机的蓝牙连接关系\" loading=\"lazy\" src=\"/images/csdn/163436393/%E6%89%8B%E6%9C%BA%E4%B8%8ETWS%E8%80%B3%E6%9C%BA%E7%9A%84%E8%93%9D%E7%89%99%E8%BF%9E%E6%8E%A5%E5%85%B3%E7%B3%BB.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图中把 GATT 单独放在 BLE 连接下。严格来说，协议内部的承载关系还要更细，这里只保留我在应用开发中经常接触到的部分。\u003c/p\u003e\n\u003cp\u003e我目前也就是先把这些常见概念分清。这样遇到异常时，至少知道先看哪一类状态，不至于一上来就把所有现象都归结成“蓝牙有问题”。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、实际项目里，我能接触到什么\u003c/h2\u003e\n\u003cp\u003e以我使用的中科蓝讯 \u003ccode\u003ebt8910\u003c/code\u003e SDK 为例，工程里可以看到 A2DP、HFP、SPP 和 TWS 相关的配置及适配代码，但真正的核心实现主要由预编译的 \u003ccode\u003elibbtstack.a\u003c/code\u003e 提供。\u003c/p\u003e\n\u003cp\u003e应用层这边，A2DP 和 HFP 主要是调整部分参数、接收事件和处理业务回调；SPP 的 SDP Record 和数据收发回调可以看到并修改；GATT 可以添加 Service、Characteristic 和读写回调；TWS 这边则能做一些业务状态同步和主从切换处理。\u003c/p\u003e","title":"做 TWS 应用开发，到底需不需要懂协议栈"},{"content":"一、先看整体 如果暂时不考虑具体芯片平台，TWS 耳机的软件大致可以分成下面几个部分：\n这不是严格的分层架构。实际 SDK 中，各部分往往会互相调用，甚至放在同一个模块里。这样划分主要是为了看清楚：一个耳机功能背后，通常有哪些软件在配合。\n软件模块 主要负责什么 常见内容 系统与 RTOS 让程序和多个任务运行起来 任务、消息、定时器、内存、看门狗 驱动与电源 与耳机硬件交互 GPIO、ADC、I2C、I2S、充电、休眠、唤醒 蓝牙连接 与手机建立和维护蓝牙业务 配对、回连、A2DP、AVRCP、HFP、BLE 双耳协同 让左右耳保持一致 双耳组对、角色、状态同步、音频协同 音频系统 管理声音输入和输出 编解码、播放、录音、提示音、ANC、ENC 产品应用 决定耳机具体怎样工作 按键、LED、佩戴检测、状态机、功能优先级 数据与升级 保存配置并更新固件 配对记录、用户设置、OTA、版本管理 调试与测试 帮助发现和定位问题 日志、异常信息、测试模式、量产支持 二、系统与 RTOS TWS 耳机一般不是从上到下只执行一条固定流程。蓝牙事件、按键、音频、充电和定时任务可能在不同时间发生，因此软件需要同时管理多种事情。\n这一部分通常包括：\n系统启动和各模块初始化 任务或线程的创建与调度 事件、消息和队列 软件定时器 内存分配与任务栈 中断处理 看门狗和异常恢复 应用层开发中经常接触到的，并不是 RTOS 调度器本身，而是平台提供的任务、消息和回调接口。\n例如，按键驱动检测到一次双击后，不一定会直接执行“切换降噪模式”。它可能先产生按键事件，再把事件投递给应用任务，最后由应用代码根据当前状态决定是否执行这个功能。\n所以，当我们阅读一个新 SDK 时，找到事件入口和消息流转路径很重要。\n三、硬件驱动与电源管理 耳机虽然体积不大，但里面仍然有不少硬件。软件需要通过不同接口读取状态或者控制这些器件。\n常见内容包括：\nGPIO：NTC供电、LED、使能信号 ADC：按键电压、其他模拟量 UART：日志输出、机盒 充电与盒仓检测：识别 DC、入盒、出盒、开盖、关盖、充电和满电等状态 佩戴检测：红外、触摸或其他传感器 I2C、SPI：传感器、外部芯片或存储器 I2S、PCM：数字音频数据传输 驱动层负责把硬件状态转换成软件可以识别的数据或事件，应用层再决定怎样处理。\n“耳机入盒”背后涉及盒仓状态检测、TWS 对耳同步、按键禁用、ANC 和音乐处理、DAC 控制及主从切换；关盖后还要关闭蓝牙并进入 FUNC_IDLE或 charge等其他模式。处理不完整，可能导致左右耳状态不一致、盒内仍播放声音或关盖后仍连接手机。\n耳机进入低功耗前，要先确认没有正在播放的提示音、运行中的 ANC 或待处理的 App 和充电盒事件。确认可以休眠后，再关闭或降低 DAC、Sensor、LED、定时器和时钟的功耗。某个模块一直工作，耳机就可能睡不着；硬件没有关干净，休眠后的耗电也会偏高\n四、蓝牙连接与 Profile 蓝牙部分很容易让人想到庞大的协议栈。按照 Bluetooth Core 的架构，蓝牙系统内部还可以继续分成 Host、Controller 以及它们之间的 HCI。\n但在很多芯片平台的应用开发中，协议栈和底层控制器已经由芯片原厂提供。应用层更多是通过 SDK 接口接收蓝牙事件、配置功能，并维护产品需要的连接逻辑。\n对 TWS 耳机来说，常见内容主要有以下几类。\n配对与连接管理 这部分负责：\n首次进入可发现、可连接状态 与手机完成配对并保存记录 开机后回连历史设备 连接失败后的超时和重试 清除记录和恢复出厂设置 多设备连接时选择当前活动设备 “蓝牙已经连接”也不是一个足够完整的状态。底层链路连上以后，音乐、媒体控制和通话对应的 Profile 还可能处于不同的连接阶段。\n经典蓝牙音频 传统 TWS 耳机中经常接触到：\nA2DP：传输音乐音频 AVRCP：播放、暂停、切歌和音量等媒体控制 HFP：来电、接听、挂断以及通话语音 这几个 Profile 不是同一个功能。音乐可以播放，不代表通话链路一定正常；手机显示已连接，也不代表所有 Profile 都已经可用。\nBLE 功能 BLE 在耳机中常用于配套手机 App、状态读取、参数控制或固件升级。\n应用层经常看到的是 GATT 中的 Service 和 Characteristic，以及读、写、通知等操作。至于 BLE 底层链路如何收发数据，通常仍由平台协议栈处理。\n五、TWS 双耳协同 普通蓝牙音箱设备只需要考虑和手机之间的关系，TWS 还多了一层左右耳之间的协作。\n这一部分可能包括：\n左右耳组对 双耳连接状态管理 角色分配或角色变化 音量、电量和模式等状态同步 按键操作同步 音乐和通话状态协同 双耳升级和版本一致性 需要注意的是，TWS 双耳协同并不是一个所有芯片平台都采用相同接口和相同内部实现的应用 Profile。实际项目中，它通常由芯片平台提供整套方案，因此中科蓝讯、杰理和恒玄 SDK 中看到的接口与处理方式可能差别很大。\n应用层更常面对的问题是：什么状态需要同步、什么时候同步、重复消息怎样处理，以及一只耳机断开后另一只耳机应该怎样继续工作。\n例如，用户在右耳切换了通透模式，左耳是否要同步更新？如果同步消息发送时双耳刚好断开，重连后是否需要再次校准状态？这些都属于产品应用需要考虑的内容。\n六、音频系统 音频是 TWS 耳机的核心部分之一。\n音乐播放可以先简单理解为：\n手机音频 → 蓝牙接收 → 音频解码 → 音频处理 → DAC → 扬声器\n通话时除了播放远端声音，还需要把本地麦克风采集的声音处理后发送给手机：\n麦克风 → ADC → 通话音频处理 → 蓝牙发送 → 手机\n音频软件通常还会涉及：\nSBC、AAC 等音频编解码 音频流的打开、停止与切换 采样率、位宽和声道 音量控制 提示音播放 音乐、通话和提示音的优先级 ANC、ENC、AEC 或 EQ 等功能的接入 应用层一般不会自己从头实现音频编解码器或降噪算法，但需要知道这些模块在什么时候启动、使用什么参数，以及怎样接入产品功能。\n一个常见问题是提示音与音乐或通话发生冲突。如果没有处理好音频资源和优先级，可能出现提示音没声音、音乐无法恢复，或者通话过程中播放了不合适的提示音。\n七、产品应用逻辑 同一个芯片平台可以做出很多不同的耳机，真正形成产品差异的部分，很多都在应用逻辑里。\n常见功能包括：\n单击、双击、长按等按键操作 LED 和提示音 开机、关机和恢复出厂设置 自动回连和配对超时 佩戴检测与自动播放暂停 ANC、通透和普通模式切换 游戏低延迟模式 双设备连接 电量上报 入仓、出仓和充电处理 这些功能单独看通常不复杂，难点往往在它们组合以后。\n例如，双击操作在听歌时可能是切歌，来电时可能是拒接，通话时又可能没有作用。应用代码必须先判断当前状态，再决定执行哪个动作。\n八、数据保存与 OTA 耳机重新开机后，通常需要记住一些信息，例如：\n手机配对记录 TWS 双耳关系 用户设置的音量 ANC 或其他功能模式 设备名称和部分产品配置 校准参数 这些内容一般保存在 Flash、NV或平台提供的其他持久化区域中。\n保存数据时还要考虑默认值、写入时机、掉电保护和版本兼容。SDK 或产品配置升级后，如果旧数据格式不能被新固件正确识别，就可能出现配置异常。\nOTA 则负责更新耳机固件。对于 TWS 产品，升级还要考虑左右耳的传输顺序、版本一致性、中断恢复和失败回退，通常比单设备升级更复杂。\n九、日志、测试与问题定位 产品开发不可能只考虑正常流程，还需要为异常情况留下分析入口。\n这一部分可能包括：\n系统和蓝牙运行日志 关键状态变化记录 复位原因 异常地址或崩溃信息 测试模式 产测和校准接口 固件版本与配置识别 对于应用层问题，日志最重要的作用通常是还原时间线。\n单独看到一句“连接失败”并不能说明原因。更有用的信息是：耳机当时处于什么状态、之前发生过什么事件、连接的是哪类设备、失败后有没有重试，以及另一只耳机在做什么。\n如果问题可能位于平台底层，也需要先把应用侧能够确认的现象、版本、复现步骤、修改范围和日志整理清楚，再交给芯片原厂继续分析。\n十、用来电场景串联各模块 前面列出了很多模块，但它们并不是各自独立工作的。\n假设用户正在听音乐，这时手机收到一个来电，耳机软件可能会经历下面的过程：\n蓝牙模块收到 HFP 来电状态。 应用层把产品状态从音乐切换到来电。 音频模块暂停或关闭音乐流，准备通话相关资源。 TWS 模块把来电状态同步给另一只耳机。 提示音或铃声模块根据产品配置进行播放。 用户按键接听后，应用层处理按键事件。 HFP 控制状态发生变化，通话语音链路建立。 通话结束后，音频和应用状态再恢复到音乐场景。 只要其中一个环节的状态或时序没有处理好，就可能出现单耳没有铃声、接听无效、通话没声音或者挂断后音乐无法恢复。\n从这个例子也能看出，一个看起来简单的产品现象，背后可能同时涉及蓝牙、音频、TWS、按键和状态管理。\n十一、写在最后 一副 TWS 耳机的软件并不只是蓝牙协议栈，也不只是几个按键功能。它更像是一个小型嵌入式系统：底层连接硬件，中间处理蓝牙和音频，上层再把各种事件组合成完整的产品行为。\n作为应用层开发者来说，重点关注的是：\n能不能看懂 SDK 的启动、状态机和事件处理链路 能不能分清蓝牙连接、Profile 和产品业务状态 能不能处理双耳同步与功能交叉 能不能根据日志逐步缩小问题范围 能不能把修改后的功能完整验证一遍 这篇先建立项目的整体结构。后面分析具体功能时，可以先确定他在系统中的位置，在沿调用链深入代码，避免一开始就被大量实现细节干扰。\n参考资料 Bluetooth Core Specification：Architecture Bluetooth SIG：Traditional Profile Specifications Bluetooth SIG：The Bluetooth Low Energy Primer 原文：CSDN · 一副 TWS 耳机里，有哪些软件模块？ ","permalink":"https://btio.cn/posts/2026/07/31/%E4%B8%80%E5%89%AF-tws-%E8%80%B3%E6%9C%BA%E9%87%8C%E6%9C%89%E5%93%AA%E4%BA%9B%E8%BD%AF%E4%BB%B6%E6%A8%A1%E5%9D%97/","summary":"\u003ch2 id=\"section-1\"\u003e一、先看整体\u003c/h2\u003e\n\u003cp\u003e如果暂时不考虑具体芯片平台，TWS 耳机的软件大致可以分成下面几个部分：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"TWS 耳机软件模块组成\" loading=\"lazy\" src=\"/images/csdn/163373023/software-modules.svg\"\u003e\u003c/p\u003e\n\u003cp\u003e这不是严格的分层架构。实际 SDK 中，各部分往往会互相调用，甚至放在同一个模块里。这样划分主要是为了看清楚：一个耳机功能背后，通常有哪些软件在配合。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e软件模块\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e主要负责什么\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e常见内容\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e系统与 RTOS\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e让程序和多个任务运行起来\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e任务、消息、定时器、内存、看门狗\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e驱动与电源\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e与耳机硬件交互\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGPIO、ADC、I2C、I2S、充电、休眠、唤醒\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e蓝牙连接\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e与手机建立和维护蓝牙业务\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e配对、回连、A2DP、AVRCP、HFP、BLE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e双耳协同\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e让左右耳保持一致\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e双耳组对、角色、状态同步、音频协同\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e音频系统\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e管理声音输入和输出\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e编解码、播放、录音、提示音、ANC、ENC\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e产品应用\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e决定耳机具体怎样工作\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e按键、LED、佩戴检测、状态机、功能优先级\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e数据与升级\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e保存配置并更新固件\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e配对记录、用户设置、OTA、版本管理\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e调试与测试\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e帮助发现和定位问题\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e日志、异常信息、测试模式、量产支持\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"section-2\"\u003e二、系统与 RTOS\u003c/h2\u003e\n\u003cp\u003eTWS 耳机一般不是从上到下只执行一条固定流程。蓝牙事件、按键、音频、充电和定时任务可能在不同时间发生，因此软件需要同时管理多种事情。\u003c/p\u003e\n\u003cp\u003e这一部分通常包括：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e系统启动和各模块初始化\u003c/li\u003e\n\u003cli\u003e任务或线程的创建与调度\u003c/li\u003e\n\u003cli\u003e事件、消息和队列\u003c/li\u003e\n\u003cli\u003e软件定时器\u003c/li\u003e\n\u003cli\u003e内存分配与任务栈\u003c/li\u003e\n\u003cli\u003e中断处理\u003c/li\u003e\n\u003cli\u003e看门狗和异常恢复\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e应用层开发中经常接触到的，并不是 RTOS 调度器本身，而是平台提供的任务、消息和回调接口。\u003c/p\u003e\n\u003cp\u003e例如，按键驱动检测到一次双击后，不一定会直接执行“切换降噪模式”。它可能先产生按键事件，再把事件投递给应用任务，最后由应用代码根据当前状态决定是否执行这个功能。\u003c/p\u003e\n\u003cp\u003e所以，当我们阅读一个新 SDK 时，找到事件入口和消息流转路径很重要。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、硬件驱动与电源管理\u003c/h2\u003e\n\u003cp\u003e耳机虽然体积不大，但里面仍然有不少硬件。软件需要通过不同接口读取状态或者控制这些器件。\u003c/p\u003e\n\u003cp\u003e常见内容包括：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGPIO：NTC供电、LED、使能信号\u003c/li\u003e\n\u003cli\u003eADC：按键电压、其他模拟量\u003c/li\u003e\n\u003cli\u003eUART：日志输出、机盒\u003c/li\u003e\n\u003cli\u003e充电与盒仓检测：识别 DC、入盒、出盒、开盖、关盖、充电和满电等状态\u003c/li\u003e\n\u003cli\u003e佩戴检测：红外、触摸或其他传感器\u003c/li\u003e\n\u003cli\u003eI2C、SPI：传感器、外部芯片或存储器\u003c/li\u003e\n\u003cli\u003eI2S、PCM：数字音频数据传输\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e驱动层负责把硬件状态转换成软件可以识别的数据或事件，应用层再决定怎样处理。\u003c/p\u003e\n\u003cp\u003e“耳机入盒”背后涉及盒仓状态检测、TWS 对耳同步、按键禁用、ANC 和音乐处理、DAC 控制及主从切换；关盖后还要关闭蓝牙并进入 \u003ccode\u003eFUNC_IDLE\u003c/code\u003e或 charge等其他模式。处理不完整，可能导致左右耳状态不一致、盒内仍播放声音或关盖后仍连接手机。\u003c/p\u003e","title":"一副 TWS 耳机里，有哪些软件模块"},{"content":"","permalink":"https://btio.cn/about/","summary":"","title":"关于"},{"content":"一、蓝牙的两种模式 （1）经典蓝牙（BR/EDR） 用于高数据速率应用，如无线耳机、音箱、键盘。\n特点是连接稳定，适合持续传输音频或大量数据。\n（2）低功耗蓝牙（BLE） 专为低功耗设备设计，如智能手环、温度计、传感器。\n特点是耗电低，适合间歇性传输小数据。\n总结：两种模式底层都用无线信号传输数据，都遵循蓝牙协议，只是侧重点不同。经典蓝牙重视速度和稳定，BLE重视省电和简单连接。\n二、蓝牙核心系统组成 蓝牙核心系统由主机（Host）和控制器（Controller）两部分组成。\n主机负责高层协议和控制逻辑，控制器处理底层射频和信号传输。两者分工合作，设备才能正常工作。\n三、主机部分（Host） 主机包含多个协议模块，经典蓝牙和BLE有所不同。\n经典蓝牙主要模块：\nGAP（通用访问协议）：管理设备发现和连接建立。\nSDP（服务发现协议）：帮助设备发现对方支持的服务。\nL2CAP（逻辑链路控制协议）：管理数据流，负责数据分段和重组。\nSMP（安全管理协议）：负责加密连接和密钥管理。\nBLE主要模块：\nGAP（通用访问协议）：管理设备发现和连接建立。\nGATT（通用属性协议）：规定设备如何描述和交换数据属性。\nATT（属性协议）：按照GATT规则传输数据。\nSMP（安全管理协议）：负责加密连接和密钥管理。\n经典蓝牙工作流程：\nGAP搜索附近设备并建立连接\nSDP查找对方支持的服务\nL2CAP处理数据传输，分段重组\nSMP管理密钥和加密\nBLE工作流程：\nGAP搜索附近设备并建立连接\nGATT列出设备支持的服务和数据属性\nATT按照GATT规定格式传送数据\nSMP保障连接和数据安全\n四、控制器部分（Controller） 控制器处理蓝牙底层功能，直接与硬件交互。\n主要模块：\nLL（链路层）：管理蓝牙设备的链路状态和数据包传输。\nRF（射频层）：处理蓝牙信号的发送和接收。\nIAL（等时音频层）：BLE特有，处理高实时性数据传输，例如音频。\n控制器主要负责：\n链路管理：连接的建立、维持、断开。\n无线信号传输：信号调制解调、频率管理、纠错编码。\n硬件交互：直接控制无线电模块发射和接收。\n五、HCI（主机控制器接口） HCI是连接主机和控制器的桥梁，负责两部分之间通信。\n特点：\n标准化接口，让不同厂商的设备互相兼容。\n传输命令、事件和数据包。\n举例：手机的应用层通过HCI命令告诉蓝牙芯片\u0026quot;开始扫描附近设备\u0026quot;，芯片执行后通过HCI事件返回扫描结果。\n六、双模蓝牙 双模蓝牙同时支持经典蓝牙和BLE，一个设备两种能力\n硬件层面同时支持BR/EDR和BLE。软件层面整合经典蓝牙和BLE的高层协议。\n支持经典蓝牙的SDP和BLE的GATT，灵活切换。\n举例：手机可以同时连接蓝牙耳机（经典蓝牙）和智能手环（BLE）\n七、总结 经典蓝牙和BLE虽然侧重不同，但架构相似：都是主机管逻辑，控制器管硬件，通过HCI通信\n经典蓝牙适合持续大数据传输，BLE适合低功耗间歇传输。\n双模设备兼具两种能力，应用更广泛。\n原文：CSDN · 蓝牙核心系统架构介绍 ","permalink":"https://btio.cn/posts/2025/11/10/%E8%93%9D%E7%89%99%E6%A0%B8%E5%BF%83%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E4%BB%8B%E7%BB%8D/","summary":"\u003ch2 id=\"section-1\"\u003e一、蓝牙的两种模式\u003c/h2\u003e\n\u003ch3 id=\"1经典蓝牙bredr\"\u003e（1）经典蓝牙（BR/EDR）\u003c/h3\u003e\n\u003cp\u003e用于高数据速率应用，如无线耳机、音箱、键盘。\u003c/p\u003e\n\u003cp\u003e特点是连接稳定，适合持续传输音频或大量数据。\u003c/p\u003e\n\u003ch3 id=\"2低功耗蓝牙ble\"\u003e（2）低功耗蓝牙（BLE）\u003c/h3\u003e\n\u003cp\u003e专为低功耗设备设计，如智能手环、温度计、传感器。\u003c/p\u003e\n\u003cp\u003e特点是耗电低，适合间歇性传输小数据。\u003c/p\u003e\n\u003cp\u003e总结：两种模式底层都用无线信号传输数据，都遵循蓝牙协议，只是侧重点不同。经典蓝牙重视速度和稳定，BLE重视省电和简单连接。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、蓝牙核心系统组成\u003c/h2\u003e\n\u003cp\u003e蓝牙核心系统由主机（Host）和控制器（Controller）两部分组成。\u003c/p\u003e\n\u003cp\u003e主机负责高层协议和控制逻辑，控制器处理底层射频和信号传输。两者分工合作，设备才能正常工作。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e三、主机部分（Host）\u003c/h2\u003e\n\u003cp\u003e主机包含多个协议模块，经典蓝牙和BLE有所不同。\u003c/p\u003e\n\u003cp\u003e经典蓝牙主要模块：\u003c/p\u003e\n\u003cp\u003eGAP（通用访问协议）：管理设备发现和连接建立。\u003c/p\u003e\n\u003cp\u003eSDP（服务发现协议）：帮助设备发现对方支持的服务。\u003c/p\u003e\n\u003cp\u003eL2CAP（逻辑链路控制协议）：管理数据流，负责数据分段和重组。\u003c/p\u003e\n\u003cp\u003eSMP（安全管理协议）：负责加密连接和密钥管理。\u003c/p\u003e\n\u003cp\u003eBLE主要模块：\u003c/p\u003e\n\u003cp\u003eGAP（通用访问协议）：管理设备发现和连接建立。\u003c/p\u003e\n\u003cp\u003eGATT（通用属性协议）：规定设备如何描述和交换数据属性。\u003c/p\u003e\n\u003cp\u003eATT（属性协议）：按照GATT规则传输数据。\u003c/p\u003e\n\u003cp\u003eSMP（安全管理协议）：负责加密连接和密钥管理。\u003c/p\u003e\n\u003cp\u003e经典蓝牙工作流程：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eGAP搜索附近设备并建立连接\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eSDP查找对方支持的服务\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eL2CAP处理数据传输，分段重组\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eSMP管理密钥和加密\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eBLE工作流程：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eGAP搜索附近设备并建立连接\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eGATT列出设备支持的服务和数据属性\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eATT按照GATT规定格式传送数据\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eSMP保障连接和数据安全\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"section-4\"\u003e四、控制器部分（Controller）\u003c/h2\u003e\n\u003cp\u003e控制器处理蓝牙底层功能，直接与硬件交互。\u003c/p\u003e\n\u003cp\u003e主要模块：\u003c/p\u003e\n\u003cp\u003eLL（链路层）：管理蓝牙设备的链路状态和数据包传输。\u003c/p\u003e\n\u003cp\u003eRF（射频层）：处理蓝牙信号的发送和接收。\u003c/p\u003e\n\u003cp\u003eIAL（等时音频层）：BLE特有，处理高实时性数据传输，例如音频。\u003c/p\u003e\n\u003cp\u003e控制器主要负责：\u003c/p\u003e\n\u003cp\u003e链路管理：连接的建立、维持、断开。\u003c/p\u003e\n\u003cp\u003e无线信号传输：信号调制解调、频率管理、纠错编码。\u003c/p\u003e\n\u003cp\u003e硬件交互：直接控制无线电模块发射和接收。\u003c/p\u003e\n\u003ch2 id=\"section-5\"\u003e五、HCI（主机控制器接口）\u003c/h2\u003e\n\u003cp\u003eHCI是连接主机和控制器的桥梁，负责两部分之间通信。\u003c/p\u003e\n\u003cp\u003e特点：\u003c/p\u003e\n\u003cp\u003e标准化接口，让不同厂商的设备互相兼容。\u003c/p\u003e\n\u003cp\u003e传输命令、事件和数据包。\u003c/p\u003e\n\u003cp\u003e举例：手机的应用层通过HCI命令告诉蓝牙芯片\u0026quot;开始扫描附近设备\u0026quot;，芯片执行后通过HCI事件返回扫描结果。\u003c/p\u003e\n\u003ch2 id=\"section-6\"\u003e六、双模蓝牙\u003c/h2\u003e\n\u003cp\u003e双模蓝牙同时支持经典蓝牙和BLE，一个设备两种能力\u003c/p\u003e\n\u003cp\u003e硬件层面同时支持BR/EDR和BLE。软件层面整合经典蓝牙和BLE的高层协议。\u003c/p\u003e\n\u003cp\u003e支持经典蓝牙的SDP和BLE的GATT，灵活切换。\u003c/p\u003e\n\u003cp\u003e举例：手机可以同时连接蓝牙耳机（经典蓝牙）和智能手环（BLE）\u003c/p\u003e\n\u003ch2 id=\"section-7\"\u003e七、总结\u003c/h2\u003e\n\u003cp\u003e经典蓝牙和BLE虽然侧重不同，但架构相似：都是主机管逻辑，控制器管硬件，通过HCI通信\u003c/p\u003e\n\u003cp\u003e经典蓝牙适合持续大数据传输，BLE适合低功耗间歇传输。\u003c/p\u003e\n\u003cp\u003e双模设备兼具两种能力，应用更广泛。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原文：\u003ca href=\"https://blog.csdn.net/qq_56070244/article/details/154621561\"\u003eCSDN · 蓝牙核心系统架构介绍\u003c/a\u003e\n\u003c/p\u003e","title":"蓝牙核心系统架构介绍"},{"content":"什么是BLE设备地址 BLE设备地址48位长度，用来识别低功耗蓝牙设备。\n地址分为公共地址和随机地址两大类，现代BLE设备为保护隐私，很少直接用公共地址广播。\n注意：BLE地址和经典蓝牙地址虽然都是48位，但机制不同。\n一、公共设备地址 由IEEE官方分配，全球唯一，永久不变。\n常见设备：早期蓝牙键盘、鼠标。\n优点：全球唯一，不会冲突。\n缺点：申请要钱，容易被追踪定位。\n地址结构：通常前3字节是厂商代码，后3字节厂商自己分配，也可以自定义。\n举例：00:1A:7D:12:34:56\n00:1A:7D = 苹果厂商代码 12:34:56 = 苹果内部设备序号\n二、随机设备地址 BLE随机设备地址，设备自己生成，分三种：\n1. 静态随机地址 开机时随机生成，本次开机内不变，重启后重新生成。\n地址特征：最高两位为\u0026quot;11\u0026quot;，地址C0-FF开头。\n常见设备：智能手环、体重秤、温度计。\n优点：免费，开机内稳定连接。\n缺点：设备多了可能冲突，重启后地址变化可能影响自动重连。\n2. 可解析私有地址 BLE隐私保护核心。地址定期变化（通常15分钟），配对过的设备可通过IRK密钥识别真实身份。\n地址特征：最高两位为\u0026quot;01\u0026quot;，地址40-7F开头。\n常见设备：iPhone、Apple Watch、Android手机、AirPods。\n工作原理：设备用IRK密钥生成临时地址，配对设备存有IRK可验证身份，陌生设备只看到随机地址。\n举例：商场里手机地址每10分钟变一次，商家无法追踪你。但AirPods能认出你的手机，因为有共享密钥。\n优点：防位置追踪，不影响已配对设备连接。\n缺点：需要密钥管理和地址解析算法。\n3. 不可解析私有地址（NRPA） 完全随机临时地址，任何设备都无法识别。\n地址特征：最高两位为\u0026quot;00\u0026quot;，地址00-3F开头\n常见场景：BLE Beacon广播、一次性数据推送。\n举例：商场Beacon广播促销信息，路人收到即可，无需连接和识别。\n优点：最强隐私保护。\n缺点：无法建立连接，只能单向广播。\n三、应用场景对比 蓝牙键盘鼠标：公共地址，稳定识别。\n智能手环体重秤：静态随机地址，成本低。\niPhone和智能手表：可解析私有地址，防追踪。\nBLE Beacon：不可解析私有地址，一次性推送。\n四、BLE和经典蓝牙地址区别 经典蓝牙主要用公共地址，很少变化。 BLE主流用随机地址（特别是可解析私有地址），定期变化保护隐私。 同一设备可能同时有经典蓝牙地址和BLE地址，它们独立。 BLE地址最高2位标识类型，经典蓝牙没有。 五、实用知识 查看BLE地址（Android）：设置→关于手机→状态信息→蓝牙地址。\n手机地址为什么总变？现代手机用可解析私有地址，每10-15分钟换一次，防止被追踪。\n识别地址类型看第一字节：\nC0-FF：静态随机 40-7F：可解析私有 00-3F：不可解析私有 其他：公共地址 总结： BLE地址虽然分为四类，但一般都使用随机地址，在开发中\n小型外设一般用静态随机，简单省钱\n手机可穿戴一般用可解析私有，保护隐私\n广播类一般用不可解析私有，单向推送\n原文：CSDN · BLE设备地址介绍 ","permalink":"https://btio.cn/posts/2025/11/09/ble%E8%AE%BE%E5%A4%87%E5%9C%B0%E5%9D%80%E4%BB%8B%E7%BB%8D/","summary":"\u003ch2 id=\"section-1\"\u003e什么是BLE设备地址\u003c/h2\u003e\n\u003cp\u003eBLE设备地址48位长度，用来识别低功耗蓝牙设备。\u003c/p\u003e\n\u003cp\u003e地址分为公共地址和随机地址两大类，现代BLE设备为保护隐私，很少直接用公共地址广播。\u003cbr\u003e\n注意：BLE地址和经典蓝牙地址虽然都是48位，但机制不同。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e一、公共设备地址\u003c/h2\u003e\n\u003cp\u003e由IEEE官方分配，全球唯一，永久不变。\u003cbr\u003e\n常见设备：早期蓝牙键盘、鼠标。\u003cbr\u003e\n优点：全球唯一，不会冲突。\u003cbr\u003e\n缺点：申请要钱，容易被追踪定位。\u003cbr\u003e\n地址结构：通常前3字节是厂商代码，后3字节厂商自己分配，也可以自定义。\u003c/p\u003e\n\u003cp\u003e举例：00:1A:7D:12:34:56\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e00:1A:7D = 苹果厂商代码\u003c/li\u003e\n\u003cli\u003e12:34:56 = 苹果内部设备序号\u003cbr\u003e\n  \u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"section-3\"\u003e二、随机设备地址\u003c/h2\u003e\n\u003cp\u003eBLE随机设备地址，设备自己生成，分三种：\u003c/p\u003e\n\u003ch3 id=\"1-静态随机地址\"\u003e1. 静态随机地址\u003c/h3\u003e\n\u003cp\u003e开机时随机生成，本次开机内不变，重启后重新生成。\u003cbr\u003e\n地址特征：最高两位为\u0026quot;11\u0026quot;，地址C0-FF开头。\u003cbr\u003e\n常见设备：智能手环、体重秤、温度计。\u003cbr\u003e\n优点：免费，开机内稳定连接。\u003cbr\u003e\n缺点：设备多了可能冲突，重启后地址变化可能影响自动重连。\u003c/p\u003e\n\u003ch3 id=\"2-可解析私有地址\"\u003e2. 可解析私有地址\u003c/h3\u003e\n\u003cp\u003eBLE隐私保护核心。地址定期变化（通常15分钟），配对过的设备可通过IRK密钥识别真实身份。\u003cbr\u003e\n地址特征：最高两位为\u0026quot;01\u0026quot;，地址40-7F开头。\u003cbr\u003e\n常见设备：iPhone、Apple Watch、Android手机、AirPods。\u003cbr\u003e\n工作原理：设备用IRK密钥生成临时地址，配对设备存有IRK可验证身份，陌生设备只看到随机地址。\u003cbr\u003e\n举例：商场里手机地址每10分钟变一次，商家无法追踪你。但AirPods能认出你的手机，因为有共享密钥。\u003cbr\u003e\n优点：防位置追踪，不影响已配对设备连接。\u003cbr\u003e\n缺点：需要密钥管理和地址解析算法。\u003c/p\u003e\n\u003ch3 id=\"3-不可解析私有地址nrpa\"\u003e3. 不可解析私有地址（NRPA）\u003c/h3\u003e\n\u003cp\u003e完全随机临时地址，任何设备都无法识别。\u003cbr\u003e\n地址特征：最高两位为\u0026quot;00\u0026quot;，地址00-3F开头\u003cbr\u003e\n常见场景：BLE Beacon广播、一次性数据推送。\u003cbr\u003e\n举例：商场Beacon广播促销信息，路人收到即可，无需连接和识别。\u003cbr\u003e\n优点：最强隐私保护。\u003cbr\u003e\n缺点：无法建立连接，只能单向广播。\u003c/p\u003e\n\u003ch2 id=\"section-4\"\u003e三、应用场景对比\u003c/h2\u003e\n\u003cp\u003e蓝牙键盘鼠标：公共地址，稳定识别。\u003cbr\u003e\n智能手环体重秤：静态随机地址，成本低。\u003cbr\u003e\niPhone和智能手表：可解析私有地址，防追踪。\u003cbr\u003e\nBLE Beacon：不可解析私有地址，一次性推送。\u003c/p\u003e\n\u003ch2 id=\"section-5\"\u003e四、BLE和经典蓝牙地址区别\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e经典蓝牙主要用公共地址，很少变化。\u003c/li\u003e\n\u003cli\u003eBLE主流用随机地址（特别是可解析私有地址），定期变化保护隐私。\u003c/li\u003e\n\u003cli\u003e同一设备可能同时有经典蓝牙地址和BLE地址，它们独立。\u003c/li\u003e\n\u003cli\u003eBLE地址最高2位标识类型，经典蓝牙没有。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"section-6\"\u003e五、实用知识\u003c/h2\u003e\n\u003cp\u003e查看BLE地址（Android）：设置→关于手机→状态信息→蓝牙地址。\u003cbr\u003e\n手机地址为什么总变？现代手机用可解析私有地址，每10-15分钟换一次，防止被追踪。\u003c/p\u003e\n\u003cp\u003e识别地址类型看第一字节：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eC0-FF：静态随机\u003c/li\u003e\n\u003cli\u003e40-7F：可解析私有\u003c/li\u003e\n\u003cli\u003e00-3F：不可解析私有\u003c/li\u003e\n\u003cli\u003e其他：公共地址\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"section-7\"\u003e总结：\u003c/h2\u003e\n\u003cp\u003eBLE地址虽然分为四类，但一般都使用随机地址，在开发中\u003c/p\u003e\n\u003cp\u003e小型外设一般用静态随机，简单省钱\u003cbr\u003e\n手机可穿戴一般用可解析私有，保护隐私\u003cbr\u003e\n广播类一般用不可解析私有，单向推送\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原文：\u003ca href=\"https://blog.csdn.net/qq_56070244/article/details/154617680\"\u003eCSDN · BLE设备地址介绍\u003c/a\u003e\n\u003c/p\u003e","title":"BLE设备地址介绍"},{"content":"BLE芯片的链路层定义了7种工作状态，同一时间只能处于一种状态，不同状态之间可以切换，一颗芯片可以有多个状态机同时工作，下面对这些状态进行介绍。\n七大状态介绍 一、待机状态（Idle） 芯片既不发送也不接收数据，处于休眠。\n特点：\n可以从任何状态进入待机状态\n可以从待机进入除连接状态外的所有状态\n不发射不接收，最省电\n举例：你的手环没在用时，芯片就在待机状态睡觉。\n二、广播状态（Advertising） 芯片主动发送广告包，告诉周围\u0026quot;我在这里\u0026quot;。\n作用：让别人发现你，并能回复扫描请求。\n举例：蓝牙音箱进入配对模式，不断广播\u0026quot;我是XX音箱，快来连我\u0026quot;。\n可以从广告状态直接进入连接状态。\n三、扫描状态（Scanning） 芯片监听周围的广告包，寻找可用设备。\n作用：发现附近的BLE设备。\n举例：手机打开蓝牙，扫描附近设备，此时手机处于扫描状态。\n四、发起状态（Initiating） 芯片接收特定设备的广告包，并回复连接请求。\n作用：主动发起连接。\n举例：手机点击\u0026quot;连接手环\u0026quot;，进入发起状态，向手环发送连接请求。\n可以从发起状态进入连接状态。\n五、连接状态（Connection） 设备已建立连接，可以双向传输数据。\n作用：进行数据交互。\n举例：手环和手机已连接，正在同步步数和心率数据。\n注意：只能从广播状态或发起状态进入连接状态，不能从待机直接连接\n六、同步状态（Synchronization） 芯片接收周期性广告的数据包。\n作用：同步接收定期更新的信息。\n举例：接收BLE定位信标的周期性位置信息。\n适用场景：需要定时接收广播数据但不建立连接。\n七、等时广播状态（Isochronous Broadcasting） 芯片在等时信道上发送数据包。\n作用：发送对时间敏感的数据流。\n举例：BLE音频设备同步传输音乐到多个耳机（LE Audio）。\n适用场景：需要精确时序的音频或数据传输。\n八、状态切换规则 常见切换路径：\n待机 → 广播 → 连接（外设被连接）\n待机 → 扫描 → 发起 → 连接（中心设备主动连接）\n连接 → 待机（断开连接）\n任何状态 → 待机（随时可以休眠省电）\n待机 → 广播/扫描/同步/等时广播（但不能直接到连接）\n九、实际应用场景 智能手环：\n没用时：待机状态（省电）\n等待连接：广播状态（被发现）\n已连接：连接状态（传数据）\n断开后：回到待机\n手机：\n搜索设备：扫描状态\n点击连接：发起状态\n连接成功：连接状态\n关闭蓝牙：待机状态\n十、开发注意事项 状态切换要合理：不能从待机直接到连接，必须经过广播或扫描→发起。\n多状态机场景：一颗芯片可同时运行多个状态机，比如一边广播一边扫描。\n功耗优化：设备空闲时尽快进入待机状态，减少无意义的广播或扫描。\n状态同步：应用层要了解当前链路层状态，避免在错误状态下操作。\n原文：CSDN · BLE的7种工作状态 ","permalink":"https://btio.cn/posts/2025/11/09/ble%E7%9A%847%E7%A7%8D%E5%B7%A5%E4%BD%9C%E7%8A%B6%E6%80%81/","summary":"\u003cp\u003eBLE芯片的链路层定义了7种工作状态，同一时间只能处于一种状态，不同状态之间可以切换，一颗芯片可以有多个状态机同时工作，下面对这些状态进行介绍。\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e七大状态介绍\u003c/h2\u003e\n\u003ch3 id=\"section-2\"\u003e一、待机状态（Idle）\u003c/h3\u003e\n\u003cp\u003e芯片既不发送也不接收数据，处于休眠。\u003c/p\u003e\n\u003cp\u003e特点：\u003c/p\u003e\n\u003cp\u003e可以从任何状态进入待机状态\u003c/p\u003e\n\u003cp\u003e可以从待机进入除连接状态外的所有状态\u003c/p\u003e\n\u003cp\u003e不发射不接收，最省电\u003c/p\u003e\n\u003cp\u003e举例：你的手环没在用时，芯片就在待机状态睡觉。\u003c/p\u003e\n\u003ch3 id=\"section-3\"\u003e二、广播状态（Advertising）\u003c/h3\u003e\n\u003cp\u003e芯片主动发送广告包，告诉周围\u0026quot;我在这里\u0026quot;。\u003c/p\u003e\n\u003cp\u003e作用：让别人发现你，并能回复扫描请求。\u003c/p\u003e\n\u003cp\u003e举例：蓝牙音箱进入配对模式，不断广播\u0026quot;我是XX音箱，快来连我\u0026quot;。\u003c/p\u003e\n\u003cp\u003e可以从广告状态直接进入连接状态。\u003c/p\u003e\n\u003ch3 id=\"section-4\"\u003e三、扫描状态（Scanning）\u003c/h3\u003e\n\u003cp\u003e芯片监听周围的广告包，寻找可用设备。\u003c/p\u003e\n\u003cp\u003e作用：发现附近的BLE设备。\u003c/p\u003e\n\u003cp\u003e举例：手机打开蓝牙，扫描附近设备，此时手机处于扫描状态。\u003c/p\u003e\n\u003ch3 id=\"section-5\"\u003e四、发起状态（Initiating）\u003c/h3\u003e\n\u003cp\u003e芯片接收特定设备的广告包，并回复连接请求。\u003c/p\u003e\n\u003cp\u003e作用：主动发起连接。\u003c/p\u003e\n\u003cp\u003e举例：手机点击\u0026quot;连接手环\u0026quot;，进入发起状态，向手环发送连接请求。\u003c/p\u003e\n\u003cp\u003e可以从发起状态进入连接状态。\u003c/p\u003e\n\u003ch3 id=\"section-6\"\u003e五、连接状态（Connection）\u003c/h3\u003e\n\u003cp\u003e设备已建立连接，可以双向传输数据。\u003c/p\u003e\n\u003cp\u003e作用：进行数据交互。\u003c/p\u003e\n\u003cp\u003e举例：手环和手机已连接，正在同步步数和心率数据。\u003c/p\u003e\n\u003cp\u003e注意：只能从广播状态或发起状态进入连接状态，不能从待机直接连接\u003c/p\u003e\n\u003ch3 id=\"section-7\"\u003e六、同步状态（Synchronization）\u003c/h3\u003e\n\u003cp\u003e芯片接收周期性广告的数据包。\u003c/p\u003e\n\u003cp\u003e作用：同步接收定期更新的信息。\u003c/p\u003e\n\u003cp\u003e举例：接收BLE定位信标的周期性位置信息。\u003c/p\u003e\n\u003cp\u003e适用场景：需要定时接收广播数据但不建立连接。\u003c/p\u003e\n\u003ch3 id=\"section-8\"\u003e七、等时广播状态（Isochronous Broadcasting）\u003c/h3\u003e\n\u003cp\u003e芯片在等时信道上发送数据包。\u003c/p\u003e\n\u003cp\u003e作用：发送对时间敏感的数据流。\u003c/p\u003e\n\u003cp\u003e举例：BLE音频设备同步传输音乐到多个耳机（LE Audio）。\u003c/p\u003e\n\u003cp\u003e适用场景：需要精确时序的音频或数据传输。\u003c/p\u003e\n\u003ch3 id=\"section-9\"\u003e八、状态切换规则\u003c/h3\u003e\n\u003cp\u003e常见切换路径：\u003c/p\u003e\n\u003cp\u003e待机 → 广播 → 连接（外设被连接）\u003c/p\u003e\n\u003cp\u003e待机 → 扫描 → 发起 → 连接（中心设备主动连接）\u003c/p\u003e\n\u003cp\u003e连接 → 待机（断开连接）\u003c/p\u003e\n\u003cp\u003e任何状态 → 待机（随时可以休眠省电）\u003c/p\u003e\n\u003cp\u003e待机 → 广播/扫描/同步/等时广播（但不能直接到连接）\u003c/p\u003e\n\u003ch3 id=\"section-10\"\u003e九、实际应用场景\u003c/h3\u003e\n\u003cp\u003e智能手环：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e没用时：待机状态（省电）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e等待连接：广播状态（被发现）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e已连接：连接状态（传数据）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e断开后：回到待机\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e手机：\u003c/p\u003e","title":"BLE的7种工作状态"},{"content":"无线通信技术早已经融入我们的生活，如手机通信，WiFi，蓝牙。背后都离不开它的支撑。那么，这项技术究竟是如何实现的呢？\n一、基本实现原理 无线通信用的是电磁波信号，我们说话的语音，拍的图片，听的音乐，这些信息首先通过调制技术加载到无线载波上，然后由发射设备发送出去。接收方通过解调操作，把载波上的信息取下来，恢复成原始信号。\n二、系统构建复杂性 构建一个无线通信系统很复杂，它主要涉及以下几个部分：\n1.模数转换 （确保传输和处理时兼容）\n2.RF射频设计 （负责信号发射和接收频率调控）\n3.协议栈 （无线通信标准）\n专业开发要求\n硬件设计需要高精度元器件匹配，系统调试需专门开发环境与经验，因此市场上很少从头搭建系统，而是普遍采用常用无线通信模块。把多个核心元器件集成在一起，直接实现特定通信功能，大大降低了应用门槛。\n无线通信模块\n无线通信模块就像是一个与装好的工具箱，主要由基带芯片，射频收发器等关键元器件组成，能直接作为通信终端使用，其中两大核心部分尤为重要。\n1.系带芯片：调制解调。同时还会检测传输中可能出现的错误，并及时纠正，确保信息准确。\n2.射频前端电路：信号强化器。射频信号在传输过程中可能会衰减，混入杂音，射频前端就负责对信号进行放大（提升强度，让传输更远）和滤波（过滤杂音）处理好的信号在通过天线发射出去，确保对方能清晰收到。\n总结\n无线通信系统的实现就是信息数字化“编码调制-射频发射-接收解调-信息还原”的闭环，在通过核心硬件的协同和统一标准的规定，最终实现稳定，高效的无线通信。\n原文：CSDN · 无线通信是如何实现的 ","permalink":"https://btio.cn/posts/2025/11/08/%E6%97%A0%E7%BA%BF%E9%80%9A%E4%BF%A1%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84/","summary":"\u003cp\u003e无线通信技术早已经融入我们的生活，如手机通信，WiFi，蓝牙。背后都离不开它的支撑。那么，这项技术究竟是如何实现的呢？\u003c/p\u003e\n\u003ch2 id=\"section-1\"\u003e一、基本实现原理\u003c/h2\u003e\n\u003cp\u003e无线通信用的是电磁波信号，我们说话的语音，拍的图片，听的音乐，这些信息首先通过调制技术加载到无线载波上，然后由发射设备发送出去。接收方通过解调操作，把载波上的信息取下来，恢复成原始信号。\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003e二、系统构建复杂性\u003c/h2\u003e\n\u003cp\u003e构建一个无线通信系统很复杂，它主要涉及以下几个部分：\u003c/p\u003e\n\u003cp\u003e1.模数转换           （确保传输和处理时兼容）\u003c/p\u003e\n\u003cp\u003e2.RF射频设计      （负责信号发射和接收频率调控）\u003c/p\u003e\n\u003cp\u003e3.协议栈                （无线通信标准）\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e专业开发要求\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e硬件设计需要高精度元器件匹配，系统调试需专门开发环境与经验，因此市场上很少从头搭建系统，而是普遍采用常用无线通信模块。把多个核心元器件集成在一起，直接实现特定通信功能，大大降低了应用门槛。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e无线通信模块\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e无线通信模块就像是一个与装好的工具箱，主要由基带芯片，射频收发器等关键元器件组成，能直接作为通信终端使用，其中两大核心部分尤为重要。\u003c/p\u003e\n\u003cp\u003e1.系带芯片：调制解调。同时还会检测传输中可能出现的错误，并及时纠正，确保信息准确。\u003c/p\u003e\n\u003cp\u003e2.射频前端电路：信号强化器。射频信号在传输过程中可能会衰减，混入杂音，射频前端就负责对信号进行放大（提升强度，让传输更远）和滤波（过滤杂音）处理好的信号在通过天线发射出去，确保对方能清晰收到。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e总结\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e无线通信系统的实现就是信息数字化“编码调制-射频发射-接收解调-信息还原”的闭环，在通过核心硬件的协同和统一标准的规定，最终实现稳定，高效的无线通信。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原文：\u003ca href=\"https://blog.csdn.net/qq_56070244/article/details/154576210\"\u003eCSDN · 无线通信是如何实现的\u003c/a\u003e\n\u003c/p\u003e","title":"无线通信是如何实现的"},{"content":"1. 无线通信的本质：把信息“寄”到空气里 无线通信就是利用电磁波在空中传递信息——类似寄快递：\n电磁波 = 快递盒：看不见摸不着，却能把内容送出去。 要传的内容 = 信息：音乐、指令、传感器数据等。 无线通信的流程只有两步：\n调制（打包）：把信息装进“电磁波快递盒”。 解调（拆包）：收件方拆开快递，还原信息。 ✏️ 蓝牙通信实例 调制：手机播放音乐时，蓝牙芯片把音频数据调制到 2.4 GHz 电磁波上，同时选择合适的频道，避免和 Wi-Fi “撞车”。 解调：蓝牙耳机接收到电磁波后，芯片解调数据并还原成声音。 2. 无线通信绕不开的麻烦：干扰 常见两种干扰类型：\n同频干扰 Wi-Fi、蓝牙鼠标都在用 2.4 GHz，像多辆快递车挤同一条路。一旦有人开大功率 Wi-Fi，旁边的蓝牙键盘就会延迟。解决办法是让蓝牙跳频，类似换车道。 环境干扰 金属、墙体会衰减信号；厚衣物、人体遮挡都会减弱电磁波；旁边的蓝牙音箱可能会“滋滋”响。 3. 蓝牙的抗干扰技术 自适应跳频（AFH） 蓝牙在 2.4 GHz 频段的 79 个子信道里高速跳频，每秒最多跳 1600 次。检测到某个频道被占用，就换到空闲频道，降低碰撞概率。 纠错编码（FEC） 发送时附加冗余信息，即使部分数据受干扰被破坏，也能用冗余位恢复原数据，保证语音、音乐的连续性。 4. 常见无线技术对比 技术 典型用途 特点与蓝牙对比 蓝牙 耳机、音箱、手表、键鼠 功耗低、延迟小、距离短（约 10 米），适合随身设备 Wi-Fi 上网、智能家居联网 速率高、覆盖远，但功耗大，不适合小电池设备 LoRa / NB-IoT 远程传感器、智慧城市场景 覆盖远、功耗低，但速率慢，延迟大，不适合音视频 5. 常见蓝牙连接问题排查 现象 可能原因 建议处理 蓝牙耳机偶尔卡顿 Wi-Fi 同频干扰 远离路由器或尝试重连 穿墙后信号大幅衰减 墙体、金属屏蔽电磁波 尽量保持直线距离，避免多层阻挡 口袋里手机连接手表不稳定 人体吸收 2.4 GHz 信号 手机放包里或手表靠近手机 微波炉一开蓝牙噪音变大 微波炉辐射同频信号 避免同时使用或保持距离 6. 总结 无线通信的核心是“调制 + 电磁波传输 + 解调”。蓝牙之所以适合日常设备，是因为它在功耗、抗干扰和延迟之间做到了平衡。如果遇到连接问题，多半是干扰或遮挡导致。\n欢迎在评论区提出蓝牙相关问题，我们继续深入交流！\n原文：CSDN · 无线通信原理 ","permalink":"https://btio.cn/posts/2025/11/08/%E6%97%A0%E7%BA%BF%E9%80%9A%E4%BF%A1%E5%8E%9F%E7%90%86/","summary":"\u003ch2 id=\"section-1\"\u003e1. 无线通信的本质：把信息“寄”到空气里\u003c/h2\u003e\n\u003cp\u003e无线通信就是利用\u003cstrong\u003e电磁波\u003c/strong\u003e在空中传递信息——类似寄快递：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e电磁波 = 快递盒\u003c/strong\u003e：看不见摸不着，却能把内容送出去。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e要传的内容 = 信息\u003c/strong\u003e：音乐、指令、传感器数据等。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e无线通信的流程只有两步：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e调制（打包）\u003c/strong\u003e：把信息装进“电磁波快递盒”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解调（拆包）\u003c/strong\u003e：收件方拆开快递，还原信息。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"section-2\"\u003e✏️ 蓝牙通信实例\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e调制\u003c/strong\u003e：手机播放音乐时，蓝牙芯片把音频数据调制到 2.4 GHz 电磁波上，同时选择合适的频道，避免和 Wi-Fi “撞车”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解调\u003c/strong\u003e：蓝牙耳机接收到电磁波后，芯片解调数据并还原成声音。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"section-3\"\u003e2. 无线通信绕不开的麻烦：干扰\u003c/h2\u003e\n\u003cp\u003e常见两种干扰类型：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e同频干扰\u003c/strong\u003e Wi-Fi、蓝牙鼠标都在用 2.4 GHz，像多辆快递车挤同一条路。一旦有人开大功率 Wi-Fi，旁边的蓝牙键盘就会延迟。解决办法是让蓝牙跳频，类似换车道。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e环境干扰\u003c/strong\u003e 金属、墙体会衰减信号；厚衣物、人体遮挡都会减弱电磁波；旁边的蓝牙音箱可能会“滋滋”响。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"section-4\"\u003e3. 蓝牙的抗干扰技术\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e自适应跳频（AFH）\u003c/strong\u003e 蓝牙在 2.4 GHz 频段的 79 个子信道里高速跳频，每秒最多跳 1600 次。检测到某个频道被占用，就换到空闲频道，降低碰撞概率。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e纠错编码（FEC）\u003c/strong\u003e 发送时附加冗余信息，即使部分数据受干扰被破坏，也能用冗余位恢复原数据，保证语音、音乐的连续性。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"section-5\"\u003e4. 常见无线技术对比\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e技术\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e典型用途\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e特点与蓝牙对比\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e蓝牙\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e耳机、音箱、手表、键鼠\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e功耗低、延迟小、距离短（约 10 米），适合随身设备\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWi-Fi\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e上网、智能家居联网\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e速率高、覆盖远，但功耗大，不适合小电池设备\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLoRa / NB-IoT\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e远程传感器、智慧城市场景\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e覆盖远、功耗低，但速率慢，延迟大，不适合音视频\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"section-6\"\u003e5. 常见蓝牙连接问题排查\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e现象\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e可能原因\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e建议处理\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e蓝牙耳机偶尔卡顿\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWi-Fi 同频干扰\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e远离路由器或尝试重连\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e穿墙后信号大幅衰减\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e墙体、金属屏蔽电磁波\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e尽量保持直线距离，避免多层阻挡\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e口袋里手机连接手表不稳定\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e人体吸收 2.4 GHz 信号\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e手机放包里或手表靠近手机\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e微波炉一开蓝牙噪音变大\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e微波炉辐射同频信号\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e避免同时使用或保持距离\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"section-7\"\u003e6. 总结\u003c/h2\u003e\n\u003cp\u003e无线通信的核心是“调制 + 电磁波传输 + 解调”。蓝牙之所以适合日常设备，是因为它在功耗、抗干扰和延迟之间做到了平衡。如果遇到连接问题，多半是干扰或遮挡导致。\u003c/p\u003e","title":"无线通信原理"},{"content":"LE audio介绍 LE Audio是它全称是Low Energy Audio（低功耗音频）\n是蓝牙技术联盟在5.2核心规范里引入的全新，低功耗音频技术标准\n在低功耗、多设备连接和音频共享上实现了超大突破！\nLC3编解码器 全新的音频编解码器LC3,是LE audio的灵魂所在，\n和传统SBC编解码器比起来，哪怕数据速率更低，也能提供相同甚至更好的音质。\n不管是日常听歌还是打电话，都能在省功耗的同时稳稳保住音质。\n多流音频技术 多流音频是LE audio的另一种优势！以前耳机主副耳要转发信号，现在智能手机等源设备能独立同步给两个耳机发多个音频流，直接消除了转发环节。 带来的好处：连接更可靠、延迟更低、左右声道同步更完美！\n比如听歌时来电话，不用断开音乐，能无缝切到通话，挂完电话又自动切回音乐，丝滑不卡顿。\n广播音频功能 广播音频功能是LE Audio最具有革命性的功能，直接开启了音频共享的新时代！蓝牙联盟将其品牌化为 Auracast™。 它允许一个音频源设备（比如手机）能以发射器身份，把一个音频流广播给附近无数个接收设备（比如不同品牌的耳机，只要支持LE Audio就能订阅，Auracast支持跨品牌设备接收） 比如：大家可以用不同的耳机听同一个音频，可以是音乐，电影，广播。\n总结：LE Audio为啥是未来趋势？ 简单说就是三个字：长！高！稳！ 更长的续航，更高的音质、更稳定的连接，现在支持LE Audio的设备越来越多，妥妥的未来蓝牙音频主流方向。 新手宝子如果想了解蓝牙音频新技术，LE Audio一定要重点关注！收藏这篇，随时回看。\n原文：CSDN · LE audio介绍 ","permalink":"https://btio.cn/posts/2025/11/07/le-audio%E4%BB%8B%E7%BB%8D/","summary":"\u003ch2 id=\"section-1\"\u003eLE audio介绍\u003c/h2\u003e\n\u003cp\u003eLE Audio是它全称是Low Energy Audio（低功耗音频）\u003c/p\u003e\n\u003cp\u003e是蓝牙技术联盟在5.2核心规范里引入的全新，低功耗音频技术标准\u003c/p\u003e\n\u003cp\u003e在低功耗、多设备连接和音频共享上实现了超大突破！\u003c/p\u003e\n\u003ch2 id=\"section-2\"\u003eLC3编解码器\u003c/h2\u003e\n\u003cp\u003e全新的音频编解码器LC3,是LE audio的灵魂所在，\u003c/p\u003e\n\u003cp\u003e和传统SBC编解码器比起来，哪怕数据速率更低，也能提供相同甚至更好的音质。\u003c/p\u003e\n\u003cp\u003e不管是日常听歌还是打电话，都能在省功耗的同时稳稳保住音质。\u003c/p\u003e\n\u003ch2 id=\"section-3\"\u003e多流音频技术\u003c/h2\u003e\n\u003cp\u003e多流音频是LE audio的另一种优势！以前耳机主副耳要转发信号，现在智能手机等源设备能独立同步给两个耳机发多个音频流，直接消除了转发环节。 带来的好处：连接更可靠、延迟更低、左右声道同步更完美！\u003c/p\u003e\n\u003cp\u003e比如听歌时来电话，不用断开音乐，能无缝切到通话，挂完电话又自动切回音乐，丝滑不卡顿。\u003c/p\u003e\n\u003ch2 id=\"section-4\"\u003e广播音频功能\u003c/h2\u003e\n\u003cp\u003e广播音频功能是LE Audio最具有革命性的功能，直接开启了音频共享的新时代！蓝牙联盟将其品牌化为 Auracast™。 它允许一个音频源设备（比如手机）能以发射器身份，把一个音频流广播给附近无数个接收设备（比如不同品牌的耳机，只要支持LE Audio就能订阅，Auracast支持跨品牌设备接收） 比如：大家可以用不同的耳机听同一个音频，可以是音乐，电影，广播。\u003c/p\u003e\n\u003ch2 id=\"section-5\"\u003e总结：LE Audio为啥是未来趋势？\u003c/h2\u003e\n\u003cp\u003e简单说就是三个字：长！高！稳！ 更长的续航，更高的音质、更稳定的连接，现在支持LE Audio的设备越来越多，妥妥的未来蓝牙音频主流方向。 新手宝子如果想了解蓝牙音频新技术，LE Audio一定要重点关注！收藏这篇，随时回看。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原文：\u003ca href=\"https://blog.csdn.net/qq_56070244/article/details/154504131\"\u003eCSDN · LE audio介绍\u003c/a\u003e\n\u003c/p\u003e","title":"LE audio介绍"},{"content":"蓝牙入门知识 一、蓝牙名字的由来 蓝牙名字,来自1000年前丹麦国王\u0026quot;哈拉尔德·蓝牙\u0026quot;的名字,因为这位国王统一了丹麦和挪威的部落。而蓝牙技术则统一了无线通信标准。\n二、蓝牙解决了什么问题 代替有线连接：告别数据线的束缚 设备互联互通：不同设备也能通信（统一标准） 低成本低功耗：比wifi省钱，比红外线灵活 蓝牙让短距离无线连接变得简单，便宜，省电。\n三、蓝牙是如何工作的 发现（Discovery）:搜索附近的蓝牙设备 配对（Paring）:首次建立信任，交换秘钥 连接（Connection）:正式通信，传输数据 技术层面：\n跳频，采用2.4Ghz 频段，这个频段很多设备。为了不被干扰，蓝牙采用了跳频技术，所以不会被窃听或干扰。 握手，设备首次建立信任，交换密钥，验证双方的身份，验证通过后，就能传输数据。 蓝牙传输数据，蓝牙传输数据就像是寄快递，会把大文件切成多个小包（经典蓝牙单包最多339字节，BLE单包最多251字节），每个包都贴上“收件人地址”和“顺序编号”，通过无线电波“快递”过去，对方收到后按编号重新拼装。 连接稳定性，当距离太远（一般有效范围10~100米，取决于蓝牙功率，一般设置为10米左右），障碍物遮挡，电量不足，电磁干扰（wifi,微波炉），发射和接收功率设置，蓝牙版本不兼容，都会影响蓝牙连接的稳定性。 补充：蓝牙是如何空气中传输数据的\n四、什么是2.4GHz频段 频率范围：2.400-2.4835GHZ\n全球免许可：不需要申请就能用\n穿透力适中：能穿墙但不会太远，10米左右\n注意：2.4Ghz频段很拥挤，WIFI，微波炉，无线鼠标都在用，所以蓝牙必须有抗干扰技术\n蓝牙为什么不用其他频段？\n低频段（比如900Mhz）:穿透性强但传输速度慢，天线太大装不进小设备\n高频段（比如5GHz）:传输速度快但穿透性差，耗电更多\n2.4Ghz刚好平衡了所有需求，够快，够小，够省电，够穿透\n五、跳频技术原理 经典蓝牙把2.4Ghz分成79个信道（每个1Mhz）,频率范围2402~2480，跳频速率固定1600次/秒（每625微秒跳一次，一个时隙625微秒），跳频序列是随机的，只有配对设备才知道。工作原理是提前约定好下一跳的信道，在79个信道中快速切换见面。经典蓝牙没有专门的广播专用信道，数据信道79个都可以用于传输。\nBLE和经典蓝牙不同的点在于，BLE把2.4Ghz分成40个信道（每个2Mhz），跳频速率自适应，有3个广播专用通道，37个数据通道。\n六、为什么蓝牙不会互相干扰 跳频序列不同，每个蓝牙设备都在不同的跳频，即使跳到同一信道，时间也是错开的，万一真的碰上，有重传机制。\n七、主设备\u0026amp;从设备 主设备（Master）:发起连接，管理网络，控制时钟\n从设备（Slave）：响应连接，被动接收指令\n举例：\n手机连蓝牙总是手机主动，耳机被动。这是蓝牙世界的基本规则，所以必须分出老大和小弟，这是蓝牙世界的等级制度。其中，一个当老大（Master），其他当小弟（Slave）,通常是主动发起连接的当老大，老大说了算，小弟负责听指挥。\n责任划分：\n主设备决定大家按什么顺序跳频，分配谁什么时候可以“说话”，当多个从设备同时要传输数据时，主设备决定谁先传谁后传。\n从设备必须跟着主设备时钟走，主从设备会定期进行时钟同步，短时间偏差会通过重传机制纠正，只有长时间失去同步才会断连。蓝牙采用时分双工机制，主设备占用偶数时隙，从设备占用奇数时隙，每个时隙为625微秒。\n分主从是避免混乱，如果设备都平等，谁来决定跳频顺序和分配传输时间。所以必须分主从来进行管理。主从结构让协议变的简单，主设备负责复杂的管理工作，从设备只需听指令就行。主设备下达命令，从设备立即执行，不用讨论。但主从之间有距离限制，距离太远，主设备管不到的地方，从设备就会断连。主从角色在某些情况可以切换，比如在TWS耳机中，耳机组队可以协商谁当主设备，谁当从设备。\n典型例子：\n手机=主设备，蓝牙耳机=从设备\n电脑=主设备，蓝牙鼠标=从设备\n注意：一个主设备最多连接7个从设备（蓝牙使用3位地址标识从设备，其中000保留，所以实际可用001~111=7个）\n八、微微网概念 **核心理解：**微微网是蓝牙世界的基本单位，微微网=1个主设备+最多7个从设备组成的蓝牙小网络。 基本规则：必须有一个主设备，最多有8个成员，并且保持同一个频率节奏，所有成员必须同步跳频，跟着主设备的节拍走。 生命周期：主设备发起网络连接，从设备陆续加入，大家按规则通信，主设备可以选择关闭或断开从设备的通信。 通信规则：从设备只能和主设备说话，不能直接和其他从设备聊天，必须通过主设备中转。 轮流通信：主设备分配时隙，每个从设备按顺序说话，每0.625毫秒为一个时间片。 时间轴（每个时隙625微秒）： 0 1 2 3 4 5 6 7 |____|____|____|____|____|____|____| 主 从1 主 从2 主 从3 主 从1 规则： - 偶数时隙：主设备发送 - 奇数时隙：从设备发送 为什么叫微微网？\n比局域网更小的网络，通常只有几个设备，覆盖范围很小（通常10m左右）\n微微网的优缺点？\n优点是管理简单，规则清晰，功耗低，适合小设备，连接稳定，冲突少。\n缺点是最多8个设备，中心化结构，依赖于主设备，从设备之间不能直接通信。\n九、蓝牙设备发现过程 谁在找谁 大多数人误解：手机主动搜索耳机，手机像雷达扫描。实际上是耳机在“喊话”，手机在“监听”，本质 = 广播 + 监听。\n发现过程 步骤1：耳机进入配对状态后，每隔一段时间广播一次，广播内容包括设备名、地址和类型。注意：耳机的广播时功耗远高于待机状态，所以广播一段时间会自动关闭省电。\n步骤2：打开蓝牙点击搜索设备，手机开始扫描多个询问频率，通常持续几秒到几十秒。注意：在监听过程中，蓝牙使用了专门的发现频率，避开WiFi干扰。\n步骤3：手机解析广播信息，包括设备名、MAC地址、信号强度等，然后按信号强度排序，显示在列表里。\n3. 为什么搜不到？ 原因1：耳机没\u0026quot;开口\u0026quot;，可发现模式超时会自动关闭省电。\n原因2：距离太远，蓝牙有效范围有限，穿墙信号会衰减。\n原因3：WiFi干扰，蓝牙和WiFi都工作在2.4GHz频段，频段重叠会相互干扰\n原因4：电量不足，电量低时，设备会降低广播功率或延长广播间隔省电。\n原因5：设备太多，多个设备同时广播会产生冲突。\n4. 技术深度解析 信号强度：信号越强，距离越近，连接越稳定。dBm值是衡量信号强度的单位，数值越大信号越强。\n功耗对比：可发现模式功耗远高于待机状态，这就是为什么会自动关闭的原因。\n发现频率设计：蓝牙使用专门的发现信道，避开WiFi最拥挤的频段，减少干扰。\n5. 速度对决 经典蓝牙：扫描时间较长，发现速度较慢\nBLE蓝牙：扫描时间短，发现速度快，功耗更低。\n应用场景：经典蓝牙通常用于耳机音响，BLE通常用于手表手环。\n6. 总结 蓝牙搜索的本质是一场精心设计的\u0026quot;广播-监听\u0026quot;游戏。看似简单，实则涉及频率协调、时序控制、功率管理和干扰规避，搜不到设备：不是手机找不到耳机，是耳机没在喊，或者手机没在听\n十、什么是蓝牙配对 配对是什么？\n配对就是两个蓝牙设备第一次连接时，建立信任关系的过程。实际上就是交换密钥和身份验证。\n为什么要配对？防止陌生设备随意连接，确保只有授权设备能访问。配对成功后，设备会保存密钥，以后自动连接。\n配对过程？\n步骤1：发现和选择，手机搜索到设备，用户点击连接，开始配对流程。\n步骤2：身份验证，根据设备类型不同，可能需要输入PIN码、确认数字或自动验证。\n步骤3：密钥交换，两个设备各自生成随机数，互相交换并计算出共同的密钥。\n步骤4：配对成功，密钥保存在设备内存中，显示配对成功，以后用这个密钥自动连接。\n配对方式有哪些？\nPIN码配对：设备有固定PIN码如0000或1234，用户输入数字完成配对。安全性较低，老设备常用。\n数字确认配对SSP：两个设备显示相同的6位数字，用户确认一致即可。安全性高，现代设备主流方式。\n一键配对：用户只需点击配对按钮，设备自动完成验证。最方便，安全性也好。\n配对失败原因？\n原因1：PIN码错误，输入了错误的PIN码或设备PIN码已更改。\n原因2：距离太远，配对需要稳定连接，距离太远信号不稳定会失败。\n原因3：配对数量达到上限，需要删除旧的配对记录腾出空间。\n原因4：操作超时，配对有时间限制，超时需要重新开始。\n原因5：设备模式不对，设备未进入配对模式或已退出。\n配对后发生什么？\n密钥存储：配对密钥保存在非易失性内存，断电不丢失。\n自动连接：下次开机自动尝试连接，不需要重新配对。\n信任关系：配对过的设备被标记为可信，可以访问更多功能。\n取消配对？\n为什么取消配对：换新设备、设备丢失、解决连接问题。\n如何取消：在蓝牙设置中找到设备，点击忘记或取消配对。\n取消后果：设备失去信任关系，下次连接需要重新配对。\n总结\n配对本质是密钥交换和身份验证，目的是建立安全的通信通道。一次配对，终身记忆（除非主动删除）。配对成功后，设备间的所有通信都会用这个密钥加密，保证数据安全。\n十一、蓝牙连接建立过程 连接是什么？\n连接是设备间建立通信通道的过程。配对是一次性的密钥交换，连接是每次使用都要做的握手过程\n为什么要连接？虽然已经配对，但每次使用前还要建立物理链路、同步时钟、验证身份，确保通信正常进行。\n连接触发条件？\n主动连接：用户点击连接按钮。\n自动连接：设备开机后自动搜索已配对设备，检测到就主动连接。\n连接建立过程？\n步骤1：设备搜索，主设备开始扫描，寻找已配对的从设备，从设备定期发送信号。\n步骤2：建立物理链路，主设备发送连接请求，从设备响应接受，建立基础无线链路。\n步骤3：同步时钟和跳频，主设备告诉从设备当前时间，同步跳频序列，确保双方节拍一致。\n步骤4：身份验证，使用配对时保存的密钥，相互验证身份，确保设备真实性。\n步骤5：建立逻辑连接，创建数据传输通道，分配传输参数，建立具体服务连接。\n连接参数协商？\n传输速度：根据设备能力协商最优速度，选择双方都支持的速度。\n功率等级：根据距离调整传输功率，近距离用小功率省电，远距离用大功率保证稳定。\n服务类型：确定使用什么功能，音频播放、文件传输还是键盘输入，不同服务有不同参数。\n连接失败原因？\n原因1：设备不在范围内，超出通信距离或信号被严重阻挡。\n原因2：配对信息丢失，设备被重置或配对密钥被删除。\n原因3：资源占用，设备已连接其他设备或达到连接数量上限。\n原因4：电量不足，设备电量过低无法维持稳定连接。\n原因5：身份验证失败，密钥不匹配或密钥过期。\n连接状态管理？\n连接中：正在建立连接，设备握手协商中。\n已连接：可以正常通信，数据可以传输。\n断开中：正在释放连接，清理资源。\n未连接：没有任何连接，设备空闲。\n设备会实时监控连接状态，出现异常时自动重连或断开。\n七、总结\n连接过程看似简单，实则包含搜索、链路建立、时钟同步、身份验证、参数协商多个步骤。每一步都必须成功，整个连接才能建立。连接成功后，设备会维持心跳检测，确保连接稳定。\n十二、蓝牙协议栈是什么 协议栈是什么？\n协议栈就是蓝牙通信的分层架构，每一层负责不同的功能，从底层硬件到顶层应用，分工明确。实际上就像一座办公大楼，每层负责不同工作，协同完成通信任务。\n为什么要分层？专业分工，每层只负责自己的工作，便于维护和扩展，某一层出问题不影响其他层。\n协议栈有哪些层？\n物理层（射频层）：负责发射和接收无线电波，工作在2.4GHz频段，管理天线和功率。\n链路层（基带层）：负责跳频、时间同步、数据包传输，确保基本通信正常。\n链路管理层LMP：负责连接建立和维护、安全认证、电源管理。\n逻辑链路层L2CAP：负责数据包拆分重组，为上层应用提供统一接口，管理多个应用数据流。\n上层协议层：RFCOMM模拟串口、SDP服务发现、OBEX对象交换等专用协议。\n应用层：A2DP音频传输、HFP免提通话、HID人机接口等具体应用。\n数据如何流动？\n发送数据（从上往下）：\n应用层发起请求 → 应用协议打包数据 → L2CAP分成小包 → LMP安排发送 → 基带调制信号 → 射频发射出去\n接收数据（从下往上）：\n射频接收信号 → 基带解调同步 → LMP验证身份 → L2CAP重组数据 → 应用协议解码 → 应用层使用数据\n各层的核心职责？\n物理层：发射接收无线电波，功率控制，频段管理。\n链路层：跳频控制，时钟同步，差错检测。\nLMP层：连接管理，安全加密，角色切换。\nL2CAP层：数据分片，服务质量控制，多路复用。\n协议层：具体业务实现，数据格式转换。\n应用层：用户交互，功能实现。\n协议栈的特点？\n分层解耦：每层独立工作，接口标准化，便于替换升级。\n向下兼容：新版本协议栈支持旧版本设备。\n灵活组合：根据需求选择不同的上层协议组合。\n标准统一：每层都有规范，确保不同厂商设备互通。\n协议栈的影响？\n延迟：数据经过每层处理会产生延迟，层数越多延迟越大。\n功耗：每层都消耗电量，优化协议栈可以降低功耗。\n兼容性：每层都要遵守标准才能与其他设备互通。\n性能：协议栈实现质量直接影响蓝牙性能。\n总结\n蓝牙协议栈是一个精心设计的分层架构，从底层无线电波到顶层应用功能，每层各司其职。这种分层设计使得蓝牙既强大又灵活，能够支持音频、数据、控制等多种应用场景。理解协议栈，就理解了蓝牙通信的核心机制。\n十三、物理层的作用 物理层是什么？\n物理层是蓝牙协议栈的最底层，负责发射和接收无线电波。实际上就是把数字信号变成电磁波发出去，再把接收到的电磁波变回数字信号。\n为什么重要？物理层是蓝牙通信的基础，没有它就无法传输数据。所有的蓝牙通信最终都要通过物理层实现。\n物理层的核心工作？\n频率管理：工作在2.4GHz ISM频段，从2402MHz到2480MHz，分成79个1MHz宽的信道。\n调制解调：发送时把数字信号调制成无线电波，接收时把无线电波解调成数字信号。\n功率控制：根据距离和需求调整发射功率，近距离用小功率省电，远距离用大功率保证稳定。\n跳频控制：每隔一段时间切换工作频率，每秒跳频1600次，避开干扰提高可靠性。\n跳频技术原理？\n什么是跳频？在79个信道之间快速切换，按照伪随机序列跳跃，主从设备同步跳频。\n为什么跳频？避开干扰频段，提高通信可靠性，增强安全性防窃听。\n跳频速度？经典蓝牙每秒1600跳，每个时隙625微秒。\n调制技术演进？\nGFSK调制：早期蓝牙使用，速度较慢但稳定，传输速率约1Mbps。\n增强调制EDR：蓝牙2.0引入，使用π/4-DQPSK和8DPSK，速率提升到2-3Mbps。\nBLE调制：低功耗蓝牙使用优化的GFSK，功耗更低，适合低速率应用。\n天线的作用？\n天线是什么？发射和接收电磁波的部件，相当于蓝牙的嘴巴和耳朵。\n天线类型？PCB天线成本低体积小，陶瓷天线性能好，外置天线传输距离远。\n天线影响？天线设计直接影响传输距离、信号强度和功耗。\n物理层的限制？\n传输距离：Class 1设备约100米，Class 2设备约10米，Class 3设备约1米。大部分消费设备是Class 2。\n传输速度：受调制方式限制，受干扰环境影响，受设备能力限制。\n功耗问题：发射功率越大越耗电，频繁跳频也消耗电量，需要平衡性能和功耗。\n干扰问题：2.4GHz频段拥挤，WiFi微波炉等都会干扰。\n物理层优化技术？\n自适应跳频AFH：检测哪些频段干扰严重，避开拥挤频段，只在干净频段跳跃。\n功率控制：实时调整发射功率，距离近时降低功率省电，距离远时提高功率保证连接。\n误码纠正：采用FEC前向纠错，检测并纠正传输错误，提高可靠性。\n总结\n物理层是蓝牙通信的基础，负责无线电波的发射接收。跳频技术使蓝牙能够在拥挤的2.4GHz频段稳定工作，调制技术决定了传输速度，功率控制平衡了距离和功耗。理解物理层，就理解了蓝牙为什么能够实现无线通信。\n十四、链路层的功能 链路层是什么？\n链路层在物理层之上，负责数据包传输的控制和管理。实际上就是决定什么时候发送数据、如何组织数据、如何处理错误，是蓝牙通信的智能控制中心。\n为什么重要？链路层保证数据可靠传输，管理通信时序，处理传输错误，是蓝牙稳定工作的关键。\n链路层的核心功能？\n时间分配：使用时分复用，主设备和从设备轮流发送，每个时隙625微秒。\n数据组织：定义数据包格式，包括访问码、包头、数据载荷，确保数据结构清晰。\n错误控制：采用CRC校验检测错误，使用ARQ机制重传错误数据，保证可靠传输。\n流量控制：防止发送过快导致接收方处理不过来，采用停等或滑动窗口协议。\n连接管理：建立连接、维持连接、释放连接，管理设备状态。\n时间分片机制？\n什么是时隙？蓝牙把时间划分成625微秒的时隙，主设备占偶数时隙，从设备占奇数时隙。\n为什么分时？避免主从设备同时发送产生冲突，保证通信有序进行。\n时隙使用？可以占用1个、3个或5个连续时隙，数据包越大占用时隙越多。\n数据包格式？\n访问码：标识微微网身份，防止接收到其他网络的数据，长度72位。\n包头：包含地址、类型、序号、校验等信息，长度54位。\n数据载荷：真正要传输的数据内容，长度0到2745位不等。\n错误检测和纠正？\nCRC校验：对数据计算校验码，接收方重新计算对比，发现错误就丢弃数据包。\nARQ重传：发送方等待接收方确认，超时未收到确认就重传，直到成功或达到最大次数。\nFEC纠错：在数据中加入冗余信息，接收方可以自动纠正部分错误，不需要重传。\n流量控制机制？\n停等协议：发送一个包等待确认，收到确认再发下一个，简单但效率低。\n滑动窗口：可以连续发送多个包，不必等每个包确认，提高传输效率。\n背压机制：接收方忙碌时通知发送方暂停发送，处理完再恢复。\n连接管理？\n建立连接：交换设备信息，同步时钟和跳频序列，协商连接参数。\n维持连接：定期发送心跳包保持连接，监控链路质量，动态调整参数。\n释放连接：正常断开流程，清理资源，通知上层应用。\n链路层的优化？\n自适应跳频：根据干扰情况动态调整跳频序列，避开干扰严重的频段。\n功率控制：根据信号质量调整发射功率，信号好时降低功率省电，信号差时提高功率。\n包类型选择：根据数据量选择合适的包类型，短数据用短包，长数据用长包。\n总结\n链路层是蓝牙的控制中心，负责时间分配、数据组织、错误控制、流量管理。通过时隙机制保证主从设备有序通信，通过校验和重传保证数据可靠传输。链路层的智能优化使蓝牙能够适应复杂的无线环境，保持稳定连接。\n十五、应用层协议介绍 应用层协议是什么\n应用层协议是蓝牙最上层的协议，定义了具体应用的通信规则。实际上就是不同功能使用不同的\u0026quot;语言\u0026quot;，音频、数据、控制各有专门的协议。\n为什么需要？底层协议只负责传输数据，应用层协议定义数据的含义和使用方式，让设备知道如何处理接收到的数据。\n音频类协议\nA2DP高级音频分发：专门传输高质量音乐，支持立体声，支持多种音频编码如SBC、AAC、aptX。\nAVRCP音视频远程控制：控制播放暂停、切换歌曲、调节音量、显示歌曲信息。\nHFP免提协议：专门用于电话通话，处理来电挂电话、传输语音、控制通话质量。\nHSP耳机协议：简化的通话协议，功能比HFP少，老设备使用。\n数据传输协议\nOBEX对象交换协议：传输文件、联系人、日历等对象，类似HTTP但用于蓝牙。\nFTP文件传输协议：浏览设备文件夹、上传下载文件，类似电脑的文件管理。\nPBAP电话簿访问协议：访问和同步联系人信息，车载系统常用。\n人机接口协议\nHID人机接口设备：连接键盘鼠标游戏手柄，传输按键和移动信息，支持USB HID规范。\nSPP串口协议：模拟传统RS232串口，通用接口什么都能传，但不针对特定应用优化。\n新兴协议：物联网时代\nBLE专用协议：GATT：通用属性协议，专门为低功耗设备设计，传输传感器数据、控制信号\nMesh协议：多设备组网通信，智能家居的专用语言，一个指令可以传递给所有设备\n协议的选择原则\n根据应用选择：听音乐用A2DP，打电话用HFP，传文件用OBEX，连鼠标用HID。\n根据设备能力：简单设备支持少数协议，复杂设备支持多种协议，手机几乎全支持。\n根据性能要求：实时性高用专用协议，功耗低用BLE协议，通用性高用SPP。\n协议的兼容性问题\n版本兼容：新协议通常向下兼容旧版本，老设备可能不支持新协议功能。\n厂商兼容：标准协议兼容性最好，私有协议只能同品牌使用。\n功能差异：廉价设备功能阉割，高端设备功能齐全，选购时注意支持的协议。\n无线耳机：使用A2DP传输音乐，AVRCP控制播放，HFP处理电话。\n蓝牙鼠标键盘：使用HID协议传输输入信息。\n文件传输：使用OBEX或FTP协议在设备间传文件。\n车载系统：使用HFP打电话，A2DP播音乐，PBAP同步联系人。\n智能手环：使用BLE GATT传输心率步数等健康数据。\n总结\n应用层协议是蓝牙的最后一环，定义了数据的具体含义和使用方式。不同应用使用不同协议，音频有A2DP和HFP，数据有OBEX，控制有HID，低功耗有GATT。理解应用层协议，就理解了蓝牙如何支持丰富多样的功能。\n原文：CSDN · 蓝牙启蒙认知 ","permalink":"https://btio.cn/posts/2025/10/14/%E8%93%9D%E7%89%99%E5%90%AF%E8%92%99%E8%AE%A4%E7%9F%A5/","summary":"\u003ch2 id=\"section-1\"\u003e蓝牙入门知识\u003c/h2\u003e\n\u003ch3 id=\"section-2\"\u003e一、蓝牙名字的由来\u003c/h3\u003e\n\u003cp\u003e        蓝牙名字,来自1000年前丹麦国王\u0026quot;哈拉尔德·蓝牙\u0026quot;的名字,因为这位国王统一了丹麦和挪威的部落。而蓝牙技术则统一了无线通信标准。\u003c/p\u003e\n\u003ch3 id=\"section-3\"\u003e二、蓝牙解决了什么问题\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e代替有线连接：告别数据线的束缚\u003c/li\u003e\n\u003cli\u003e设备互联互通：不同设备也能通信（统一标准）\u003c/li\u003e\n\u003cli\u003e低成本低功耗：比wifi省钱，比红外线灵活\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e蓝牙让短距离无线连接变得简单，便宜，省电。\u003c/p\u003e\n\u003ch3 id=\"section-4\"\u003e三、蓝牙是如何工作的\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e发现（Discovery）:搜索附近的蓝牙设备\u003c/li\u003e\n\u003cli\u003e配对（Paring）:首次建立信任，交换秘钥\u003c/li\u003e\n\u003cli\u003e连接（Connection）:正式通信，传输数据\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e技术层面：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e跳频，采用2.4Ghz 频段，这个频段很多设备。为了不被干扰，蓝牙采用了跳频技术，所以不会被窃听或干扰。\u003c/li\u003e\n\u003cli\u003e握手，设备首次建立信任，交换密钥，验证双方的身份，验证通过后，就能传输数据。\u003c/li\u003e\n\u003cli\u003e蓝牙传输数据，蓝牙传输数据就像是寄快递，会把大文件切成多个小包（经典蓝牙单包最多339字节，BLE单包最多251字节），每个包都贴上“收件人地址”和“顺序编号”，通过无线电波“快递”过去，对方收到后按编号重新拼装。\u003c/li\u003e\n\u003cli\u003e连接稳定性，当距离太远（一般有效范围10~100米，取决于蓝牙功率，一般设置为10米左右），障碍物遮挡，电量不足，电磁干扰（wifi,微波炉），发射和接收功率设置，蓝牙版本不兼容，都会影响蓝牙连接的稳定性。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e补充：蓝牙是如何空气中传输数据的\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003cimg alt=\"正文配图 1\" loading=\"lazy\" src=\"/images/csdn/152378955/image-01.png\"\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"section-5\"\u003e四、什么是2.4GHz频段\u003c/h3\u003e\n\u003cp\u003e频率范围：2.400-2.4835GHZ\u003c/p\u003e\n\u003cp\u003e全球免许可：不需要申请就能用\u003c/p\u003e\n\u003cp\u003e穿透力适中：能穿墙但不会太远，10米左右\u003c/p\u003e\n\u003cp\u003e注意：2.4Ghz频段很拥挤，WIFI，微波炉，无线鼠标都在用，所以蓝牙必须有抗干扰技术\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e蓝牙为什么不用其他频段？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e低频段（比如900Mhz）:穿透性强但传输速度慢，天线太大装不进小设备\u003c/p\u003e\n\u003cp\u003e        高频段（比如5GHz）:传输速度快但穿透性差，耗电更多\u003c/p\u003e\n\u003cp\u003e        2.4Ghz刚好平衡了所有需求，够快，够小，够省电，够穿透\u003c/p\u003e\n\u003ch3 id=\"section-6\"\u003e五、跳频技术原理\u003c/h3\u003e\n\u003cp\u003e经典蓝牙把2.4Ghz分成79个信道（每个1Mhz）,频率范围2402~2480，跳频速率固定1600次/秒（每625微秒跳一次，一个时隙625微秒），跳频序列是随机的，只有配对设备才知道。工作原理是提前约定好下一跳的信道，在79个信道中快速切换见面。经典蓝牙没有专门的广播专用信道，数据信道79个都可以用于传输。\u003c/p\u003e\n\u003cp\u003eBLE和经典蓝牙不同的点在于，BLE把2.4Ghz分成40个信道（每个2Mhz），跳频速率自适应，有3个广播专用通道，37个数据通道。\u003c/p\u003e\n\u003ch3 id=\"section-7\"\u003e六、为什么蓝牙不会互相干扰\u003c/h3\u003e\n\u003cp\u003e跳频序列不同，每个蓝牙设备都在不同的跳频，即使跳到同一信道，时间也是错开的，万一真的碰上，有重传机制。\u003c/p\u003e\n\u003ch3 id=\"section-8\"\u003e七、主设备\u0026amp;从设备\u003c/h3\u003e\n\u003cp\u003e主设备（Master）:发起连接，管理网络，控制时钟\u003c/p\u003e\n\u003cp\u003e从设备（Slave）：响应连接，被动接收指令\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e举例：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e        手机连蓝牙总是手机主动，耳机被动。这是蓝牙世界的基本规则，所以必须分出老大和小弟，这是蓝牙世界的等级制度。其中，一个当老大（Master），其他当小弟（Slave）,通常是主动发起连接的当老大，老大说了算，小弟负责听指挥。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e责任划分：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e        主设备决定大家按什么顺序跳频，分配谁什么时候可以“说话”，当多个从设备同时要传输数据时，主设备决定谁先传谁后传。\u003c/p\u003e\n\u003cp\u003e        从设备必须跟着主设备时钟走，主从设备会定期进行时钟同步，短时间偏差会通过重传机制纠正，只有长时间失去同步才会断连。蓝牙采用时分双工机制，主设备占用偶数时隙，从设备占用奇数时隙，每个时隙为625微秒。\u003c/p\u003e\n\u003cp\u003e        分主从是避免混乱，如果设备都平等，谁来决定跳频顺序和分配传输时间。所以必须分主从来进行管理。主从结构让协议变的简单，主设备负责复杂的管理工作，从设备只需听指令就行。主设备下达命令，从设备立即执行，不用讨论。但主从之间有距离限制，距离太远，主设备管不到的地方，从设备就会断连。主从角色在某些情况可以切换，比如在TWS耳机中，耳机组队可以协商谁当主设备，谁当从设备。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e典型例子：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e        手机=主设备，蓝牙耳机=从设备\u003c/p\u003e\n\u003cp\u003e        电脑=主设备，蓝牙鼠标=从设备\u003c/p\u003e\n\u003cp\u003e        注意：一个主设备最多连接7个从设备（蓝牙使用3位地址标识从设备，其中000保留，所以实际可用001~111=7个）\u003c/p\u003e\n\u003ch3 id=\"section-9\"\u003e八、微微网概念\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e**核心理解：**微微网是蓝牙世界的基本单位，微微网=1个主设备+最多7个从设备组成的蓝牙小网络。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e基本规则\u003c/strong\u003e：必须有一个主设备，最多有8个成员，并且保持同一个频率节奏，所有成员必须同步跳频，跟着主设备的节拍走。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e生命周期\u003c/strong\u003e：主设备发起网络连接，从设备陆续加入，大家按规则通信，主设备可以选择关闭或断开从设备的通信。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e通信规则\u003c/strong\u003e：从设备只能和主设备说话，不能直接和其他从设备聊天，必须通过主设备中转。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e轮流通信\u003c/strong\u003e：主设备分配时隙，每个从设备按顺序说话，每0.625毫秒为一个时间片。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e时间轴（每个时隙625微秒）：\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e0    1    2    3    4    5    6    7\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e|____|____|____|____|____|____|____|\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e 主   从1  主   从2  主   从3  主   从1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e规则：\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- 偶数时隙：主设备发送\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- 奇数时隙：从设备发送\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e为什么叫微微网？\u003c/strong\u003e\u003c/p\u003e","title":"蓝牙启蒙认知"},{"content":"01 蓝牙入门-蓝牙协议栈介绍 一、蓝牙类型 特性 经典蓝牙（BR,EDR) 低功耗蓝牙(BLE) 备注 全称 Basic Rate / Enhanced Data Rate Bluetooth Low Energy 经典蓝牙在嵌入式领域基本被放弃使用，现在着重使用低功耗蓝牙 主要用途 音频传输 低功耗设备 经典蓝牙适合大数据传输，BLE适合小数据、低功耗场景 传输速度 1-3 Mbps 1 Mbps 功耗 高 极低 BLE的功耗是经典蓝牙的1/10到1/100，适合电池供电设备 连接时间 较长（通常需要几秒） 极短（毫秒级） BLE连接快，适合频繁连接和断开的场景 兼容性 仅支持经典蓝牙 仅支持BLE的蓝牙 双模蓝牙设备可以同时支持经典蓝牙和BLE，兼容性更强 说明：\n这两种协议都包括 搜索管理，连接管理等机制，但他们底层是相互独立，不能互通的技术 厂家如果只实现了一种，那么只能与同样实现该技术的设备互通，如果厂家需要所有的蓝牙设备互通，那么就必须同时实现两种技术，而不去管是否真的需要 二、方案介绍 三种方案对比：\n方案 单芯片方案 蓝牙+MCU方案 Host+Controller方案 适用场景 蓝牙音响、蓝牙耳机 智能手环、智能手表 手机、平板等复杂设备 功能复杂度 功能固定，较简单 功能中等，需要额外处理能力 功能复杂，需要高性能处理 开发难度 低 中等 高 成本 低 中等 高 功耗 低 中等 高 蓝牙协议栈位置 集成在单芯片中 蓝牙芯片负责通信，MCU负责逻辑处理 蓝牙协议栈在主芯片中，蓝牙芯片负责底层通信 三、蓝牙协议栈 HFP：接电话、挂电话。 【蓝牙电话协议】 A2DP：听音乐。 【蓝牙音频传输协议】 AVRCP：切歌、调音量。 【蓝牙音视频远程控制协议】 PBAP：同步联系人。 【蓝牙电话本访问协议】 MAP：查看短信。 【蓝牙短信访问协议】 蓝牙协议的简化架构图\n协议栈说明： Application（应用层）： 这是用户直接接触的部分，比如用蓝牙耳机听音乐、接电话。 应用层通过调用协议层实现具体功能 Profiles（协议层）： 协议层定义了具体的功能，比如HFP用于电话，A2DP用于音乐。 每个协议对应一种特定的应用场景。 Host Layer（主机层）： 主机层负责管理蓝牙设备的连接、数据传输和安全。 包括GAP（通用访问配置）、GATT（通用属性协议）、ATT（属性协议）等。 Controller（控制器层）： 控制器层负责蓝牙的底层通信，包括HCI（主机控制接口）、LL（链路层）、PHY（物理层）。 这一层决定了蓝牙的通信方式（如广播、连接）。 Hardware（硬件层）： 硬件层是蓝牙芯片和射频模块，负责实际的信号发送和接收。 架构的核心部分 蓝牙协议架构分为两部分：Host（主机）**和**Controller（控制器）。这两部分通过**HCI（主机控制接口）**连接在一起，负责传递指令和数据。上层是逻辑实现，接近应用层，主要负责实现某些具体功能。下层是链路层，负责在数据通信时通过蓝牙协议进行传输。【蓝牙协议的主要修改通常集中在BT Controller这一层】\nHost负责逻辑功能，决定设备做什么。它处理高层的协议和应用逻辑，比如GAP、GATT、ATT等，确保设备能够执行特定的任务，如数据传输、设备发现、连接管理等。\nController负责底层通信，决定设备怎么做。它管理物理层和链路层的操作，包括射频信号的发送与接收、数据包的封装与解析、链路的建立与维护等。Controller是蓝牙通信的“执行者”，确保数据能够准确、高效地在设备之间传输。\nHCI是连接Host和Controller的桥梁，确保它们能够协同工作。HCI通过标准化的接口传递指令和数据，使得Host和Controller可以独立开发，但又能无缝配合。HCI可以通过硬件接口（如UART、SPI、USB）或软件API实现。\n在蓝牙芯片的选择上，有单模芯片和双模芯片两种。单模芯片只支持一种蓝牙技术，功能相对单一，通常用于低功耗蓝牙（BLE）应用。双模芯片则支持两种蓝牙技术，既能支持经典蓝牙（BR/EDR），也能支持低功耗蓝牙（BLE），功能更强大，兼容性更好，适合需要同时支持多种蓝牙应用的场景。\n协议栈是蓝牙设备的“操作手册”，定义了设备如何完成任务。它包含了从物理层到应用层的所有协议和规范，确保设备能够按照统一的标准进行通信。协议栈的实现通常由芯片厂商提供，开发者只需要在其基础上开发应用即可。\n总结来说，蓝牙协议架构的核心在于Host和Controller的分工协作，HCI作为两者的桥梁，确保指令和数据的顺畅传递。单模芯片和双模芯片的选择取决于应用需求，而协议栈则是蓝牙设备能够正常工作的基础。理解这些层次和模块的作用，能够帮助我们更好地开发和优化蓝牙应用\n官方文档说明 蓝牙协议栈极其复杂，官方文档有800多页，是由几家大型的公司联合起来，各自负责自己擅长的领域，最终实现的。初学者研究这个东西，不利于快速的上手学习和实用产品的开发，除非在蓝牙领域有十多二十年了，想成为蓝牙方面的专家，那你去彻底的把蓝牙协议吃透，那是可以的，作为初学者只需要简单了解，芯片的厂家一般会把蓝牙协议栈封装好，去做在芯片里面，我们直接使用就可以\n快速上手应用开发 要实现一个BLE应用，首先得有个支持BLE射频的芯片，还得有个配套的BLE协议栈，最后在协议栈上开发自己的应用。通常这些都已经封装在芯片里了，我们只需要在此基础上开发应用即可。协议栈的作用就是对应用数据进行层层封包，生成一个符合BLE协议的空中数据包。简单来说，就是把应用数据包在一系列的帧头和帧尾里。\n物理层（PHY）主要负责在物理信道上发送和接收数据包。BLE使用了40个射频信道，频率范围在2.4GHz（用来发送0和1）。链路层（LL）主要负责创建、修改和释放链路。它控制链路层状态机处于五种状态之一：准备、广播、扫描、发起连接、已连接。主机控制接口层（HCI）为主机和控制器提供了一个标准化的接口。这层可以通过软件API实现，或者使用硬件接口如UART、SPI、USB来控制。\nHOST层包括几个关键部分。通用访问配置层（GAP）代表了所有蓝牙设备通用的基本功能，比如设备发现、链接模式、安全、身份验证等。GAP服务包括设备的发现、链接模式、安全、身份验证、关联模块和服务发现。通信方式有两种：一种是单独开辟的信道用来配对链接，另一种是核心的信道用来发送数据。L2CAP层负责数据的分段重组和发送。不管是通过GAP和SMP进行数据配对，还是GATT和ATT进行数据传输，最后都要通过L2CAP进行数据的中转操作。安全管理层（SMP）是和GAP配套的，当你进行配对连接时，会用到SM层。这里会生成加密密钥和身份密钥，并管理它们的存储。SMP还负责生成随机地址，并将随机地址解析为已知设备身份。通用属性配置文件（GATT）用于配置属性文件，表述属性服务可选的客户端功能。不同的特征表示不同的操作，比如有的特征是写，有的是读。属性协议层（ATT）是封装了的数据传输协议。\n了解到这里就足够了。后续使用蓝牙时，上面这些内容通常都被厂家直接封装在芯片里，我们不需要过多关注。学习的重心应该放在如何使用蓝牙芯片上。\n在BLE中，设备可以划分为主机和从机。从机负责广播，主机可以发起连接。通常设备作为从机，而主机往往是手机。GATT角色中，设备可以分为服务端和客户端。主机和从机都可以作为服务端或客户端。在配对连接时，需要一个编码来识别蓝牙设备，类似于身份证号码。公共地址是48位，商业收费，具有全球唯一性。随机地址是厂家免费使用，提高了隐私和数据安全性。\n总结来说，BLE的应用开发，核心在于理解如何使用蓝牙芯片，而不是深入协议栈的底层细节。掌握GAP、GATT、ATT等高层协议的使用，能够帮助我们快速实现BLE应用。\n使用，提高了隐私和数据安全性。\n总结来说，BLE的应用开发，核心在于理解如何使用蓝牙芯片，而不是深入协议栈的底层细节。掌握GAP、GATT、ATT等高层协议的使用，能够帮助我们快速实现BLE应用\n原文：CSDN · 01-蓝牙协议栈介绍 ","permalink":"https://btio.cn/posts/2025/03/19/01-%E8%93%9D%E7%89%99%E5%8D%8F%E8%AE%AE%E6%A0%88%E4%BB%8B%E7%BB%8D/","summary":"\u003ch2 id=\"section-1\"\u003e01 蓝牙入门-蓝牙协议栈介绍\u003c/h2\u003e\n\u003ch3 id=\"section-2\"\u003e一、蓝牙类型\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e特性\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e经典蓝牙（BR,EDR)\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e低功耗蓝牙(BLE)\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e备注\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e全称\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBasic Rate / Enhanced Data Rate\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBluetooth Low Energy\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e经典蓝牙在嵌入式领域基本被放弃使用，现在着重使用低功耗蓝牙\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e主要用途\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e音频传输\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低功耗设备\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e经典蓝牙适合大数据传输，BLE适合小数据、低功耗场景\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e传输速度\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e1-3 Mbps\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e1 Mbps\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e功耗\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e高\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e极低\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBLE的功耗是经典蓝牙的1/10到1/100，适合电池供电设备\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e连接时间\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e较长\u003c/strong\u003e（通常需要几秒）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e极短\u003c/strong\u003e（毫秒级）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBLE连接快，适合频繁连接和断开的场景\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e兼容性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e仅支持经典蓝牙\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e仅支持BLE的蓝牙\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e双模蓝牙设备可以同时支持经典蓝牙和BLE，兼容性更强\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e说明\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e这两种协议都包括 搜索管理，连接管理等机制，但他们底层是相互独立，不能互通的技术\u003c/li\u003e\n\u003cli\u003e厂家如果只实现了一种，那么只能与同样实现该技术的设备互通，如果厂家需要所有的蓝牙设备互通，那么就必须同时实现两种技术，而不去管是否真的需要\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"section-3\"\u003e二、方案介绍\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e三种方案对比：\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e方案\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e单芯片方案\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e蓝牙+MCU方案\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003eHost+Controller方案\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e适用场景\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e蓝牙音响、蓝牙耳机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e智能手环、智能手表\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e手机、平板等复杂设备\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e功能复杂度\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e功能固定，较简单\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e功能中等，需要额外处理能力\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e功能复杂，需要高性能处理\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e开发难度\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e低\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e中等\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e高\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e成本\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e低\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e中等\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e高\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e功耗\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e低\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e中等\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e高\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e蓝牙协议栈位置\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e集成在单芯片中\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e蓝牙芯片负责通信，MCU负责逻辑处理\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e蓝牙协议栈在主芯片中，蓝牙芯片负责底层通信\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"section-4\"\u003e三、蓝牙协议栈\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHFP\u003c/strong\u003e：接电话、挂电话。 【蓝牙电话协议】\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA2DP\u003c/strong\u003e：听音乐。 【蓝牙音频传输协议】\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAVRCP\u003c/strong\u003e：切歌、调音量。 【蓝牙音视频远程控制协议】\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePBAP\u003c/strong\u003e：同步联系人。 【蓝牙电话本访问协议】\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMAP\u003c/strong\u003e：查看短信。 【蓝牙短信访问协议】\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e蓝牙协议的简化架构图\u003c/strong\u003e\u003c/p\u003e","title":"01-蓝牙协议栈介绍"}]