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

上面这张图把两层循环串起来了——外层 func_run 负责"当前在哪个模式",内层 func_bt 负责"这个模式里每轮做什么"。那么 process、message、display 分别干了什么?以蓝牙模式为例拆开看看。
process——每轮扫一眼的事
void func_bt_process(void)
{
func_process(); // 公共事件:充电检测、低电检测、关机检测、设备插拔
// 根据蓝牙状态,临时切换按键表
if (来电) {
切到通话按键表 → 跑响铃逻辑 → 切回音乐按键表
} else if (OTA升级) {
跑升级逻辑
} else if (通话中) {
切到通话按键表 → 跑通话逻辑 → 切回音乐按键表
}
// 该休眠了?该关机了?扫一眼,没事就退出
}
process 不像 message 那样来了事件就处理,而是每 10ms 左右快速扫一眼——充电插入了没?电还够不够?该休眠了没?有情况就处理,没情况直接退出。大部分时候其实什么都没做,就是进去逛一圈又出来了。
message——响应"一次性事件"
void func_bt_message_do(u16 msg)
{
switch (msg) {
case 播放暂停: 调蓝牙播放暂停; break;
case 上一曲: 切上一首; break;
case 下一曲: 切下一首; break;
case 接听: 接电话; break;
case 挂断: 挂电话; break;
// ...
}
}
按键、系统事件、TWS 同步指令,最终都变成一个消息号,在这里 switch 分发。message 和 process 一个是"来事了就干",一个是"逛一圈看看有没有事"——有人按了键,消息立刻丢过来,message 马上处理;但电池还剩多少、该不该休眠,这种事没人通知你,得靠 process 自己隔一会扫一眼。
display——管灯怎么闪
display 干了件最简单的事——根据当前状态(连上了、播放中、通话中、没电了…)决定状态灯怎么闪。跟硬件绑得比较紧,不同板子 LED 接线不一样,所以单独放在 projects/display/ 下面,不和通用逻辑搅在一起。
三个子函数拆完,回到循环里看就很清楚了——每轮先扫一眼状态,再处理积压的消息,最后刷一下灯,完事。下一轮再来一遍。
二、三个问题,定位任何功能
有了"模式状态机"这个前提,后面不管做什么功能,我习惯先问自己三个问题:
第一个问题:这个功能发生在哪个模式?
是在蓝牙音乐模式下?通话模式下?充电模式下?还是所有模式都通用的?这个问题决定了代码加在 func_bt.c 还是 func.c。
拿音量加减来说——它在蓝牙和非蓝牙模式下的逻辑不一样(蓝牙模式要走 A2DP 音量同步,非蓝牙模式直接调系统音量),所以 func_volume_up() 里有一个 if (func_cb.sta == FUNC_BT) 的分支。但它又确实是所有模式都能触发的,所以放在 func.c 的 func_message() 里作为公共消息处理。
第二个问题:是什么触发的?按键?系统事件?还是轮询状态变化?
三种触发方式对应三个入口:
| 触发方式 | 入口 | 例子 |
|---|---|---|
| 按键 | func_message() 或 func_bt_message() | 单击播放/暂停 |
| 系统事件 | func_message() 里的 EVT_xxx 消息 | 手机端调音量、来电、TWS 同步 |
| 状态轮询 | func_process() 或 func_bt_process() | 低电检测、入盒检测、充电状态变化 |
表格看着三类触发源,但其实真正的入口就两个:按键和系统事件最终都进了 func_message(),状态轮询走 func_process()。之前遇到问题到处找源头,现在知道了——顺着这两个入口往回追就行。
消息本身也是分层的,但这个分层有意思——不是"先判断再分发",而是模式专属先处理,处理不了的才往下落。比如 func_bt_message_do() 先处理播放/暂停/上下曲这些蓝牙消息,它不认识的消息走 default 落到 func_message() 处理公共逻辑(音量、关机、EQ 切换)。加功能的时候想清楚是"蓝牙专属"还是"所有模式通用",就知道放哪一层了。
第三个问题:受哪个宏控制?编译期的还是运行时的?
这个在上一篇 config.h 里提过,但值得再强调一次——因为踩过坑。有些宏决定了"这段代码有没有编译进去"(比如 FUNC_BT_EN),关了就真没了。有些宏只是默认值,运行时会被 Downloader Setting(xcfg_cb)覆盖。
改完宏不确定有没有生效的时候,有个简单的验证办法——在对应的 #if 代码块里随便敲几个乱码字符,如果编译过了,说明这段代码根本没参与编译,也就是宏没开。不用烧录就能确认,比在那猜快得多。
三、拿到一个新 SDK,从哪里看?
不讲步骤了,就是自己的一点习惯:
先点开 main.c,看 main() 里调了什么——基本上就是 sys_init() + func_run(),确认入口。
然后点开 func_run(),看 switch 里有哪些 case——就知道这个 SDK 支持哪些模式。
然后随便挑一个模式(蓝牙最好,最常用),点进去看它的 enter → loop → process → message → display → exit。看完一个模式,其他模式的结构就全知道了。
最后扫一眼 config.h,看有哪些功能宏——知道哪些功能可以开关、哪些参数可以调。
读完这四个文件(main.c、func.c、func_bt.c、config.h),SDK 的骨架就清楚了。剩下的模块代码是用到的时候再翻,不需要提前全读一遍。
四、回顾一下
三篇连起来,从零到能改功能的路大概是这样的:
- 环境跑通,能把代码烧进去(第一篇)
- 知道代码放在芯片的什么地方跑——com 区和 bank 区(第二篇)
- 脑子里有张 SDK 的地图——模式状态机、怎么定位功能、从哪开始看(这篇)
后面再往里加功能,不管是改按键、加 ANC 切换、做入盒检测,都是在这个框架里填东西。框架是原厂搭好的,应用层要做的,是在正确的地方加正确的逻辑。