接手这块 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_TX,GND 必须共地,流控引脚务必接上。我见过很多“串口能发数据但蓝牙就是注册不上”的案例,一查全是 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中,默认有很多路 UART(uart0 ~ 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_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_BCSP=y CONFIG_BT_HCIUART_LL=y CONFIG_BT_HCIUART_3WIRE=y CONFIG_SERIAL_8250_DMA=yCONFIG_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/serial@fe660000/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 插件桥接 BlueZ:
bluealsa -i hci0 & # 然后在 /etc/asound.conf 里配置相应的 PCM 设备蓝牙键盘鼠标则依赖 HID over GATT(BLE)或 BT 经典 HID 协议。BLE HID 不需要额外的内核配置,因为 BlueZ 通过 BlueZ Profile 机制直接处理 HID 服务;BT 经典 HID 则需要hidp内核模块加载,并且用户态需要运行bluetoothd的--noplugin=input时确认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:临时绑定串口为蓝牙设备,适合初期验证模块是否正常。
- btmon:BlueZ 自带的 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交叉验证,能少走很多弯路。这套链路我调了整整一周才跑通,把这篇文章里的步骤走一遍,相信你能比我快得多。