作为一个经常折腾RK系列平台的嵌入式工程师,我最近在一款基于RK3568的板卡上,做了一套UART接口蓝牙主机外设驱动的完整移植。这活儿乍一听挺简单——不就是接个串口、连个蓝牙模块吗?实际走下来才发现,里面全是细节,从硬件电平匹配、设备树配置,到内核协议栈裁剪、固件加载时序,再到用户空间的rfkill和蓝牙调试工具,任何一个环节没弄对,hci0就出不来,或者出来了也连不上设备。
这篇文章我把整个实现过程原原本本记录下来,包括我是怎么选方案、怎么改设备树、怎么配内核、怎么排查问题的。如果你也在RK3568或者类似瑞芯微平台上做蓝牙外设驱动,或者刚开始接触Linux蓝牙协议栈,这篇文章可以直接拿来当参考。整套思路换到IMX8、树莓派、全志T507上也同样适用,因为底层都是同一个BlueZ协议栈加HCI UART传输层。
1. 需求梳理与方案选型:为什么UART接口做蓝牙外设仍然是主流
1.1 项目目标与适用场景
先说清楚这次项目的目标。板卡主控是RK3568,四核Cortex-A55,跑的是Linux系统,需要通过UART外挂一个经典蓝牙模块,让整机支持蓝牙配网、SPP透传和BLE数据通信。说白了,就是要把一个串口改造成蓝牙数据的传输通道,让上层应用能通过socket、串口或BlueZ标准接口去操作蓝牙设备。
这个需求在工控、物联网网关、医疗设备、手持终端、智能家电里非常常见。很多模组厂商出货量最大的蓝牙模块,都是UART接口的,比如BCM系列、AP6212、AP6256,还有一堆国产BT3.0/BT4.0双模模块。原因很简单——UART是几乎所有主控都剩余的接口,成本低、布线简单、驱动成熟。相比之下USB蓝牙适配器虽然即插即用,但在嵌入式板卡上,USB资源往往被4G模组、U盘、摄像头占用,不如UART可靠。
1.2 主机与外设概念的理解误区
很多人搞不清楚“蓝牙主机外设驱动”这个说法。这里说的主机,指的是蓝牙协议栈的Host侧,也就是跑在RK3568上的Linux内核BlueZ协议栈;外设就是那个UART接口的蓝牙模组,也叫Bluetooth Controller。两者之间通过HCI(Host Controller Interface)协议通信,UART只是HCI传输层的物理载体。
换句话说,我们做的事情不是去写蓝牙协议栈,而是把Linux内核里现成的BlueZ协议栈,通过HCI UART传输层,接到一个具体的蓝牙Controller芯片上。外设驱动要解决的核心问题有两个:第一,让内核知道这个UART口上挂了一个蓝牙设备,这叫设备树描述和驱动匹配;第二,让HCI协议数据能稳定地在UART上收发,这涉及到流控、波特率、固件加载和电源管理。
我见过不少新手把“蓝牙驱动”理解成“蓝牙协议栈的驱动”,一上来就想改l2cap或者rfcomm的代码,其实根本不需要。真正要打交道的,是hci_uart驱动、设备树节点、rfkill机制和固件加载,理解了这一层,整个项目思路就清晰了。
1.3 三种HCI传输方式怎么选
蓝牙Controller和Host之间,常见的传输方式有三种:UART、USB、SDIO。我做了个对比表格,方便大家直观理解:
| 传输方式 | 典型模组 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| UART | BCM43438、AP6212、RTL8723、HC05/06 | 接口通用、成本低、布线简单 | 带宽有限,A2DP音频流需额外I2S/PCM | 数据透传、BLE、SPP、低功耗设备 |
| USB | CSR8510、RTL8761 | 即插即用、驱动现成、带宽高 | 占用USB Host资源,成本偏高 | 开发板外接、PC适配器 |
| SDIO | AP6255、RTL8822 | 带宽高,可同时跑WiFi/BT | 驱动和固件流程复杂,一般由WiFi驱动统一管理 | 高性能网卡方案 |
对于大多数RK3568产品来说,如果蓝牙只是用来做配网、数据透传、BLE控制,UART接口完全够用。如果既想要蓝牙又要WiFi,模组厂商一般会把WiFi用SDIO、蓝牙用UART同时接出来,SDIO的蓝牙部分其实也是通过HCI UART模拟的。所以不管怎么看,UART这条路径都绕不开,这也是我坚持要写这篇文章的原因。
2. 硬件连接与设备树配置:把蓝牙外设的信息告诉内核
2.1 选对UART通道,避开默认调试口
RK3568这颗芯片内部有多个UART控制器,具体每个串口复用在哪些引脚上,要看芯片的引脚复用表(IOMUX)。我这次用的是UART1,因为UART2默认就是调试串口,已经被内核log占用了,如果硬要拿来挂蓝牙,调试信息会混在一起,非常痛苦。
选UART通道的时候有几个经验:
- 优先选带硬件流控的串口,也就是有CTS和RTS信号的那组。蓝牙模块对数据实时性要求高,没有流控,大流量数据一上来就会丢。
- 尽量避开调试口。调试串口在bootloader阶段就要用,kernel起来后还要打印日志,和蓝牙抢同一个串口纯属给自己挖坑。
- 查清楚这组UART在设备树里的节点名。RK3568的设备树里一般叫uart0、uart1、uart2等,对应/serial@fe6c0000这种地址,这个地址可以在芯片手册里查到。
硬件接线其实很直接,主要就是把主控的UART_TX接到模块的RXD,主控的UART_RX接到模块的TXD,然后CTS、RTS交叉连接。还有一个容易被忽略的:模块的VCC和GND必须和主控共地,否则逻辑电平参考不一致,会出现间歇性通信失败。
2.2 电平匹配和流控信号,出了问题九成在这
UART接口最怕的就是电平不匹配。RK3568的GPIO一般在1.8V或3.3V,而蓝牙模组的IO电平可能是1.8V,也可能是3.3V,甚至有的宽电压模块能兼容2.8V到3.6V。接入之前一定要查模块数据手册的VIO引脚要求,如果电平不一致,中间必须加电平转换芯片,比如TXS0108EPWR、SGM4553这类,不能直接飞线。
流控信号很多人偷懒不接。HC-05、JDY-31这类低速BT3.0模块,可能不接CTS/RTS也勉强能跑;但到了BCM43438、AP6212这种高速双模模块,不接流控,数据量稍微大一点就会出现蓝牙协议栈报HCI timeout,或者上层socket直接断开。我这次是全部接了CTS、RTS的,设备树里也开了硬件流控,后面调起来几乎没有丢包问题。
还有一个容易被忽略的引脚是模块的LPO时钟输入。很多蓝牙模组的内部32.768kHz低功耗时钟,需要主控提供一个外部时钟源。RK3568的XOUT或CLKOUT引脚可以直接输出这个频率。如果这个时钟没有接好,模块可能上电能出hci0,但蓝牙扫描一会儿就死掉,非常隐蔽。
2.3 设备树节点怎么写
RK3568的Linux内核使用的是设备树机制,外设的描述全部在dts文件里。一个典型的UART挂蓝牙模块的设备树节点,长下面这样:
&uart1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart1_xfer &uart1_cts &uart1_rts>; uart-has-rtscts; dma-names = "tx", "rx"; dmas = <&dmac0 4>, <&dmac0 5>; bluetooth { compatible = "brcm,bcm43438-bt"; clocks = <&rk3568_clkout1>; clock-names = "lpo"; max-speed = <3000000>; pinctrl-names = "default"; pinctrl-0 = <&bluetooth_en>; shutdown-gpios = <&gpio3 RK_PA2 GPIO_ACTIVE_HIGH>; host-wakeup-gpios = <&gpio3 RK_PA3 GPIO_ACTIVE_LOW>; device-wakeup-gpios = <&gpio3 RK_PA4 GPIO_ACTIVE_HIGH>; }; };这里有几个关键点得解释清楚。
compatible属性决定了内核中的哪个驱动来和这个节点匹配。brcm,bcm43438-bt对应的是drivers/bluetooth/hci_h4_bcm.c里的驱动,不同厂商的模块要换成对应的compatible,比如RTL8723系列是realtek,rtl8723-bt,AP6256可能要挂到brcm的驱动上,具体看模组厂商给的patch里怎么写的。
uart-has-rtscts这个属性必须要有,它告诉内核这个串口已经接了硬件流控线,驱动会去配置UART控制器的自动流控功能。如果你接线时把CTS/RTS省了,这个属性就别写,写了反而会导致发送被卡住。
shutdown-gpios是模块的使能/复位引脚,驱动在初始化序列里会正确拉高或拉低这个脚,控制模块的重置。host-wakeup-gpios和device-wakeup-gpios是低功耗唤醒脚,如果不需要休眠唤醒,可以只保留shutdown。
这里的clocks节点,给的就是上面提到的32.768kHz LPO时钟。rk3568_clkout1是RK3568的一个外部时钟输出复用引脚,我把它配置成了蓝牙模块的睡眠时钟。如果没有这个时钟,直接用模块内部RCO,多数情况下也能工作,但蓝牙扫描、广播的时钟精度会下降,连接稳定性也会受影响。
2.4 pinctrl引脚的确认方法
很多人在这一步卡住,提示GPIO被占用或者功能冲突。RK3568的pinctrl信息在设备树里一般已经定义好了,但具体到某个GPIO能不能复用成UART功能,要看SoC的pinctrl头文件。
在kernel/arch/arm64/boot/dts/rockchip/目录下,有对应的rk3568-pinctrl.dtsi。里面定义了串口和蓝牙的pin group。修改的时候,要仔细确认引脚没有被其他节点引用,比如某些开发板的I2C、SPI、PWM也可能复用同一个pin。
我个人的调试习惯是,改完设备树以后,先在内核起来后用下面命令看一下引脚复用状态:
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep uart1如果显示了类似uart1-xfer的pin group,说明IOMUX配置正常。如果显示unknown或者被其他模块占用,就要回到dts里查重复引用。
3. 内核配置与驱动编译:让HCI UART串起整个蓝牙协议栈
3.1 内核必须开启的蓝牙相关选项
设备树描述的是硬件拓扑,真正让系统跑起来,还要在内核配置里打开对应功能。RK3568的SDK默认内核配置一般已经开了大部分蓝牙选项,但每个项目拿到后都会裁剪,所以最好自己过一遍。
我通常习惯用menuconfig去配,快速检查路径是:
make ARCH=arm64 menuconfig然后按下面路径逐项检查:
Networking support -> Bluetooth subsystem support (CONFIG_BT) -> Bluetooth device drivers -> HCI UART driver (CONFIG_BT_HCIUART) -> UART (H4) protocol support (CONFIG_BT_HCIUART_H4) -> BCSP protocol support (CONFIG_BT_HCIUART_BCSP) -> 3-Wire UART (H5) protocol support (CONFIG_BT_HCIUART_3WIRE) -> Broadcom protocol support (CONFIG_BT_HCIUART_BCM)其实最终在.config文件里体现的核心项就这些:
CONFIG_BT=y CONFIG_BT_RFCOMM=y CONFIG_BT_RFCOMM_TTY=y CONFIG_BT_BNEP=y CONFIG_BT_HIDP=y CONFIG_BT_HCIUART=y CONFIG_BT_HCIUART_H4=y CONFIG_BT_HCIUART_BCM=y CONFIG_BT_HCIUART_LL=y CONFIG_RFKILL=y CONFIG_RFKILL_GPIO=yCONFIG_BT_RFCOMM_TTY这个开关特别重要。如果你要用SPP蓝牙串口透传,上层要通过rfcomm协议把蓝牙通道映射成一个虚拟tty设备,这个选项不开的话,/dev/rfcomm0根本创建不出来。
CONFIG_RFKILL的意义在于管理蓝牙的电源开关。RK3568的蓝牙使能脚一般会挂在射频开关框架下,系统启动时如果rfkill状态是blocked,hci0设备就不会出现,或者出现后被立刻禁用。后面我会专门说这个问题。
3.2 固件加载机制,为什么没固件hci0就不出来
大部分UART蓝牙模组内部没有Flash,或者只有一小段ROM引导程序。上电后模块会等待Host通过UART下发真正的固件,也就是说,Linux驱动必须在hci0注册之前,先把固件文件写到模块里去。
这个流程在BCM系列上特别典型。驱动会先通过HCI Reset命令和模块的ROM引导程序握手,然后把/lib/firmware/brcm/BCM43438A1.hcd这样的固件文件通过HCI厂家命令发送到模块,发送完成后模块重启生效,hci0设备才真正被注册。
所以固件文件放错位置,或者文件名不匹配,是hci0出不来的最大原因之一。
正确的放置位置一般在:
/lib/firmware/brcm/ /lib/firmware/rtl_bt/不同模块的具体文件名可以看驱动代码里的MODULE_FIRMWARE声明。比如Rockchip SDK经常用的AP6212/AP6256,对应BCM43438A1/BCM4345C5,文件名形如:
BCM43438A1.hcd BCM4345C5.hcd更新固件以后要记得执行depmod -a,重启或者重新加载驱动模块。在使用旧版内核时,我甚至遇到过驱动加载顺序问题导致固件发送失败的情况——解决办法是重新插拔一下使能脚,让模块回到ROM引导状态。
3.3 编译和烧录RK3568内核镜像
RK3568的SDK一般使用build.sh脚本完成内核编译。这里我给一个通用流程,具体镜像路径不同SDK会有差异:
cd kernel make ARCH=arm64 rockchip_linux_defconfig make ARCH=arm64 menuconfig # 确认蓝牙相关选项 make ARCH=arm64 rk3568-evb.dtb make ARCH=arm64 boot.img -j16编译完的boot.img烧进去以后,Windows下用瑞芯微的烧录工具直接选boot分区烧录就行;Linux环境下也可以用upgrade_tool或者rkdeveloptool:
rkdeveloptool wl 0x4000 boot.img rkdeveloptool rd也可以只更新dtb:
make ARCH=arm64 dtbs然后单独把rk3568-evb.dtb烧进resource分区。不过我自己实践下来,dtb单独烧容易搞混版本,不如直接整包编译boot.img,省得烧完查半天找不到问题。
4. 用户空间初始化与功能调试:让蓝牙真正跑起来
4.1 rfkill、hci0与BlueZ服务启动
内核配置好了,设备树对上了,固件也放进文件系统了,上电以后怎么知道蓝牙驱动有没有工作?
第一个看的命令是:
rfkill list正常情况下应该能看到类似这样的输出:
0: phy0: Bluetooth Soft blocked: no Hard blocked: no如果Soft blocked显示yes,说明系统默认把蓝牙关了,执行:
rfkill unblock bluetooth如果是Hard blocked,检查一下shutdown引脚对应的GPIO是否拉到了正确电平,或者设备树里的GPIO_ACTIVE_HIGH方向是不是反了。
然后看hci设备:
hciconfig -a如果内核驱动和固件都没问题,这里会显示hci0及其类型、地址和UP状态。有时候显示DOWN,需要手动执行:
hciconfig hci0 up会有很多人问,为什么不是自动UP?这跟BlueZ的设计有关系,协议栈默认把管理权交给bluetoothd服务,如果你想开机自动可用,需要配置bluetoothd服务正常起来。在systemd系统里可以执行:
systemctl enable bluetooth systemctl start bluetooth当然,如果想省事,也可以在开机脚本里直接加hciconfig hci0 up,很多量产产品就是这么干的。
4.2 经典蓝牙SPP和BLE功能验证
hci0起来了,功能怎么验证?我一般分两步,先看扫描,再看连接。
扫描经典蓝牙设备:
hcitool scan扫描BLE设备:
hcitool lescan如果扫描正常,说明底层射频、协议栈、HCI链路全是通的。接着可以试试和手机配对。
使用bluetoothctl是目前最常用的方式:
bluetoothctl power on agent on scan on pair <MAC> trust <MAC> connect <MAC>对于SPP透传功能,需要在用户空间创建一个rfcomm设备:
rfcomm bind 0 <MAC> 1绑定成功以后,会生成/dev/rfcomm0,这时候串口工具就能像操作普通串口一样操作蓝牙了。产品需要更灵活的话,也可以直接用C语言调用BlueZ的socket接口,走RFCOMM协议去读写,这和操作TCP socket非常像。
BLE开发调试用的更多的是bluetoothctl的GATT子交互命令,实时读特征值、写特征值都很方便。在批量产线验证时,我习惯写一个简单脚本,自动执行hcitool scan加bluetoothctl pair,循环压测连接成功率,不过这里不展开脚本细节。
4.3 蓝牙调试日志,怎么打开关键信息
遇到问题只看dmesg是不够的,因为蓝牙协议栈很多关键信息在bluetoothd的日志里。
先看内核日志,打开蓝牙相关的动态打印:
echo 0xffff > /sys/kernel/debug/bluetooth/hci0/adv # 这里只是示例 dmesg -w | grep -i bluetooth更常用的是开启bluetoothd的调试输出,在启动参数里加-d:
bluetoothd -n -d输出会非常详细,从HCI事件到L2CAP通道建立,都有记录。内核底层HCI数据帧,可以用btmon抓包:
btmon -w /tmp/btsnoop.log也可以使用hcidump(老一点工具):
hcidump -X抓到的hci数据包是分析蓝牙连接失败的最直接证据,比如配对失败时,可以看到到底是在LMP层报错,还是在SMP层报错,方向一下子就清楚了。
5. 踩坑实录与排查速查:UART蓝牙驱动的10个典型问题
这一部分我把自己实际遇到过的、以及在技术社区里看人翻车最多的几个问题整理出来。UART蓝牙的坑其实高度重复,掌握这几个排查方向,能省掉大把时间。
5.1 上电后hci0设备完全不出现
这种情况最常见。排查顺序我建议是:硬件 -> 设备树 -> 固件 -> 驱动配置。
硬件层面,用示波器看模块的TXD引脚有没有数据输出,如果模块ROM引导程序正常运行,上电后会主动发HCI Reset或HCI Command Complete的响应帧。如果TXD完全是平的,可能模块没有正常供电或复位脚没拉起来,优先查VCC、GND、RST。
设备树层面,检查uart节点有没有status = "okay",pinctrl对应的引脚有没有被其他节点占用。
固件层面,看dmesg有没有类似这样的错误信息:
Bluetooth: hci0: command 0x1009 tx timeout Bluetooth: hci0: BCM: patch brcm/BCM43438A1.hcd not found前者说明HCI通信都没有建立,后者说明固件文件缺失。
5.2 hci0出现但rfkill无法解除
有绑定的GPIO在rfkill框架下,系统起来后如果硬blocked,通常不是GPIO坏了,而是设备树里的rfkill-gpio节点配置不对。
检查一下dts里有没有这样一个节点,GPIO号要改成实际接的引脚:
rfkill_bluetooth: rfkill-bluetooth { compatible = "rfkill-gpio"; label = "bluetooth"; radio-type = "bluetooth"; shutdown-gpios = <&gpio3 RK_PA2 GPIO_ACTIVE_HIGH>; status = "okay"; };如果不希望用rfkill管理,也可以直接删掉这个节点,让蓝牙始终上电,靠shutdown-gpios去控制模块开关。
5.3 能扫描到设备但连接不上
连接失败原因比较复杂,先排除射频信号问题。天线区域不要被金属外壳遮挡,模块的PCB天线区不要覆盖地铜,虽然这听起来像硬件工程师的活,但真的很多人在这上面栽过跟头。
然后看流控和波特率。模块和主控之间的UART波特率如果不一致,可能会出现能扫描到一些广播数据,但一建立连接就断开的情况。因为扫描阶段数据量小,丢几帧看不出来,连接成功后双向数据一多,问题立刻暴露。用逻辑分析仪抓一下UART波形,看一下实际波特率是最快的办法。
最后看兼容性。有些模块在和其他品牌的手机配对时,需要配置特定的安全级别。在bluetoothctl里可以尝试:
pair <MAC>如果显示AuthenticationFailed,就要看看模块固件版本是不是太老,找模组厂商升级固件。
5.4 蓝牙地址无效或为全零
量产阶段容易遇到这个问题。有些模块的出厂地址是无效的,比如00:00:00:00:00:00,或者所有模块地址相同。蓝牙地址相同会导致多台设备互相干扰,在产线上必须批量写入独立地址。
写地址可以用hcitool直接下发HCI命令,保持hci0处于down状态执行:
hciconfig hci0 down hcitool cmd 0x03 0x0001 0x00 0x00 0x00 0x00 0x00 0x00 0x00上面命令中,第3到8个字节就是蓝牙地址,按照厂家的字节序填写。
更可靠的方式,是在驱动层面通过私有HCI命令写,这需要参考模组原厂提供的驱动patch。不过对于国内常见模块,厂商一般会提供一个独立的烧录串口演示工具,可以在产线阶段写一次性写入。
5.5 与WiFi共存时的干扰问题
RK3568板卡上往往同时有WiFi模组,如果WiFi和蓝牙共用同一个天线(或者天线隔离度不够),会发现蓝牙吞吐率下降、连接偶发断开。这类问题优先在设备树里打开共存引脚配置,常见的是WiFi的BT_ACTIVE和蓝牙的WLAN_ACTIVE两个GPIO做握手。
另外一个非常隐蔽的坑,是WiFi驱动占用了和蓝牙LPO时钟同一个clk,一旦WiFi进入功率调节,蓝牙时钟就抖一下,连接就断了。遇到这种情况,把两个外设的clk_disable_unused关掉,或者单独给蓝牙配一个独立晶振。
5.6 典型问题排查速查表
| 现象 | 最可能原因 | 排查方向 |
|---|---|---|
| 无hci0且TXD无数据 | 模块未上电/复位未拉起 | 检查VCC、GND、RST |
| 无hci0但TXD有数据 | 固件缺失/设备树匹配不对 | dmesg查询固件报错 |
| 有hci0但DOWN | rfkill阻塞或bluetoothd未启动 | rfkill list、systemctl |
| 扫描不到设备 | 天线辐射不良/射频未校准 | 检查天线净空区 |
| 扫描到但连接失败 | 流控未开/波特率不一致 | 逻辑分析仪抓UART波形 |
| 蓝牙地址全0 | 模块未烧录地址 | HCI命令写BD_ADDR |
| 连接后频繁断 | WiFi共存干扰/电源纹波 | 检查共存GPIO、供电稳定性 |
| SPP应用打不开/dev/rfcomm0 | BT_RFCOMM_TTY未开启 | 检查内核配置 |
最后再多说一句,做RK3568的UART蓝牙驱动,说白了就是硬件链路、设备树描述、内核配置、固件加载和用户空间调试这五步。设备树和内核配置统一了之后,实际上你会好用了很多习惯,流控一定要接全。电压务必查对,固件的位置放对,最后用btmon加dmesg去查。只要逻辑链路通了,剩下的问题都能按套路上的方法定位到。我这些年踩过太多“感觉是软件问题,结果发现是模块TXD根本没信号”的坑,所以总喜欢先看示波器,再看日志,顺序反了,只会白白浪费时间。希望这篇记录能帮你少走几步弯路。