上一篇把 MUSEPaper2 蓝牙从设置页面到 RTL8852BS 的完整链路梳理了一遍,这一篇开始真正修改。刚开始的现象很简单:系统里有 BluetoothHost,设置页也有蓝牙开关,但是蓝牙不能稳定完成初始化和扫描。这种问题如果只看最终现象,很容易直接怀疑内核串口驱动。但按照上一篇的分层思路,应该先找“最后一个已经成功的点”,再检查它后面的第一处失败。
第一阶段:确认真实 UART,不要相信遗留配置
Realtek vendor 配置文件位于 vendor/spacemit/musepaper2/bluetooth/rtkbt.conf,原来的配置是:
BtDeviceNode=/dev/ttyS1
但是根据 MUSEPaper2 的 DTS、pinctrl 和设备节点,实际蓝牙连接的是 UART2:
/dev/ttyS2
所以这里修改为:

这个问题看起来很基础,但是 AI 告诉我说板级移植中非常常见。配置可能来自另一块板、另一个 SoC 版本或旧硬件设计,文件名看起来都对,里面却藏着一个错误的设备节点。
如果 vendor 库一直打开 /dev/ttyS1,上层看到的通常只会是“Controller 无响应”,并不会友好地提示“这份配置是从别的板子复制来的”。
第二阶段:让 blue_host 真的有权限访问硬件
串口节点存在,不代表 blue_host 能打开。
OpenHarmony 的 HDF 服务会运行在自己的用户和进程中,不能拿 root shell 的结果直接代替服务进程结果。
板级 init 配置位于 device/board/spacemit/musepaper2/cfg/init.musepaper2.cfg 蓝牙需要:
"chmod 775 /sys/class/rfkill/rfkill0/state",
"chown blue_host blue_host /sys/class/rfkill/rfkill0/state",
"chown blue_host blue_host /dev/ttyS2",
"chown bluetooth bluetooth /dev/uhid"
这里分别解决:
rfkill state -> blue_host 可以控制蓝牙上电/断电
/dev/ttyS2 -> blue_host 可以打开实际 UART
/dev/uhid -> 蓝牙 HID 相关能力可以访问 uhid
可以在设备上检查:
ls -l /dev/ttyS2
ls -l /sys/class/rfkill/rfkill0/state

这里依然是适配完成后的输出。
第三阶段:修正固件查找路径
这一点错误对于我来说太难发现了,所以会运用一些 AI 来帮我发现。嘿嘿。
RTL8852BS 的蓝牙 Controller 在初始化过程中需要加载 firmware patch 和 config。
vendor/spacemit/musepaper2/bluetooth/BUILD.gn 中的固件目标使用:

最终文件进入 vendor 的 /vendor/etc/firmware/,但是 Realtek 头文件原来定义的是:
#define FW_PATCHFILE_LOCATION "/vendor/firmware/"
也就是构建系统把固件放在一个位置,运行时却去另一个位置寻找。
不要在源码里寻找这个路径,因为它是设备运行时路径。RTL8852BS 的原始固件保存在 vendor/spacemit/musepaper2/bluetooth/ 中。BUILD.gn 使用 ohos_prebuilt_etc,通过 install_images = [ chipset_base_dir ] 指定安装进 vendor 镜像,再通过 relative_install_dir = "firmware" 指定安装到 etc/firmware。因此全量构建后,文件出现在 out/musepaper2/packages/phone/vendor/etc/firmware/,烧录并挂载 vendor 分区后,设备看到的路径才是 /vendor/etc/firmware/。
最终在 vendor/spacemit/musepaper2/bluetooth/include/bt_vendor_rtk.h 修改为:

这让我再次感受到,源码里“有固件文件”完全不等于设备能加载固件。至少要同时确认:
源码文件存在
GN target 包含它
安装目录正确
设备上最终文件存在
运行时代码查找路径一致
少一个都不算闭环。
第四阶段:Realtek vendor 库到底做了什么
OpenHarmony 的 VendorInterface 会动态加载:


然后通过 dlsym 查找 Realtek vendor 库导出的 BLUETOOTH_VENDOR_LIB_INTERFACE。

在 vendor/spacemit/musepaper2/bluetooth/src/bt_vendor_rtk.c 中:

初始化时会先读取 rtkbt.conf,保存蓝牙地址并初始化 H5 相关状态。
HDI 随后调用 BT_OP_POWER_ON。
Realtek vendor 库中的处理大致是:
upio_set_bluetooth_power(UPIO_BT_POWER_OFF);
usleep(20000L);
upio_set_bluetooth_power(UPIO_BT_POWER_ON);
也就是先通过 rfkill 断电,再短暂延时,然后重新上电。
先关闭、等待 20 ms、再开启,是为了确保 RTL8852BS 的蓝牙 Controller 真正经历一次有效复位,清除上一轮固件、H5、UART 波特率和内部状态,从双方都能预期的初始状态重新建立通信。
upio_set_bluetooth_power() 会扫描 /sys/class/rfkill/rfkill*/type,找到类型为 bluetooth 的实例,再向它的 state 写入 0 或 1。
之后调用 BT_OP_HCI_CHANNEL_OPEN,Realtek vendor 库打开 /dev/ttyS2,初始化 UART/H5,并向 HDI 返回一个逻辑 HCI 通道 FD。
最后的 BT_OP_INIT 会开始真正的 Controller 初始化,包括:
读取 HCI/LMP 版本
选择匹配的 firmware 和 config
读取芯片参数
必要时修改 Controller UART 波特率
同步修改 Host UART 波特率
分片下载 firmware patch
等待每一步 Command Complete Event
通知上层初始化完成
到这里,一个很重要的要求就出现了:初始化命令发出去之前,接收 HCI Event 的通道必须已经准备好。
否则命令虽然通过 UART 发给了 Controller,Controller 的响应却没人读取,初始化还是无法完成。
第五阶段:串口已经打开,blue_host 却崩溃
完成前面的调整后,已经能够证明:
rfkill 可以拉高
blue_host 可以打开 /dev/ttyS2
Realtek vendor 库已经进入 HCI channel open
但 blue_host 仍然会在后续阶段崩溃。这时候问题的性质已经变了。
如果 /dev/ttyS2 已经成功打开,说明至少下面这些部分基本成立:
UART 驱动 probe
设备节点创建
init 权限
vendor 配置节点
基本 open 路径
因此继续修改串口核心的证据并不充分,应该沿着用户态调用栈向上看。
OpenHarmony HCI HDI 的初始化入口在 drivers/peripheral/bluetooth/hci/hdi_service/hci_interface_impl.cpp,HciInterfaceImpl::Init() 会调用 VendorInterface::GetInstance()。

接着进入 drivers/peripheral/bluetooth/hci/hdi_service/implement/vendor_interface.cpp,其中的顺序是:
dlopen vendor 库
↓
dlsym vendor interface
↓
vendor init
↓
BT_OP_POWER_ON
↓
WatchHciChannel
↓
watcher Start
↓
BT_OP_INIT
问题就藏在 WatchHciChannel() 中。
最终根因:HciWatcher 没有创建
WatchHciChannel() 打开 HCI 通道以后,会根据通道数量创建协议对象。
单通道时:

多通道时则会创建 MctProtocol,分别注册 ACL 和 Event FD。

但是当时的 VendorInterface::Initialize() 中,watcher_ 并没有被构造。
也就是说,代码实际上在做:
// watcher_ 还是 nullptr
watcher_->AddFdToWatcher(...);
watcher_ 是一个 std::shared_ptr<HciWatcher>。默认构造的 shared_ptr 为空,对它使用 -> 就是空指针解引用,结果是 blue_host 直接崩溃。
这个问题的现象很像“蓝牙串口不工作”,实际却只是一个 C++ 对象生命周期问题。
最终在 BT_OP_POWER_ON 成功后、调用 WatchHciChannel() 之前恢复:

HciWatcher 为什么这么重要
HciWatcher 位于 drivers/peripheral/bluetooth/hci/hdi_service/implement/hci_watcher.cpp,它内部使用 select() 等待多个 FD:
HCI channel fd
wakeup pipe
可选低功耗 timeout
FD 是 Linux 为进程已经打开的文件、设备、Socket 或 Pipe 分配的数字编号。程序通过这个数字进行读取、写入、监听和关闭;本项目中 blue_host 的某个 FD 指向 /dev/ttyS2,而 HciWatcher 负责监听 vendor 层返回的 HCI 通道 FD。
工作流程可以简化成:
把 HCI FD 加入 read fd_set
↓
select 阻塞等待
↓
FD 可读
↓
调用 H4Protocol/MctProtocol 的读取函数
↓
解析 HCI Event / ACL / SCO / ISO
↓
通过 HDI callback 送回 bluetooth_service
它还创建了一个 wakeup pipe。当新增 FD、删除 FD、修改 timeout 或停止线程时,向 pipe 写一个字节,就能让正在 select() 中阻塞的线程立即醒来。
蓝牙 Controller 初始化依赖大量“发送命令—等待事件”的交互。如果没有 watcher,命令可能发出去了,但是响应无法回到上层,整个初始化自然无法继续。
修复后的扫描结果
修复并部署 libhci_interface_service_1.0.z.so 后,再次开启蓝牙:
hidumper -s BluetoothHost -a "-br"
可以看到:

到这里,蓝牙才从“串口能打开”变成了“整个 HCI 链路真的工作”。
这个问题看起来很大:设置页打不开、扫描不到设备、服务反复失败。
最后的直接修复却只有一行。
这就很令人感慨,——等等,这是技术博客,不是人生哲理分享,还是算了吧。。
下一篇记录最后的修复与测试过程,也会认真复盘中间那段不太愉快的经历:因为错误地继续修改 UART/serdev 核心,设备一度卡在 SpacemiT Logo。相比成功的那一行代码,那次失败反而让我更清楚应该怎样安全地调试启动链。



