RK3568 UART蓝牙HCI驱动实现与BlueZ SPP透传实战

RK3568 UART蓝牙HCI驱动实现与BlueZ SPP透传实战 最近在折腾一块 RK3568 的板子客户那边要求蓝牙必须走 UART 接口不能上 USB。一开始我还想省事直接怼一个 USB 蓝牙模块但看了看结构设计、供电和成本最后还是老老实实把 UART 蓝牙主机外设驱动这条路走通了。整个过程踩了不少坑也把 RK3568 设备树、内核蓝牙协议栈、BlueZ 用户态工具串起来梳理了一遍。这篇文章就把这次的实现过程、关键配置和排查思路完整地记录下来给后面要在 RK3568 或者其他瑞芯微平台上做 UART 蓝牙的朋友一个参考。这篇文章适合手里有 RK3568 开发板、想把蓝牙模块通过串口接到 Linux 系统里并且希望用蓝牙进行 SPP 透传或者其他经典蓝牙应用的开发者。不管是自己做板子还是用现成的评估板只要内核是 4.19 或 5.10 这种常见版本方法基本通用。下面我就按整个实现流程来拆。1. 方案设计为什么是 UART 蓝牙以及整体框架怎么搭1.1 需求背景和原始约束这个项目做的是一台边缘计算网关类设备主控选了瑞芯微 RK3568四核 A55跑 Linux。业务上需要一个经典蓝牙通道用来和现场的蓝牙透传模块通信完成参数下发和状态上报。最初考虑过用 USB 蓝牙毕竟 BlueZ 对 USB 蓝牙的支持最省心插上就能用。但结构工程师给了个硬性约束主板上已经没有多余的 USB 接口位置而且整机的供电余量不大USB 蓝牙的峰值电流对电源模块来说有点紧张。于是只能回到 UART 方案。其实 RK3568 上通过串口接蓝牙模块是一个非常传统且成熟的方案很多物联网设备、工业控制板、车载盒子都是这么干的。UART 接口本身就支持低速、低功耗的近距通信和蓝牙模块的 HCI 接口天然匹配。市面上大多数经典蓝牙模块比如 CSR 的 BC417、瑞昱的 RTL8761、或者是国产杰理、炬芯的一些模组都有 UART 引脚可以直接和主控串口相连。客户选的是杰理家的一个支持 SPP 协议的蓝牙从机模块这正好和标题里热词提到的 SPP 协议场景完全吻合。1.2 方案选型自研 HCI UART 驱动还是走内核现有框架动手之前要先想清楚一个关键问题这个蓝牙模块挂在 UART 上我们到底需要写多少代码如果这个蓝牙模块是一个“透明传输模块”也就是模块内部自己实现了蓝牙协议栈对外只提供串口透传那我们主机侧根本不需要写蓝牙驱动只需要写一个普通的串口应用程序往里发 AT 指令或者透传数据就行简单得很。但如果模块是标准 HCI 接口也就是模块只负责物理层和链路层链路管理和上层协议都在主控侧跑那我们就得让 Linux 的蓝牙协议栈通过 UART 把这个 HCI 设备认出来。这就要用到的内核里 hci_uart 驱动框架。项目里用的杰理模块恰好是标准 HCI 接口所以选择走内核的 hci_uart 框架。这相当于 Linux 内核已经把 90% 的协议处理做好了我们只需要告诉内核“这个串口上挂了一个 HCI 蓝牙用 H4 协议收发数据”然后配置好设备树和内核选项把用户态 BlueZ 跑起来剩下的事情系统自己搞定。这个方案的好处是不需要自己维护复杂的蓝牙协议栈后续支持 BLE、经典蓝牙、SPP、A2DP 都白捡。1.3 整体软件框架从硬件串口到用户态蓝牙应用整个链路的层次大概是这样的硬件层面RK3568 的一路 UART 引脚通过电平转换如果需要接到蓝牙模块的 UART 引脚模块的电源、使能引脚、唤醒引脚也接到主控的 GPIO 上。这一层要保证电压匹配RK3568 的 IO 电平一般是 3.3V蓝牙模块也大多工作在 3.3V但有些老模块是 1.8V 逻辑所以原理图阶段就得确认。内核层面UART 控制器驱动负责把串口数据收发跑起来hci_uart 驱动把串口字符流解析成 HCI 数据包再交给蓝牙核心层Bluetooth core生成一个 hci0 设备。这里最关键的文件是drivers/bluetooth/hci_h4.c和drivers/bluetooth/hci_ldisc.c前者是协议解析后者是线路规程挂接。用户态层面BlueZ 的 bluetoothd 守护进程负责协议栈初始化、设备发现、连接管理hciconfig / bluetoothctl / hcitool 这些工具用来调试和验证。应用层如果要用 SPP可以直接通过 BlueZ 提供的 socket API 或 D-Bus API 去连接远端从机。2. 硬件连接与 RK3568 设备树配置要点2.1 原理图阶段就要盯住的 6 个信号不要急着写软件先把硬件连接理清楚。一条标准的 UART 蓝牙 HCI 链路最少需要 4 根线推荐 6 根。这 6 根信号分别是UART_TX、UART_RX、UART_CTS、UART_RTS、GND、POWER_EN或者是 RESET。TX 和 RX 是数据收发必须交叉连接主控的 TX 接模块的 RX主控的 RX 接模块的 TX这是新手最爱错的地方。CTS 和 RTS 是硬件流控强烈建议接上因为蓝牙 HCI 数据包对时序有一定要求如果没有流控在高波特率下极容易丢字节导致蓝牙设备无法注册。POWER_EN 通常是一个 GPIO控制模块的供电或者复位软件侧要在初始化时拉高或者拉低让模块先复位一下。另外还有一个 BLE_WAKE 和 HOST_WAKE这俩在低功耗场景下很有用。模块收到主机数据时通过 HOST_WAKE 唤醒主控主控要发数据时先拉 BLE_WAKE 唤醒模块。如果只是做功能验证这两个脚可以先不接但产品化最好还是接出来。2.2 确认 RK3568 哪个串口可用避开调试串口RK3568 这颗芯片原生串口资源很多一共 9 路 UART但具体引出哪几路要看核心板厂商的原理图。比如 RK3568 的 EVB 板习惯把 UART2 作为调试串口打印内核日志。如果你的蓝牙也挂在调试串口上那我建议趁早换因为调试串口在 SPI 阶段的 bootloader 和内核早期就会疯狂输出日志会直接把蓝牙的 HCI 数据冲掉根本没法用。我这次用的是 UART4也就是设备树里的uart4节点。选它的原因很简单核心板的 pin mux 默认把它引出到了 40pin 排针或者模块接口上而且没有和 console 冲突。你在自己的板子上一定要先查清楚自己平台的串口占用情况用dts里的chosen节点看stdout-path确认调试串口是哪个然后避开它。2.3 设备树配置实操以 uart4 为例RK3568 的设备树在arch/arm64/boot/dts/rockchip/目录下常见的有rk3568-evb.dts、rk3568-industrie.dts这种。我们要改的是跟核心板配套的板级 dts 文件。核心思路是把 uart4 这个节点打开指定 pinctrl关闭 DMA 干扰如果支持就使能硬件流控。uart4 { status okay; pinctrl-names default; pinctrl-0 uart4m0_xfer uart4m0_ctsrts; dma-names ; assigned-clocks cru SCLK_UART4; assigned-clock-rates 115200; };注意pinctrl-0里的uart4m0_xfer uart4m0_ctsrts表示这个串口的 RX、TX、CTS、RTS 用的是 M0 组引脚这个要根据你的原理图上的实际引脚来定不一定所有板子都是 M0。可以打开 rk3568-pinctrl.dtsi 搜索uart4看看所有可选引脚组合。另外dma-names 这个操作是故意把 DMA 关掉。为什么因为 hci_uart 驱动在传统上依赖 tty 层的数据收发开了 DMA 之后如果串口驱动和蓝牙驱动配合不好可能在低延时场景下丢数据。RK3568 的串口 DMA 本身够快但多一层 DMA 就多一个变量初期调试为了减少干扰先把 DMA 关掉更稳妥。产品化之后想提升吞吐再开也行。2.4 波特率怎么选还要注意 console 冲突波特率是另一个高频翻车点。UART 蓝牙模组常见的默认波特率有 115200、921600、15000001.5M几种。老一些的 CSR 8 系列模块多是 115200新一些的杰理、瑞昱模块可能默认 921600。建议第一次调试一律从 115200 起步等链路稳定后再提高避免因为线材质量、干扰导致高速率下数据出错。这里还有一个坑如果你选的这个 UART 对应引脚上同时还有 SD 卡、PWM、I2C 等功能复用需要确认 pinctrl 的 iomux 配置不会和其他外设打架。原来板子上有没有把同一个引脚配置成了别的功能直接在 pinctrl 节点里搜对应引脚编号就可以看到。另外如果 uart4 在 bootloader 阶段也被用作日志输出记得在 bootargs 里改掉console参数比如consolettyS2,1500000让调试串口继续打印日志而要把我们用的 uart4 从 console 里摘出去否则内核启动后会一直往这个串口写日志干扰蓝牙数据。3. Linux 蓝牙驱动框架HCI 协议、hci_uart 与 hci_h4 内核配置3.1 先弄明白蓝牙在 Linux 内核里的分层Linux 的蓝牙协议栈分得挺清楚最底下是蓝牙控制器可以是 USB、SDIO、UART 等物理接口往上是 HCI 层也就是 Host Controller Interface它定义了主机和控制器之间怎么交换命令、事件和数据再往上就是 L2CAP、RFCOMM、SCO 这些协议层以及 BlueZ 用户态生态。我们把蓝牙模块接到 URAT 上本质就是把“控制器”接到了“主机”的串口上。控制器通过串口发送的数据包格式就是 HCI 数据包而 HCI 数据包必须有一个明确的边界划分否则主机根本不知道一帧从哪里开始、到哪里结束。这就是 H4 协议存在的意义每个 HCI 包都有一个一字节的开头用来指示包类型。比如0x01是 command0x02是 ACL 数据0x03是 SCO 数据0x04是 event0x05是 vendor event。hci_uart 驱动里的hci_h4就是负责把串口字节流切成一个个完整 HCI 包。3.2 内核配置把需要的驱动都选上启动内核配置界面无论是用menuconfig还是直接改 defconfig下面这几个选项必须确保打开。这是整个驱动实现的核心配置少了任何一个蓝牙设备都无法注册出来。CONFIG_BTy CONFIG_BT_BREDRy CONFIG_BT_LEy CONFIG_BT_HCIUARTy CONFIG_BT_HCIUART_H4y CONFIG_BT_HCIUART_BCSPy CONFIG_BT_HCIUART_LLy CONFIG_BT_HCIUART_3WIREy CONFIG_BT_HCIUART_BCMy解释一下这几个选项。CONFIG_BT是总开关CONFIG_BT_BREDR是经典蓝牙CONFIG_BT_LE是低功耗蓝牙。考虑到模块支持 SPPSPP 是基于经典蓝牙 RFCOMM 的BREDR 必须打开。CONFIG_BT_HCIUART是串口 HCI 框架这个等于总闸。然后根据模块支持的协议打开对应协议支持。H4是基本也最常用BCSP和3WIRE是带重传机制的串口协议老一点 CSR 模块可能用LL是 Nokia 的串口协议兼容性测试时可以全选上反正编译出来不会大多少。还有一种情况如果模块是罗姆、博通这类可能还会需要额外的 vendor 驱动比如CONFIG_BT_HCIUART_BCM对应博通方案。我们项目里用的是杰理走通用 H4 即可所以这几个通用配置就够。实际操作中我在kernel/arch/arm64/configs/rockchip_linux_defconfig里手动加上这些配置然后重新编译内核。如果你用的是 SDK 的 build.sh 脚本改完 defconfig 后执行./build.sh kernel就能增量编译不用整个系统重编能省不少时间。3.3 内核驱动加载方式编进内核还是编成模块我建议在调试阶段把 hci_uart 编成模块也就是CONFIG_BT_HCIUARTm这样每次改驱动参数或者想换协议可以直接 rmmod 再 insmod不用反复重启。而且如果系统启动后挂载蓝牙失败可以很快判断是驱动没加载还是模块本身的问题。但如果产品要量产建议编进内核。原因无他就是少一个模块加载时序问题系统起来后 bluetoothd 直接就能初始化 hci0。编进内核后可以通过启动日志直接看到类似Bluetooth: HCI UART driver ver 2.3这样的打印说明 hci_uart 框架已经就绪。命令空间里还有一个hciattach工具很多人对它又爱又恨。它实际上是一个用户态工具用于在系统启动早期把 UART 设备“附加”到蓝牙子系统。内核的 hci_uart 驱动和这个工具的关系有点微妙如果没有把 hci_uart 编进内核并自动识别设备树就需要靠 hciattach 在用户态手动绑定。但我们现在已经在设备树里把串口节点配置好了并且内核的 serdev 机制可以直接配合 hci_uart 生成 hci0所以一般不需要 hciattach。这算是一种更现代、更干净的做法。4. 驱动编译、加载与 BlueZ 用户态集成4.1 编译内核并确认 hci_uart 驱动自动识别按照上面配置把内核编译好烧录到 RK3568 板子上。启动后进入系统先看 dmesg 有没有识别到串口蓝牙设备。dmesg | grep -i bluetooth dmesg | grep -i hci dmesg | grep -i uart4如果设备树配置正确、驱动加载成功至少能看到类似下面这样的日志Bluetooth: HCI UART driver ver 2.3 Bluetooth: HCI UART protocol H4 registered Bluetooth: HCI UART protocol LL registered Bluetooth: HCI UART protocol ATH3K registered这一步说明 hci_uart 驱动注册了各类协议但还没有实际和硬件握手。继续查看 hci0 设备是否生成hciconfig -a如果看到hci0: Type: Primary Bus: UART这样的输出说明设备树里的串口已经被识别并挂载成了蓝牙 HCI 设备这是个里程碑。如果这里没有输出或者报错就回到设备树、pinctrl、电源 GPIO 这些硬件配置上查。4.2 模块电源和复位时序的 GPIO 控制很多 RK3568 调试板上蓝牙模块的供电和复位不是直接拉死的而是由 GPIO 控制。这些 GPIO 的控制方式有两种一是直接在设备树里通过gpio-export或者regulator-fixed控制电源二是写一个简单的小驱动在系统启动时按序拉高使能、拉低复位再释放。我这次直接在板级 dts 里增加了一个电源控制节点/ { bluetooth_power: bluetooth-power { compatible regulator-fixed; regulator-name bluetooth_power; enable-active-high; regulator-boot-on; gpio gpio1 RK_PA5 GPIO_ACTIVE_HIGH; }; };这个节点表示用一个 GPIO 给蓝牙模块供电设备树解析后系统会在蓝牙驱动匹配前把电源拉起来。如果你上电后发现蓝牙一直不响应先排查这个 GPIO 有没有拉高用示波器或者万用表测一下模块供电脚。还有个小技巧系统启动后如果想快速复位蓝牙模块可以在 shell 里手动控制 GPIO。比如echo 25 /sys/class/gpio/export echo out /sys/class/gpio/gpio25/direction echo 0 /sys/class/gpio/gpio25/value sleep 0.5 echo 1 /sys/class/gpio/gpio25/value这里的 25 是你实际 GPIO 编号可以通过cat /sys/kernel/debug/gpio查到。手动拉一下复位再重新扫描可以解决很多初始化时序导致的“模块起飞”问题。4.3 用 btmon 和 bluetoothctl 验证 HCI 通道驱动和硬件都就绪后首选调试工具是btmonBluetooth Monitor它可以抓取所有经过 Bluetooth subsystem 的 HCI 数据包。执行btmon 然后在另一个终端使用bluetoothctlbluetoothctl power on scan on如果在btmon里能看到大量的 HCI Event 和 Advertising Report说明 UART 链路、H4 协议、HCI 解析都正常蓝牙主机外设驱动这一步就算真正通了。我记得第一次看到 MGMT Event: Found Device输出时心里一下子就踏实了。4.4 如果需要hciattach 的备选方案上面说设备树自动生成 hci0 是最干净的方式但如果你用的 SDK 内核版本比较老或者 hci_uart 没有实现 serdev 绑定那就得退回使用 hciattach 的方式。系统启动脚本里加一行/usr/bin/hciattach /dev/ttyS4 any 115200 flow这行命令的意思是把/dev/ttyS4这个串口用 H4 协议、波特率 115200、开启硬件流控的方式绑定到蓝牙子系统。注意要把/dev/ttyS4替换成你实际使用的串口设备节点。串口设备节点可以通过ls /dev/ttyS*来确认。老手一般区分ttyS*和ttyFK*这类节点瑞芯微的串口驱动有时会注册成别的名字。直接看/dev下的设备或者cat /proc/tty/driver/serial也能看到对应串口的信息。5. 调试实录从“扫描不到”到“稳定连接”5.1 常用调试工具与日志定位方法搞 UART 蓝牙这种链路最大的麻烦是问题可能出现在任意一层硬件电压不对、串口收发反了、波特率不匹配、HCI 包解析失败、BlueZ 配置错误每层都有各自的表现。这时候一定要按“从底向上”的顺序排查。第一层看串口用stty -F /dev/ttyS4 -a查看串口参数确认波特率、流控和硬件里的设置一致。再狠一点直接用minicom或者cat /dev/ttyS4看有没有数据回显不过要注意HCI 是二进制协议直接用串口工具会看到乱码属正常现象。第二层看蓝牙驱动dmesg | grep Bluetooth看有没有Bluetooth: HCI device ...或者错误信息hciconfig hci0 up手动把设备拉起。第三层看 BlueZsystemctl status bluetooth确认 bluetoothd 是否在运行bluetoothctl list看控制器是否被 BlueZ 管理。如果蓝牙设备没被 BlueZ 认到那 SPP 这些上层协议根本无从谈起。5.2 我踩过的 6 个经典坑这里整理一下我这次项目里实际遇到过的几个问题按频率从高到低排列。第一个坑最隐蔽流控接反。CTS 和 RTS 是交叉连接还是直连很容易在画原理图时搞反。模块的 RTS 要接主控的 CTS模块的 CTS 要接主控的 RTS这是交叉的。接反了之后明明 RX/TX 数据能过去但一上流控就卡住hciconfig 起来后蓝牙一直报错或者死等。第二个坑是调试串口冲突。一开始我图省事把蓝牙挂在了默认打印日志的串口上结果内核日志把 HCI 初始化包搅得一团糟。后来看了 bootargs发现 console 占用的就是那一路串口换到空闲串口后问题立刻消失。第三个坑是波特率不匹配。模块默认 152000 波特率我设备树里写的是 115200结果 HCI 包时不时能解析出正常的 header但 ACL 数据老是校验不过。把assigned-clock-rates改成模块实际波特率后一切正常。记住UART 蓝牙不能搞“自适应波特率”必须两边固定一致。第四个坑是 DMA 干扰。平台 SDK 的模版 dts 里默认给串口开了 DMA但和 hci_uart 配合时偶尔会出现数据丢半个包的情况。把dma-names清空让串口走 PIO 模式问题立即消失。这也是我在上面设备树代码里特意把 DMA 关掉的原因。第五个坑是 BlueZ 版本太老。SDK 自带的 BlueZ 5.43 这种老古董在处理某些 LE scan 时经常有 bug。后来我把用户态 BlueZ 升级到 5.66 以上实验功能也更全比如扫描过滤、Mesh 支持等。看来内核驱动只是一层用户态的协议栈版本同样重要。第六个坑是电源噪声导致蓝牙模块随机死机。这个比较难查系统运行一会儿后 hci0 就消失dmesg 里没有明显报错。后来用示波器观察模块供电发现负荷高时电源纹波很大在模块供电脚就近加了一个 100uF 电容和一个小磁珠问题解决。这也是产品化调试中最考验经验的一部分。5.3 经典蓝牙 SPP 测试流程参考驱动通了以后建议先用 SPP 做一次完整的数据通路验证这比单纯 ping 设备的 RSSI 更有说服力。可以用 BlueZ 提供的sdptool添加一个 SPP 服务记录再用手机或者其他蓝牙设备连接进行串口数据透传。sdptool add SP然后查看服务是否已经注册sdptool browse local如果能看到Service Name: Serial Port和Protocol Descriptor: L2CAP, RFCOMM这类信息说明协议栈已经把 SPP 服务发布出去了。手机端打开蓝牙用任意一个支持 SPP 的串口调试 App 连接该设备尝试发几个字节如果主机这边能在/dev/rfcomm0或者通过 BlueZ socket 收到那整个 UART 蓝牙主机外设通道就彻底打通了。5.4 性能与稳定性低功耗唤醒和内核配置优化最后说一下低功耗场景。如果设备要做电池供电就不能让蓝牙模块一直全速运行。这就要用上模块的BT_WAKE和HOST_WAKE引脚。激活方式一般在模块厂商提供的 datasheet 里有需要在内核设备树里配置对应的 GPIO 中断和 wakeup-source 属性并且在 BlueZ 中开启对应的 power saving。这个功能调试起来很费时建议产品开发阶段第一版先不启用等基本功能稳定后再逐步加省电策略。另外如果发现蓝牙连接一段时间后吞吐量上不去或者音频卡顿可以考虑把串口波特率从 115200 提高到 921600 甚至 1500000前提是模块和外设都支持而且线材质量足够好。提高波特率也有代价对信号完整性要求更高如果布线太长、没有屏蔽可能会增加误码率。所以我在项目里先用 115200 验证功能稳定后再评估是否提高。我在实际调试中还有一个体会给 UART 蓝牙驱动做验证时不要一上来就上复杂应用先做“扫描、配对、SPP 透传”这条最小路径任何一层出问题都能快速定位。这个思路看着简单却能省下很多查“玄学问题”的时间。如果你也在 RK3568 或者类似 Linux 平台上搞 UART 蓝牙不妨按这个顺序走一遍少走弯路。