做嵌入式 Linux 开发这些年,我在不同板子上折腾过的 WiFi 模组一只手数不过来:USB 接口的、SDIO 接口的、PCIe 接口的,甚至有那种焊在板子上的从模组到天线全得自己调的方案。接触 Linux WiFi 设备驱动开发的次数越多,越发现一个规律——很多人不是不会写代码,而是搞不清楚这块驱动在 Linux 整个网络体系里到底站在哪个位置。这篇文章不是教科书,是一份从实际项目里摸出来的经验总结,适合正在做嵌入式驱动开发、Linux 内核学习,或者手里正好有一块 WiFi 模组死活调不通的开发者。我会从整体架构、开发环境、核心代码结构、调试方法几个角度把 Linux WiFi 设备驱动开发这件事讲透,尽量做到你看完能直接上手抄作业。
1. WiFi驱动的整体框架和方案选型
1.1 先搞清楚WiFi驱动在Linux里的定位
很多人第一次接触 WiFi 驱动时会下意识地往字符设备驱动那个方向想。其实大错特错,WiFi 设备在 Linux 里的本质是一个网络设备,走的是 net_device 那套体系,而不是 file_operations 那套。也就是说,当内核需要操作 WiFi 硬件时,最终面对的是struct net_device这样的接口,上层通过套接字收发数据,而不是 open/read/write。
这一点必须先掰扯清楚,否则后面看代码会完全迷失方向。我见过有同学把 WiFi 驱动当成平台驱动去写,非要注册 miscdevice,结果折腾半天连数据包都不知道从哪进内核。WiFi 驱动的正确坐标应该是:最底层是具体的 WiFi 芯片,往上是对应总线的设备驱动(比如 SDIO、USB、PCIe),再往上是通过 cfg80211/mac80211 框架接入到内核网络协议栈,最顶层才是 NetworkManager、wpa_supplicant 这些用户态工具。
在实际项目里,我们写的驱动代码通常集中在两处:一处是让内核能被正确识别并注册 WiFi 硬件的能力,另一处是真正控制芯片收发数据包的逻辑。中间那一大坨协议处理,内核已经帮你做掉了一部分,但具体做多少,取决于芯片本身的架构。
1.2 FullMAC和SoftMAC,驱动开发者的分工差别
搞 WiFi 驱动开发,第一个要选的就是芯片的架构模式。市面上主流 WiFi 芯片分成 FullMAC 和 SoftMAC 两种。FullMAC 芯片的 MAC 层管理、扫描、认证关联、帧控制这些活都在芯片内部完成,驱动这边相对轻松,只需要把硬件能力上报给内核,接收命令再返回结果,很像一种黑盒玩法。SoftMAC 则相反,芯片只负责收发比特流,所有 802.11 协议功能全靠驱动配合 mac80211 框架去实现,驱动要处理的细节多到让人头疼。
对初学者来说,选择 FullMAC 芯片做入门确实会友好一些,比如一些 USB WiFi 网卡。但实际产品中,尤其是做路由器、物联网网关类设备时,SoftMAC 架构往往更常见,因为灵活性高,协议栈层面能自己定制。我目前项目里的主力芯片就是 SoftMAC 架构,走的是 SDIO 总线,配合 mac80211 架构来写驱动。
这里必须提醒一句:选 FullMAC 还是 SoftMAC,不是单纯看谁的代码好写,还要看产品需求。比如你想要支持 mesh、支持自定义帧,那 FullMAC 芯片往往做不了太深的定制;反过来如果你只想要一个简单的 STA 模式,用 SoftMAC 会把简单的功能复杂化,反而没那个必要。
2. 开发前必须搞定的环境与配置
2.1 内核源码、交叉编译工具链的搭配问题
开始动手写驱动之前得先把环境收拾利索,这一步我现在都会提前确认好。要先确定目标板子的内核版本,然后在 PC 上建一个版本完全对应的内核源码树,千万不要随便拿一个高版本内核去编低版本驱动。内核接口变化非常频繁,struct net_device_ops里的回调几乎每个大版本都会动,你拿 5.10 的代码编出来,放到 6.1 的内核上,编译报错只是最轻的惩罚,最怕的是编译通过但运行崩溃。
交叉编译工具链也尽量用厂商 SDK 自带的版本,或者与目标系统 glibc 匹配的版本。之前一个项目里我偷懒用了一个较新的通用工具链去编一个老的 BSP 内核,结果在内核模块加载时直接报version magic不匹配,白白浪费半天时间。内核编译前,make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig这一步是少不了的,配置结果保存到 .config 之后,一定要记得看include/generated/autoconf.h里是否确实生成了预期宏开关。
2.2 内核配置里那些容易漏掉的无线选项
写 WiFi 驱动时内核配置是最容易出问题的地方。很多人只关注自己的驱动目录有没有选中,往往忽略了下层依赖。以经典配置为例,必须打开CONFIG_NET、CONFIG_WIRELESS、CONFIG_CFG80211,如果你用的是 SoftMAC 芯片,还要打开CONFIG_MAC80211。这三个选项是地基,任何 WiFi 驱动都绕不开。
接着还要看总线和协议相关的配置。假设你的模组挂在 SDIO 总线上,那CONFIG_MMC、CONFIG_MMC_SDIO要确保开着;如果是 USB 接口,CONFIG_USB、CONFIG_USB_NET_DRIVERS得检查;如果走 PCIe 接口,CONFIG_PCI相关支持避免不了。这些看起来无关紧要,但少了任何一个,板子启动后都可能出现设备枚举正常但驱动 probe 不进去的诡异情况。
再往下走就是具体厂商支持的选项了。比如用瑞昱芯片,得开CONFIG_RTL8822CS或相关的驱动选项;用某些国产芯片,也可能要开对应的 vendor 选项。最好在make menuconfig里用斜杠搜索一下芯片名字,确认对应的 Kconfig 项到底叫什么,然后在代码里看一下依赖关系。我曾经遇到一个情况:驱动模块编出来了,insmod的时候报 symbol 找不到,最后发现是因为 cfg80211 这个依赖模块没编进内核,驱动里的符号解析全部失败。
2.3 设备树描述:让内核找到硬件
现在的嵌入式平台,基本都在设备树里描述硬件是怎么接的,WiFi 芯片也不例外。设备树节点写错了,后面怎么调试都是白费。平时我会把 WiFi 芯片接在哪个总线、挂在哪个控制器下,先梳理清楚,再去写设备树,顺序不能反。
以 SDIO WiFi 为例,设备树节点一般在 mmc 控制器的子节点上,关键属性有compatible、reg、interrupts、vmmc-supply、reset-gpio等。compatible要跟驱动里of_device_id数组里的字符串严格匹配,这个是最容易看漏的。之前调试一块模组,dmesg里一直报 "sdio: mmc0:0001:1: unknown device",最后查来查去是compatible里少了一个通配符,系统在总线上识别到了卡,但不知道把这张卡交给哪个驱动去 probe。
设备树里还要注意电源和复位引脚的时序。有些 WiFi 芯片必须先上电再拉复位,或者要求复位信号保持一段低电平时间。如果不在设备树里正确描述power-gpio或reset-gpio控制逻辑,芯片启动一半就挂,驱动加载像个”神经病“一样时好时坏。这块建议拿到硬件原理图后先对着把引脚关系写清楚,再写代码。
- 核心代码结构:从probe到收发数据包
3.1 驱动入口与生命周期管理
一个标准的 WiFi 驱动,入口函数里要做的事情不多,真正复杂的是probe。入口无非是module_init、module_exit加总线驱动结构体注册,比如 SDIO 驱动就是注册一个struct sdio_driver,里面最关键的两个成员是id_table和probe。
probe函数是整个驱动的核心舞台。以 SDIO WiFi 驱动为例,流程大致是:先用sdio_func使能功能号,读取芯片寄存器确认硬件版本,申请struct ieee80211_hw或struct wiphy(取决于你的架构),设置信道数、频段、比特率、天线配置等能力,再调用mac80211或cfg80211的注册接口把能力告诉内核。最后还要初始化中断、任务队列、DMA 缓冲池,并注册net_device。
这段流程里最需要注意的就是回调函数的注册时机。你注册完 wiphy 之后,内核随时可能调用你的回调函数,比如用户态执行iw dev wlan0 scan,你的.scan回调瞬间就会触发。如果这时候内部数据结构还没初始化好,那基本就是空指针解引用,直接 kernel panic。所以我一般先把所有内部状态准备好,最后一步才去注册到内核框架里,顺序一定不能反过来。
3.2 数据收发路径和中断管理
数据收发是 WiFi 驱动最核心的环节。发送方向,上层协议栈调用ndo_start_xmit,驱动在这里拿到struct sk_buff,然后要根据芯片协议加上头、分片、塞到 DMA 描述符里,触发硬件发送。发送完之后,还要在完成中断里把 skb 释放掉,否则内核会把你的memleak账单记在小本本上。
接收方向要复杂一些,因为 WiFi 上的接收帧不光是数据帧,还有管理帧和控制系统帧。通常在硬件收到数据后会产生中断,中断处理函数里要识别帧类型,如果是对应本机的数据帧,就封装成sk_buff上报给 mac80211/cfg80211,如果是管理帧,要直接丢给协议栈处理。为了降低中断压力,很多驱动会用 NAPI 机制,把收包从硬中断延后到软中断轮询里处理,效率会高不少。
这块的坑主要集中在 DMA 对齐和字节序问题上。WiFi 帧头是 802.11 格式,网络协议栈期望的是 802.3 格式,转换过程要在驱动里做,做不好就会出现”能抓包但 ping 不通”的诡异现象。另外 DMA 缓冲区最好用kmalloc分配的时候特别留意对齐,有些芯片要求起始地址 4 字节或者 8 字节对齐,你稍微偏一点,收上来的数据就花屏式地乱码。
3.3 驱动和用户态工具怎么协作
写完驱动不代表完事,还得让上层工具跟你配合好。WiFi 驱动在整个系统中不是孤立的,用户在图形界面里点击连接 WiFi,实际动作是 NetworkManager 调用 wpa_supplicant,wpa_supplicant 通过 nl80211 的 socket 与内核里的 cfg80211 通信,最终到达你的驱动回调。
这是驱动开发中很多人容易忽略的一环:你在驱动里转发给上层的扫描结果,格式必须符合 nl80211 对scan results的属性要求。如果驱动里上报的 SSID 是乱码、或者 signal 强度显示为负数加符号错位,用户层根本连不上。我建议在调试驱动基本能力时,先不用图形界面,直接命令行iw dev wlan0 scan、wpa_supplicant -Dnl80211 -iwlan0 -c.conf来验证,排除了上层界面的干扰,定位驱动问题会快很多。
4. 调试实录:那些年我踩过的WiFi驱动坑
4.1 启动阶段:设备识别不了、驱动没绑上
最让人崩溃的问题往往发生在启动早期:内核起来了,但 WiFi 设备根本没有出现在系统里。这时候先别急着看驱动代码,先确认硬件到底有没有被总线枚举到。SDIO 设备可以查看/sys/bus/sdio/devices/,USB 设备看lsusb,PCIe 设备看lspci -v。如果设备节点都不存在,问题大概率在硬件连接、供电、时钟或者设备树描述上。
这里要特别提一句 hot word 里那个 unclaimed 现象。我在调一块 PCIe 接口的 WiFi 网卡时,lspci显示设备状态是”unclaimed“,说明内核总线上看到了这个硬件,但没有驱动愿意认领它。这种情况要么是驱动没编进内核,要么是pci_device_id表里没有匹配的 ID,要么是设备树没给对应的 compatible。解决办法是先确认硬件 ID 和驱动源码里的 ID 表是否一致,再加打印定位驱动 probe 有没有被调用。
4.2 运行阶段:搜不到AP、连不上、速率极低
设备识别了,驱动也 probe 了,但iw dev wlan0 scan搜不到任何热点,这是第二个大坑。常见原因是固件没有正确加载。很多 WiFi 芯片是”无脑”的射频前端,真正的协议逻辑在固件里,驱动 proc 的时候要把固件从文件系统读到芯片内部。固件路径一般放在/lib/firmware/,如果文件名、版本不匹配,驱动通常会报错或者直接挂掉,dmesg里会有明确提示。
搜到热点但是连不上,则要检查信道和区域代码。Linux 的无线网络有监管域(regulatory domain)的概念,不同国家允许使用的信道不一样。驱动默认可能会把某些信道锁住,如果你所在区域和默认值不一样,扫描和连接都会有影响。执行iw reg get看看当前 region,必要时用iw reg set CN临时设置,我遇到过信道在国外是可用的、国内被禁用的奇葩案例,开发阶段要特别注意这一点。
速率低的问题,大概率跟天线和调制有关。先查iw dev wlan0 link里的带宽、RX/TX 速率是否异常。驱动里很多调制参数是表驱动的,比如 MCS 速率表配置错一位,最高速率就被锁在很低的挡位。另外还要确认有没有开启 40MHz 带宽或 80MHz 带宽的支持,设备树或驱动常量里经常默认只开 20MHz,速率自然上不去。
4.3 界面层问题:系统有网却显示无WiFi图标
还有一个让我印象很深的场景:板子用 NetworkManager 管理网络,WiFi 能上网,但桌面右上角就是没有 WiFi 图标,或者显示已连接但图标是个叉。这种问题很多人会误判为驱动 bug,折腾半天驱动,其实跟内核驱动关系不太大。
关键点在于 NetworkManager 如何感知设备状态。它很多时候依赖/sys/class/net/wlan0/device是否存在对应的驱动符号链接,以及rfkill状态。如果 rfkill 把 WiFi 软件屏蔽了,界面就显示无 WiFi,但网络依然能通。排查方式是用rfkill list查看状态,用rfkill unblock all解锁。还有一个原因,是 NetworkManager 的”已连接但无图标“,通常因为 NM 认为设备处于”受限“状态,需要检查 IP 获取、网关、DNS 等是否完全正常,我见过只配了 IP 没配网关导致图标异常的案例。
4.4 问题排查速查表与实用技巧
调 WiFi 驱动时我习惯按下面这个顺序查问题,效率比乱试高很多。总结成一张表,方便你直接对照。
| 现象 | 优先排查方向 | 常用命令/工具 |
|---|---|---|
| 设备完全不存在 | 硬件供电、时钟、总线枚举、设备树 | ls /sys/bus/sdio/devices/、lsusb、lspci |
| 设备存在但无驱动绑定 | 驱动 ID 表、设备树 compatible、内核配置 | dmesg、cat /sys/bus/sdio/devices/*/modalias |
| probe 失败 | 固件路径、GPIO 时序、中断申请失败 | dmesg、cat /proc/interrupts |
| 搜不到 AP | 固件加载、天线开关、监管域、扫描回调 | iw dev wlan0 scan、iw reg get、dmesg |
| 能搜到但连不上 | 认证方式、密码协议、信道限制、wpa_supplicant | wpa_supplicant -dd、iw dev wlan0 connect |
| 能连上但网速低 | 带宽设置、MCS 表、天线、电源管理 | iw dev wlan0 link、iperf3 |
| 界面不显示 WiFi | rfkill 状态、NetworkManager 状态、路由/DNS | rfkill list、nmcli dev status、ip route |
最后分享几个我自己的调试习惯。第一,重要阶段都加 pr_info 打印,并且保留一个“调试开关”,正式版关掉,开发版打开,不用频繁改代码。第二,不要迷信 dmesg 一行行盯着看,建议在驱动里注册一个 debugfs 节点,把寄存器状态、连接状态、信号强度全部导出来,cat一下就能看到,比反复加打印高效太多。第三,WiFi 驱动调不通时,先检查 SDIO 总线是否能稳定读写底层寄存器,再谈协议栈。总线都没通就冲进 802.11 的协议堆里,只能越调越乱。
我做 WiFi 驱动开发这几年,最深的感觉是:这活儿七分靠硬件,三分靠代码。很多看似玄学的驱动问题,追到根上都是供电纹波、时钟抖动、引脚虚焊这类基础问题。所以拿到一块新模组,别急着写代码,先把硬件量一遍,把芯片手册里的上电时序、初始化序列和寄存器默认值过一遍,再动手。等把底层通信调稳了,你就会发现 mac80211 那些回调其实都挺老实的,照着框架填就是了。驱动开发的乐趣,很大程度就在于把一团乱麻慢慢理成一条清晰路径的过程。