4、Wi-Fi驱动框架解析:Linux无线子系统与驱动分层模型
各位同学,今天我们来啃一块硬骨头——Linux的无线子系统。说实话,我刚接触这块的时候也被绕晕过。cfg80211、mac80211、还有底下的SDIO、USB、SPI接口,一层套一层,像个俄罗斯套娃。但等你真正搞懂了这套分层设计,你会发现它其实非常优雅。
咱们这节课的目标很明确:搞清楚Wi-Fi驱动在Linux里到底是怎么组织的,每一层负责什么,以及我们做移植时最需要关注哪些点。
4.1 整体架构:三层两接口
Linux无线子系统可以概括为“三层两接口”。我画了张图,你先有个整体印象。
从用户空间到底层硬件,数据是这样流动的:
- 用户空间工具:wpa_supplicant、iw、NetworkManager 等,通过 netlink 与内核通信。
- cfg80211:内核无线配置管理层。它定义了一套标准API,供用户空间调用。
- mac80211:软件MAC层。负责802.11帧处理、协议栈逻辑。
- 驱动层:具体芯片的驱动,比如ath9k、brcmfmac、rtlwifi。
- 总线层:SDIO、USB、SPI,负责物理数据传输。
核心要点:cfg80211和mac80211是内核提供的通用框架,驱动开发者只需要实现底层的硬件操作函数。说白了,内核把大部分脏活累活都干了,你只需要填好那几个回调函数。
4.2 cfg80211:无线配置的“总开关”
cfg80211是Linux 2.6.33之后引入的,目的是统一无线配置接口。以前每个驱动都有自己的ioctl,乱得很。cfg80211把这些全部标准化了。
它主要干这几件事:
- 扫描管理:处理用户发起的扫描请求,返回扫描结果。
- 连接管理:关联、认证、去关联等操作。
- 监管域管理:不同国家有不同信道限制,cfg80211负责合规。
- 虚拟接口管理:支持多个虚拟无线接口(比如同时做AP和Station)。
驱动需要注册一个 struct cfg80211_ops 结构体,里面全是函数指针。我列几个最关键的:
| 回调函数 | 功能 | 备注 |
|---|---|---|
start_ap |
启动AP模式 | 热点功能必备 |
stop_ap |
停止AP模式 | — |
add_key |
添加加密密钥 | WPA/WPA2需要 |
set_channel |
设置工作信道 | 频率切换 |
scan |
发起扫描 | 被动/主动扫描 |
connect |
发起连接 | Station模式 |
disconnect |
断开连接 | — |
我的经验:做移植时,set_channel 和 scan 这两个回调最容易出问题。我曾经在一个SDIO模组上,扫描回调返回的数据格式不对,导致wpa_supplicant一直拿不到BSS列表。查了两天才发现是扫描结果上报时少填了一个字段。
4.3 mac80211:软件MAC的“心脏”
mac80211是软件MAC层的实现。什么叫软件MAC?就是802.11协议栈中,那些不需要硬件加速的部分,由内核软件来完成。
mac80211负责:
- 帧封装/解封装:添加/去除802.11头部。
- 序列号管理:每个数据帧分配序列号。
- 重传逻辑:确认帧超时后自动重传。
- 速率控制:选择合适的调制编码方案。
- 电源管理:PS-Poll、U-APSD等。
驱动需要实现 struct ieee80211_ops,这个结构体比cfg80211的更底层。举个例子:
static const struct ieee80211_ops my_wifi_ops = {
.tx = my_wifi_tx,
.start = my_wifi_start,
.stop = my_wifi_stop,
.add_interface = my_wifi_add_interface,
.remove_interface = my_wifi_remove_interface,
.config = my_wifi_config,
.configure_filter = my_wifi_configure_filter,
.set_key = my_wifi_set_key,
.set_rts_threshold = my_wifi_set_rts_threshold,
};
这里最核心的是 tx 回调——它负责把mac80211组装好的帧发送到硬件。而接收方向,驱动通过 ieee80211_rx() 函数把收到的帧喂给mac80211。
注意:mac80211的 tx 回调是在原子上下文调用的。你不能在里面做睡眠操作,比如等待硬件响应。我曾经在 tx 里加了个msleep,结果系统直接死锁。嗯,血的教训。
4.4 驱动分层模型:SDIO vs USB vs SPI
Wi-Fi芯片和主控之间的连接方式,常见的有三种:SDIO、USB、SPI。每种总线有各自的驱动模型和适配方式。
| 特性 | SDIO | USB | SPI |
|---|---|---|---|
| 吞吐量 | 高(理论可达200Mbps+) | 中高(USB 2.0: 480Mbps) | 中低(通常50Mbps以下) |
| 实时性 | 较好 | 一般(受USB调度影响) | 好(无协议开销) |
| 驱动复杂度 | 中等 | 较低(USB核心层封装好) | 较高(需自行处理DMA/中断) |
| 典型芯片 | Broadcom BCM43xx、Marvell 88W88xx | RTL8188EU、MT7601U | ESP8266(AT模式)、某些IoT Wi-Fi模组 |
| Linux子系统 | mmc子系统(sdio驱动) | usb子系统(usb驱动) | spi子系统(spi驱动) |
4.4.1 SDIO接口
SDIO是嵌入式设备上最常见的Wi-Fi接口。它基于SD卡协议扩展,支持4位并行传输,速度很快。
驱动开发时,你需要:
- 注册一个SDIO驱动(
sdio_driver)。 - 在探测函数中,初始化Wi-Fi芯片(下载固件、设置寄存器)。
- 通过
sdio_readb/sdio_writeb等函数读写芯片寄存器。 - 注册mac80211和cfg80211的回调。
我记得第一次调SDIO Wi-Fi时,芯片一直不响应CMD52。后来发现是SDIO时钟频率太高,芯片跟不上。降到25MHz就好了。这种坑,你不踩一次真的记不住。
4.4.2 USB接口
USB Wi-Fi适配器大家都很熟悉。Linux下USB驱动相对好写,因为USB核心层提供了urb(USB Request Block)机制,你只需要填充urb然后提交。
关键步骤:
- 注册
usb_driver,匹配VID/PID。 - 在探测函数中,分配urb、设置端点。
- 发送时:填充urb,调用
usb_submit_urb。 - 接收时:在urb完成回调中,把数据传给
ieee80211_rx。
避坑指南:USB Wi-Fi驱动里,urb的缓冲区必须用 usb_alloc_coherent 分配,不能用kmalloc。否则在某些平台上会出现DMA对齐问题,导致数据错位。我曾经因为这个,抓包发现所有帧的FCS校验都不过,折腾了一整天。
4.4.3 SPI接口
SPI接口在IoT设备上比较常见,比如ESP8266通过SPI和主控通信。SPI驱动开发最麻烦的地方在于:没有像USB那样的高层封装,所有协议都得自己写。
你需要处理:
- SPI片选控制。
- SPI时钟极性/相位配置。
- 数据帧格式定义(比如4字节头+数据体)。
- 中断处理(通常用GPIO中断通知主控有数据)。
SPI的吞吐量虽然不高,但延迟很低,适合对实时性要求高的场景。
4.5 驱动注册流程:从probe到联网
我把一个典型的Wi-Fi驱动初始化流程梳理一下,你照着这个顺序去读代码,会清晰很多:
- 总线探测:内核发现设备,调用驱动的probe函数。
- 硬件初始化:复位芯片、下载固件、读取MAC地址。
- 分配ieee80211_hw:调用
ieee80211_alloc_hw。 - 设置硬件参数:填充
ieee80211_hw的成员,如信道数、支持的数据率、加密方式等。 - 注册mac80211:调用
ieee80211_register_hw。 - 注册cfg80211:mac80211内部会自动注册cfg80211,你不需要手动操作。
- 创建虚拟接口:用户通过iw命令创建wlan0。
- 扫描/连接:wpa_supplicant发起扫描,驱动上报结果,然后连接。
// 伪代码:驱动probe函数的核心流程
static int my_wifi_probe(struct sdio_func *func, const struct sdio_device_id *id)
{
struct ieee80211_hw *hw;
struct my_wifi_priv *priv;
// 1. 分配ieee80211_hw结构体
hw = ieee80211_alloc_hw(sizeof(*priv), &my_wifi_ops);
if (!hw)
return -ENOMEM;
priv = hw->priv;
priv->func = func;
sdio_set_drvdata(func, priv);
// 2. 初始化硬件
my_wifi_hw_init(priv); // 下载固件、设置寄存器
// 3. 设置硬件能力
hw->wiphy->interface_modes = BIT(NL80211_IFTYPE_STATION) |
BIT(NL80211_IFTYPE_AP);
hw->channel_change_time = 100; // 100ms
hw->queues = 4;
// 4. 注册到mac80211
ret = ieee80211_register_hw(hw);
if (ret)
goto err_free_hw;
return 0;
err_free_hw:
ieee80211_free_hw(hw);
return ret;
}
关键点:ieee80211_alloc_hw 的第二个参数是 ieee80211_ops,这个结构体定义了驱动如何与mac80211交互。第三个参数是私有数据的大小,驱动自己的数据结构就放在这里。
4.6 数据路径:收发包的完整旅程
理解了初始化,我们再看看数据是怎么走的。以发送为例:
- 用户程序通过socket发送数据。
- 内核网络栈处理,交给mac80211。
- mac80211封装802.11头部,选择速率,分配序列号。
- 调用驱动的
tx回调。 - 驱动把帧写入硬件发送队列。
- 硬件通过SDIO/USB/SPI把数据发出去。
接收方向反过来:
- 硬件收到数据,通过中断或DMA通知驱动。
- 驱动从硬件读取数据。
- 调用
ieee80211_rx把帧交给mac80211。 - mac80211解析头部,去除802.11封装,交给网络栈。
- 最终到达用户程序。
性能提示:接收路径上,ieee80211_rx 是在软中断上下文中调用的。你不能在这里做耗时操作,否则会拖垮整个网络吞吐。我见过有人在这里加打印调试,结果吞吐从50Mbps掉到5Mbps。
4.7 移植要点总结
最后,我把做Wi-Fi驱动移植时最常遇到的几个坑列出来,你心里有个数:
| 问题 | 现象 | 常见原因 |
|---|---|---|
| 扫描不到AP | iw dev wlan0 scan 返回空 | 扫描回调未实现或信道设置错误 |
| 连接失败 | wpa_supplicant反复尝试连接 | 认证帧处理不对,或密钥设置回调缺失 |
| 吞吐量低 | iperf测试只有几Mbps | DMA对齐问题、中断处理太慢、速率控制未生效 |
| 设备无法识别 | dmesg无输出或报错 | 总线驱动未加载、VID/PID不匹配、电源未使能 |
| 固件加载失败 | 驱动probe时卡住或报错 | 固件路径不对、固件版本不匹配、SDIO时钟太快 |
嗯,这节课内容不少。你先把架构图记在脑子里,然后找一份真实的驱动代码(比如ath9k或brcmfmac),对照着看一遍probe和tx/rx流程。下次我们讲具体怎么移植一个模组,到时候这些知识全都会用上。