
简介BES2500/2600系列TWS项目原生SDK源代码包面向蓝牙音频设备开发工程师覆盖BES2500ZP/YP与BES2600YP平台适配官方开发板支持蓝牙双连、抢连及BLE适用于耳机方案评估、驱动移植与协议栈调试等场景。压缩包45.47MB共2000个文件核心源码主要由1509个h头文件、1155个c文件与206个cpp文件构成另有200个makefile、16个mk及cmake构建脚本以及txt/readme说明文档工程组织完整便于阅读与二次编译。代码覆盖通用算法库、音频编解码、电源管理、USB音频应用等模块目录结构清晰可逐层深入TWS蓝牙实现细节。资源已有2354人关注学习对基于BES平台做TWS耳机、智能音频设备开发的开发者具有工程参考价值。1. 一套从零开始点亮的 TWS 蓝牙音频 SDKBES2500YP/BES2600YP 源码到底能做什么拿到一块只焊了主控芯片和必要外围的最小板想让它在串口里打印一条日志、连上手机播一首歌这件事听起来并不难。但真上手这套 BES2500YP/BES2600YP 的 SDK 源码才会发现点不亮的原因往往不在你写的业务代码而在于你还没摸清它的工程组织方式、补丁机制和启动链路。这套源码覆盖了从 RTOS 系统启动、蓝牙协议栈连接、音频通路到产测命令的完整闭环适合想从零开始做一款 TWS/OWS 耳机、或者打算系统理解国产音频 SoC 开发流程的工程师。接下来按「工程结构 → 核心模块 → 编译烧录 → 避坑排查 → 二次开发」的顺序展开目标是让新手能在真实板子上把工程跑起来让熟手能直接定位改功能的位置。2. SDK 工程结构拆解源码目录、芯片资源与启动链路2.1 源码目录映射每层目录在管什么这套 SDK 的目录结构基本继承自原厂多年的项目积累第一眼会觉得很「散」但每个目录职责都非常明确。顶层目录常见是这样的目录职责btdm/蓝牙协议栈底层包含 BLE、经典蓝牙的控制器实现audio/音频框架ANC、EQ、DAC/ADC 管理是整套 SDK 里最厚的目录drivers/芯片外设驱动GPIO、I2C、SPI、UART、PWM、Keysense 等os/RTOS 内核层自带轻量调度器并提供 FreeRTOS 兼容层services/应用层服务配对、产测、OTA、充电盒通信、传感器管理projects/对应具体芯片型号和开发板的工程入口编译时最先看这里我拿到一份陌生 SDK 时会先做一件事看 projects 目录下有没有直接以 BES2500YP/BES2600YP 命名的子目录再翻 config/ 下的配置头文件看宏开关。因为这两个型号的工程高度同源不少代码是上一颗芯片改名复制过来的真正区分功能差异的往往是引脚的 PIN 映射表和头文件里的功能开关而不是「每一行都不一样」。projects 底下还有一个容易忽略的 patch 子目录里面放的是「为了修固化 ROM 里的 bug 而覆盖掉原函数」的补丁版本。这是这类音频芯片很常见的做法芯片内置了一部分固化代码SDK 编译时利用 -ffunction-sections 把同名的补丁函数替换进去实现运行期「打补丁」。如果你看某个函数时觉得逻辑不完整或者中途拐了个弯先怀疑它是不是在 patch 目录里定义了新版本否则很容易看半天源码也没找到真正的函数体。2.2 双核架构与内存布局这套芯片的地基BES2500YP/BES2600YP 属于典型的双核音频 SoC一颗核跑蓝牙协议栈和应用控制逻辑另一颗核专注音频采集与播放核间通过共享内存加消息队列通信。SDK 里对应的名称一般叫 CM 核和 DSP 核有的版本里也叫 app 核与 audio 核。这两颗核分工差异很大。应用核上跑的是 RTOS所有业务逻辑、按键、LED、蓝牙事件都在这里处理音频核则负责讲算 ANC 滤波器、EQ、混音几乎承担了所有实时计算。当初做第一个工程时我在应用核里看到很多「audio_xxx」开头的接口想当然地把音频处理逻辑往业务代码里塞翻车点在后面调试时才发现这些接口只是给音频核发命令真正的运算在另一个核上两边的数据通路要靠 SDK 提供的 audio framework 搭起来不能跨核直接操作内存。内存布局上这套 SDK 给每颗核划分了独占的 RAM 区域共享内存区通常专门用来放音频数据和核间消息。编译链接脚本不需要你自己从零写SDK 的 projects 目录里已经按芯片型号预置好了链接脚本。但有一点血泪经验如果你想加一块很大的 buffer不要随手在业务代码里定义一个静态大数组更稳妥的做法是去链接脚本里确认这块内存归属哪颗核再用 SDK 提供的内存管理接口申请。否则一个不够天然的跨界访问轻则 cache 一致性问题重则直接 HardFault。2.3 启动流程从复位向量到 RTOS 跑起来这类 SDK 的启动链路比普通 MCU 工程长很多从板子上电到业务代码运行中间大概要经过三个阶段固化 ROM 引导、外设初始化、RTOS 调度器启动。先看最前面的启动代码一般会在 projects/xxx/startup 下找到汇编写的复位向量文件。里面做的事情很固定关中断、初始化栈指针、清 BSS、拷贝 data 段。关键的是这个阶段结束之前会调用一个hal_platform_init()把系统时钟、SRAM 分区、Cache 策略和关键外设的电源域全部拉起来。我刚接触时曾经跳过这一步直接跑 main结果串口打印乱码蓝牙广播也不出来后来才明白所有 SDK 库函数都默认这些外设已经初始化过少了这一步等于在沙子地上盖楼。第二阶段进入 main 函数后SDK 会做一次「时钟和电源域」的全面配置然后启动 RTOS。RTOS 跑起来之后才是真正意义的应用入口一般叫app_init()或者app_main()里面做完 LED、按键、I2C 等外设的注册最后把蓝牙协议栈拉起来。下面是一段从 SDK 里抽象出来的初始化顺序伪代码常见工程的结构基本一致// apps/main.cpp 或 apps/app_main.c #include app_main.h int main(void) { // 1. 必须最先调用否则时钟/电源域全都不对 hal_platform_init(); // 2. 串口与日志系统提前打开方便后面打印 hal_uart_init(HAL_UART_ID_0); log_init(); // 3. 注册按键、LED 等外设 app_key_init(); app_led_init(); // 4. 启动蓝牙协议栈内部会拉起 BT/BLE 控制器 bt_if_init(BT_IF_MODE_MAIN); // 5. 启动 RTOS 调度从此之后代码跑在任务里 os_scheduler_start(); // 永远不会执行到这里调度器接管后不再返回 return 0; }这段代码的逻辑并不复杂但顺序非常讲究。hal_platform_init()必须最先执行因为后面的所有外设操作都依赖它配置好的时钟树bt_if_init()会向系统注册一堆回调这些回调由协议栈线程触发os_scheduler_start()一旦执行就不再返回。如果你需要加自己的逻辑比较规范的做法是在app_init()里创建一个独立任务而不是在 main 里直接写一个 while 死循环。我在第一次移植时就犯过这个错直接在 main 里加了 for 循环做传感器轮询结果调度器跑起来之后我的轮询逻辑占用了唯一一个启动核蓝牙事件完全得不到响应。3. 核心模块实战蓝牙协议栈、音频链路与产测命令3.1 蓝牙协议栈初始化与连接事件回调蓝牙协议栈是这套 SDK 里最容易被当「黑匣子」的部分因为平时的业务代码几乎不会直接碰底层控制器而是通过 SDK 的事件回调机制感知链路状态。以经典蓝牙的 A2DP 连接流程为例SDK 会以回调的方式通知你「连接到了哪台设备」「音频流是否正在播放」。// services/bt/bt_app_interface.h 中的连接回调结构简化后 typedef struct { // 设备已连接返回对端地址与类型 void (*on_connected)(bt_bdaddr_t *addr, bt_dev_class_t dev_class); // 设备断开连接reason 为断开原因码 void (*on_disconnected)(bt_bdaddr_t *addr, uint8_t reason); // A2DP 媒体通道打开可以开始处理音频流事件 void (*on_a2dp_stream_started)(bt_bdaddr_t *addr); void (*on_a2dp_stream_stopped)(bt_bdaddr_t *addr); } bt_app_callback_t;这段回调注册很简单但技术含量在于「线程上下文」。这些回调不是在业务线程里同步触发的而是由蓝牙协议栈的任务线程分发过来的。如果你的回调里直接调用了阻塞接口或者操作了非线程安全的数据结构就有概率碰上概率性的死锁和崩溃。常见做法是在回调里只做事件标记把真正重的处理丢给自己的业务任务去消费。SDK 里会自带一个消息队列接口我一般习惯把连接状态包装成一个 app 内部消息发出去。参数上有两个值得留意的点reason断连原因码是排查问题的重要线索系统提示「断连但不掉电」往往要先看这个值而bt_dev_class_t决定了对端是什么设备类型在 TWS 耳机里对端既可能是手机也可能是另一只耳塞分支判断别写死。3.2 音频通路从 ANC 到扬声器的完整链路音频部分是这套 SDK 的精髓也是两个型号之间差异最大的点。BES2500YP 相对基础BES2600YP 因为算力更强可以承载更复杂的降噪和多麦克风阵列算法但 SDK 的音频通路框架基本一致外部声音经模拟麦克风进 ADC变成数字信号后进入一颗核的 DSP 处理链经过 ANC 降噪、EQ 增益、混音之后再送到 DAC 驱动扬声器。音频处理链在 SDK 里一般用「单元连接」的方式描述也就是把每段处理当成一个节点按顺序串成一个图。// audio/aud_anc/ 下的 ANC 处理链配置示例 #include aud_anc.h static void audio_anc_setup(void) { // 第一步选择内外麦克风输入对应前馈/反馈降噪 aud_anc_cfg_t cfg; cfg.mic_src AUD_ANC_MIC_FF; // 前馈麦克风ANC 产线上通常会分类测试 cfg.mic_gain MIC_GAIN_44DB; // 麦克风模拟增益调大容易吃底噪 cfg.bypass false; // true 可以绕过整个 ANC 逻辑用于对照测试 // 第二步把 ANC 模块挂进主处理链 aud_anc_open(cfg, audio_anc_event_handler); // 第三步设置降噪深度一般设到 -35dB 以上会开始有负压感需要按腔体实测 aud_anc_set_noise_level(AUD_ANC_LEVEL_NORMAL); }ANC 调试里的一个残酷现实是代码里的参数只是起点最终效果几乎完全取决于耳机前腔、后腔、泄压孔的声学设计。如果你的板子是公模开发板照搬 SDK 默认参数大概率能跑通但降噪曲线未必好看。我一般会先把bypass打开跑一遍音频通路确认扬声器出声正常再分步接入 ANC。mic_gain这个参数尤其容易踩雷调太高会在安静环境下听到明显底噪声调太低 ANC 又「压不下去」。音频链路还有一个容易误解的地方DAC 输出的音量控制分模拟和数字两级。SDK 里既有纯软件的数字音量也有由芯片内部的模拟增益控制的硬件音量。调试时如果发现音量调不动先确认你写的是不是「硬件音量的寄存器操作」很多刚上手的人改的是软件音量但硬件级已经把信号压没了怎么调软件都白搭。3.3 产测与配置工厂模式下看板子还活着TWS 耳机这种产品有一条特殊的开发需求链单个板子跑通功能只算第一步工厂量产时每只耳塞都要检查麦克风、扬声器、按键、充电仓通信是否正常。所以 SDK 里专门留了一套产测模式入口一般叫 FCT 或者 factory test。它启动后会跳过正常的配对流程直接监听 UART 命令每一个外设都由产测上位机通过串口发指令驱动。// services/fct/fct_main.c简化后的产测命令分发 #include fct.h static void fct_handle_rx(uint8_t cmd, uint8_t *param, uint16_t len) { switch (cmd) { case FCT_CMD_LED_TEST: // 点亮红色 LED产测人员肉眼确认或光耦检测 hal_led_set_color(HAL_LED_COLOR_RED); fct_reply(FCT_STATUS_PASS, NULL, 0); break; case FCT_CMD_MIC_TEST: // 打开麦克风路采集 3 秒数据做能量统计后回传 fct_mic_collect(param[0], 3000); break; case FCT_CMD_ANC_TEST: // 利用内置信号发生器播放扫频检查扬声器频率响应 fct_anc_sweep(20, 20000, 10); break; default: fct_reply(FCT_STATUS_UNKNOWN_CMD, NULL, 0); break; } }产测这一块对做产品的工程师尤其重要因为大部分开发版环境下你不会遇到「十个板子有三个不良」的问题但工厂里这是常态。产测代码在设计上有两个原则一是所有测试项必须可以在没有手机的情况下独立完成二是每个测试结果必须有明确返回码配合上位机形成报表。我习惯保留下位机的产测协议解析代码后面开发不管改到什么功能先在产测里把对应 GPIO 点亮确认硬件通路还活着再谈业务逻辑。4. 编译烧录与调试把工程跑在真实板子上4.1 工具链与编译入口这类 SDK 的编译方式和平时的 MCU 工程有一点差别它不是用一个 IDE 点按钮生成一个 hex 就结束而是要配合脚本完成宏开关配置、多段镜像拼装、补丁提取等步骤。SDK 通常会提供一套基于 makefile 的编译系统顶层入口在 projects 目录下。编译前要先看两个东西工具链和编译目标。工具链一般是 ARM GCC 或者原厂基于 LLVM 定制的编译器SDK 文档里会写明版本要求。我踩过最大的一次坑是用了系统里比较新的 GCC 版本去编译老工程链接阶段爆出一堆.ARM.exidx相关的警告最后 SDK 编译不过去。从那以后我都是老老实实使用 SDK 自带的交叉编译器目录不试图用系统 GCC 去碰运气。编译命令一般长这样# 进入工程目录以 BES2600YP 开发板工程为例 cd projects/BES2600YP_evb # 编译前先清一次避免旧配置残留 make clean # 指定编译配置通常在 Makefile 里通过环境变量控制 make BUILD_TYPErelease BOARDBES2600YP_EVB AUDIO_MODULEanc # 生成最终烧录镜像: 产物一般在 out/ 或 build/ 目录下BUILD_TYPE可以取 debug/release 两种debug 会带完整的调试符号AUDIO_MODULEanc会把 ANC 相关代码编进去这个宏开关如果配置错了后面编译出来的固件功能会和板子上的物料对应不上。命令执行完成后产物文件夹会出现多个.bin文件它们会由脚本拼合成一个烧录镜像——烧录时不能只取其中一个 bin 下进去否则芯片启动时找不到对应的启动头这是很多第一次接触的人最容易踩的坑。4.2 烧录与日志两种调试姿势烧录工具链一般是原厂上位机加 USB 下载器。把板子接上按住烧录按键进入下载模式上位机会识别出一个「USB 下载设备」。整个烧录过程大概几十秒完成后板子会自动复位运行。如果你烧录的是产测固件串口上会以固定波特率打印产测启动日志此时可以直接用串口工具发指令。调试姿势上我强烈建议先打通日志链路再调功能。SDK 的日志系统通常支持分级控制通过宏或运行期接口调整打印等级// 日志模块使用示例console/log.h #include log.h void app_ble_handle_connected(void) { // DEBUG: 详细状态信息平时一般关掉 LOG_DEBUG(BLE connected, addr%02x:%02x:%02x, addr[0], addr[1], addr[2]); // INFO: 常规事件开发期常开 LOG_INFO(BLE link established); // ERROR: 异常时固定打印产线也靠这个判断 LOG_ERROR(BT controller init failed, ret%d, ret); }日志等级对应关系是DEBUG INFO WARN ERROR运行时用模块名控制开关。我一般习惯在关键业务链路里加LOG_INFO在参数异常、内存申请失败这类分支里加LOG_ERROR。很多看起来「偶现」的问题其实只要在足够多的节点打了日志跑几轮就能看出规律。别小看这个笨办法在这套 SDK 里它比在线调试器好用因为蓝牙时序一断点就乱了。4.3 失败观察编译通过不算点亮很多人在工程编译通过之后就觉得「板子点起来了」但嵌入式开发里编译通过只是万里长征第一步。我判断「工程跑起来」有一套动作模板第一看启动日志。复位之后串口如果打印出完整的版本号和系统初始化信息说明启动链路没问题。第二看蓝牙广播。用手机去搜设备列表能找到 SDK 默认的广播名说明控制器正常。第三看音频通路。连上手机播放音乐能听到声音且音量可调说明 DAC 链路走通了。第四看 ADC 数据。用产测命令读一次麦克风能量值确认录音方向通。如果第 2 步过不了问题大概率在射频匹配或天线跟软件关系不大如果第 3 步过不了常见原因是 I2S 或 DAC 的时钟配置不对得先从音频核的日志里看有没有报clk not locked之类的错误。这套动作模板跑下来基本能把「系统级问题」和「应用级问题」区分开再往深处排查效率会高很多。5. 避坑与排查编译溢出、连接异常、音频啸叫的排查清单5.1 编译期避坑链接溢出与宏开关不一致现象链接阶段提示.bss will not fit in region RAM而且溢出的量不大不小几千字节的样子。原因常见的有两类。一是你往业务代码里加了比较大的静态缓冲区SRAM 分区放不下二是 BES2600YP 和 BES2500YP 的内存容量本来就不一样你把 2600 的工程配置强行用在 2500 上链接脚本里的内存区域上限不符。解决先看链接脚本里每个 region 的起始地址和大小确认是总量不足还是某个核的区域不够。如果是某个核的地址空间不够把大 buffer 挪到共享内存区前提是保证访问时序安全如果是芯片型号配错换回对应的链接脚本重新编译。我一般会在 Makefile 里写一句「禁止随便换 BOARD 参数」的注释提醒自己每次编译前先核对板型。现象编译通过但烧录后功能缺失比如 ANC 菜单调不出来。原因宏开关编译期不一致。这种工程里几乎每个功能模块都有一堆CFG_xxx或者AUDIO_MODULE_xxx的宏分布在多个头文件里。你在一个文件里打开了 ANC 宏可实际上某段关键代码在预编译时拿到的宏定义是关着的这个模块整个会被裁掉。解决编译产物里搜不到对应函数符号基本可以断定是被宏裁剪。解决方法是统一查定义来源先用grep -rn AUDIO_MODULE_ANC config/找到宏定义位置再用make -e或顶层配置脚本统一覆盖不要零散地在各处手改。我第一次调 ANC 时就经历过编译通过但功能缺失的情况后来发现是 Makefile 里传进去的变量跟头文件里的 __WEAK 宏打架统一之后立刻正常。5.2 运行期避坑ANC 啸叫、蓝牙断连与低功耗唤醒异常现象打开 ANC 后耳机出现明显啸叫尤其用手堵住耳塞时啸叫更严重。原因遗留了声学分立问题不能先改代码。ANC 的降噪链路里前馈麦克风采到的信号和扬声器输出的信号在声路上发生相位叠加如果麦克风布置离出声孔太近或者前腔密封不符合预期系统就变成正向反馈。解决先检查麦克风装配位置和出声孔的距离确认腔体密封再在 SDK 里按前馈/反馈分类重新校准参数把mic_gain适当压低一档重新跑 ANC 测试。代码层面能做的其实很有限真正决定天花板的是声学结构。现象功放连接不稳定听歌时偶发断连但看reason断连码是正常断开比如值等于 0x13 等常规值。原因射频上的瞬断和蓝牙协议栈的重连策略共同作用。SDK 默认的重连机制可能不够激进链路一断没有立刻触发重连或者因为天线匹配问题让接收灵敏度不够这种情况常见于用手握着板子、板子天线区域被遮挡。解决先排除硬件天线因素把设备放到固定位置测试再在 SDK 的「善后策略」里缩短重连间隔、提高重连次数上限。如果断连经常发生在打游戏这种高负载多任务并发场景需要看一下蓝牙优先级配置必要时调低音频码率换取链路稳定性。现象低功耗模式下系统偶尔唤醒但各外设状态错乱比如按键没反应或屏幕闪了一下就不动了。原因唤醒源配置不完整。这套 SDK 里低功耗是由一个专门的电源管理模块统一管理唤醒事件可能是 GPIO 边沿、定时器、充电仓通信等任意一路。如果你注册了某个唤醒源却没有在唤醒后的应用层初始化流程里补上对应的外设复位系统就处于「半睡半醒」状态。解决把pm_sleep的唤醒日志打开看每次唤醒是由哪个中断源触发然后检查唤醒事件处理函数里是不是把相关外设重新初始化了一遍。我自己后来定了条规矩每次要加新外设都要同时回答「它睡不睡」和「醒了以后怎么恢复」两个问题答复不出来就不允许合入代码。5.3 排查手段日志分级、寄存器 dump 与配置对比这类双核 SDK 的疑难杂症不能靠感觉猜要建立一套固定的排查方法。我的固定组合分成三步第一步把日志分级拉到最低开 DEBUG 级输出重点看音频核的日志。音频核和应用核的日志往往会通过不同虚拟通道传给串口先确认两个核的日志都出来了。第二步做寄存器 dump。SDK 一般带了命令行或被测试钩子可以在异常时把当前各核寄存器上下文打印出来拿到异常现场。第三步对比配置。找个参照物如果手头至少有两块板子一块正常一块异常把正常板子的配置头文件 dump 下来对比 diff这对发现宏开关漂移特别有效。我自己遇到过一个「同一版固件在 A 板上正常、在 B 板上死机」的问题靠的就是配置 diff 定位到 B 板的 EEPROM 里存的是旧校准参数运行时读出来覆盖了 SDK 的内存参数导致音频核拿到的缓冲区指针越界。这种问题在代码里反复读读不出结果的只有对比配置或者对比日志才能发现。6. 二次开发思路从改一个 GPIO 按键到自定义事件分发6.1 最小改动点选择拿到这套源码后如果想快速找到「对的感觉」最值得做的第一个改动是加一个自定义的按键功能。为什么选按键因为按键涉及外设驱动、事件注册、业务分发三层几乎是 SDK 串联体系的缩影又不需要碰蓝牙音频这种复杂的时序链路。以给耳机加一个「三击切换 EQ 模式」为例改动点分布在三处。驱动层确认 GPIO 型号、按键对应 IO 是否被其他外设占用事件层看 SDK 按键模块的短按/长按/连击识别机制需不需要补一个「三击」识别状态业务层在事件分发回调里把 EQ 切换命令发送到音频核。// 业务层接收按键事件并触发 EQ 模式切换简化示例 #include app_key.h #include aud_eq.h static void app_key_event_handler(uint32_t key_id, uint32_t event) { switch (key_id) { case KEY_ID_MULTIMEDIA: if (event KEY_EVENT_THREE_CLICKS) { // 三击事件 static uint8_t eq_mode 0; eq_mode (eq_mode 1) % 4; // 在 4 种预设间轮换 aud_eq_set_preset(eq_mode); // 下发到音频核 } break; default: break; } }这里的KEY_ID_MULTIMEDIA对应板子上键位表中的某个键KEY_EVENT_THREE_CLICKS是按键模块已经识别好的事件类型。如果 SDK 不支持三击就得在按键驱动里补一段计时计数状态机或者查一下现有短按事件的释放间隔用两次短按近似实现。aud_eq_set_preset是发命令给音频核不要在应用核里直接写 EQ 参数那样不会生效。6.2 验证闭环改完代码后的验证我分为三层。第一层是编译验证确认新增的函数不破坏原有构建第二层是行为验证烧录后按键触发时通过日志接口打印「当前 EQ 模式已切换」确认事件确确实实到达了业务层第三层是效果验证播放一首低频较多的歌听不同 EQ 模式的听感差异。如果切换音频模式时出现爆音往往是aud_eq_set_preset执行时音频线程正在处理数据需要先在业务层加一个互斥锁或延迟切换的机制等当前音频帧处理完再切换。类似的竞态问题在这套 SDK 里很多核心思路永远是提前在事件分发里做缓冲而不是在主链路里抢资源。6.3 习惯建议从那以后我每次拿到一套新 SDK都强制自己先走一遍「最小外设改动 → 编译 → 烧录 → 日志观察」的完整闭环不急着看业务大模块而是先确认驱动层和事件层是通的再去碰蓝牙和音频这种复杂链路。希望这套 BES2500YP/BES2600YP 的拆解方法也能帮你在自己的板子上少走几次弯路先把串口日志打出来再说别的。本文还有配套的精品资源点击获取