RK3568 UART蓝牙主机开发:从设备树到BlueZ全链路调试 接手这块 RK3568 平台时项目需求很直接主控通过 UART 接口外接一个蓝牙模块跑主机Host侧协议栈最终要实现蓝牙键盘、鼠标和音频设备的同时连接。说白了就是让 RK3568 这颗芯片通过串口“驱动”一颗蓝牙控制器让系统里出现一个标准的蓝牙适配器。这个场景在工控板、平板、智能音箱里非常常见但真要做时坑并不少——设备树怎么配、内核蓝牙子系统怎么对接、hci_uart 驱动如何注册、BlueZ 用户态工具怎么验证每一步都能卡住人。这篇文章把我从零调通全套链路的经验完整写出来覆盖方案选型、设备树配置、HCI UART 驱动实现、用户态协议栈联调以及我实打实踩过的问题和排查思路。适合正在做 RK3568 或其他瑞芯微平台蓝牙开发、嵌入式 Linux 驱动开发的同学参考哪怕你之前完全没碰过蓝牙协议栈按这个思路走也能把链路拉起来。1. 整体设计与方案选型1.1 为什么选 UART 做蓝牙主机接口蓝牙控制器和主机之间的通信接口业界常用的有三种UART、SDIO、USB。RK3568 这颗 SoC 对三种接口都支持但实际项目里UART 是最省事、也最容易出问题的一条路。UART 的优势在于引脚少基本只要 TX、RX 和流控 CTS/RTS、协议简单、几乎所有的低成本蓝牙模块都支持 HCI over UART 这种标准传输方式。相比之下SDIO 带宽高但信号完整性问题多驱动调试难度大适合 WiFi/BT 二合一模组USB 接口的蓝牙适配器插上就能用但硬件形态固定不适合直接贴片到主板上。工控和消费类产品里主控上留一路 UART 给蓝牙模块是非常主流的设计。这条链路整体是这样的蓝牙模块里面跑着控制器固件即 Controller通过 UART 物理链路连接到 RK3568 的某个串口控制器上内核里的串口驱动把数据收上来后交给蓝牙子系统中的 hci_uart 驱动做 HCI 分组解析然后 BlueZ 协议栈Host 侧通过 HCI 通道和控制器通信向上提供 socket 接口给应用层使用。一个人容易绕晕的点是这里说的“驱动”不是指去驱动一个具体的蓝牙芯片比如某颗模组内部的私有协议而是驱动一个HCI 传输层。内核的 hci_uart 已经帮你把 HCI 协议和串口字节流之间的转换封装好了你真正要做的是让一个符合 HCI UART 传输规范的蓝牙控制器在 RK3568 平台上被正确识别并注册到蓝牙子系统中。1.2 硬件连接与模块选型的经验硬件上第一件事是确认模块的工作模式。市面上很多蓝牙模块支持“主机模式”和“从机模式”像 HC05 这类模块默认跑的是从机透传而 RK3568 需要的是纯 HCI 模式的控制器两者的接口协议完全不同。选型时务必问清楚模块是否支持 HCI over UART是否支持硬件流控默认波特率是多少有没有 PWR_EN、BT_WAKE、HOST_WAKE 这类控制引脚。一个常见且稳妥的做法是选市面上成熟的双模模组比如 AP6256、RTL8723DS 这类 WiFi/BT 二合一模组或者单独的 CSR、Realtek、Beken 系列蓝牙控制器。它们大多在数据手册里明确写了 HCI UART 的传输规范并且瑞芯微平台的 SDK 里通常已经内置了对应 patch 和驱动代码。连接方面模块的 UART_TX 接主控的 UART_RX模块的 UART_RX 接主控的 UART_TXGND 必须共地流控引脚务必接上。我见过很多“串口能发数据但蓝牙就是注册不上”的案例一查全是 CTS/RTS 悬空导致的丢包。另外模块的供电要单独看有些模块 3.3V 供电峰值电流能到 300mA 以上不能直接依赖开发板上的 LDO 输出最好用单独的 DC-DC 或 LDO 供电否则蓝牙一开启电压跌落直接导致模块复位。模块的 PWR_EN 或 BT_EN 引脚建议接到主控的一个 GPIO 上驱动里通过 GPIO 控制模块上电时序。这个细节很多人忽略但蓝牙模块的上电时序不规范会导致固件加载失败、HCI 命令无响应等疑难杂症。1.3 整体软件框架梳理从软件角度看整个蓝牙主机功能可以拆成四层串口驱动程序层负责配置 RK3568 的 UART 控制器完成波特率、流控、DMA 等参数设置。这一层在内核中通常对应ttyS设备。蓝牙 HCI 传输层内核中的drivers/bluetooth/hci_uart.c及其协议子模块h4、bcsp、h5 等。它负责把串口收到的字节解析成 HCI 事件包把上层下发的 HCI 命令包封装成字节流发出去。蓝牙核心层包括 BlueZ 内核部分net/bluetooth/实现了 HCI 设备管理、L2CAP、SCO、ACL 等协议。用户态协议栈与应用BlueZ 的bluetoothd、bluetoothctl以及应用层 socket 接口。调试时要记住一条主线串口通了 → 内核能看到 hci0 设备 → BlueZ 能管理这个设备 → 上层应用能使用蓝牙功能。后面所有的排查工作都是在确认当前卡在哪条线上。2. 设备树配置与内核内核选项2.1 RK3568 设备树中 UART 节点的配置要点RK3568 的串口设备树节点通常位于arch/arm64/boot/dts/rockchip/下的rk3568.dtsi中默认有很多路 UARTuart0 ~ uart9。我们需要找到板级 dts 文件把对应的 UART 节点状态改为okay并正确配置 pinctrl。我以 uart2 为例实际项目中要根据硬件连接选择对应的 uart。典型配置长这样uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer, uart2m0_ctsn, uart2m0_rtsn; dma-names tx, rx; dmas dmac0 10, dmac0 11; };这里有几个关键点pinctrl必须同时配置 TX/RX 和 CTS/RTS 引脚否则硬件流控不生效。瑞芯微平台同一个外设通常有多组引脚复用m0/m1要根据原理图确认当前用的是哪一组。dma-names和dmas建议保留。UART 加 DMA 能显著降低 CPU 占用尤其在蓝牙音频这类高吞吐场景下没有 DMA 会导致丢包率上升。确认引脚复用还有个笨但可靠的方法把pinctrl-0里暂时只留xfer即 TX/RX流控先不配然后在内核启动后用 GPIO 调试接口检查引脚复用状态确认无误后再补上 CTS/RTS。2.2 内核配置项怎么勾选内核配置建议直接用make menuconfig或直接在 defconfig 中打开以下选项CONFIG_BTy CONFIG_BT_RFCOMMy CONFIG_BT_RFCOMM_TTYy CONFIG_BT_BNEPy CONFIG_BT_HIDPy CONFIG_BT_HCIUARTy CONFIG_BT_HCIUART_H4y CONFIG_BT_HCIUART_BCSPy CONFIG_BT_HCIUART_LLy CONFIG_BT_HCIUART_3WIREy CONFIG_SERIAL_8250_DMAyCONFIG_BT_HCIUART必须编译成模块或编入内核后面的 H4、BCSP、3WIRE 是不同传输协议H4 是最常用的 UART HCI 协议建议全部打开方便在调试不同模块时切换。另外注意RK3568 平台如果用的是 BSP 内核比如 4.19 或 5.10 分支drivers/bluetooth/下可能已经带了瑞芯微定制的 hci_uart 驱动补丁。编译前先确认内核里是否已经有现成的hci_uart驱动避免重复造轮子。2.3 时钟、电源与复位引脚的设备树描述除了串口节点本身蓝牙模块的电源、复位、时钟也可能需要在设备树里体现。如果模块的 PWR_EN 接在 GPIO 上可以在根节点下添加一个简单的 regulator 描述bt_power: bt-power-regulator { compatible regulator-fixed; regulator-name bt_power; regulator-boot-on; regulator-always-on; gpio gpio3 RK_PB4 GPIO_ACTIVE_HIGH; enable-active-high; };实际项目中我更推荐把 GPIO 控制放到驱动里去做而不是直接做成 regulator原因是蓝牙模块的上电时序需要和 UART 初始化严格配合——先上电、再打开串口、再加载固件单纯用 regulator 无法精准控制时序。关于驱动里怎么做下一章会展开。如果模块需要外部 32.768kHz 时钟通常硬件上已经由晶振提供设备树里不用特别配置。若使用 SoC 输出的时钟则需要在clocks属性中引用对应时钟源这一点一定要对照模块手册核对。2.4 设备树配置完成后如何验证改完设备树重新编译 boot.img 烧录后首先检查串口是否已经注册成功# 查看串口设备节点分配情况 ls -l /dev/ttyS* # 查看 uart2 的设备树状态是否确实生效 cat /proc/device-tree/serialfe660000/status确认/dev/ttyS*出现了目标串口后用回环测试验证串口硬件通路把 TX 和 RX 短接然后用echo和cat测试收发。但这里有一个大坑在蓝牙驱动接管串口之后串口设备本身可能不会再被应用层直接访问因为 hci_uart 驱动会通过tty_register_device注册为线路规程Line Discipline。所以如果你在蓝牙工作状态下发现/dev/ttyS2不存在或者打不开不要慌这说明 hci_uart 已经成功接管了这个串口这在后面是正常的。3. 蓝牙主机驱动的核心实现3.1 hci_uart 驱动的工作机制在 Linux 内核中串口驱动负责收发字节而蓝牙 HCI 传输层通过线路规程Line Discipline机制附着在串口上。简单理解普通的 tty 设备默认使用 N_TTY 线路规程也就是传统终端模式而蓝牙驱动会注册一个名为N_HCI的线路规程当它被附着到某个 tty 设备上时这个串口就不再是普通终端了而是变成蓝牙 HCI 的传输通道。内核代码里drivers/bluetooth/hci_uart.c的核心思路是注册一个hci_uart_proto结构体里面包含open、close、recv、enqueue、flush等操作函数。不同传输协议H4、H5、BCSP对应不同的hci_uart_proto实例。HCI 核心层通过hci_register_dev注册一个hci_dev协议栈所有命令下发最终都会走到hci_uart_send_frame把 HCI 分组交给对应的 proto 处理然后写入 tty 设备的发送队列。接收方向更复杂一些串口收到字节后通过tty_ldisc的receive_buf回调进入 hci_uart 驱动驱动内部维护一个接收状态机根据 HCI 分组类型HCI Command、HCI Event、HCI ACL Data、HCI SCO Data逐字节解析解析完成后再通过hci_recv_frame上报给蓝牙核心层。3.2 新增一个自定义 HCI UART 协议驱动的完整流程如果模块使用标准 H4 协议内核默认已经支持代码一行都不用写。但如果你拿到的是一个使用私有协议的模块比如某些国产低功耗蓝牙芯片厂商会给出定制 HCI 命令做固件 patch就需要自己实现一个hci_uart_proto。参考内核现有的 h4 实现自定义协议的驱动骨架如下#include linux/module.h #include linux/kernel.h #include linux/serial_core.h #include net/bluetooth/bluetooth.h #include net/bluetooth/hci_core.h #include hci_uart.h struct my_bt_dev { struct hci_uart *hu; struct sk_buff *rx_skb; unsigned long flags; }; static int my_bt_open(struct hci_uart *hu) { struct my_bt_dev *mydev; mydev kzalloc(sizeof(*mydev), GFP_KERNEL); if (!mydev) return -ENOMEM; hu-priv mydev; mydev-hu hu; // 设置串口参数通常保持硬件流控开启 return 0; } static int my_bt_close(struct hci_uart *hu) { struct my_bt_dev *mydev hu-priv; if (mydev-rx_skb) kfree_skb(mydev-rx_skb); kfree(mydev); hu-priv NULL; return 0; } static int my_bt_recv(struct hci_uart *hu, const void *data, int count) { // 这里实现接收状态机解析 HCI 分组头完整成帧后调用 hci_recv_frame // 解析逻辑需要按照模块的 HCI 分组格式实现 return 0; } static int my_bt_enqueue(struct hci_uart *hu, struct sk_buff *skb) { // 将待发送的 HCI 分组放入发送队列 skb_queue_tail(hu-write_q, skb); return 0; } static struct hci_uart_proto my_bt_proto { .id 0xFC, // 自定义协议 id注意不要与内核已分配的 id 冲突 .name my_bt, .open my_bt_open, .close my_bt_close, .recv my_bt_recv, .enqueue my_bt_enqueue, .dequeue hci_uart_dequeue, .flush hci_uart_flush, }; int my_bt_init(void) { return hci_uart_register_proto(my_bt_proto); } void my_bt_exit(void) { hci_uart_unregister_proto(my_bt_proto); } module_init(my_bt_init); module_exit(my_bt_exit); MODULE_LICENSE(GPL);这个骨架最核心的难点在my_bt_recv。H4 协议的开头是一个类型字节0x01 表示 Command、0x02 表示 ACL Data、0x03 表示 SCO Data、0x04 表示 Event、0x05 表示 Vendor Specific。收到类型字节后根据类型查找对应的分组头长度再根据分组头里的长度字段读取完整的有效载荷。只有完整收到一个 HCI 分组后才能调用hci_recv_frame上报到上层。3.3 实际项目中通过 GPIO 控制模块上电时序设备树只描述了硬件连接关系真正的上电时序需要在驱动里控制。我的做法是写一个平台驱动在probe里申请 GPIO 和时钟然后控制蓝牙模块的电源引脚。static int bt_power_probe(struct platform_device *pdev) { struct gpio_desc *bt_en_gpio; struct gpio_desc *bt_wake_gpio; bt_en_gpio devm_gpiod_get(pdev-dev, bt_en, GPIOD_OUT_LOW); if (IS_ERR(bt_en_gpio)) return PTR_ERR(bt_en_gpio); // 先拉低确保模块处于复位状态 gpiod_set_value(bt_en_gpio, 0); msleep(50); // 拉高使能引脚 gpiod_set_value(bt_en_gpio, 1); msleep(100); // 等待模块稳定之后 hci_uart 协议驱动会加载固件 return 0; }关于时序不同模块要求的延时差别很大。我调过一颗模块手册上写着使能后需要 200ms 稳定但实际测试时 150ms 也能起来反而 250ms 时因为 UART 上已经有残留数据干扰了蓝牙模块的启动导致 HCI 命令一直无响应。遇到这种问题最有效的办法是拿逻辑分析仪抓 UART 波形看模块启动后到底有没有主动发 HCI Event 数据出来。3.4 注册蓝牙设备时需要注意的细节当 hci_uart 协议驱动完成open后需要调用hci_uart_register_dev把hci_dev注册到蓝牙核心。这个过程通常由hci_uart核心自动完成但要注意设置正确的hdev属性hdev-bus HCI_UART声明蓝牙控制器连接在 UART 总线上。hdev-manufacturer对应厂商 ID需要查询蓝牙 SIG 的制造商分配表。hdev-setup如果有固件需要加载在这里通过 HCI 命令下发 patch。hdev-setup是个非常关键的回调。很多蓝牙模块上电后固件并不完整需要主机通过 HCI Vendor 命令下载 patch。我在调试一款国产芯片时发现模块正常发出第一个 HCI Event 后就没有下文了排查到最后就是setup回调里下发的 Baudrate 切换命令时序不对模块在波特率切换完成后没有重新同步导致后续命令全部失败。4. 联调与功能验证4.1 BlueZ 用户态栈的启动和验证内核侧蓝牙子系统已经注册好 hci0 设备后接下来是用户态部分。大多数嵌入式 Linux 系统都使用 BlueZ 作为蓝牙协议栈。启动顺序一般是# 启动 dbus 和蓝牙守护进程 /etc/init.d/dbus start bluetoothd -n -d 然后使用bluetoothctl或hciconfig检查适配器状态# 查看蓝牙适配器列表 hciconfig -a # 如果设备处于 down 状态先启动 hciconfig hci0 up # 查看设备详细信息 hciconfig hci0 hci0: Type: Primary Bus: UART如果hciconfig -a里看不到 hci0说明内核侧注册失败问题大概率还在 UART 或者 hci_uart 驱动上。如果能看到 hci0 但bluetoothctl扫描不到任何设备就要检查蓝牙控制器的固件是否已经正常加载这类问题我会在下一章详细展开。用bluetoothctl做功能测试的基本流程bluetoothctl # 进入交互环境后 power on agent on default-agent scan on # 等设备被发现 pair 设备MAC地址 trust 设备MAC地址 connect 设备MAC地址4.2 扫描、配对、连接全程验证扫描这一步能暴露很多问题。如果scan on后一个设备都扫不到优先怀疑射频天线或者蓝牙模块的 TX/RX 通路可能有问题。但如果在 UART 上用逻辑分析仪能看到模块正常发出 Inquiry Result 事件而上层没有收到问题就在 hci_uart 驱动的接收状态机——八成是 HCI 分组的类型字节没有正确解析。配对连接的核心原理是主机发出配对请求控制器参与配对流程若需要 PIN 码或确认BlueZ 会通过 D-Bus 向应用层发出 agent 请求。这也是我调试时最常用的验证路径能扫描到设备 → 说明 HCI 命令和事件通路正常能配对 → 说明底层加密流程正常能连接并保持稳定 → 说明 ACL 数据链路正常。如果连接后频繁掉线优先检查 UART 是否有丢字节。可以打开内核的CONFIG_BT_DBG和CONFIG_BT_HCIUART_DEBUG然后查看dmesg里的 HCI 日志里面会打印收发的原始 HCI 帧配合出错计数器能快速定位。4.3 音频和键盘鼠标等场景的额外配置如果最终产品需要支持蓝牙音频A2DP 或 HFP用户态还需要拉起pipewire、pulseaudio或BlueALSA其中一个。常见做法是用bluealsa它实现了一个 ALSA 插件桥接 BlueZbluealsa -i hci0 # 然后在 /etc/asound.conf 里配置相应的 PCM 设备蓝牙键盘鼠标则依赖 HID over GATTBLE或 BT 经典 HID 协议。BLE HID 不需要额外的内核配置因为 BlueZ 通过 BlueZ Profile 机制直接处理 HID 服务BT 经典 HID 则需要hidp内核模块加载并且用户态需要运行bluetoothd的--noplugininput时确认input插件没有冲突。我实际遇到过一个问题蓝牙鼠标配对成功但连接后光标不动反复排查发现是内核的 HIDP 层没编译进内核。这种问题往往不是驱动逻辑错而是内核配置缺项所以内核 .config 一定不要等出了问题再检查前期就按我第 2 章的配置清单一项项过。5. 常见问题与排查技巧5.1 “hci0 始终不出现”的排查路径如果内核启动后hciconfig看不到任何蓝牙设备最有效的排查步骤是确认 UART 设备节点是否存在ls -l /dev/ttyS*如果连串口节点都没有说明设备树或串口驱动有问题。确认 hci_uart 驱动是否注册成功cat /sys/kernel/debug/bluetooth/hci0如果有 debugfs或者dmesg | grep hci。查看dmesg里有无蓝牙相关报错最常见的错误是hci_uart_tty_open: Failed to open UART表示 hci_uart 无法绑定到对应的 tty 设备。手动加载线路规程来临时测试# 先确保串口未被占用 echo 0 /sys/class/tty/ttyS2/device/power/runtime_enable 2/dev/null # 手动挂载 HCI 线路规程 sudo hciattach -s 115200 ttyS2 anyhciattach是一个非常实用的调试工具它能绕过内核注册流程直接在用户态把串口绑定为蓝牙 HCI 通道。如果hciattach能成功生成 hci0说明内核蓝牙核心和串口本身都没问题问题基本锁定在 hci_uart 驱动和设备树配置的差异上。5.2 扫描不到设备如何定位是射频还是协议问题扫描不到设备时先别急着怀疑天线。可以做一个最简单的排查用另一台手机或电脑开启蓝牙并不断发送广播包把 RK3568 板子凑近信号源通过dmesg观察 HCI 层有没有收到原始 Inquiry Result 事件。如果事件有但bluetoothctl界面不刷新问题在 BlueZ 层如果事件没有靠逻辑分析仪或示波器抓 UART 引脚确认模块是否真的把事件发出去了。这一步能把问题从“驱动还是模块”的大层面隔离出来。我调试时踩过一个特别坑的问题板子靠近手机能扫描到设备远离 1 米就完全丢失实测是蓝牙模块的天线匹配电路有问题导致发射功率和接收灵敏度都严重下降。这类问题在驱动日志里完全看不出异常只能靠射频测试解决。所以如果驱动链路检查都正常但距离稍远就连接不上建议直接用频谱仪测天线口的信号质量。5.3 波特率不匹配导致的诡异现象HCI UART 波特率不匹配是最隐蔽的问题之一。模块默认可能是 115200但驱动在setup阶段下发命令把波特率切到了 1500000如果切换时序处理不好链路直接卡死。我总结了一套比较稳的处理方法初始化阶段用模块默认波特率通信下发厂商命令切换波特率后先将本端串口波特率切换到新值再给模块发送确认信息切换完成后等待模块返回切换完成事件而不是立即下发下一条命令。static int my_bt_setup(struct hci_dev *hdev) { // 已切换到新波特率后的初始化命令 struct sk_buff *skb; // 常见的 vendor 命令比如设置 baudrate 为 1500000 skb __hci_cmd_sync(hdev, 0xFC09, 2, data, HCI_INIT_TIMEOUT); if (IS_ERR(skb)) return PTR_ERR(skb); kfree_skb(skb); // 切换串口波特率 hci_uart_set_baudrate(hu, 1500000); // 等待模块完成切换 msleep(100); // 继续下发 patch return 0; }这段逻辑的难点在于“先串口还是先模块”的顺序以及切换后要留足稳定时间。每个模块略有差异建议用逻辑分析仪同时抓主控 TX 和模块 RX 两路信号确认切换过程中的字节流没有乱序。5.4 常见问题速查表现象大概率原因排查/解决办法hciconfig无 hci0设备树 UART 节点未启用检查节点 status、pinctrl 和 dma 配置hci_uart_tty_open 报错串口被其他驱动占用确认 dts 中是否有其他节点引用了同一 uart蓝牙模块无响应电源时序不对或模块未上电检查 BT_EN 引脚电平用 dmesg 确认 GPIO 申请是否成功能扫描但连接失败配对过程异常或模块固件问题抓 HCI 日志查看 link key 事件是否正常连接后频繁断开UART 丢包或流控异常确认 CTS/RTS 连接降低波特率测试音频卡顿UART 带宽不足开启 DMA检查模块是否支持更大的吞吐功耗异常高蓝牙模块未进入休眠检查蓝牙协议的 sleep 配置和 HOST_WAKE / BT_WAKE 引脚状态5.5 调试工具与独家心得最后分享几个我在实际调试中觉得特别有用的工具和方法hciattach临时绑定串口为蓝牙设备适合初期验证模块是否正常。btmonBlueZ 自带的 HCI 抓包工具能看到完整的 HCI 命令和事件流比hciconfig -a的日志详细得多。bluetoothctl 的list和show命令查看当前适配器和控制器详细状态。逻辑分析仪Saleae解码 UART 信号能直接看到字节是否正确发出、收到是排查波特率和流控问题的神器。我个人踩过的最大的坑是在设备树里开了uart2m0_xfer的 pinctrl但 CTS/RTS 引脚复用错了组导致硬件流控信号没接到模块上。这种问题用cat /sys/kernel/debug/pinctrl/pinctrl-handles能查看到当前引脚的复用状态但更直观的办法还是看原理图把每根线都对照一遍。另外调试蓝牙驱动时内核日志一定要开 DEBUG。可以在menuconfig里打开CONFIG_BT_DBG和CONFIG_BT_HCIUART_DEBUG然后启动参数加ignore_loglevel这样能看到每个 HCI 分组的收发。不过有个坏处——日志量非常大音频传输时每秒几千条消息会显著拖慢系统所以调试完记得关掉。还有个心得hci_uart驱动和蓝牙核心层的接口在 4.19 和 5.10 内核版本之间有一些变化如果你用的是比较新的主线内核参考老的 BSP 代码时注意接口差异。比如hci_uart_proto的recv函数签名在老内核里是返回void新内核改成了返回int照抄老代码会导致编译错误。最后再提一件事很多人调试蓝牙驱动时习惯把问题都归到驱动代码上但其实很多“驱动问题”本质上是用户态 BlueZ 配置问题。内核只负责把 HCI 分组正确传上来而配对、扫描策略、服务发现这些都是 BlueZ 管理的。遇到诡异问题先用最简单的hcitool scan和bluetoothctl交叉验证能少走很多弯路。这套链路我调了整整一周才跑通把这篇文章里的步骤走一遍相信你能比我快得多。