BSP调试做到第6篇,我估摸着也该轮到无线这一层了。这次调的是RK3588平台上的aic8800 WiFi模组,一颗支持WiFi 6的SDIO接口芯片。调试环境是某方案公司的核心板加转接底板,内核基于5.10版本,整体调试周期大约两天半,中间穿插了老式遗留的坑,整体下来收获不小,值得把过程沉淀成一篇笔记。
如果你也是在做RK3588或者类似平台的WiFi SDIO适配,手头同样是aic8800这颗物料,或者正准备把一颗新的WiFi芯片塞进自己的BSP里,那么这篇内容应该能帮你把弯路省掉大半。我会把从硬件确认、设备树配置、驱动编译、固件加载到实际联调的完整链路都过一遍,重点写那些光看datasheet或者源码注释根本看不出来的细节。
1. 调试前的方案设计与目标拆解
1.1 先搞清楚这颗芯片是什么来头
aic8800属于WiFi 6(802.11ax)单芯片方案,支持SDIO 3.0和USB 2.0两种主机接口,在嵌入式Linux平台上绝大多数场景走的是SDIO。选它主要有几个考量:一是成本相比同级别的其他WiFi 6方案有优势,二是SDIO接口在RK3588这类带多个SDIO控制器的平台上属于常规操作,三是原厂提供的Linux驱动相对完整。
但“相对完整”并不等于“拿来就能用”。这颗芯片的驱动代码、固件文件和配置文件是分开的,而且原厂驱动对内核版本比较敏感。我这次用的是5.10内核,驱动代码是原厂SDK里带的版本,多少也因此踩了一些坑,后面会细说。
调试目标的拆解需要分得很清楚,不能上来就拿着iw命令去扫描,那属于顺序错误。我的调试路径是这样拆的:
- 硬件通路确认:SDIO数据线、时钟线、复位脚、供电是否到位。
- 设备树配置:让内核能在SDIO总线上枚举到这颗设备。
- 驱动编译与加载:拿到原厂驱动,编成内核模块,加载后挂在设备上。
- 固件加载验证:驱动把固件从文件系统写到芯片里,这个环节看不到日志就是一场空。
- 射频功能联调:扫描、连接、Ping、iperf吞吐。
每一层都依赖前一层,所以不要跳步。我在实际调试中发现,很多人卡在“wlan0不出来”这个现象上,但根因往往在最底层的设备树或者供电上面,检查顺序错了会非常浪费时间。
1.2 硬件层面的第一道关卡
WiFi模块的硬件调试是最容易被忽略但又是最决定成败的环节。aic8800通过SDIO挂在主控上,SDIO总线和普通SD卡不一样的是,它还承载了中断信号。设备树里如果不把cap-sdio-irq打开,芯片的中断根本到不了主机,驱动加载之后收发就会异常。
我先对照原理图把每个引脚过了一遍,重点关注这几组信号:
- SDIO_CLK:时钟频率一般配置到150MHz,需要确认走线短且无stub。
- SDIO_CMD / SDIO_DATA0-3:这4条数据线必须加上拉电阻,否则高速模式下会丢数据。
- WL_REG_ON / WL_HOST_WAKE:这是WiFi芯片的使能脚和唤醒脚,接到主控的GPIO上,设备树里必须对应。
- 供电:3.3V和1.8V两路都要查,特别是1.8V那路的带载能力不够是很多“莫名其妙驱动加载失败”的元凶。
当时我用万用表把这几组信号全部量了一遍,确认在模组上电后电压都稳定,时钟脚有波形才继续往下走。如果你用的是转接板,还要额外确认转接板的SDIO信号线是否符合阻抗要求,我在实际项目中见过因为用了杜邦线拉长SDIO走线导致无法高速枚举的情况,这种问题只能改板或者降低时钟频率才能绕过去。
1.3 调试工具链准备
RK3588平台的调试需要准备三样东西:交叉编译工具链、串口终端、根文件系统访问能力。串口是必需的,因为在内核还没完全起来之前,只有串口能输出日志。WiFi驱动的加载和固件阶段的调试,绝大多数信息都靠内核的dmesg来观察。
文件传输方面,我用的是TFTP和NFS两种方式配合。内核对设备树和内核镜像的加载用TFTP,编译好的WiFi驱动模块放到NFS目录里直接insmod,不用反复烧写根文件系统,调试效率高很多。建议先把板子的网络存储环境配好,不然后面每改一次驱动都要重新烧固件,极其痛苦。
需要准备的软件材料有:
- 原厂提供的aic8800驱动源码压缩包。
- 匹配内核版本的SDK内核源码树,特别是
drivers/net/wireless目录结构。 - 设备树源文件,一般在SDK的
arch/arm64/boot/dts/rockchip/下面。 - 固件文件,通常是
fw.bin和fw_loader.bin这类名字。
这些材料缺一不可。尤其是匹配内核版本这一点,原厂驱动一般不会维护太多内核分支,拿到手之后先看Makefile和头文件引用了哪些内核API,跟自己的内核源码对一下版本,能省掉后面的编译报错。
2. 设备树配置与SDIO接口解析
2.1 设备树中SDIO控制器部分
RK3588有多个SDIO控制器,具体用哪个取决于板级设计。我这次的模组挂在SDIO控制器1下面,设备树里要先把该控制器的状态改成okay,然后把总线频率拉上去。
设备树中对SDIO控制器的基础配置是这个样子的:
&sdio1 { status = "okay"; max-frequency = <150000000>; bus-width = <4>; cap-sd-highspeed; cap-sdio-irq; keep-power-in-suspend; non-removable; rockchip,default-sample-phase = <90>; };有些刚接触这个的人不理解non-removable的意义。简单说,WiFi模组是焊死在板上的,不是插拔的SD卡,如果少了这一项,内核会把它当成可移动设备,SDIO子系统会在尝试检测卡插入/移除的时候反复重置,导致驱动加载不稳定。
cap-sdio-irq允许SDIO设备通过DAT1线向主机发送中断通知,这是SDIO WiFi芯片的工作基础。没有它,芯片只能靠轮询来告诉主机有数据到了,效率极大下降,而且很多驱动逻辑默认依赖中断,漏配了这个会直接导致驱动挂死。
rockchip,default-sample-phase是瑞芯微平台特有的采样相位配置。SDIO时钟和数据之间的相位偏移受PCB走线长度影响很大,这个参数可以在不重新画板的情况下调整采样点,是解决高频不稳的利器。我实际调试时发现在150MHz下如果默认相位不调整,dmesg里会出现SDIO CRC错误,把这个值调到90度之后恢复正常。
2.2 WiFi设备节点与GPIO配置
在SDIO控制器节点之外,还需要单独给WiFi设备建立一个节点,用来描述它的GPIO资源、电源控制方式和复位时序。aic8800的驱动probe时通过设备树拿到这些资源。
&wifi { compatible = "aic,aic8800"; status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&wifi_wake_host>; wifi_wake_host = <&gpio4 RK_PA1 GPIO_ACTIVE_HIGH>; reset_gpio = <&gpio4 RK_PA0 GPIO_ACTIVE_LOW>; wifi_poweren_gpio = <&gpio4 RK_PA2 GPIO_ACTIVE_HIGH>; bus = "sdio"; };需要注意的是reset_gpio的极性。WiFi模组内部集成了一颗PMU,复位脚是低有效还是高有效跟芯片内部的设计习惯有关,我查了datasheet确认是低有效。如果你的板子上复位脚的电平恰好相反,驱动初始化的时候时序会彻底乱掉,表现出来就是“驱动加载每次都在不同位置卡死”的间歇性故障。
还有一点,wifi_poweren_gpio这个脚在模组上电时序里很重要,需要先拉高使能PMU,之后等内部稳压器稳定,再释放复位。这个时序关系在原厂驱动里有msleep的调用,但如果硬件设计上把电源使能跟复位绑定了,就要小心驱动里的延时是否足够让模组完成上电复位。
2.3 pinmux复用不要忽略
RK3588的引脚复用非常灵活,SDIO信号脚和GPIO、UART等脚位经常复用。设备树里除了SDIO控制器节点的配置外,还要确认对应的pinctrl已经被正确设置,否则引脚不会切换成SDIO功能。
&pinctrl { sdio1 { sdio1_bus4: sdio1-bus4 { rockchip,pins = <3 RK_PA0 RK_FUNC_1 &pcfg_pull_up>, <3 RK_PA1 RK_FUNC_1 &pcfg_pull_up>, <3 RK_PA2 RK_FUNC_1 &pcfg_pull_up>, <3 RK_PA3 RK_FUNC_1 &pcfg_pull_up>; }; }; };这里有一个很容易掉的坑:SDIO的数据线内部上拉电阻如果没配,默认状态是浮空,高频率通信时信号边沿会不确定,导致CRC错误。我在调试中就遇到过扫描能搜到AP,但连接之后老掉线的症状,排查到最后是DATA1和DATA2的上拉没有配置,补上之后立刻稳定了。
我建议调试时把设备树里SDIO相关的pinctrl-0全部显式配出来,不要依赖bootloader里遗留的引脚状态去碰运气。踏实把引脚配置、上拉、复用都写清楚,可以省掉一打包的诡异问题。
3. 驱动源码集成与内核编译
3.1 驱动目录结构与编译方式
aic8800的原厂驱动一般不在内核主线里,而是以独立目录的形式提供。拿到手的源码结构通常是这样:
aic8800_fdrv/ ├── aic8800_fdrv.c ├── aic8800_fdrv.h ├── fw.c ├── io.c ├── Makefile ├── config/ └── include/原厂驱动的编译方式一般是直接进入驱动目录执行make,配合环境变量指定交叉编译器和内核源码路径。但实际项目里最好是把它放进内核的drivers/net/wireless/目录下面,挂到内核的Kconfig里,用内核的统一构建系统来编,这样跟内核版本的接口匹配度最高。
把原厂驱动编成独立内核模块是最灵活的调试方式,因为模块和内核解耦,编译一次可以在多块板子上复用,也方便反复加载和卸载。我这次是把整个aic8800_fdrv目录拷贝到了内核源码的drivers/net/wireless/aic8800/下,然后在Kconfig里加了一个配置项CONFIG_WLAN_VENDOR_AIC8800。
3.2 内核无线子系统配置
WiFi驱动依赖的内核配置选项集中在device drivers -> network device support -> wireless LAN目录下,有几个是必须开的:
CONFIG_CFG80211=y或者=m,这是无线配置管理的基础框架。CONFIG_MAC80211=y,mac80211协议栈,SDIO WiFi驱动基本都依赖它。CONFIG_WIRELESS=y,无线子系统总开关。CONFIG_WLAN=y,WLAN设备支持。CONFIG_CRC32等基础库依赖,一般默认打开。
内核配置的时候注意compile成模块还是编入内核,这会影响rootfs阶段的使用方式。如果打算在系统起来之后手动insmod驱动,就把CONFIG_CFG80211和CONFIG_MAC80211编成模块,驱动模块加载的时候会自动处理依赖。如果配置成编入内核,那么驱动模块加载时只要引用内核符号就可以,两者都行,我习惯用模块方式调试。
另外特别注意一下内核版本与驱动源码的匹配。5.10内核里cfg80211的wiphy_register等接口和刷新的6.1内核差异不小,驱动源码里如果用到老接口,编译阶段会报错。我这次遇到的是struct cfg80211_ops里某个回调函数名差异,改一下函数签名就能解决。
3.3 交叉编译的坑
交叉编译有几点容易出问题,我逐条列一下:
- 环境变量
CROSS_COMPILE必须指向编译器绝对路径前缀,比如aarch64-none-linux-gnu-。 ARCH=arm64这一项容易被忽略,尤其是之前编过ARM32平台的环境,不带上的话Kconfig会找错架构。- 驱动模块编译需要的内核源码树必须已经完成过一次编译,生成了
Module.symvers,否则模块编译时符号表缺失,insmod会报“Unknown symbol”。
还有一点容易忽视:模块编译出来的.ko文件的版本格式需要跟目标板内核保持一致。如果板子上内核是5.10.110,你的模块却在5.10.198的源码树里编出来,insmod的时候会因为 vermagic 不匹配被拒。解决方式无非两个,要么把目标板内核源码树切到正确版本再编译,要么用modprobe --force-vermagic强行加载,后者只适合临时验证,正经做项目不建议用。
4. 固件加载与驱动调试全流程
4.1 固件文件放置与加载机制
aic8800的固件加载分为两个阶段。第一阶段是芯片内部的ROM loader接收一个小的loader固件,加载成功后由这个loader再从主机读取主固件写入芯片的RAM。这套机制跟很多WiFi芯片的做法是类似的,好处是芯片内部不需要大容量flash。
固件文件需要手动拷贝到根文件系统的某个目录里,驱动默认从以下路径按顺序查找:
/lib/firmware/aic8800//etc/firmware/aic8800//vendor/firmware/aic8800/
我实际使用的是/lib/firmware/aic8800/,目录下需要这几个文件:
aic8800_fw.bin aic8800_loader.bin aic8800_fw_ram.bin aic8800_rf.bin不同版本的驱动包对固件文件名的定义可能有差异,先阅读fw.c源码里request_firmware调用的文件名,以那为准。我遇到过厂商在驱动里加了版本判断的情况,固件文件名带了_v1后缀,如果跟驱动里的字符串对不上,日志会一直报firmware request failed。
4.2 驱动加载与日志验证
一切准备就绪后,加载驱动的顺序有讲究。我的做法是先在板子上看一眼SDIO设备有没有被内核枚举到:
cat /sys/bus/sdio/devices/*/device如果枚举成功,会输出类似AIC8800的设备ID。这一步是整个调试的分水岭:如果这里什么都看不到,问题一定出在设备树或者硬件层面,先不要急着碰驱动代码。
确认设备挂上之后,再执行 insmod:
insmod aic8800_fdrv.ko然后立刻用dmesg | tail -n 80观察输出。正常的加载流程大概分为三个阶段:
- 驱动注册SDIO驱动并完成probe。
- 通过SDIO总线向芯片发送loader固件。
- 主固件加载完成后,调用cfg80211注册无线接口。
固件加载阶段如果失败,典型日志如下:
aic8800: firmware request failed aic8800: failed to load firmware这类问题先检查文件系统和固件路径,不用怀疑芯片坏掉,大概率是文件名不匹配或者权限问题。文件系统如果烧用的是只读根文件系统,还要确认/lib/firmware是否真的写进去了,我遇到过在宿主机上拷贝成功但启动后被同步工具擦掉的情况。
4.3 无线接口出现后的后续动作
驱动加载完成后,正常状态下执行ifconfig -a会看到wlan0接口。但光有接口不够,还要验证它能不能正常工作。先用iw dev查看无线信息,确认type为 managed,且wiphy编号正常。
这里值得提一下,系统里如果没有安装iw工具,可以用iwconfig替代,但iw能提供的信息多得多,建议在根文件系统里预装好。如果根文件系统里没有,还可以用busybox自带的iwconfig应付扫描和连接,但调试WiFi 6的特性参数还是得有iw。
我习惯在这个阶段做几个基础验证:
ifconfig wlan0 up之后观察dmesg是否有异常中断或超时。iw dev wlan0 scan | head -n 20扫描一下周边AP,确认射频通路可以收到数据。- 如果扫描正常,接下来才做连接和网络层验证。
如果扫描为空,先检查信道、天线以及射频板级设计,不要把时间浪费在驱动代码里,频繁iw scan也会触发驱动间歇性问题,遇到时抓串口日志更直观。
5. 功能联调与典型问题速查
5.1 连接测试与网络层联调
扫描能看到AP之后,我一般使用wpa_supplicant来完成WPA2/WPA3的连接。这一层的配置比较常规,但要注意driver参数,aic8800原厂文档一般推荐wext或nl80211,不同的驱动实现选择不一样,用错了会一直报Unsupported driver。
我用的是nl80211,配置示例大概长这样:
ctrl_interface=/var/run/wpa_supplicant network={ ssid="TestAP" psk="password123" key_mgmt=WPA-PSK }连接成功之后的下一个动作是拿IP。如果使用的是udhcpc,记得要在 udhcpc 脚本里把wlan0的MTU调整一下,WiFi接口的默认MTU一般是1500,正好够用。但如果你在后面测试发现某些网页打不开,要多留意MTU和分片的组合问题。
连接稳定性的验证不要只看ping 网关,那说明不了什么。我的建议是同时跑三个方向的测试:ping外部服务器测延迟抖动、iperf3测TCP吞吐、连续播放一段网络流媒体测长时间稳定性。这三项基本能覆盖WiFi驱动常见的劣化场景。
5.2 吞吐量测试与调优思路
aic8800支持WiFi 6,80MHz频宽,2x2 MIMO,理论上限可以到1201Mbps的链路速率。但在实际嵌入式平台上,SDIO接口会成为主要瓶颈。SDIO 4线150MHz的传输能力大约在750Mbps有效带宽左右,能跑到500Mbps以上的实测吞吐已经算不错,对绝大多数工控、商显、机器视觉场景都够用了。
iperf3的测试命令比较固定:
iperf3 -s // 服务器端 iperf3 -c 192.168.1.100 -t 60 -P 4 // 客户端,4并发吞吐量明显偏低的时候,按以下顺序排查:
- 确认链路速率:
iw dev wlan0 link看tx rate和rx rate。 - 如果速率只有150Mbps左右,说明80MHz频宽没协商上,检查AP端的配置以及
iw dev wlan0 set channel是否有异常。 - 如果速率协议正确,但吞吐持续偏低,检查SDIO总线是否处于高负载,用
cat /sys/kernel/debug/mmc1/ios看实际时钟和总线宽度。 - 检查天线:断开一颗天线或者天线接触不良会导致2x2掉到1x1,吞吐直接减半。
另外,SDIO WiFi的吞吐性能跟DMA缓存分配也有关系。驱动代码里如果skb的分配策略不合理,在高速传输的时候很容易出现Failed to allocate skb之类的报错。这个问题可以适当加大发送队列深度来缓解,但根子还是得靠原厂去优化驱动。
5.3 常见问题与排查思路整理
调试过程中遇到的现象我整理成了一张速查表,方便读者对照参考:
| 现象 | 可能的根因 | 排查方式 |
|---|---|---|
| SDIO设备枚举不到 | 设备树未配置、引脚复用错误 | 检查/sys/bus/sdio/devices/,核对pinctrl |
| 驱动probe失败 | 复位GPIO极性不对、VLDO供电异常 | 查dmesg定位停止位置,量GPIO电平时延 |
| 固件加载不上 | 固件路径错误、文件权限不符 | 核对文件名与源码中request_firmware参数 |
| wlan0不出现 | cfg80211注册失败、mac80211接口不匹配 | 查cfg80211日志,确认内核配置项 |
| 扫描无结果 | 天线问题、射频链路失败 | 用另一台设备在同样位置确认AP信号 |
| 吞吐量减半 | 天线脱落、SDIO信号质量差 | iw link查看链路速率,检查SDIO采样相位 |
还有一些经验层面的东西我觉得值得单独说一下。调试WiFi驱动切忌反复开关扫描,因为每轮扫描驱动底层都会做一轮信道遍历和射频校准,强烈频繁操作有可能让PMU进入异常状态。我在调试中遇到过驱动加载后第一次扫描正常,第二次再扫就Connection timed out,重启后才能恢复,后来定位是驱动中断处理里的竞态问题。遇到这种情况,先把 dmesg 完整抓下来,观察中断上报的频率和时间点,再去驱动代码里找host wake的处理路径,比盲目改扫描参数靠谱。
5.4 一个隐蔽的功控问题
最后分享一个比较隐蔽的问题,它出现在我用iperf3打流测试的时候。TCP吞吐在短距离下能跑到400Mbps以上,但一旦把距离拉开到3米开外,吞吐掉到只剩60Mbps。先怀疑是天线问题,换了天线测试还是差不多。后来用频谱仪看射频输出,发现TX功率比标称值低了约5dBm。问题出在当前信道对应的功率校准表没有生效,驱动加载过程中读取aic8800_rf.bin时没有正确解析到目标信道频段的校准值。
这个问题的排查确实费了一番周折,因为驱动加载日志全部正常,链路协商速率也从没掉过,只有射频功率这个维度能看出问题。解决方式是手动确认rf bin文件版本和驱动源码里射频表头的匹配情况,更换新版本rf固件后,距离衰减曲线立刻恢复正常了。
这桩事给我的启发是,WiFi调试的信号路径问题不能只盯着驱动代码里看得见的部分,射频固件、功率表、天线匹配网络这些隐性因素往往才是最终决定用户体验的环节。调试到后期如果怀疑射频指标,该动用频谱仪的时候不要犹豫,少猜多看数据,能少走很多冤枉路。
调试ac8800这颗WiFi芯片整体来说还算顺利,真正卡人的是几处硬件时序和固件配置上的细节,而不是驱动代码本身。如果你也在调同一颗芯片,我的建议是把硬件层面的状态先确认扎实,然后把固件、设备树和驱动的版本一一对应好,后面出现的各种现象都能找到合理的解释路径。