前两篇记录了环境搭建和 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 的两层循环结构

上面这张图把两层循环串起来了——外层 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 的骨架就清楚了。剩下的模块代码是用到的时候再翻,不需要提前全读一遍。

四、回顾一下

三篇连起来,从零到能改功能的路大概是这样的:

  1. 环境跑通,能把代码烧进去(第一篇)
  2. 知道代码放在芯片的什么地方跑——com 区和 bank 区(第二篇)
  3. 脑子里有张 SDK 的地图——模式状态机、怎么定位功能、从哪开始看(这篇)

后面再往里加功能,不管是改按键、加 ANC 切换、做入盒检测,都是在这个框架里填东西。框架是原厂搭好的,应用层要做的,是在正确的地方加正确的逻辑。


原文:CSDN · 【中科蓝讯】聊聊我对这套SDK的理解