3. Zephyr 蓝牙初始化流程:从 HCI 驱动到 Enable 回调

蓝牙协议栈跑起来,第一步是什么?很多人上来就调 bt_enable(),结果发现要么卡死,要么回调没触发。我刚开始接触 Zephyr 蓝牙时也踩过这个坑——说白了,初始化不是一行 API 的事,而是一条完整的链路。

这条链路从硬件 HCI 驱动开始,经过 Controller 初始化、Host 层启动,最后才落到你注册的 bt_ready_cb 回调。今天我们就把它拆开,看看每一层到底干了什么。

核心要点: 蓝牙初始化 = HCI 底层驱动加载 → Controller 启动 → Host 协议栈初始化 → 应用回调通知。缺任何一环,蓝牙都跑不起来。

3.1 初始化全景图

先看整体流程。我习惯把它分成四个阶段:

  1. HCI 驱动加载——让 CPU 能和蓝牙芯片通信(UART/USB/SPI)
  2. Controller 初始化——下发 HCI Reset,等待 Controller 就绪
  3. Host 层初始化——L2CAP、ATT、GATT、SM 等协议模块依次启动
  4. Enable 回调——通知应用层:协议栈已就绪,可以开始干活了

下面这张图把整个流程串起来了,建议你保存下来对照着看代码。

Zephyr 蓝牙初始化流程 HCI 驱动加载 Controller 初始化 Host 层初始化 UART/SPI/USB 初始化 HCI 传输层注册 发送 HCI Reset 等待 Command Complete L2CAP / ATT / GATT SM(安全管理) bt_ready_cb 回调触发 📌 关键说明 • HCI 驱动加载由设备树和 Kconfig 自动完成,开发者通常不需要手动干预 • Controller 初始化是同步的,Host 层初始化是异步的 • bt_enable() 返回后不代表蓝牙可用,必须等回调触发

3.2 HCI 驱动加载:藏在设备树里的秘密

Zephyr 的 HCI 驱动加载是自动的。你只需要在设备树里配好节点,Kconfig 选对驱动类型,剩下的交给系统。

举个例子,如果你用的是 nRF52840 搭配 UART HCI:

// 设备树片段:uart0 节点上挂载 HCI
&uart0 {
    compatible = "nordic,nrf-uarte";
    current-speed = <1000000>;
    hci-uart {
        compatible = "zephyr,bt-hci-uart";
        hci-uart-rts = <24>;
        hci-uart-cts = <23>;
    };
};

系统启动时,bt_hci_driver_init() 会被自动调用。它做了三件事:

  • 初始化 UART/SPI/USB 外设
  • 注册 HCI 传输层回调(收发包接口)
  • 将驱动挂到蓝牙协议栈的 HCI 总线上
我的习惯: 调试初期先确认 HCI 驱动是否加载成功。可以在 bt_hci_driver_init() 里加个打印,或者看系统日志里有没有 "HCI: driver registered" 字样。没有这个,后面全是白搭。

3.3 Controller 初始化:第一次握手

驱动加载完,协议栈会发第一条 HCI 命令——HCI Reset。这是蓝牙芯片的"你好,在吗?"

流程是这样的:

  1. Host 构造 HCI Reset 命令包
  2. 通过 HCI 传输层发给 Controller
  3. Controller 回复 Command Complete 事件
  4. Host 收到后,更新状态为 "Controller Ready"

这里有个容易忽略的点:HCI Reset 是同步的。Zephyr 内部用信号量等待回复,超时时间默认 5 秒。如果芯片没回应,bt_enable() 会直接返回错误。

我曾经遇到过一个坑: 某款蓝牙模块上电后需要 200ms 才能响应 HCI 命令。Zephyr 默认的等待时间够用,但如果你的模块启动特别慢,记得在 Kconfig 里调 CONFIG_BT_HCI_RESET_TIMEOUT。

3.4 Host 层初始化:协议栈的"拼图"

Controller 就绪后,Host 层开始初始化。这部分是异步的,也是整个流程里最复杂的。

Zephyr 的 Host 层初始化顺序是固定的:

步骤 模块 作用
1 L2CAP 建立逻辑链路控制层,为上层提供通道
2 ATT 属性协议层,处理读写请求
3 GATT 通用属性协议层,管理 Service/Characteristic
4 SM 安全管理层,负责配对和加密
5 HCI Event 处理 启动事件循环,开始处理 Controller 上报的事件

每个模块初始化时,会注册自己的回调函数到 HCI 事件分发器。比如 GATT 注册了 bt_gatt_init(),SM 注册了 bt_sm_init()。

你想想看,如果某个模块初始化失败,比如 L2CAP 没起来,那 GATT 和 SM 就算注册了也没用。所以 Zephyr 内部做了依赖检查——前一个模块初始化成功,才会继续下一个。

3.5 Enable 回调:终于轮到你了

所有模块初始化完成后,协议栈调用你注册的 bt_ready_cb。这个回调是在系统工作队列里执行的,不是中断上下文,所以你可以放心做耗时操作。

典型的回调实现:

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

    printk("蓝牙协议栈已就绪\n");

    // 开始广播
    bt_le_adv_start(...);
}

注意:bt_enable() 返回 0 不代表蓝牙可用。必须等回调里的 err == 0,才能调用其他蓝牙 API。我见过有人在这上面栽跟头——bt_enable() 返回后立刻调 bt_le_adv_start(),结果系统崩溃。

黄金法则: 所有蓝牙操作(广播、扫描、连接、GATT 读写)都必须在 bt_ready_cb 回调触发之后调用。在此之前,协议栈还没准备好。

3.6 初始化时序图:谁先谁后

为了让你看得更清楚,我把整个时序画出来:

初始化时序图 App Host Controller bt_enable() HCI Reset Command Complete Host Init bt_ready_cb(err)

3.7 常见问题与避坑指南

做项目这么多年,我总结了几条初始化阶段的常见问题:

现象 原因 解决办法
bt_enable() 返回 -ETIMEDOUT HCI Reset 超时,Controller 没响应 检查硬件连接、波特率、电源
bt_enable() 返回 0 但回调没触发 系统工作队列没跑,或者回调被阻塞 检查 CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE
回调触发后调用 API 崩溃 在中断上下文调用了蓝牙 API 确保所有蓝牙操作在线程上下文中执行
蓝牙广播不出去 Controller 初始化成功,但 Host 层 GATT 没注册 检查 CONFIG_BT_GATT 是否启用
我曾经踩过的一个坑: 某次移植到新平台,bt_enable() 一直返回 -ETIMEDOUT。查了两天才发现是 UART 的 RTS/CTS 接反了。HCI Reset 发出去,Controller 收到了,但回复的 Command Complete 被流控卡住了。所以,硬件连接一定要先确认。

3.8 小结

Zephyr 蓝牙初始化,说白了就是一条从硬件到应用的链路。HCI 驱动是基础,Controller 握手是前提,Host 层拼图是核心,回调通知是终点。

我个人建议,每次开始一个新项目,先跑通初始化流程。用逻辑分析仪抓一下 HCI 数据,或者加几个打印,确认每一层都正常。这一步走稳了,后面的广播、连接、GATT 操作才有意义。