4. Zephyr BLE初始化流程:Kconfig配置与代码结构

好,咱们正式开始聊BLE的初始化。很多新手拿到Zephyr,第一件事就是照着例程把bt_enable()一调,灯一亮就以为完事了。嗯,我当年也这么干过,结果项目里加了几个自定义Service之后,蓝牙死活连不上——后来才发现是Kconfig配漏了。

所以这一节,咱们把初始化流程掰开揉碎。你跟着我走一遍,以后遇到BLE起不来的问题,心里就有底了。

4.1 Kconfig配置:别小看这几行

Zephyr的构建系统里,Kconfig是灵魂。说白了,你配了什么,系统就编译什么。BLE协议栈也不例外。

最基本的,你得打开这几个开关:

CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CENTRAL=n   # 如果你只需要外设角色
CONFIG_BT_DEVICE_NAME="MyZephyrDevice"
CONFIG_BT_MAX_CONN=1

我个人习惯,在prj.conf里把CONFIG_BT放在第一行。为什么?因为它是总开关,后面的配置都依赖它。你想想看,如果CONFIG_BT没开,后面配再多也是白搭。

核心配置速查表:

配置项 说明 我的建议
CONFIG_BT BLE协议栈总开关 必选,放在第一行
CONFIG_BT_PERIPHERAL 外设角色(广播、连接) 大多数项目必选
CONFIG_BT_CENTRAL 中心角色(扫描、发起连接) 按需开启
CONFIG_BT_DEVICE_NAME 广播时的设备名 建议不超过20字节
CONFIG_BT_MAX_CONN 最大并发连接数 外设通常设为1

注意:我曾经在项目里把CONFIG_BT_MAX_CONN设成了3,结果内存占用直接飙了10KB+。对于资源受限的MCU,这可不是小事。所以,按需配置,别贪多。

4.2 初始化代码结构:bt_enable与bt_ready回调

配置搞定,接下来写代码。Zephyr的BLE初始化,核心就两个东西:bt_enable()bt_ready回调。

先看一个最简的初始化流程:

#include <zephyr/bluetooth/bluetooth.h>

static void bt_ready(int err)
{
    if (err) {
        printk("蓝牙初始化失败: %d\n", err);
        return;
    }

    printk("蓝牙初始化成功!\n");

    // 在这里开始广播
    start_advertising();
}

void main(void)
{
    int err;

    printk("BLE示例启动\n");

    err = bt_enable(bt_ready);
    if (err) {
        printk("bt_enable返回错误: %d\n", err);
        return;
    }

    // 注意:这里不要直接调用start_advertising()
    // 因为bt_enable是异步的
}

这里有个关键点:bt_enable()是异步的。什么意思?就是你调用它之后,它不会等协议栈初始化完才返回。它会立刻返回0,然后等后台初始化完成,再调用你传进去的bt_ready回调。

为什么会这样?因为BLE协议栈初始化涉及到底层硬件配置、HCI驱动加载、Controller初始化等一系列操作。如果同步等待,你的主线程就被卡住了。Zephyr的设计哲学是「非阻塞」,所以这里用了回调机制。

我个人习惯,在bt_ready回调里做两件事:

  1. 打印初始化状态(方便调试)
  2. 启动广播或扫描

千万别在main()里直接调用广播函数。我见过有人这么写,结果广播没发出去,因为协议栈还没准备好。

小技巧:如果你不想用回调,也可以传NULL给bt_enable()。但这样你就得自己轮询状态,或者用信号量同步。我个人觉得回调更干净。

4.3 理解Zephyr BLE线程模型:workqueue与ISR

好,接下来聊点深入的。很多初学者搞不懂:我调了bt_enable之后,协议栈到底在哪个线程里跑?

答案是:Zephyr的BLE协议栈跑在一个专用的系统workqueue里

什么是workqueue?你可以把它理解成一个后台任务队列。协议栈把各种事件(比如连接建立、数据接收)封装成work item,然后丢到这个队列里。系统workqueue会一个一个地处理。

那ISR(中断服务程序)呢?BLE的Controller(比如nRF52840的Radio)会产生中断。中断里只做最紧急的事,比如把收到的数据包从FIFO里搬出来。然后它会触发一个work item,让workqueue去处理协议栈的逻辑。

画个简图帮你理解:

硬件中断 (ISR)
    |
    |-- 快速处理:搬数据、清标志
    |-- 提交 work item 到系统 workqueue
    |
系统 workqueue
    |
    |-- 慢速处理:协议栈解析、回调用户代码
    |-- 比如你的 bt_ready、connected callback

这意味着什么?意味着你的回调函数(比如连接回调、数据接收回调)都是在workqueue的上下文里执行的

我曾经踩过一个坑:在连接回调里调了一个k_sleep(),结果整个协议栈都卡住了。为什么?因为workqueue被阻塞了,后续的协议栈事件没人处理了。

避坑指南:在BLE回调函数里,绝对不要调用阻塞函数(如k_sleep()k_sem_take()等)。如果确实需要等待,请使用workqueue的异步机制,或者把耗时操作放到另一个线程里。

另外,还有一个细节:bt_enable()本身也是在workqueue里初始化的。所以你在main()里调用bt_enable()时,它只是提交了一个初始化work item,然后立即返回。等workqueue空闲了,才会真正执行初始化。

嗯,这里要注意:如果你的main()在调用bt_enable()之后直接退出了,那整个应用就结束了。所以通常main()最后要有一个死循环或者k_sleep(),让应用一直运行。

4.4 小结:初始化流程的四个关键点

咱们总结一下,BLE初始化你只要记住这四点:

  1. Kconfig配全CONFIG_BT是总开关,角色配置别漏了
  2. bt_enable异步:别在它后面直接调广播,等回调
  3. bt_ready里干活:初始化成功后的逻辑都放这里
  4. workqueue上下文:回调里别阻塞,否则协议栈会卡死

下一节,咱们会聊广播配置和扫描。到时候你会发现,理解了线程模型,很多问题就迎刃而解了。