4、Wi-Fi驱动框架解析:Linux无线子系统与驱动分层模型

各位同学,今天我们来啃一块硬骨头——Linux的无线子系统。说实话,我刚接触这块的时候也被绕晕过。cfg80211、mac80211、还有底下的SDIO、USB、SPI接口,一层套一层,像个俄罗斯套娃。但等你真正搞懂了这套分层设计,你会发现它其实非常优雅。

咱们这节课的目标很明确:搞清楚Wi-Fi驱动在Linux里到底是怎么组织的,每一层负责什么,以及我们做移植时最需要关注哪些点。

4.1 整体架构:三层两接口

Linux无线子系统可以概括为“三层两接口”。我画了张图,你先有个整体印象。

Linux Wi-Fi 驱动分层架构 用户空间:wpa_supplicant / iw / NetworkManager cfg80211 — 内核无线配置管理层 mac80211 — 软件MAC层(帧处理/协议) 硬件驱动层:具体芯片驱动(如ath9k、brcmfmac、rtlwifi) SDIO USB SPI Wi-Fi 芯片硬件(SDIO/USB/SPI 接口) 用 户 空 间 内 核 空 间

从用户空间到底层硬件,数据是这样流动的:

  1. 用户空间工具:wpa_supplicant、iw、NetworkManager 等,通过 netlink 与内核通信。
  2. cfg80211:内核无线配置管理层。它定义了一套标准API,供用户空间调用。
  3. mac80211:软件MAC层。负责802.11帧处理、协议栈逻辑。
  4. 驱动层:具体芯片的驱动,比如ath9k、brcmfmac、rtlwifi。
  5. 总线层: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位并行传输,速度很快。

驱动开发时,你需要:

  1. 注册一个SDIO驱动(sdio_driver)。
  2. 在探测函数中,初始化Wi-Fi芯片(下载固件、设置寄存器)。
  3. 通过 sdio_readb/sdio_writeb 等函数读写芯片寄存器。
  4. 注册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驱动初始化流程梳理一下,你照着这个顺序去读代码,会清晰很多:

  1. 总线探测:内核发现设备,调用驱动的probe函数。
  2. 硬件初始化:复位芯片、下载固件、读取MAC地址。
  3. 分配ieee80211_hw:调用 ieee80211_alloc_hw。
  4. 设置硬件参数:填充 ieee80211_hw 的成员,如信道数、支持的数据率、加密方式等。
  5. 注册mac80211:调用 ieee80211_register_hw。
  6. 注册cfg80211:mac80211内部会自动注册cfg80211,你不需要手动操作。
  7. 创建虚拟接口:用户通过iw命令创建wlan0。
  8. 扫描/连接: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 数据路径:收发包的完整旅程

理解了初始化,我们再看看数据是怎么走的。以发送为例:

  1. 用户程序通过socket发送数据。
  2. 内核网络栈处理,交给mac80211。
  3. mac80211封装802.11头部,选择速率,分配序列号。
  4. 调用驱动的 tx 回调。
  5. 驱动把帧写入硬件发送队列。
  6. 硬件通过SDIO/USB/SPI把数据发出去。

接收方向反过来:

  1. 硬件收到数据,通过中断或DMA通知驱动。
  2. 驱动从硬件读取数据。
  3. 调用 ieee80211_rx 把帧交给mac80211。
  4. mac80211解析头部,去除802.11封装,交给网络栈。
  5. 最终到达用户程序。

性能提示:接收路径上,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流程。下次我们讲具体怎么移植一个模组,到时候这些知识全都会用上。