Android 14 + Linux 6.1 平台 RTL8922AE 蓝牙适配实战:从无法启动到成功拉起
一、问题现象
平台升级到 Android 14 和 Linux 6.1 之后,Realtek RTL8922AE 蓝牙侧已经能够被系统枚举到,但蓝牙服务始终无法稳定启动。早期现象主要有两类:
- 蓝牙服务反复崩溃,系统蓝牙状态始终停留在
OFF。 - 即使内核侧已经识别到 USB 蓝牙设备,用户态仍然无法完成固件匹配和后续初始化。
典型日志大致会经历两个阶段。
第一阶段:
hw_usb_config_start, transtype = 0x20, pid = 0x0000, vid = 0x0000第二阶段:
hw_usb_config_start, transtype = 0x20, pid = 0xd922, vid = 0x0bda rtk_usb_get_fw_table_entry: No fw table entry found这两个阶段非常关键,因为它们分别对应两层不同的问题。
二、先不要急着怀疑固件或 SELinux
做 Android 蓝牙移植时,很多人看到权限相关日志,第一反应是 SELinux 策略有问题;看到固件相关日志,又会怀疑/vendor/firmware下的文件缺失。
但这次问题说明,排障不能只看“最像”的那条报错,而要看控制路径是否闭环。
实际排查结果是:
- 设备已经处于
Permissive,SELinux 并不是当前决定性阻塞点。 - 固件文件本身存在,
rtl8922au_fw和rtl8922au_config都在标准 firmware 目录下。 - 真正的问题在于 Realtek 私有栈的用户态和内核态协议不匹配,以及用户态库内部设备表项不完整。
这类问题如果定位方向一开始跑偏,后面会浪费很多时间在无效修补上。
三、第一层根因:内核与用户态的 ioctl ABI 漂移
1. 为什么会出现pid = 0x0000, vid = 0x0000
Realtek 这套蓝牙方案并不是单纯走标准 HCI 通路,而是通过/dev/rtkbt_dev这个字符设备,配合用户态libbt-vendor-realtek.so做私有 ioctl 通信。
用户态会通过 vendor 库向内核发一组固定编号的 ioctl,请求诸如:
- 固件下载
- USB 信息读取
- ISO 参数设置
- 固件下载完成通知
问题在于,Linux 6.1 树中的rtk_btusb已经和当前 Android 14 设备里使用的用户态 vendor 库发生了 ABI 漂移:
- ioctl magic 和编号不一致。
GET_USB_INFO这类请求在参数传递语义上和用户态预期不一致。- 内核里缺少用户态仍在使用的若干 ioctl 分支。
结果就是:用户态虽然调用成功返回,但拿到的 USB 设备信息是错误的,于是前端日志长期表现为:
pid = 0x0000, vid = 0x00002. 内核侧如何修复
修复方向不是简单“改几个宏”,而是要恢复成和当前 vendor 库一致的协议行为。
核心修复点包括:
- 把
rtk_btusb.h中的 ioctl 号恢复成与现网 Realtek 用户态库兼容的定义。 - 补回
DWFW_CMPLT和SET_ISO_MIN_HANDLE这类用户态仍会使用的命令。 - 把
GET_USB_INFO改成通过put_user()向用户态回写结果,而不是按错误的本地语义返回。 - 把
SET_ISO_CFG改成通过get_user()获取用户态传入参数。 - 保证
btchr_ioctl()中的控制路径和 donor 平台的行为一致。
这一步修完之后,日志立刻发生了非常有价值的变化:
hw_usb_config_start, transtype = 0x20, pid = 0xd922, vid = 0x0bda这说明第一层问题已经解决,用户态终于拿到了真实设备 ID。
四、第二层根因:vendor 库不认识0x0BDA:0xD922
内核 ABI 修复后,蓝牙依旧没有立刻启动成功,而是进入了下一阶段错误:
rtk_usb_get_fw_table_entry: No fw table entry found这条日志的含义非常明确:用户态已经拿到了正确的 VID/PID,但它在自己的固件匹配表中找不到0x0BDA:0xD922这一项。
进一步分析可以发现,当前 Realtek vendor 库内部只包含类似下面这样的映射关系:
0x0BDA:0x892A -> 0x8922 -> rtl8922au_fw / rtl8922au_config而我们的设备实际暴露出来的是:
0x0BDA:0xD922也就是说,芯片家族本身是对的,但用户态库的 USB 设备别名表不完整。
五、最后真正让蓝牙启动的关键一步
这一轮 bring-up 里,最后真正把蓝牙从“仍然失败”推进到“成功进入 ON”的关键步骤,就是修补了设备上的libbt-vendor-realtek.so。
修补方式并不复杂,本质上是把库中原有的 USB 设备表项:
0x0BDA:0x892A -> 0x8922改成:
0x0BDA:0xD922 -> 0x8922对应到二进制字节层面,就是把:
da 0b 2a 89 22 89替换为:
da 0b 22 d9 22 89这里前两个字节是 VID,中间两个字节是 PID,最后两个字节是芯片 ID。
需要同时修补 64 位和 32 位两个库:
/vendor/lib64/libbt-vendor-realtek.so/vendor/lib/libbt-vendor-realtek.so
修补完成并重启蓝牙 HAL 后,之前的No fw table entry found不再出现,蓝牙初始化开始继续向后执行。
六、成功后的关键日志长什么样
这一步很重要,因为很多时候“日志没报错了”并不等于“蓝牙真的起来了”。
真正成功时,至少要看到以下几类日志:
hw_usb_config_start, transtype = 0x20, pid = 0xd922, vid = 0x0bda BT config file: /vendor/firmware/rtl8922au_config BT fw file: /vendor/firmware/rtl8922au_fw OnFirmwareConfigured result: 0 Firmware configured in ... HCI HAL initialization completed同时,系统状态应当能进入:
enabled: true state: ON address: XX:XX:XX:XX:XX:XX这说明固件配置完成、HCI 初始化完成、蓝牙地址已经拿到,蓝牙已经不再是“服务没挂但功能不可用”的假成功状态。
七、重新刷机后,如何快速恢复蓝牙
这一部分是很多人最关心的。
情况 1:只重刷 boot.img
如果只是重刷 boot.img,而 vendor 分区没有被覆盖,那么通常只要你保留了内核侧的 Realtek 修复,蓝牙仍然会工作。
情况 2:vendor 分区也被恢复成原始版本
如果 vendor 库被覆盖回原版,那么即使 boot.img 里的内核修复仍然在,蓝牙也会再次卡在:
rtk_usb_get_fw_table_entry: No fw table entry found这时就需要重新做一遍 vendor 库补丁。一个通用的 adb 恢复流程如下。
1. 获取 root 并 remount
adb root adb remount2. 备份设备上的原始库
adb shell'cp -f /vendor/lib64/libbt-vendor-realtek.so /data/local/tmp/libbt-vendor-realtek.so.bak'adb shell'cp -f /vendor/lib/libbt-vendor-realtek.so /data/local/tmp/libbt-vendor-realtek-32.so.bak'3. 拉取两个库到主机
adb pull /vendor/lib64/libbt-vendor-realtek.so /tmp/libbt-vendor-realtek.so adb pull /vendor/lib/libbt-vendor-realtek.so /tmp/libbt-vendor-realtek-32.so4. 本地执行二进制替换
python3 -<<'PY' from pathlib import Path targets = [ Path('/tmp/libbt-vendor-realtek.so'), Path('/tmp/libbt-vendor-realtek-32.so'), ] old = b'\xda\x0b\x2a\x89\x22\x89' new = b'\xda\x0b\x22\xd9\x22\x89' for path in targets: data = path.read_bytes() count = data.count(old) if count != 1: raise SystemExit(f'{path}: expected exactly 1 match, got {count}') path.write_bytes(data.replace(old, new, 1)) print(f'patched: {path}') PY5. 推回设备并恢复权限
adb push /tmp/libbt-vendor-realtek.so /vendor/lib64/libbt-vendor-realtek.so adb push /tmp/libbt-vendor-realtek-32.so /vendor/lib/libbt-vendor-realtek.so adb shell'chmod 644 /vendor/lib64/libbt-vendor-realtek.so /vendor/lib/libbt-vendor-realtek.so'adb shell'restorecon /vendor/lib64/libbt-vendor-realtek.so /vendor/lib/libbt-vendor-realtek.so'6. 重启蓝牙服务并验证
adb shell'logcat -b all -c'adb shell'setprop ctl.restart vendor.bluetooth-1-0'adb shell'cmd bluetooth_manager enable'adb shell'cmd bluetooth_manager wait-for-state:STATE_ON || true'adb shell'dumpsys bluetooth_manager | sed -n "1,40p"'如果输出中已经出现enabled: true、state: ON和有效蓝牙地址,说明恢复成功。
八、这个案例最值得记住的三点经验
1. Realtek 私有协议栈要优先检查 ABI 对齐
对这类厂商私有蓝牙栈来说,驱动编译通过、设备枚举正常,并不代表内核和用户态真的能协同工作。只要 ioctl 号、参数语义或命令集合不一致,蓝牙依然会在很早期就失败。
2.pid/vid日志是非常高价值的分水岭
如果看到pid = 0x0000, vid = 0x0000,优先检查内核和用户态 ABI。
如果看到真实pid/vid但又报No fw table entry found,说明问题已经推进到用户态固件映射表。
3. 不要把“没有报错”误判成“已经成功”
真正成功的标准,不是日志安静下来,而是:
- 固件配置完成。
- HCI HAL 初始化完成。
dumpsys bluetooth_manager显示state: ON。- 蓝牙地址可用。
只有这四项连起来,才算蓝牙真的被拉起。
九、总结
这次 RTL8922AE 蓝牙适配的最终结论可以概括为一句话:
要让蓝牙真正启动,既要修正 Linux 6.1 内核rtk_btusb与 Android 14 用户态 Realtek vendor 库之间的 ioctl ABI 漂移,也要让libbt-vendor-realtek.so正确认识 USB 设备0x0BDA:0xD922。
前者解决的是“用户态能不能拿到真实设备信息”,后者解决的是“拿到真实设备信息之后,能不能找到正确的固件配置路径”。两者缺一不可。
如果后续要做长期稳定的产品化方案,建议把D922表项正式固化进 vendor 库源码构建流程,而不是依赖每次刷机后手工做 adb 二进制补丁;但在源码不完整、需要快速验证的场景下,这套方法已经足够有效。
