做嵌入式 Linux 的人都知道,一块开发板能不能真正用起来,往往不取决于主控本身,而取决于外设驱动和协议栈能不能打通。我最近给一个基于瑞芯微 RV1106 的本地音频播报项目做功能验证,板子上没有预留喇叭接口,最合理的方式就是把音频推给现成的蓝牙音箱,于是绕不开 AIC8800DC 这个双模蓝牙模块——从内核配置、驱动编译、固件加载,到 BlueZ 连接、A2DP 音频路由,整条链路折腾了几天,也踩了不少坑。这篇文章把整个从零到出声的过程完整复盘一遍,内容按实际踩坑顺序整理,适合手里有 RV1106 开发板、准备用 AIC8800DC 做蓝牙音频方案的开发者参考,也适合对嵌入式蓝牙协议栈不太熟、想看一下完整流程的朋友。
1. 方案选型:为什么是 RV1106 + AIC8800DC
1.1 硬件平台与模块定位
先看硬件。RV1106 是瑞芯微面向 IPC 摄像头和边缘小设备市场推的 Linux 级 SoC,单核 Cortex-A7,主频大概 1.2GHz,内置 0.5 TOPS 的 NPU,支持 5M 像素 ISP,跑轻量 Linux(Buildroot 或者 Debian)都很轻松。这款芯片的特点是成本低、启动快、外设齐全,很多廉价摄像头模组和 AI 识别盒子都在用。相比更高级的 RV1126,RV1106 的资源确实紧张,但处理蓝牙音频流这种场景是绰绰有余的。
AIC8800DC 是爱科微(AICSemi)出的 Wi-Fi + 蓝牙二合一模块。Wi-Fi 部分走 SDIO,支持 802.11 b/g/n;蓝牙部分走 UART,支持经典蓝牙和 BLE 双模。市面上不少 RV1106 开发板直接把模块焊在板上,比如带无线功能的 Luckfox Pico 系列和一些 IPC 模组,板卡 SDK 里也会集成对应驱动。我选这套组合没有犹豫,因为板子自带、SDK 有驱动、支持 A2DP,三个条件同时满足的模块在入门方案里并不多。硬件上唯一要注意的是模块供电和天线,不过那些都是后话,驱动先跑通再说。
1.2 音频链路的关键:先搞清楚 profile 再动手
开始之前必须先纠正一个容易搞混的概念:蓝牙音频不是“连上蓝牙就有声音”,它取决于双方协商的 profile。板子做音源、蓝牙音箱做接收端,走的是 A2DP(Advanced Audio Distribution Profile),A2DP 里板子担任 source 角色,把音频以流媒体形式推给音箱解码播放,编码一般用 SBC,也支持 AAC、aptX 这类扩展。A2DP 是单向的,只负责音频流,不负责通话拾音。如果要双向音频,比如语音对讲,那要走 HFP 或者 HSP 免提协议,采样率会被限制到 8k 或 16k,音质和 A2DP 完全不是一个级别。
这里顺便把网上的高频问题说清楚。很多人搜“蓝牙模块控制继电器”,用的是 HC05、HM10 这类 SPP/BLE 串口透传模块,它们只处理串口数据和 BLE GATT 服务,压根没有 A2DP 协议栈,所以指望 HC05 把音乐推给蓝牙音箱,硬件上就不成立。HC05 上折腾音频失败,不是代码问题,是选型问题。AIC8800DC 这类双模模块也支持 SPP、BLE GATT 等 profile,可以用来控制继电器,但默认配置未必全部打开,需要按用途在 BlueZ 里逐个配置。所以拿到模块第一步,先看 datasheet 里的 profile 列表,再决定整个方案的走向。
2. 驱动编译前的内核与设备树准备
2.1 SDK 与交叉编译环境
RV1106 官方 SDK 是基于 Buildroot 的,包含 u-boot、kernel、rootfs,直接解压后就能编译完整固件。如果你用的是 Luckfox Pico 这类第三方板卡,就用板卡官方维护的 SDK,里面一般已经把 AIC8800DC 的驱动源码集成进 kernel 了。手头没有 SDK,也可以单独从 AICSemi 的仓库拿 aic8800 Linux 驱动源码,配合目标内核的 headers 做模块编译。
编译环境和普通内核模块没有区别,在 x86 机器上装好交叉编译工具链,然后设置环境变量:
export ARCH=arm export CROSS_COMPILE=arm-rockchip830-linux-uclibcgnueabihf- export KERNELDIR=/path/to/rv1106_sdk/kernel工具链具体前缀要看 SDK 里的 buildroot 配置,不同版本可能不一样。我个人建议别手动设一堆变量,直接 source SDK 的 buildroot 环境脚本,或者用 SDK 自带的 rebuild.sh 编译整包固件,先把默认环境跑通,再考虑单独编模块。第一次可以先整体编译一遍,确认能正常启动,避免后面把所有问题堆到一起才排查。真要用模块方式单独编,确认 KERNELDIR 指向 SDK 里那份已经 make 过的内核源码,否则会因为缺少 Module.symvers 报出一堆 undefined symbol 的错误。
2.2 内核蓝牙协议栈配置
内核里蓝牙协议栈相关选项没打开,后面就算模块 insmod 了,BlueZ 也无法正常工作。RV1106 的 kernel 配置一般在 SDK 的 kernel/arch/arm/configs/ 目录下,以 rv1106_linux_defconfig 为例,至少要保证这些配置项:
CONFIG_BT=y CONFIG_BT_RFCOMM=y CONFIG_BT_BNEP=y CONFIG_BT_HIDP=y CONFIG_BT_HCIUART=y CONFIG_BT_HCIUART_H4=y CONFIG_BT_HCIUART_3WIRE=y CONFIG_BT_RTK=y CONFIG_BT_BCM=y新内核可能还需要再开 CONFIG_BT_HCIBTUSB 和 CONFIG_BT_HCIBTSDIO,不过 AIC8800DC 是 UART 接口,最核心的就是 CONFIG_BT_HCIUART。它对应的是 BlueZ 的 hci_uart 驱动,负责在 UART 上跑标准 HCI 协议,H4 是这个协议在四线 UART 上的标准封装,几乎所有 UART 蓝牙芯片都依赖它。
用 menuconfig 修改也很方便:
cd kernel make rv1106_linux_defconfig make menuconfig进去以后按 Networking support -> Bluetooth subsystem 找,逐个勾上,保存退出。这里有个细节:BT_RTK 和 BT_BCM 其实是给瑞昱、博通芯片用的,开不开不影响 AIC 模块,但很多板卡 SDK 默认会开一堆厂商驱动。我建议先用 SDK 默认配置,能跑通再裁剪,别一上来追求最小化配置,给自己增加变量。
2.3 设备树里的 UART 节点
设备树里最容易被忽略的就是 UART 节点的引脚复用和流控配置。AIC8800DC 的蓝牙挂在哪个 UART 上,具体要看板子原理图,常见的是 uart0 或者 uart3,我这边用的是 uart3,对应系统里的 /dev/ttyS3。dts 里大致是这样:
&uart3 { pinctrl-0 = <&uart3m0_xfer &uart3m0_ctsn &uart3m0_rtsn>; pinctrl-names = "default"; uart-has-rtscts; status = "okay"; };两个关键点:status 必须是 "okay",uart-has-rtscts 必须保留。蓝牙 UART 一般带硬件流控,CTS/RTS 两个引脚如果没配好,高速通信时就会出现随机丢包,表现为 hciattach 超时、HCI command timeout、蓝牙频繁断连,这类问题最难排查,因为你很难从日志里一眼定位是软件还是硬件问题。流控的作用可以理解成两个人在一条窄路上搬运物资,收发双方通过 CTS/RTS 随时报“准备好了”或“慢点来”,没有这层握手,数据一多就互相踩脚。
这里要特别留个心眼:很多 RV1106 板卡的调试串口和蓝牙 UART 会复用同一组引脚,如果调试串口同时也开了,两个功能就会打架。我调第一块板子时就遇到过这样的情况,一开始以为是驱动编译错了,后来用 GPIO 工具逐个核查引脚才发现两个节点冲突,把调试串口的 status 改成 disabled 才恢复正常。所以拿到新板子的第一步,不是急着编译,而是对着原理图把引脚功能表列出来,确认哪组引脚归调试串口、哪组归蓝牙,这个步骤能省掉后面一整天的排查时间。
3. AIC8800DC 驱动编译与固件部署
3.1 驱动源码结构与编译流程
驱动源码在 SDK 里通常在 kernel/drivers/net/wireless/aic8800/ 或者类似目录。如果 SDK 里没有,去爱科微的仓库把 aic8800 Linux 驱动整包拉下来,解压后能看到 WiFi 和 BT 两部分,分工很明确:
- WiFi 部分编译为 aic8800_fdrv 内核模块,负责 SDIO 通信和 Wi-Fi 协议栈;
- BT 部分编译为 aic8800_bt 内核模块,负责蓝牙固件下载和 HCI 通道创建。
BT 驱动本身是一个平台驱动,它读取固件文件、配置蓝牙相关引脚,通过 UART 与蓝牙芯片通信。编译过程比较简单:
cd driver_fdrv make cd ../bt_driver make编译后得到 .ko 文件,按顺序加载:
insmod aic8800_fdrv.ko insmod aic8800_bt.ko顺序我建议先 WiFi 再 BT,这是因为 BT 固件下载时可能依赖 WiFi 部分已经建立的时钟和电源参考。有些 SDK 在这两个模块里带了内部参数,比如 fw_path、bt_uart,加载前先用 modinfo 看一遍默认值,别直接 insmod 就完事。重点检查 fw_path 是否指向你固件实际存放的路径,我见过有人把固件放在 /etc/firmware,驱动默认找 /lib/firmware,结果固件下载日志一直报文件找不到,折腾半天。
3.2 固件部署与 hci0 检查
固件文件一般在驱动源码的 firmware 目录下,名字大概是 aic8800dc_btfw.bin、aic8800dc_wififw.bin 这类。把整个 firmware 文件夹拷贝到板子的 /lib/firmware/ 下,确保权限可读。如果用的是 SDK 整包编译,固件通常会被打包进 rootfs,不用手动处理;如果是单独烧录的 rootfs 或者自己拼的系统,这一步别漏。
加载成功后用 dmesg 确认日志,能看到类似 "AIC8800_BT: firmware download done" 的信息,说明 BT 固件已经跑起来了。此时检查 hci0:
hciconfig -a如果 hci0 没出现,可以手动执行 hciattach 把 UART 挂到 BlueZ 协议栈上:
hciattach /dev/ttyS3 any 1500000 flowany 表示让控制器自己上报类型,波特率按模块 datasheet 里的默认值填,常见的是 1500000 或者 3000000。注意 hciattach 是 BlueZ 工具包里的命令,rootfs 里得有 bluez-utils,在 Buildroot 里选上即可。这里有个容易迷糊的点:AIC8800DC 不同版本的驱动行为不完全一样,有的 insmod 后自动注册 hci0,有的需要手动 hciattach,网上搜到的教程两种都有,关键还是看自己设备的 dmesg 输出,别被教程带偏。
3.3 驱动开机自动加载
调试阶段手动 insmod 可以,产品化之后还是要做成开机自启。两个办法:一是把模块写进 rootfs 的 /etc/modules 文件,配合 /etc/modprobe.d/ 下的配置文件,系统启动时会自动加载;二是写在 SDK 的启动脚本里,比如 rv1106 SDK 的 Sxxmodules 脚本。我建议用启动脚本的方式,因为可以在里面加等待逻辑,确保 WiFi 模块先加载完成,延迟一两秒再加载 BT,避免时序问题。
关键在于别让 BlueZ 服务在模块加载之前启动。RV1106 boot 很快,如果 bluetooth.service 开机就拉起,而驱动模块还没 insmod 完,BlueZ 就可能错过控制器注册,表现为服务起来了但 hci0 始终不出现,必须手动 restart 一次蓝牙服务才正常。这种问题看起来像驱动挂了,实际是服务编排顺序不对。在产品化脚本里,最稳妥的做法是把蓝牙服务的启动依赖改成“检测到 hci0 存在”之后,或者在应用进程里做轮询等待。
4. BlueZ 连接与 A2DP 音频播放
4.1 蓝牙设备连接完整流程
驱动层通了,剩下的就是 BlueZ。先用 bluetoothctl 进入交互命令行:
bluetoothctl power on agent on default-agent scan on扫描到目标音箱后执行配对连接:
pair XX:XX:XX:XX:XX:XX trust XX:XX:XX:XX:XX:XX connect XX:XX:XX:XX:XX:XX蓝牙音箱一般不需要输入 PIN,agent on 之后配对会自动走。连接成功后 bluetoothctl 里会显示 "Connection successful",同时音箱会响提示音。如果卡在 AuthenticationFailed 或者连接后被立刻断开,先别急着怀疑模块,检查这几项:BlueZ 版本是否过老、agent 是否已经设置、设备是否处于可发现模式。我测试时用 5.55 以上版本的 BlueZ,AIC 模块的兼容性很正常;旧版 BlueZ 4.x 则容易出现配对成功马上断开的问题,能升级就升级。
还要留意一个操作习惯:配对前先把 hci0 启起来。有时候刚开机 hci0 处于 down 状态,bluetoothctl 里 power on 能拉起来,但如果你的 rootfs 里 rfkill 默认 lock 了蓝牙,power on 也会失败。先跑一下 rfkill list 看状态,如果有 soft blocked,执行 rfkill unblock bluetooth,再 power on,这个顺序很多教程不会专门提。
4.2 音频后端:BlueALSA 和 PulseAudio 怎么选
蓝牙设备成功建立 A2DP 连接后,系统音频栈还需要认识这台“蓝牙声卡”。这里有两条路:BlueALSA 和 PulseAudio。PulseAudio 在桌面系统里体验更好,自动发现设备、自动切换 sink,操作习惯和 Ubuntu 桌面一致;缺点是依赖多、体积大,RV1106 这种小内存板子跑起来有点吃力。BlueALSA 则直接把蓝牙设备模拟成一个 ALSA 设备,应用层可以用原生 aplay 播放,体量小、逻辑清晰,更适合嵌入式。
用 BlueALSA 启动很简单:
bluealsa -p a2dp-sink &连接蓝牙设备后,用 aplay 指定 bluealsa 设备播放:
aplay -D bluealsa:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp /usr/share/sounds/alsa/Front_Center.wav如果 rootfs 里用的是 PulseAudio,流程则是:
pactl list sinks pactl set-default-sink bluez_sink.XX_XX_XX_XX_XX_XX.a2dp_sink paplay /usr/share/sounds/alsa/Front_Center.wav我个人的建议:纯音频播报、播放 WAV/MP3 这种场景,BlueALSA 就够了,资源占用小、问题好定位;如果要做多路混音、精确音量控制、流媒体切换,再上 PulseAudio。RV1106 这种级别的板子,BlueALSA + aplay 的组合能覆盖 90% 的需求。还有人在问 PipeWire,RV1106 上基本用不上 PipeWire,资源不够,性能优势也发挥不出来,不建议引入。
4.3 实际播放测试与 SoundPool、移动端音频的顺带说明
播放测试先放系统自带的 PCM WAV,格式用 44.1kHz/16bit 立体声,这是最稳的基准。我当时放的是一段 Front_Center.wav,出声的一瞬间感觉整条链路终于闭环了。实测下来 RV1106 + AIC8800DC 播放这类音频非常稳,CPU 占用很低,因为 SBC 编码是模块固件内部的 DSP 处理的,主控只负责 PCM 数据搬运。如果播放过程中有卡顿,优先怀疑供电和 UART 流控,别急着查编码参数。
如果你之前在 Android 开发里用过 SoundPool,思路其实类似——都是把一段 PCM 音频交给系统的音频栈播放。但 Android 已经把蓝牙 A2DP 封装好了,应用层不需要关心链路;嵌入式 Linux 不一样,ALSA/Pulse 路由和蓝牙 sink 需要你手动接上,这也是很多应用工程师第一次碰嵌入式蓝牙会觉得别扭的原因。顺带提一句,移动端浏览器播放不了某些代码生成的音频,绝大多数是编码格式不支持,比如裸 PCM 或者特殊采样率不被浏览器接纳,不是蓝牙链路的问题;嵌入式端也一样,测蓝牙音频先用标准 WAV 打底,通了一切都好说。
5. 避坑指南:常见问题与排查实录
5.1 常见问题速查表
把这次折腾过程中遇到的典型问题整理成一张速查表,方便快速定位:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| insmod 后 dmesg 无 BT 固件日志 | 固件路径不对或驱动参数没设置 | 检查 fw_path 参数,固件拷到 /lib/firmware |
| 没有 hci0 出现 | BT 驱动未加载成功 / UART 未使能 | 确认 aic8800_bt 已加载,检查 /dev/ttyS* 是否存在 |
| hciattach 报操作超时 | UART 引脚冲突 / 波特率不对 | 检查 dts 引脚复用,按 datasheet 调整波特率 |
| bluetoothctl 扫描不到设备 | 天线问题 / BT 没有 power on | rfkill list、hciconfig hci0 up、bluetoothctl power on |
| 配对成功但马上断开 | BlueZ 版本太老 / agent 未设置 | 升级 BlueZ,配对前先 agent on |
| 连接成功但没声音 | profile 停在 HFP | pactl set-card-profile 切到 a2dp_sink |
| 播放开始后卡顿 | 模块供电不足 / UART 流控缺失 | 检查供电电流,dts 补上 uart-has-rtscts |
这张表能覆盖大多数启动和连接期的问题。要注意的是,每个板卡情况可能有差异,尤其引脚冲突这种问题,跟具体 PCB 布线强相关,遇到类似现象一定先回到原理图去查硬件,而不是反复重编驱动。
5.2 几个记忆深刻的坑
第一个坑就是 UART 引脚复用冲突。我一开始用的板卡调试串口默认输出内核日志,而这个引脚恰好和蓝牙 UART 的 RX/TX 共用。结果 hciattach 一直报 "Device can't open or not in configured mode",排查了整整一个下午,最后把 dts 里两个节点的 status 互换,蓝牙才正常工作。这里要提醒的是,RV1106 的引脚功能非常灵活,同一个物理引脚可以被多个 controller 复用,编译器根本不会给你报错,只能自己对着原理图逐个核对。
第二个坑是蓝牙服务启动时序。我把 BlueZ 加到了开机自启里,第一次启动后 hci0 就是不出现,手动 systemctl restart bluetooth 才正常。后来看了启动日志才发现,bluetooth.service 启动比模块插入早了大概两秒,BlueZ 错过了控制器注册。解决方案很简单,在应用启动脚本里循环等待 /sys/class/bluetooth/hci0 出现,超时再拉起蓝牙服务。
第三个坑是供电不足。AIC8800DC 在蓝牙 TX 瞬间电流尖峰不小,RV1106 开发板如果用的是电脑 USB 口供电,电流纹波大会导致蓝牙射频灵敏度下降,表现就是距离稍远就断连。我调试时最开始用电脑 USB 口供电,蓝牙距离不到 1 米就不稳定,换成独立电源后问题消失。这个坑在办公室环境下很容易被忽略,因为 Wi-Fi 和蓝牙同时工作时电流尖峰更明显。
5.3 一些容易忽略的细节
第一,音量控制不要直接操作本地 codec。蓝牙 A2DP 音量的正确方式是走 BlueZ 的 volume control,也就是 PulseAudio 或 BlueALSA 暴露出来的音量接口。直接 amixer 操作开发板本地声卡的混音器,对蓝牙音箱音量无效,很多人在这里反复试都调不了音量,方向就错了。
第二,固件版本和驱动版本要配套。我测试时一开始用了 SDK 自带的旧固件,蓝牙连上后偶尔出现 AVDTP 协商失败,换新版本固件后稳定。市售模组优先用模组厂商提供的固件,其次才是 SDK 里的通用固件,版本不配套会出现各种“偶发”问题,特别难排查。
第三,如果要在板子上用 Qt 写蓝牙控制界面,RV1106 的 Qt 交叉编译链在 SDK 里可以直接选,QtBluetooth 插件依赖 BlueZ 的 D-Bus 接口,前提是 rootfs 里 bluez 服务正常。也就是说,先把 hci0 稳定了,上层 Qt 只是调用封装接口的问题,不要一开始就在 Qt 层找蓝牙 bug,那会绕远路。
第四,HFP 和 A2DP 的 profile 切换。很多蓝牙音箱连上后默认进入 HFP 免提模式,系统把它当成了通话设备。PulseAudio 里表现为没有 bluez_sink,只有一个 headset_head_unit 的 card,这时候必须手动切 profile:
pactl set-card-profile bluez_card.XX_XX_XX_XX_XX_XX a2dp_sink切换后 sink 才出现,音质也恢复正常。这个坑几乎每个人都会遇到,属于“蓝牙连上了但没声音”的高频原因。
6. 一点个人体会
整个项目做下来,我最深的体会是:RV1106 这套平台的外设驱动已经比较成熟,真正耗时间的不是编译驱动,而是理解 Linux 蓝牙音频协议的层次关系。驱动编译用掉的时间可能只占三成,剩下七成都花在 BlueZ、ALSA、Pulse/BlueALSA 之间的对接上。把 profile 和音频路由先想清楚,再动手写应用,会省很多事。
这条链路打通之后,后续扩展也比较自由。可以在 bluealsa 的应用脚本里加掉线重连逻辑,把蓝牙音箱变成产品里的远程播报终端;也可以把本机 ALSA 输入和蓝牙 A2DP 输出做混音,实现本地音频和远程音频的同时播放。建议下一步把 bluealsa 的启动做成标准 systemd 服务,配上自动重连脚本,才算真正走到产品化那一步。