做RV1106这种小板子,最烦的不是算力不够,而是WiFi和蓝牙这种“外设”没人帮你配好。我这次要处理的是一块AIC8800DC二合一模组,一边是WiFi6,一边是蓝牙5.0,RV1106的Buildroot SDK里其实已经带了驱动源码,但真要把它从“能加载”变成“能连手机、能放歌”,中间至少隔着一层固件、一层设备树、一层蓝牙协议栈。这篇文章就是我个人完整走完一遍的记录,内容包括驱动移植、HCI初始化、bluetoothctl配对、BlueALSA播放音频,以及调试A2DP和HFP音频时踩过的那些坑。手头有RV1106开发板、想把AIC8800DC蓝牙用起来做音频播放的朋友,可以照着这份流程走,能省下不少时间。
1. 整体设计思路:先摸清RV1106和AIC8800DC的硬件关系
1.1 RV1106为什么适合玩AIC8800DC蓝牙
RV1106是瑞芯微面向IPC、智能摄像头这类场景推出的视觉处理芯片,单核Cortex-A7跑到1.2GHz,内置了256MB DDR和0.5TOPS NPU,所以很多做边缘视觉产品的朋友手头都有这块板子。但它的定位决定了它不会像手机SoC那样把WiFi、蓝牙都集成进去,你需要外挂一颗Combo芯片,然后自己在Linux下把驱动、固件、协议栈串起来。
AIC8800DC就是一颗主打WiFi6 + 蓝牙5.0的Combo模组,WiFi部分走SDIO接口,蓝牙部分走UART接口,音频走PCM/I2S接口。在同类芯片里,它的优势是瑞芯微的SDK里通常已经放了一版驱动源码,而且爱联官方对RV1106这类平台的适配还算积极,固件也能从SDK的vendor目录里直接拿到。相比自己从零移植一个蓝牙协议栈,这个起点已经好很多了。
不过“驱动源码有”和“能出声”是两码事。RV1106的SDK虽然带了一版AIC8800驱动,但你在实际板子上要做的事情还不少:先确认驱动版本和固件文件对得上,再配置设备树指定蓝牙UART和音频PCM引脚,然后启动BlueZ协议栈,最后还要把音频路由配置明白。整个过程涉及Linux驱动开发、蓝牙协议栈、ALSA音频三块知识,缺一样都容易卡住。
1.2 引脚接线:SDIO、UART、PCM一个都不能漏
先看硬件连接。AIC8800DC不是一颗纯蓝牙芯片,它内部同时有WiFi和蓝牙两条子系统,硬件引脚必须分开接好。以我手头这块模组为例,WiFi走SDIO接口,需要接CLK、CMD、D0-D3;蓝牙HCI走UART接口,至少要TXD、RXD、CTS、RTS四根线;音频走PCM/I2S接口,要接CLK、FS、TX、RX四根线。
我整理了一张表,方便对照:
| 功能模块 | AIC8800DC引脚 | RV1106端 | 说明 |
|---|---|---|---|
| WiFi | SDIO_CLK / SDIO_CMD / SDIO_D0-D3 | SDIO1或SDIO2 | 用于WiFi6数据通信 |
| 蓝牙HCI | BT_UART_TX / BT_UART_RX | UART3或UART4 | 用于蓝牙控制与数据 |
| 蓝牙流控 | BT_UART_CTS / BT_UART_RTS | 对应UART的CTS/RTS | 必须接,硬件流控很重要 |
| 蓝牙音频 | BT_PCM_CLK / BT_PCM_FS / BT_PCM_TX / BT_PCM_RX | I2S0或I2S1 | 用于HFP/SCO语音和部分A2DP音频路由 |
| 电源 | VDD_3V3 / VDD_1V8 | 电源轨 | 模组峰值电流不低,要留足余量 |
| 射频 | RF_ANT / GND | 天线区 | 天线下方净空,不要铺铜 |
这里最容易被忽略的是UART的RTS/CTS两根线。很多人图省事只接TXD/RXD,觉得蓝牙控制通道“能通就行”,实际上AIC8800DC的HCI通信数据量大,一旦跑A2DP音频,ACL数据包会很多,没有硬件流控时数据会丢包,表现就是连上就断、声音断断续续、设备列表刷新异常。我建议从一开始就把四线UART接全,不要省。
音频PCM引脚也要提前接好,尤其是你打算做HFP通话或者把音频桥接到板载Codec的时候。RV1106这边的I2S接口和AIC8800DC的PCM接口命名可能不完全一样,但本质是四线同步串口,CLK对应位时钟,FS对应帧同步,TX/RX对应数据线。我在后面讲HFP配置时还会再提一次。
1.3 音频链路规划:A2DP和HFP走的是两条路
很多第一次接触蓝牙音频的朋友会以为“蓝牙芯片就是音频芯片”,实际上AIC8800DC内部虽然集成了蓝牙射频,但音频数据怎么走,取决于你是用A2DP还是HFP/SCO。A2DP用于听音乐,数据量比较大;HFP用于通话,语音数据量小但实时性要求高。
A2DP这条路径,通俗说就是:主控这边的BlueZ协议栈先把PCM音频用SBC或AAC编码,变成一个个数据包,然后通过HCI UART发给AIC8800DC,由它的蓝牙射频把这些包发出去。耳机收到数据后自己解码头、播放声音。所以在这种场景下,PCM/I2S接口并不一定参与音频传输,关键瓶颈反而是UART的波特率。我实测下来,115200波特率跑A2DP几乎不可能流畅,后面会专门说。
HFP/SCO这条路径则不同。通话时,语音数据由蓝牙HCI层直接走PCM/I2S通道,AIC8800DC把从射频收到的语音包转成PCM信号,输出给主控的Codec,由Codec驱动扬声器;麦克风采集的语音也通过Codec进PCM接口,再由AIC8800DC的射频发出去。这时PCM/I2S的引脚、时钟极性、帧同步配置就直接影响通话质量。
明白这两条路,就不会在调试时想当然:A2DP没声音去改PCM配置肯定没用,HFP没声音光调ALSA也没用。先把“当前用的哪个Profile、数据走哪条通道”想清楚,再动手,能少走很多弯路。
2. 内核驱动与固件部署:先让系统认识AIC8800DC
2.1 检查SDK里的驱动代码:从哪拿、放哪
RV1106的Buildroot SDK一般会在kernel/drivers/net/wireless/下面放vendor Wi-Fi驱动,AIC8800DC的驱动目录通常叫aic8800_fdrv或者类似名字。我建议拿到SDK后先搜一下:
find . -iname "*aic8800*" 2>/dev/null如果你能找到目录,说明驱动源码已经集成了,省去网上找源码的麻烦。如果找不到,那就要确认你手上的SDK版本是不是太旧,或者需要找芯片原厂/开发板厂商要一版支持RV1106的驱动补丁。
这里有个小经验:AIC8800DC的驱动往往会分成WiFi部分和蓝牙部分。WiFi部分本质是一个网络设备驱动,负责SDIO传输、802.11协议帧处理;蓝牙部分则通过一个platform driver或者辅助模块,把UART上的HCI通道注册给内核的蓝牙子系统。两者经常是同一个驱动包编译出来的两个模块,所以排查问题时,最好在dmesg里同时看到WiFi和蓝牙两段初始化日志。
拿到驱动源码后,我的习惯是先编译一遍,不直接改任何代码。这样可以先确认SDK的编译链路是通的,后续调设备树或内核配置时,如果编译报错,也知道问题出在哪里。
2.2 内核配置与设备树:关键选项和dts示例
内核配置这一步,主要是把蓝牙子系统和AIC8800驱动打开。蓝牙协议栈依赖的选项至少包括:
- CONFIG_BT:蓝牙核心支持
- CONFIG_BT_HCIUART:让UART能注册成HCI设备
- CONFIG_BT_RFCOMM:串口仿真,部分场景会用到
- CONFIG_BT_BNEP:蓝牙网络共享,如果不需要可以不开
AIC8800的驱动选项一般在menuconfig的Device Drivers -> Network device support -> Wireless LAN下面,也可能在Vendor目录里,具体名字带AIC8800字样。编译内核或模块时,确认这些选项不是m而是y,或者你明确知道后续会insmod对应模块。
设备树部分更关键。要让蓝牙HCI跑在UART上,需要在dts里使能对应的UART节点,并配置正确的pinctrl。假设你的蓝牙接在UART3,那么大概是这样:
&uart3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart3_xfer &uart3_cts &uart3_rts>; dma-names = "tx", "rx"; dmas = <&dmac 0>, <&dmac 1>; };这里注意两点:一是pinctrl里一定要包含CTS/RTS这两个引脚的复用配置,只配了TXD/RXD,硬件流控就没法工作;二是如果这个UART在SDK里被默认当成了调试串口,需要先在uboot或kernel cmdline里把console对应关系改掉,不然HCI初始化时会被调试日志干扰。
如果你的方案要把HFP通话音频接到板载Codec,还需要使能对应的I2S/PCM节点。AIC8800DC的PCM接口可以工作在PCM或者I2S模式,设备树里要保证CLK、FS、TX、RX的引脚复用正确。这块不同的板子差异比较大,我不给死配置,但建议你先把“蓝牙HCI能注册、A2DP能出声”作为第一优先级,HFP音频放在第二步再调。
编译内核时,RV1106的SDK一般用Buildroot管理,编译命令通常类似:
make rockchip_rv1106_defconfig make编译完后把内核镜像和模块打包进系统镜像,烧录到开发板上,然后看启动日志。
2.3 固件和系统配置:firmware路径、rfkill与Bluetooth服务
驱动编译好了,没有固件照样起不来。AIC8800DC的固件文件一般会放在SDK的某个目录里,需要拷贝到板子的/lib/firmware/下面。常见的文件名包括:
- aic8800_fw.bin
- aic8800_btsram.bin
- aic8800_btfw.bin
具体文件名以你拿到的驱动包为准。我踩过的一个坑就是只拷贝了WiFi固件,没拷贝蓝牙SRAM固件,结果WiFi能连上路由器,蓝牙却始终扫不到设备。检查逻辑很简单:
ls -l /lib/firmware/aic8800/ dmesg | grep -i aic如果dmesg里有类似“Failed to load firmware”的日志,那基本就是文件路径不对、文件名不匹配,或者固件版本和驱动版本不一致。
系统侧还有两件事。第一是rfkill,有些模组的蓝牙使能引脚是独立GPIO控制的,如果SDK默认没有把它拉高或注册成rfkill设备,那蓝牙芯片可能一直处于复位状态。你可以用rfkill list看看有没有蓝牙设备,如果没有,多半是电源/使能引脚没控制好。第二是BlueZ服务,Buildroot镜像里可能没有默认启动bluetoothd,需要确保/system里的dbus和bluetooth服务正常:
systemctl status bluetooth如果没有systemd,也可以手动启动bluetoothd,我在第三部分会给出具体命令。
3. 蓝牙协议栈启动与配对:让开发板“可见”且能连上
3.1 HCI层初始化:btattach、波特率与hci0注册
蓝牙协议栈的最底层是HCI,它一般跑在UART上,内核通过一个叫btattach或hciattach的工具把UART设备“挂”成hci0。RV1106上最常用的方式是btattach:
btattach -B /dev/ttyS3 -S 1500000 &这里的/dev/ttyS3对应你设备树里使能的UART3,-S指定HCI初始化后的波特率。第一次调试我建议先用115200验证链路:把波特率参数去掉或者设为115200,先看hci0能不能注册出来:
hciconfig -a如果能看到一个hci0,说明UART链路和Bluez的基本通道没问题。这时候再测试高波特率,因为A2DP音频传输对带宽有硬性需求,115200波特率跑无损或高码率音频会出现卡顿。我实测下来,AIC8800DC的HCI至少建议跑到1500000,才能比较宽松地应付SBC编码的A2DP流。
注册成功后执行:
hciconfig hci0 up如果执行后报错,或者hci0状态一直是DOWN,优先看内核日志:
dmesg | grep -i hci dmesg | grep -i bluetooth常见问题有两个:一是UART设备节点被复用,比如调试串口也占用同一路UART;二是蓝牙固件没加载成功,HCI复位命令得不到回应。还有一种情况是pinctrl没配好,导致UART引脚根本不通。这个阶段别急着去调音频,先把hci0稳定在UP状态,后面的一切才有基础。
3.2 bluetoothctl配对与连接操作
hci0起来了,下一步就是让手机或蓝牙音箱能连到开发板。先用bluetoothctl交互式操作:
bluetoothctl进入交互界面后按顺序执行:
power on scan on pairable on agent on default-agent discoverable onscan on之后应该能看到附近的可发现蓝牙设备。如果想连接手机,手机端需要开启蓝牙并处于“可被搜索”状态。在bluetoothctl里找到对应的MAC地址,执行:
pair 12:34:56:78:9A:BC connect 12:34:56:78:9A:BC trust 12:34:56:78:9A:BC如果配对时要求输入PIN码,但因为开发板没有屏幕,确认成了大问题。这里最好在启动bluetoothd时带上参数,或者用agent自动接受。我在调试时一般优先设置一个固定PIN,或者使用JustWorks配对方式,也就是不需要输PIN的配对模式,很多耳机、音箱默认都是这种。
配对成功不代表能播放音频。你还需要确认连接后的Profile是否协商成功,特别是A2DP。可以用:
bt-device -l或者继续通过bluetoothctl查看设备信息。如果看到设备已经Trusted,但音频Profile没起来,就要进到下一步,检查和BlueALSA的联动。
3.3 用btmon和hciconfig排查链路问题
蓝牙调试有个神器叫btmon,它能把蓝牙协议栈发出去的HCI命令和事件全部打出来。调试配对失败、链路断开、Profile协商失败时,我基本都是靠它定位。比如手机和开发板都显示配对了,但过几秒就断开,这时在btmon里能看到HCI层反复在发Link Key相关的请求,或者有Error Code提示。
常用的排查组合是:
btmon -w /tmp/bt_trace.log & hciconfig hci0 piscan bt-adapter --set Powered 1然后打开手机反复尝试连接,最后把bt_trace.log拉回PC,用Wireshark的蓝牙解析功能看。Wireshark能识别HCI ACL、SDP、A2DP等协议报文,比在终端看日志直观很多。我第一次查A2DP Profile起不来的问题时,就是靠Wireshark看到SDP过程没有正确响应,才定位到BlueZ配置缺了Media服务。
如果只是想快速确认芯片能力,也可以用:
hciconfig hci0 features这条命令能看到芯片是否支持BR/EDR、LE、SCO等关键特性。AIC8800DC是双模蓝牙,BR/EDR和LE都应该能看到。如果features里连基本的声音Profile能力都没有,那就要怀疑是不是固件版本太老。
4. 音频播放链路打通与实测
4.1 轻量方案优先:为什么用BlueALSA而不是PulseAudio
嵌入式Linux下播放蓝牙音频,主流方案有两套,一套是PulseAudio,一套是BlueALSA。PulseAudio功能全,但它依赖的服务组件多,在RV1106这种小内存、低主频的板子上跑起来有点重,而且调试音频路由时会多一层抽象,不利于快速定位问题。我的建议是优先用BlueALSA,它做的事情很纯粹:把BlueZ的A2DP和HFP音频桥接给ALSA,让aplay、arecord这种标准命令直接读写蓝牙音频设备。
安装BlueALSA后,启动方式一般是这样:
bluealsa --profile a2dp-sink --profile a2dp-source &启动后,Bluez设备连上时,BlueALSA会自动创建一个ALSA设备。用aplay -l可以看到类似bluealsa的设备名。播放音乐时,直接用aplay指定设备:
aplay -D bluealsa:DEV=12:34:56:78:9A:BC,PROFILE=a2dp test.wav这里DEV参数换成你实际连接的蓝牙设备MAC。如果aplay能正常播放,耳机或音箱也出声,那A2DP链路就算通了。
BlueALSA还有一个好处是它的调试信息很直白。如果A2DP Profile没协商成功,启动时会报“device does not support A2DP”或者“Failed to open PCM”之类的提示,根据提示去查BlueZ的Profile配置,方向就很明确。
4.2 A2DP播放实测:从aplay到蓝牙耳机
我实测的播放命令是分两步走的。先生成一个测试音频文件,避免直接拿MP3测试时搞不清是解码问题还是蓝牙链路问题:
ffmpeg -f lavfi -i "sine=frequency=440:duration=10" -ar 44100 -ac 2 test.wav这是一段10秒的440Hz正弦波,如果蓝牙链路通了,耳机里会听到清晰的“嗡——”声。再用aplay播放:
aplay -D bluealsa:DEV=12:34:56:78:9A:BC,PROFILE=a2dp test.wav如果声音出来了,说明从ALSA到BlueALSA,再到BlueZ,再到AIC8800DC的HCI UART,最后到蓝牙耳机的整条A2DP链路是通的。如果声音卡顿或断续,最先怀疑的就是HCI UART波特率。刚才前面提到过,115200波特率肯定不够,SBC编码的A2DP流通常需要300-400kbps的带宽,而UART 115200的理论上限只有约115kbps,显然入不敷出。我把波特率提到1500000之后,卡顿问题基本消失。
还有一个体验技巧:A2DP播放时,可以同时开一个定时器测量一段音乐的播放时长,看它是否稳定。我在测试中发现,如果UART有偶发丢包,歌曲会播放得“忽快忽慢”,时间轴不稳定。这时在dmesg里查UART overrun相关日志,基本能确认瓶颈。
4.3 通话场景的HFP/SCO音频路由与相关坑点
A2DP搞定之后,不少人会继续做HFP通话。HFP涉及SCO音频,而SCO音频的默认通道很可能是PCM/I2S,不是HCI UART。这也是热词里常提到的“蓝牙A2DP切SCO模式”的由来。
简单说,当手机通过蓝牙HFP连上开发板时,系统会从A2DP切到SCO,音频数据改走PCM接口。AIC8800DC的BT_PCM引脚接到RV1106的I2S接口,然后由RV1106的Codec处理语音。这个链路只要有一处配置不对,就会出现“音乐正常,一通话就没声音”或者“能听到系统声音但对方听不到我说话”的怪现象。
我踩过的一个坑是I2S时钟极性问题。RV1106的I2S和AIC8800DC的PCM时钟极性不一定默认匹配。蓝牙侧通常默认PCM时钟在上升沿采样,而I2S可能默认在下降沿采样,导致数据错位,表现为有沙沙声但听不清人声。解决办法是在设备树里调整I2S的format,或者通过ALSA的配置指定同步模式。不同Codec的具体参数不一样,这块只能结合你自己的硬件确认。
另外HFP还有一种低功耗模式,SCO音频可以在HCI上走,叫做esco over HCI,但这种模式下系统负载会高一些,也更容易出现延迟。我自己实际项目里,如果产品需要长时间通话,我还是建议用PCM/I2S硬路由,稳定性和音质都更好。
5. 避坑指南:实操中最常踩的七个坑
5.1 固件加载失败与hci0不出现
这个问题出现的频率最高。现象是dmesg里看不到aic8800蓝牙初始化日志,或者日志提示firmware loading failed。排查顺序我一般是这样:
- ls /lib/firmware/aic8800/,确认固件文件确实存在,且文件名和驱动的请求名一致。
- dmesg | grep aic,看驱动是否在探测阶段就报错了。
- rfkill list,确认没有把蓝牙射频kill掉。
- 确认UART的pinctrl配置,特别是RTS/CTS是否占用冲突。
- 确认蓝牙对应的UART没被console或其他服务占用。
有一次我折腾了一下午,最后发现是内核里把同一个UART注册成了两个设备,导致HCI设备无法创建。解决办法就是把dts里的aliases和chosen部分检查清楚。
5.2 蓝牙连上但没声音:先查这三处
如果你的蓝牙设备已经连接成功,但A2DP播放没有声音,按以下顺序查:
第一,确认BlueALSA进程确实在跑,而且它监听的是系统当前使用的BlueZ实例。第二,执行aplay -L,看bluealsa设备是否出现在列表里。第三,确认蓝牙耳机/音箱支持的Profile。有些设备只支持HFP不支持A2DP,这种情况下你开着music播放,它当然不会出声音。
另外,别忘了看 /etc/bluetooth/main.conf 里的配置。BlueZ 5.x之后,A2DP相关功能可能需要在main.conf里显式启用。如果你发现SDP阶段没有回应AudioSource/AudioSink服务,多半就是这里的配置问题。
[General] Enable=Source,Sink,Media Disable=Headset上面这个Disable=Headset很关键,如果你只想测试A2DP,暂时把HFP关掉,能避免很多Profile之间互相抢占的怪问题。
5.3 A2DP切SCO导致声音丢失(经典坑)
这是我在实际项目里遇到的最让人抓狂的一个问题。蓝牙耳机连着开发板听歌,突然一个系统通知声音触发HFP Profile,或者手机打个电话进来,A2DP就断了,音乐不再播放,而通话音频也没有正确路由到Codec,结果就是两边都没声音。
原因就是BlueZ在同一时刻通常只会让一个音频Profile占用音频通道,A2DP和HFP之间需要切换,而这个切换过程如果PCM/I2S侧配置不到位,就会掉链子。我建议初调阶段先Disable掉HFP,专注打通A2DP。真正做产品时,再专门设计一个Profile切换状态机,在应用层控制音频焦点,而不是依赖底层自动切换。
如果切换到HFP后没有声音,优先检查PCM/I2S的时钟是否正常。用示波器量AIC8800DC的BT_PCM_CLK,如果切换后CLK停振,基本就是主控侧I2S没有正确进入工作状态。
5.4 音频杂音、断音、延迟偏大
音频杂音和断音,大部分时候不是软件问题,而是硬件环境。有一次我觉得SBC编码质量不够好,音质发闷,后来发现是电源纹波太大。AIC8800DC在WiFi和蓝牙同时工作时,瞬时电流可以达到几百毫安,如果3.3V供电轨没有足够的储能电容,射频发射瞬间电压跌落,音频就会爆音。
断音则要优先检查HCI UART波特率。我前面反复强调,A2DP高音质播放时,115200就是不够用,别在这个问题上浪费时间,直接把波特率拉到1500000以上,再试。
延迟偏大这个问题,A2DP本身就有固定延迟,SBC编码加蓝牙射频传输,150-250ms很正常。如果你做的是影音同步类产品,光在蓝牙协议栈上做文章不够,还得考虑视频播放端提前补偿,或者选择延迟更低的编解码方案。普通音乐播放场景,这个延迟不影响体验。
5.5 硬件布局的暗雷:天线、流控、电源
最后三个硬件坑,每一个都能让软件调试白费:
天线:AIC8800DC的RF天线区域下方一定不能铺铜,也不能有密集走线。我有一块板子为了节省面积,在天线净空区旁边走了一根电源线,结果蓝牙灵敏度直线下降,手机隔着一堵墙就断开。后来重新搞定布局,问题立刻改善。
流控:再次强调UART的CTS/RTS一定要接。我用过一块转接板,偷懒没接CTS/RTS,结果hci0能注册,但蓝牙搜索设备的列表刷得特别慢,连接后经常会断。把流控接上,世界清净了。
电源:AIC8800DC这种Combo芯片的峰值功耗不低,尤其是WiFi 6和蓝牙同时打开时,电源轨余量不足会导致芯片频繁复位。增加一个100uF左右的储能电容,或者换一个输出能力更强的LDO/DCDC,能解决很多“莫名重启”“找不到设备”的问题。
5.6 避坑汇总速查表
| 问题 | 现象 | 优先排查 | 解决方案 |
|---|---|---|---|
| hci0不出现 | 无蓝牙设备节点 | dmesg、firmware路径、UART占用 | 检查驱动加载与固件文件 |
| 配对失败 | PIN码无法确认 | bluetoothd agent配置 | 使用JustWorks或自动agent |
| 连上没声音 | A2DP无输出 | BlueALSA进程、main.conf | 启动BlueALSA并确认Profile |
| 声音卡顿 | A2DP断续 | HCI UART波特率 | 提到1500000以上 |
| 通话没声音 | HFP切换后无声 | I2S/PCM时钟极性 | 调整设备树I2S格式 |
| 杂音爆音 | 射频发射瞬间爆音 | 电源纹波 | 增加储能电容 |
| 断连严重 | 距离稍远就断开 | 天线净空区与流控 | 重新布局、接好CTS/RTS |
这几个坑我基本是按“从软件到硬件”的顺序排的,调试时也建议按照这个顺序来,先排除固件和配置问题,再查电源和射频,不然容易被表面现象带偏。
最后分享一点我自己的调试习惯:每次改动只动一个变量,然后回归测试。比如这次我把UART波特率从115200改到1500000,就只测音频卡顿这一个指标,不要同时去改asound.conf和固件版本。蓝牙音频链路涉及的东西已经够多了,变量越多越难定位。先把最慢的链路跑通,再去优化音质和延迟,这是我一直沿用的思路,你也可以试试。