4. 蓝牙驱动框架:struct hci_dev 结构体、HCI 设备注册与注销接口

在 Linux 蓝牙子系统中,struct hci_dev 是所有蓝牙控制器(Controller)在内核中的核心抽象。无论是 USB、UART、SDIO 还是虚拟蓝牙设备,驱动开发者都必须围绕这个结构体完成初始化、配置、注册与注销。本章将深入剖析 hci_dev 的成员布局、生命周期管理以及注册/注销接口的调用规范。

核心要点:hci_dev 是蓝牙驱动的“身份证”,驱动通过 hci_alloc_dev() 分配,通过 hci_register_dev() 激活,通过 hci_unregister_dev() 销毁。任何遗漏都会导致设备无法被 BlueZ 用户态识别。

4.1 struct hci_dev 结构体全景

hci_dev 定义在 include/net/bluetooth/hci_core.h 中,体量庞大(超过 200 行)。我们只聚焦驱动开发必须掌握的字段,按功能划分为 5 组:

分组 字段名 类型 说明
标识 id int HCI 设备编号(如 hci0、hci1),由内核自动分配
标识 bus __u8 总线类型:HCI_USB、HCI_UART、HCI_SDIO、HCI_VIRTUAL 等
标识 dev_type __u8 设备类型:HCI_PRIMARY(BR/EDR)、HCI_AMP(AMP)、HCI_LE(LE Only)
操作 open int (*)(struct hci_dev *) 打开底层硬件接口,驱动必须实现
操作 close int (*)(struct hci_dev *) 关闭底层硬件接口,驱动必须实现
操作 send int (*)(struct hci_dev *, struct sk_buff *) 发送 HCI 数据帧到控制器,驱动必须实现
操作 flush int (*)(struct hci_dev *) 清空发送缓冲区,可选实现
配置 set_bdaddr int (*)(struct hci_dev *, const bdaddr_t *) 设置蓝牙地址,可选实现
配置 setup int (*)(struct hci_dev *) 设备初始化序列,在注册后自动调用
数据 name char[8] 设备名称字符串(如 "hci0")
数据 dev struct device 内核通用设备结构,用于 sysfs 和电源管理
锁 lock spinlock_t 保护 hci_dev 内部状态的自旋锁
锁 req_lock mutex 保护 HCI 命令/事件同步的互斥锁
驱动开发提示:在实现 open/close/send 回调时,必须考虑原子上下文。send 回调可能在中断上下文或 tasklet 中被调用,因此不能使用可能睡眠的锁(如 mutex),应使用 spin_lock 或 spin_lock_bh。

4.2 HCI 设备生命周期与状态机

一个 HCI 设备从分配到销毁经历 5 个关键状态。下面的 SVG 图展示了完整的状态转换流程:

HCI 设备生命周期状态机 HCI 设备生命周期状态机 hci_alloc_dev hci_init_dev hci_register_dev hci_unregister_dev hci_free_dev 成功 成功 驱动卸载/错误 最后一步 ⚠ 关键规则: hci_register_dev 失败时,驱动必须调用 hci_free_dev 释放内存;hci_unregister_dev 后也必须调用 hci_free_dev。

4.3 HCI 设备注册接口详解

注册一个 HCI 设备的标准流程包含 4 个步骤,每个步骤都有对应的内核 API:

  1. 分配 hci_dev 对象:调用 struct hci_dev *hci_alloc_dev(void),返回一个已清零的 hci_dev 实例。
  2. 初始化基础字段:设置 bus、dev_type、驱动回调(open/close/send)以及私有数据指针。
  3. 调用 hci_init_dev:int hci_init_dev(struct hci_dev *hdev),初始化内核蓝牙协议栈内部状态。
  4. 注册到 HCI 核心:int hci_register_dev(struct hci_dev *hdev),将设备加入全局 hci_dev_list,创建 sysfs 节点,触发 setup 回调。
典型代码模板(USB 蓝牙驱动片段):
static int btusb_probe(struct usb_interface *intf,
                       const struct usb_device_id *id)
{
    struct hci_dev *hdev;
    int err;

    /* 1. 分配 hci_dev */
    hdev = hci_alloc_dev();
    if (!hdev)
        return -ENOMEM;

    /* 2. 设置总线类型和设备类型 */
    hdev->bus = HCI_USB;
    hdev->dev_type = HCI_PRIMARY;

    /* 3. 绑定驱动回调 */
    hdev->open  = btusb_open;
    hdev->close = btusb_close;
    hdev->send  = btusb_send_frame;

    /* 4. 设置私有数据 */
    hci_set_drvdata(hdev, intf);
    SET_HCIDEV_DEV(hdev, &intf->dev);

    /* 5. 初始化并注册 */
    err = hci_init_dev(hdev);
    if (err < 0)
        goto err_free;

    err = hci_register_dev(hdev);
    if (err < 0)
        goto err_uninit;

    return 0;

err_uninit:
    /* 注意:hci_uninit_dev 在 hci_register_dev 失败时由内核自动调用 */
err_free:
    hci_free_dev(hdev);
    return err;
}

4.4 HCI 设备注销接口详解

注销流程与注册严格对称,驱动卸载时按以下顺序执行:

  1. hci_unregister_dev:从全局列表移除设备,关闭所有用户态连接,释放 sysfs 资源。
  2. hci_free_dev:释放 hci_dev 结构体内存,必须在 unregister 之后调用。
常见错误:许多驱动在注销时只调用 hci_unregister_dev 而忘记 hci_free_dev,导致内存泄漏。反之,先 free 再 unregister 会导致内核访问已释放的内存,造成 Oops。
static void btusb_disconnect(struct usb_interface *intf)
{
    struct hci_dev *hdev = usb_get_intfdata(intf);

    if (!hdev)
        return;

    /* 标准注销序列 */
    hci_unregister_dev(hdev);
    hci_free_dev(hdev);
}

4.5 注册/注销接口对比速查表

接口 原型 调用时机 失败返回值 注意事项
hci_alloc_dev struct hci_dev *hci_alloc_dev(void) probe 最开头 NULL 使用 hci_free_dev 释放
hci_init_dev int hci_init_dev(struct hci_dev *) alloc 之后,register 之前 负错误码 失败后只需 free_dev
hci_register_dev int hci_register_dev(struct hci_dev *) init 成功之后 负错误码 失败后内核自动调用 uninit,驱动只需 free_dev
hci_unregister_dev void hci_unregister_dev(struct hci_dev *) disconnect 时 无返回值 必须在 free_dev 之前调用
hci_free_dev void hci_free_dev(struct hci_dev *) unregister 之后 无返回值 释放内存,不可逆

4.6 驱动回调函数实现要点

hci_dev 的 open/close/send 三个回调是驱动与硬件交互的桥梁,它们的实现质量直接影响蓝牙的稳定性和吞吐量:

  • open 回调:负责上电、复位控制器、申请中断、启用 DMA。必须确保硬件处于可收发 HCI 命令的状态。
  • close 回调:负责下电、释放中断、停止 DMA、关闭 UART/USB 端点。必须幂等(可被多次调用)。
  • send 回调:将 sk_buff 中的 HCI 帧写入硬件。对于 USB,通过 usb_fill_bulk_urb 提交;对于 UART,通过 serdev_device_write 写入。
性能优化提示:send 回调中应避免内存拷贝。如果硬件支持 scatter-gather,可以直接将 skb->data 映射到 DMA 缓冲区。对于 UART 蓝牙,建议使用 kfifo 或环形缓冲区批量发送,减少逐字节写入的开销。

4.7 本章小结