2. Wi-Fi 驱动框架解析:Linux 无线子系统与 Tina 平台适配

做 Wi-Fi 驱动开发,第一件事不是看芯片手册,而是搞懂 Linux 无线子系统的分层结构。说白了,你得知道你的代码站在哪一层,上下分别跟谁打交道。

我刚开始接触这块时,也犯过迷糊——明明在应用层调了 iwconfig,怎么底层寄存器就变了?后来把整个框架捋了一遍,才明白中间隔了多少层抽象。今天我们就一层层拆开来看。

2.1 整体架构:三层两接口

Linux 无线子系统可以概括为「三层两接口」:

  • 上层:用户空间工具(iw、wpa_supplicant、hostapd)
  • 中间层:cfg80211 和 mac80211(内核无线框架)
  • 下层:驱动适配层 + 总线接口(SDIO/USB/PCIe)

上下层之间通过 nl80211 和 ieee80211_ops 两个关键接口通信。我习惯把 nl80211 比作「命令通道」,把 ieee80211_ops 比作「硬件操作表」。

Linux 无线子系统三层架构图 Linux 无线子系统三层架构 用户空间 iw / wpa_supplicant / hostapd nl80211 命令通道 内核无线框架 cfg80211(策略/监管) mac80211(协议/软件 MAC) ieee80211_ops 回调表 驱动适配层 Tina 平台适配 / 芯片厂商 HAL SDIO / USB / PCIe 总线 Wi-Fi 芯片硬件 关键接口:nl80211(用户↔内核) + ieee80211_ops(框架↔驱动)

2.2 cfg80211:策略与监管中心

cfg80211 是内核无线配置管理层。它不直接操作硬件,而是负责:

  • 监管合规:不同国家/地区的信道、功率限制
  • 连接管理:扫描、关联、认证的流程控制
  • 虚拟接口:支持多 vif(station、AP、monitor 模式)
  • 扫描结果缓存:避免重复扫描

我踩过的坑:有一次客户反馈 5G 信道扫描不到。查了半天,发现是 cfg80211.regdom 默认设成了 CN,而 CN 的 5G 信道列表跟 FCC 不一样。后来我在驱动初始化时显式调了 regulatory_hint(),问题解决。

cfg80211 通过 struct cfg80211_ops 向下注册回调。驱动需要实现 add_virtual_intf、change_virtual_intf、scan、connect 等接口。你想想看,如果没有这层抽象,每个驱动都要自己处理监管列表——那得多乱。

2.3 mac80211:软件 MAC 协议引擎

mac80211 是 Linux 无线子系统的「心脏」。它实现了 802.11 MAC 层的核心协议逻辑,包括:

  • 帧处理:封装/解封装 802.11 帧头
  • 速率控制:minstrel_ht 算法选择最佳 MCS
  • 电源管理:PS-Poll、U-APSD 等省电机制
  • TX/RX 队列管理:多优先级队列调度

驱动通过 struct ieee80211_ops 向 mac80211 注册硬件操作。我列几个最常用的回调:

回调函数 作用 调用时机
start 启动硬件 网络设备 up 时
stop 停止硬件 网络设备 down 时
add_interface 创建虚拟接口 创建 vif 时
config 配置信道/功率 切换信道或调整功率时
tx 发送数据帧 有数据要发送时
sta_add 添加 station 信息 新 station 关联时

个人习惯:调试 mac80211 驱动时,我总会在 tx 和 config 回调里加 print_hex_dump,打印前 64 字节。这样能快速判断帧结构是否正确,信道是否切到位。

2.4 Tina 平台驱动适配层

Tina Linux 在标准 Linux 无线框架之上,加了一层平台适配。为什么需要这层?说白了,Allwinner 的 SoC 有自己的一套时钟、GPIO、电源管理逻辑,标准驱动没法直接复用。

适配层主要做三件事:

  1. 硬件资源抽象:把 SoC 的 GPIO、IRQ、DMA 通道封装成平台设备
  2. 电源管理桥接:对接 Tina 的 sunxi-pm 框架,实现 Wi-Fi 休眠唤醒
  3. 固件加载:从文件系统加载芯片固件,并完成初始化序列

我举个例子。在 Tina 上适配 RTL8822CS 时,驱动适配层需要实现:

// 平台设备结构体
static struct platform_driver rtl8822cs_sdio_driver = {
    .probe  = rtl8822cs_sdio_probe,
    .remove = rtl8822cs_sdio_remove,
    .driver = {
        .name = "rtl8822cs",
        .pm   = &rtl8822cs_pm_ops,  // 挂起/恢复
    },
};

// 电源管理回调
static int rtl8822cs_suspend(struct device *dev) {
    // 保存寄存器状态
    // 关闭 Wi-Fi 时钟
    // 通知 mac80211 进入休眠
    ieee80211_suspend(hw);
    return 0;
}

注意:Tina 适配层跟芯片厂商的 HAL 是两层。厂商 HAL 管芯片内部寄存器,Tina 适配层管 SoC 侧资源。两者分工明确,别混在一起写。

2.5 SDIO 与 USB 接口驱动

Wi-Fi 芯片跟主控通信,最常见的就是 SDIO 和 USB。我两种都调过,说说区别。

2.5.1 SDIO 接口

SDIO 是嵌入式 Wi-Fi 的主流选择。它复用 SD 卡控制器,速度快(SDIO 3.0 可达 200MB/s),而且支持 in-band interrupt——芯片有事件时直接拉中断线,不需要轮询。

SDIO 驱动的核心是注册一个 sdio_driver:

static const struct sdio_device_id rtl8822cs_sdio_ids[] = {
    { SDIO_DEVICE(0x024c, 0xb822) },  // VID:PID
    { /* end */ },
};

static struct sdio_driver rtl8822cs_sdio_drv = {
    .name     = "rtl8822cs",
    .id_table = rtl8822cs_sdio_ids,
    .probe    = rtl8822cs_sdio_probe,
    .remove   = rtl8822cs_sdio_remove,
};

probe 函数里要做的事:

  • 使能 SDIO 功能(sdio_enable_func)
  • 设置块大小(sdio_set_block_size)
  • 申请中断(sdio_claim_irq)
  • 分配 ieee80211_hw 结构体

2.5.2 USB 接口

USB 接口的 Wi-Fi 芯片(比如 RTL8811CU)走的是 usb_driver 框架。USB 的好处是即插即用,但延迟比 SDIO 高一些。

USB 驱动需要处理 URB(USB Request Block) 的提交和回调。我调 USB Wi-Fi 时最头疼的是 端点配置——不同芯片的 bulk 端点、interrupt 端点编号不一样,得仔细看 datasheet。

static struct usb_device_id rtl8811cu_table[] = {
    { USB_DEVICE(0x0bda, 0xc811) },
    { /* end */ },
};

static int rtl8811cu_probe(struct usb_interface *intf,
                           const struct usb_device_id *id) {
    // 获取端点描述符
    struct usb_host_interface *iface_desc = intf->cur_altsetting;
    // 遍历端点,找到 bulk in/out 和 interrupt 端点
    for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        // 保存端点地址到私有结构体
    }
    // 注册 mac80211 硬件
}

我曾经踩过的坑:USB Wi-Fi 在 Tina 上经常遇到 usb_submit_urb 返回 -ENODEV。后来发现是 USB 控制器驱动没开 CONFIG_USB_EHCI_HCD。嗯,内核配置检查一遍,问题就没了。

2.6 驱动初始化流程总结

我把整个驱动初始化流程画成了一张图,方便你对照代码看:

Wi-Fi 驱动初始化流程图 Wi-Fi 驱动初始化流程 模块加载(module_init) 总线匹配(SDIO/USB probe) 分配 ieee80211_hw 注册 mac80211(ieee80211_register_hw) 网络设备注册(register_netdev) 驱动就绪,等待用户空间操作

嗯,到这里,Wi-Fi 驱动框架的核心脉络就清楚了。从用户空间的 iw 命令,一路穿透到芯片寄存器,中间每一层都有明确的职责。你在 Tina 上做驱动移植时,只要对照这张图,就知道该改哪一层、不该动哪一层。

我个人建议:拿到一个新芯片的驱动源码,先别急着编译。花半小时把 ieee80211_ops 里的回调函数列表过一遍,看看哪些实现了、哪些是空的。空的回调往往就是后面出问题的地方。