ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux WiFi驱动开发实战:从SDIO移植到mac80211框架

Linux WiFi驱动开发实战:从SDIO移植到mac80211框架 1. 从零开始撸一块WiFi驱动先搞懂它在内核里的位置做Linux驱动开发有些年头的人应该都有体会WiFi驱动在整套内核驱动体系里属于“看着不难、上手就懵”的典型。字符驱动、块驱动、网络驱动各有各的门道而WiFi设备驱动横跨了总线接口、无线子系统、协议栈、电源管理好几个层面涉及的知识面非常广。这篇文章从一个实际的WiFi设备驱动开发项目切入把整个开发过程中涉及的核心框架、调试手段、容易出现的问题做一个系统的梳理。先说清楚这篇文章适合谁。如果你已经写过几个字符设备驱动对file_operations、platform_driver这类基础概念不陌生但一碰到无线设备就不知道怎么下手那这篇文章就是给你准备的。如果你连驱动的注册流程都还没完全理顺文章里也会把必要的基础知识讲清楚不会让你看得一头雾水。对于做嵌入式Linux的老手来说WiFi驱动这块的调优经验和疑难杂症处理多少也能带来一些参考。WiFi设备驱动的特殊性在哪儿它对时序的要求比普通设备高得多。GPIO控制个LED延时几十毫秒完全无所谓但WiFi芯片的固件加载、寄存器操作、中断处理、数据收发任何一个环节时序不对轻则吞吐率暴跌重则整个系统直接死机。这也是为什么市面上的成熟方案大多是买芯片厂商的SDK而不是从头写驱动。可一旦你买的模块方案比较冷门或者厂商的驱动和你的内核版本不兼容你就必须得亲自动手上阵。我接手这个项目时的处境估计不少同行也经历过。板子上换了一颗比较小众的WiFi 6芯片厂商提供的内核补丁只支持4.19而我们的产品主线内核是5.15直接打补丁根本编译不过去。与其花时间找人定制不如自己动手移植这才有了后面一整套的调试经验。写这篇文章算是把这些经验做个沉淀。2. 驱动开发的三个前置问题总线、协议、框架2.1 决定驱动形态的第一件事你的WiFi芯片挂在什么总线上很多人一上来就找数据手册、翻寄存器手册其实第一步应该先搞清楚你的WiFi芯片是通过什么方式和主控连接的。这是决定整个驱动代码结构的根本问题。常见的连接方式有这么几种SDIO接口嵌入式设备上最常见的WiFi连接方式。博通、瑞昱、联发科的很多芯片都走SDIO一个SDIO接口可以同时复用SD卡和WiFi。特点是管脚少、速率够用但调试起来比USB麻烦因为SDIO的协议栈比较复杂。USB接口USB WiFi网卡大家都用过即插即用的特性让它们在PC和开发板上都很流行。USB WiFi驱动用的是内核的usb_driver框架写法和普通USB设备驱动一脉相承。调试比SDIO方便得多USB协议分析仪一挂就能看到通信过程。PCIe接口高性能WiFi 6/6E网卡基本都走PCIe包括笔记本上用的Intel/AX系列网卡、桌面级网卡。PCIe提供的高带宽是WiFi 6高吞吐的基础。驱动框架自然走pci_driver和网卡驱动的写法很接近。SPI接口低端IoT WiFi模块偶尔会用到速率低但实现简单SPI从机的时序控制比较考验功力。我在项目里遇到的这颗芯片走的是SDIO接口所以后续的代码和调试经验主要围绕SDIO展开但框架性的东西对其他总线同样适用。确认总线的办法不复杂。看芯片的手册或者直接看原理图上主控和芯片之间的连接。SDIO接口一定有CLK、CMD、DATA0-3这六条线USB接口一定有D/D-PCIe有REFCLK、TXP/TXN、RXP/RXN。看一眼原理图就清楚了。2.2 内核无线子系统演进cfg80211 和 mac80211 怎么分工确认完总线之后下一步就是搞懂内核无线子系统的工作机制。你写出来的驱动最终是要挂到这两个框架之下的。Linux内核的无线子系统经过十几年演进形成了现在的结构cfg80211处于上层的配置管理框架负责和用户空间打交道。iw、wpa_supplicant这些工具通过netlink和cfg80211通信完成扫描、连接、断开、设置信道、配置加密方式等操作。mac80211处于中层的MAC层实现。它实现了802.11协议栈的大部分逻辑包括帧管理、速率控制、电源管理等。对于驱动开发者来说mac80211提供了一组ieee80211_ops操作函数你的驱动只需要实现这些操作函数比如start、stop、config、add_interface、tx等等。驱动层最底层直接操作硬件。通过SDIO/USB/PCIe读写WiFi芯片的寄存器下发固件命令处理中断收发包。打个比方这相当于饭店的后厨分工。cfg80211是前台服务员负责接收客人用户程序的点单mac80211是后厨团队负责定菜谱、把握做菜流程驱动就是那个真正在灶台上颠勺的厨师知道火候多大、放多少盐。对驱动开发者来说绝大多数精力花在最底层的驱动实现上。mac80211已经把802.11协议栈那些让人头大的状态机处理得七七八八了你的任务就是把协议栈的指令翻译成芯片能懂的话。这大大减轻了开发量。如果没有mac80211每个驱动都得自己实现完整的MAC层协议处理工作量差好几倍。2.3 分清三种角色FullMAC、SoftMAC、以及介于两者之间的方案搞WiFi驱动开发的人还必需搞清楚一个概念你的芯片是FullMAC还是SoftMAC。FullMAC方案芯片硬件自己实现了完整的MAC层功能包括扫描、关联、帧管理、速率选择等。驱动只需要通过命令通道给芯片下发指令然后接收芯片上报的事件。这种方案的驱动代码相对简单但灵活性差因为协议栈的很多功能被固件封装了内核想管也管不着。老一些的博通SDIO芯片不少走这个路线驱动更像是一个命令转发器。SoftMAC方案芯片只做物理层的收发和基础的硬件加速MAC层的管理逻辑全部交给主机端的mac80211处理。驱动需要实现所有ieee80211_ops接口工作量大不少但可控制性极强内核升级后适配相对容易。主流WiFi 6芯片基本都走这条路。介于两者之间部分芯片搞了个折中底层帧管理在固件里做但允许主机接入部分管理功能。这种方案最考验驱动的实现质量。区分方法也很简单看驱动里有没有实现ieee80211_ops的start、config这类回调函数。如果这些回调需要你去填充并做实际硬件操作那就是SoftMAC。如果你只需要把命令打包发给固件就行那就是FullMAC。我做的那颗芯片恰好属于SoftMAC这意味着它的驱动代码比较庞大但上游内核的mac80211代码可以帮我们分担不少工作。这种类型的驱动在移植到新内核时主要工作量集中在对接内核API变化上而不是重写协议逻辑。3. WiFi驱动开发中绕不开的几个核心技术点3.1 固件加载驱动工作前的那第一道坎几乎所有WiFi芯片都需要外部固件才能工作。芯片内部只有一小段ROM bootloader负责从主机端加载真正的固件到芯片内存中。固件加载的过程看似简单实际是踩坑重灾区。固件通常存放在文件系统的/lib/firmware/目录下由内核的request_firmware()机制加载。驱动的probe函数里你大概会写出这样的流程static int wifi_chip_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct wifi_chip_priv *priv; const struct firmware *fw; int ret; priv kzalloc(sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; sdio_set_drvdata(func, priv); /* 第一步获取固件 */ ret request_firmware(fw, wifi_chip/fw.bin, func-dev); if (ret 0) { dev_err(func-dev, failed to request firmware\n); goto err_free_priv; } /* 第二步通过SDIO命令把固件写入芯片 */ ret wifi_chip_download_firmware(priv, fw-data, fw-size); release_firmware(fw); if (ret 0) { dev_err(func-dev, failed to download firmware\n); goto err_free_priv; } /* 第三步启动芯片主固件 */ ret wifi_chip_start_firmware(priv); if (ret 0) { dev_err(func-dev, failed to start firmware\n); goto err_free_priv; } /* 第四步注册无线设备 */ ret wifi_chip_register_ieee80211_hw(priv); if (ret 0) { dev_err(func-dev, failed to register ieee80211_hw\n); goto err_free_priv; } return 0; err_free_priv: kfree(priv); return ret; }这段代码看着不多坑全在第二步。SDIO接口的写操作有块大小的约束芯片固件下载过程中可能要求在写入之前先发送特定的握手命令而且不同芯片的固件格式千差万别。有的固件文件是加密的有的带文件头需要解析有的要先下载一个小引导程序、由引导程序再加载主固件。这些细节只能从芯片手册里慢慢抠。一个比较实用的经验是先把固件下载的函数单独跑通再去做后面的无线注册。可以用一个最简驱动只做固件下载用dev_dbg打印每个步骤的返回值确保固件确实被写进去了、芯片也正常启动了再继续往下走。否则固件加载失败了后面注册无线设备、设置信道之类的工作全都白搭出了问题也不好定位。3.2 中断处理与数据收发的正确姿势WiFi驱动的中断处理比普通设备复杂得多。普通设备的中断处理函数里读一个寄存器、清一个标志、唤醒一个等待队列就够了WiFi芯片的中断涉及收包、发包完成、扫描结果上报、连接状态变化、固件事件通知多种事件交织在一起。SDIO接口的WiFi芯片中断处理有个特点SDIO本身没有独立的中断线WiFi芯片要上报中断需要通过SDIO命令读取芯片的中断状态寄存器来查询。这意味着你的中断处理函数里要先做的事情不是处理具体事件而是先去问芯片“你发生了什么事”。基于这个原因SDIO WiFi驱动大多采用轮询中断标志的方式芯片拉高一个GPIO触发主控中断然后驱动在中断上下文里通过SDIO读取详细的中断状态。实际项目中用到的中断处理大致是这个流程static irqreturn_t wifi_chip_irq_handler(int irq, void *data) { struct wifi_chip_priv *priv data; u32 int_status; /* 通过SDIO读取中断状态寄存器知道芯片发生了什么 */ sdio_readl(priv-func, int_status, WIFI_CHIP_INT_STATUS, NULL); if (int_status 0) return IRQ_NONE; /* 处理不同类型的中断 */ if (int_status WIFI_CHIP_INT_RX_DATA) ieee80211_queue_work(priv-hw, priv-rx_work); if (int_status WIFI_CHIP_INT_TX_DONE) ieee80211_queue_work(priv-hw, priv-tx_done_work); if (int_status WIFI_CHIP_INT_FW_EVENT) queue_work(priv-fw_event_wq, priv-fw_event_work); return IRQ_HANDLED; }注意上面代码里的ieee80211_queue_work和queue_work这是关键点。不要在中断上下文里做太多实质性的工作。SDIO的读写操作本身就可能阻塞较长时间在中断上下文里做SDIO传输是禁忌。正确的做法是中断处理函数里只确认中断类型然后把耗时的工作排到工作队列或者内核线程里去做。这算是WiFi驱动开发的一条铁律。数据接收方面常见的方式有两种一种是芯片收到数据包后通过中断通知主机主机主动去读取另一种是芯片支持DMA直接把数据写到主机内存中再通过中断告知。SDIO接口的芯片通常走前一种方式PCIe接口的芯片走后一种。我的项目用的SDIO芯片走的是第一种于是驱动里的rx_work需要主动发起SDIO读取把数据包从芯片的RX FIFO里搬出来然后交给mac80211。static void wifi_chip_rx_work(struct work_struct *work) { struct wifi_chip_priv *priv container_of(work, struct wifi_chip_priv, rx_work); struct sk_buff *skb; u16 pkt_len; int ret; while (1) { /* 读取下一个包的长度 */ ret sdio_readw(priv-func, pkt_len, WIFI_CHIP_RX_LEN, NULL); if (ret 0 || pkt_len 0) break; skb netdev_alloc_skb(priv-netdev, pkt_len SKB_RESERVED); if (!skb) break; skb_reserve(skb, SKB_RESERVED); /* 从芯片FIFO读取完整的数据包 */ ret sdio_memcpy_fromio(priv-func, skb_put(skb, pkt_len), WIFI_CHIP_RX_FIFO, pkt_len); if (ret 0) { dev_kfree_skb(skb); break; } /* 交给mac80211处理 */ ieee80211_rx(priv-hw, skb); } }这里有个容易忽视的细节ieee80211_rx要求传入的skb在数据包前面预留一定的头部空间因为mac80211会在前面添加802.11协议的头部信息。如果没有做skb_reserve收包过程中会出现数据偏移表现是连接正常但吞吐率为0或者干脆无法关联AP。3.3 连接管理、功率控制与扫描细节决定体验的问题WiFi驱动的核心功能除了数据收发还有扫描、连接管理和功耗控制。扫描是用户执行iw dev wlan0 scan时触发的一个较耗时的操作。驱动收到mac80211下发的扫描请求后需要把硬件设置到扫描模式然后把每个信道的扫描结果收集起来。SoftMAC方案中扫描对驱动的要求是必须确保在当前关联的AP上暂停数据收发。如果你的驱动没处理好这个时序用户会反馈“连接一会儿就断”因为扫描的时候把数据通路打断了AP那边超时判定你掉线了。连接管理方面mac80211负责状态机但驱动要正确处理事件。芯片关联上AP之后固件会上报一个连接事件驱动拿到事件后要调用ieee80211_bss_info_change_notify或者ieee80211_connection_loss之类的接口同步给mac80211。AP断开的时候如果固件没有及时上报断开事件驱动必须通过信号强度监测、误码率统计等机制来主动判断连接是否丢失。不然就会出现“网络图标显示已连接但实际已经断网”的尴尬情况。功耗控制在WiFi驱动中也是大头。WiFi芯片空闲时进入省电模式、WiFi芯片收到AP的DTIM信标时提前唤醒、芯片和主机之间的总线进入低功耗状态这一套流程涉及的内核接口多、时序要求严苛。开发早期阶段可以先不管功耗用性能模式跑通功能最后再做功耗优化。一上来就追求完美功耗只会让调试过程寸步难行。4. 环境搭建与核心初始化流程实操4.1 最小化环境准备内核、工具链、根文件系统在动手写代码之前先把工作环境理顺。开发WiFi驱动需要这几样东西内核源码树和你的目标系统版本一致。用uname -r确认运行时内核版本然后用对应版本的内核源码。交叉编译工具链嵌入式设备用的比如aarch64-linux-gnu-系列。能挂载的根文件系统至少需要/lib/firmware目录放固件还需要iw、wpa_supplicant这些工具。如果没有现成的可以临时用NFS挂载主机上的目录调试阶段非常方便。目标板子的串口或网络调试通道串口最保险WiFi驱动出问题时网络的调试通道本身可能不可用。内核侧的配置要注意这几个选项缺了哪个都会导致驱动虽然编译进去了但起不了作用CONFIG_CFG80211y CONFIG_MAC80211y CONFIG_WLANy CONFIG_WLAN_VENDOR_INTELy按你的芯片选择 CONFIG_PMy要碰功耗管理就必须开把上面几个选项编成模块(m)也行但在调试阶段建议直接编进内核可以减少模块加载顺序的问题。4.2 设备树里对WiFi芯片的描述如果你的WiFi芯片挂在SDIO总线上通常不需要在设备树里写太多东西SDIO本身具备探测能力。但有些硬件设计上有一个用于复位芯片或者唤醒芯片的GPIO这个必须在设备树里描述清楚。sdhci1 { status okay; bus-width 4; non-removable; cap-power-off-card; keep-power-in-suspend; mmc-pwrseq wifi_pwrseq; #address-cells 1; #size-cells 0; wifi1 { compatible vendor,wifi-chip; reg 1; interrupt-parent gpio1; interrupts 13 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio1 14 GPIO_ACTIVE_LOW; }; }; wifi_pwrseq: wifi-pwrseq { compatible mmc-pwrseq-simple; reset-gpios gpio2 8 GPIO_ACTIVE_LOW; };这里很多细节容易出错。reg 1指的是芯片在SDIO总线上的function编号通常WiFi芯片是function 1。non-removable告诉内核这张“卡”不能热拔插如果不写这一句内核可能会去检测卡的移除状态导致设备频率被重置或功能异常。keep-power-in-suspend和cap-power-off-card这两个属性涉及是否在休眠时给WiFi芯片断电要严格按照硬件设计来写错了会出现休眠唤醒后WiFi失灵的现象。还有个小坑SDIO子节点的interrupts属性配置的是WiFi芯片上报中断的GPIO这个GPIO必须支持电平触发而且配合的wifi_pwrseq里如果已经定义了reset-gpios子节点里就不要重复定义复位引脚不然驱动会不知所措。4.3 驱动模块化拆分不要指望一次把所有代码写完我见过不少初学者一口气写上千行驱动代码然后编译、加载、翻车、从头找。这种做法效率极低。WiFi驱动本身复杂度高建议按阶段拆分来写。第一阶段只写设备探测和移除。probe函数里仅仅注册一个sdio_driver加载成功后用dev_info打印一句话。目的是确认总线枚举、设备树解析、时钟和电源的初始化都没问题。第二阶段加入固件加载。这一步的目标是让芯片内的固件跑起来能读到芯片的版本号并打印出来。第三阶段实现mac80211的接口。先实现空的start和stop注册ieee80211_hw确保iw dev能看到这个无线设备。第四阶段实现数据收发。从tx函数开始然后再处理rx_work。这个阶段打通后就能和AP建立连接了。第五阶段完善扫描、功耗管理、多虚拟接口等高级功能。每个阶段的代码量控制在几百行左右每完成一个阶段就做一次完整验证。这么做的好处是一旦出现问题定位范围很小不需要在整个驱动里大海捞针。5. 移植新内核时的高频坑点与解决方法5.1 API 变更最让人崩溃的“一行改变”把驱动从老内核移植到新内核最痛苦的不是逻辑错误而是内核API变了。5.4升5.10、5.10升5.15每次版本跃迁都有宏定义、函数签名、枚举常量的变化。下面整理几个我这次移植过程中实打实碰到过的变更点给后来人提个醒。ieee80211_ops里的get_survey回调5.10之后加了struct survey_info *survey参数如果你的驱动注册了这个回调而你没同步更新编译直接报错。cfg80211_scan_request里的channels字段在5.15里变成了struct cfg80211_scan_6ghz_data *相关的新结构体处理6GHz频段的代码如果照抄老内核的实现编译能过但运行会挂。另外skb_reserve的宏定义在新内核里被多次调整如果你的驱动里使用了LL_RESERVED_SPACE这类宏要检查是否和新内核的netdevice代码兼容。最有效的做法是内核官方仓库的Documentation/networking目录下有一份mac80211-injection.txt每次大版本都会有更新。移植前先去读一遍这个文档再对照include/net/mac80211.h里的头文件看看有没有新的回调被加入。这些准备工作花不了多长时间但能让后续的移植顺畅得多。/* 老内核写法 */ static int wifi_chip_config(struct ieee80211_hw *hw, u32 changed); /* 新内核写法不同内核版本可能增加新的参数 */ static int wifi_chip_config(struct ieee80211_hw *hw, u32 changed);5.2 MAC地址管理一个看起来小但影响面巨大的问题WiFi设备的MAC地址来源比很多人认为的复杂。每台设备出厂时应该有一个唯一的MAC地址但WiFi芯片内部通常只有烧录好的factory参数区里才存有它。如果驱动不读取这个区域而是用了一个随机地址或者全零地址那用户体验会非常糟糕连接路由器时需要重新认证、局域网内可能有地址冲突、甚至因为使用了组播地址而导致连接失败。驱动里获取MAC地址的标准做法是static int wifi_chip_get_mac_address(struct wifi_chip_priv *priv, u8 *mac) { int ret; /* 先从芯片OTP或eFuse中读取出厂MAC */ ret wifi_chip_read_otp(priv, WIFI_CHIP_OTP_MAC_ADDR, mac, ETH_ALEN); if (ret 0 is_valid_ether_addr(mac)) return 0; /* OTP里没有或者无效回退到从设备树读取 */ ret of_get_mac_address(priv-dev-of_node, mac); if (ret 0 is_valid_ether_addr(mac)) return 0; /* 最后才考虑随机生成但一定不要每次都随机 */ eth_random_addr(mac); dev_warn(priv-dev, using random MAC address %pM\n, mac); return 0; }这里有个非常实际的经验不要随便用eth_random_addr。如果驱动每次加载都生成一个随机MAC地址用户每次重启后看到的WiFi地址都不同这意味着已经保存的WiFi密码、IP地址分配、路由器上的MAC过滤规则全部失效。对产品来说这基本是不可接受的缺陷。还有一个调试技巧拿到设备后先用ethtool -P eth0之类的命令确认当前网卡的MAC地址再用iw dev看到的地址对比如果两者不一致多半说明驱动里MAC地址的设置逻辑有问题。5.3 吞吐量异常先别怀疑驱动按顺序排查WiFi驱动开发中一个很常见的现象是功能已经通了连接也正常了但吞吐率只有几十Mbps上不去。很多人第一反应是驱动有问题但实际操作中先别急着改代码按下面的顺序排查第一步确认测试环境没有干扰。把路由器信道换成不拥挤的频段靠近AP测试关掉蓝牙和其他2.4GHz干扰源。WiFi吞吐测试对环境极其敏感同一套设备在办公室和实验室测出来的数据能差好几倍。第二步检查传输速率是否达到预期。用iw dev wlan0 link查看当前协商的速率。如果显示的是802.11g的54Mbps而不是WiFi 6该有的速率说明速率协商出了问题。这时候要查设备树的max-speed配置、驱动初始化时设置的带宽参数和天线数量。第三步检查SDIO总线频率。SDIO接口的WiFi芯片吞吐率严重依赖总线时钟频率。用cat /sys/kernel/debug/mmc1/ios查看当前SDIO总线的时钟频率如果被降到了25MHz以下吞吐率肯定上不去。这个问题经常出现在设备树里没配置max-frequency的场合。第四步查驱动的收包路径是否有不必要的拷贝。比如有没有在rx_work里做额外的内存拷贝、有没有调用不必要的skb_clone这类操作。一次拷贝对性能的影响可能只有百分之几但如果同时存在多个低效点累加起来吞吐率就能掉一半。排查完这四步再去动驱动代码会发现很多吞吐量问题根本不在驱动代码本身。6. 调试工具与我的几条经验6.1 用户态工具全家桶iw / iwconfig / wpa_supplicant / hostapdWiFi驱动开发离不开这几样工具iw新一代配置工具直接通过netlink和cfg80211通信。查看设备信息用iw dev扫描用iw dev wlan0 scan查看链接状态用iw dev wlan0 link。功能比老的iwconfig丰富得多建议全程使用iw。wpa_supplicant连接加密AP的标准工具。调试的时候用-dd参数打开debug日志可以看到完整的认证和关联过程。wpa_cli可以用来交互式控制连接状态。hostapd让开发板当作AP用的工具。开发WiFi驱动时经常需要先用一个已知的好AP做测试开发板作为STA去连接它反过来你也可以在板子上跑hostapd用手机连接板子来测试AP模式功能。tcpdump抓包工具。WiFi的一切问题最后都能在数据包层面找到蛛丝马迹。不管是连不上、掉线还是速度慢先抓包看帧交互过程能快速定位问题出在协议栈的哪一层。这些工具在调试WiFi驱动时缺一不可。比如wpa_supplicant -dd的日志能清楚显示驱动上报的扫描结果、关联事件有没有被正确传递到用户态一步就能判断是驱动问题还是工具配置问题。6.2 内核侧调试手段从dev_dbg到tracepoint内核驱动开发中日志是最直接的调试手段。WiFi驱动尤其依赖日志因为很多问题只有在特定时序下才会出现硬件调试器不一定能抓得到。随便举几个实用的日志输出点probe入口和出口打印result固件下载的每个大步骤打印固件大小、下载耗时注册ieee80211_hw后的hw-wiphy-fw_version每次start、stop调用的上下文每次tx和rx_work的数据包计数连接和断开事件的触发条件打印信息最好集中在几个位置加上模块名前缀方便用dmesg | grep wifi_chip过滤。# 打开调试输出 echo 0xffff /sys/module/wifi_chip/parameters/debug_mask # 或者用动态调试 echo file drivers/net/wireless/vendor/wifi_chip/* p /sys/kernel/debug/dynamic_debug/control如果遇到难缠的时序问题可以用内核的tracepoint机制来记录事件顺序。比如在驱动里打两个trace_printk分别放在中断入口和rx_work入口然后查看/sys/kernel/debug/tracing/trace输出能看到中断到收包处理的延迟到底是多少是一瞬间还是积压了几毫秒。6.3 我踩过的几个印象最深的坑移植过程中有几个问题让我印象深刻写出来算是给同行的提醒。第一个坑是固件加载后没有等芯片启动完成就去发命令。芯片固件加载完成后需要一小段时间来完成内部初始化和自检。这个时间通常在几十到几百毫秒。驱动如果立刻下发命令芯片可能还没准备好命令直接失败或者返回乱码。解决办法是固件加载完成后读取芯片的状态寄存器确认ready标志位被置位再继续或者直接调用msleep等待足够长的时间。我后来选择了前者省得在不同芯片版本上还要微调延时。第二个坑是SDIO的块模式和数据包长度不匹配。WiFi芯片的RX FIFO数据是按块组织的块大小由设备的寄存器配置决定常见的有32、64、128字节等。如果驱动用sdio_memcpy_fromio读取数据时传的长度不是块大小的整数倍读出来的数据末尾会有多余的内容或者丢数据。解决方式是在读取之前判断当前使用的块大小并按块对齐来处理。第三个坑比较隐蔽驱动模块加载时request_firmware在等待文件系统挂载完成。如果WiFi驱动是编译成模块并且在文件系统就绪前就被自动加载那么此时/lib/firmware目录可能还不可用request_firmware会一直等到超时。那时候的表现是设备在启动日志里没有任何报错但WiFi就是起不来。解决方式简单粗暴把WiFi模块的加载顺序调整到文件系统挂载之后或者等固件文件就绪后再触发加载。7. 性能调优驱动开发不只是“能跑就行”7.1 调整NAPI和队列深度不费一行代码也能提升吞吐WiFi驱动开发进行到后期功能完全正常了你就会开始琢磨性能。吞吐率、延迟、功耗每个指标都有讲究。这里说几个不涉及复杂代码重构就能见效的调优手段。NAPI机制是Linux网络驱动中提高收包效率的重要手段。传统的收包方式是每次中断到达就处理高流量下会造成大量中断开销。NAPI的做法是中断到来时先进入轮询模式在轮询周期内一口气处理多个包直到达到预设的预算值。mac80211本身就支持NAPI你需要在驱动里正确实现struct napi_struct的poll函数把数据包处理逻辑放进poll函数里。static int wifi_chip_napi_poll(struct napi_struct *napi, int budget) { struct wifi_chip_priv *priv container_of(napi, struct wifi_chip_priv, napi); int work_done 0; /* 从芯片读取数据包直到预算用完 */ while (work_done budget) { if (wifi_chip_read_one_packet(priv) 0) break; work_done; } if (work_done budget) { napi_complete_done(napi, work_done); /* 重新使能中断 */ wifi_chip_enable_irq(priv); } return work_done; }队列深度同样值得调。mac80211每个硬件队列的深度和wake_queue的时机都会影响吞吐。如果队列深度太小驱动稍微有点处理延迟队列就满了mac80211就会停掉上层的数据发送吞吐自然上不去。可以根据实际测试结果把这个值适当调大比如从默认的256调到512甚至1024观察吞吐变化。但这个不能无脑调大内存占用和延迟会跟着涨需要在吞吐和延迟之间找一个平衡点。7.2 通过debugfs验证你的调优到底有没有效果聊到性能调优就不得不提验证手段。凭感觉调参是大忌必须有数据支撑。Linux的mac80211和cfg80211框架提供了很多调试接口拿到数据非常方便。/sys/kernel/debug/ieee80211/phy0/stations/每个关联的STA都有一个目录里面有信号强度、TX/RX的字节数、重传率等数据。/sys/kernel/debug/ieee80211/phy0/queues查看队列的状态和深度。/sys/kernel/debug/ieee80211/phy0/tx_stats查看TX数据包的统计信息。自己的驱动里也可以维护统计用的计数器比如总共收发了多少包、发生了几次重传、SDIO读写的失败次数、NAPI的占用率等。把这些统计信息通过debugfs暴露出来调优的时候一眼就能看到瓶颈在哪。我习惯在调优前先采集一份完整的基线数据然后每改一个参数就重新采集一次对比数据看变化。如果你改了参数N个但吞吐没变那说明问题不在你改的地方改回来吧。这种数据驱动的调优方式效率比盲试高太多了。7.3 功耗优化放在最后先通过功能测试再说功耗优化是WiFi驱动中最容易被忽视但也最考验功力的部分。我见过不少开发者在驱动还一堆功能bug的时候就开始研究省电模式结果bug没修完功耗优化也做不出来两头都没落着。正确的姿势是这样的先跑通所有功能确保吞吐量指标基本达标再开始搞功耗。功耗优化一般从三个层面入手芯片侧的电源管理在空闲状态下让WiFi芯片进入低功耗模式。芯片支持多级功率状态从活跃到休眠有好几档要跟芯片厂商确认每档的实现方式。总线侧的低功耗SDIO设备在空闲时可以把总线频率降下来甚至让主机端的SDIO控制器进入低功耗状态。系统级的协同配合内核的runtime PM框架在系统suspend时让WiFi芯片进入深度睡眠在系统resume时快速唤醒。需要处理的事件包括唤醒源的配置、固件状态的保存恢复、连接状态的重建。如果你的产品并不是特别追求续航功耗优化可以先做最粗粒度的一档系统进入suspend时让WiFi芯片也进入睡眠系统恢复时重新初始化。这一档实现简单稳定性也相对有保障。至于runtime PM这种细粒度优化建议等项目稳定运行一段时间之后再做。8. 最后说点项目之外的话这次WiFi驱动移植开发花了大概三周时间从最初的内核配置到最终所有的功能测试通过中间踩了不少坑。回头总结最深刻的体会是WiFi驱动开发和普通驱动开发最大的区别不在于技术难度而在于它的“黑盒属性”更强。字符设备驱动出了问题你有一万种方法打印、模拟、重现WiFi驱动的问题则经常和环境强绑定同样的代码换一个AP、换一个位置、换一个时间行为就可能完全不一样。所以我的建议是做WiFi驱动开发一定不要只盯着代码看要多借助工具观察现场。wpa_supplicant的日志、tcpdump抓包、iw的站点信息这些比你想象的有用得多。芯片有异样先确认是不是环境问题再怀疑自己的代码这个顺序能省下大把时间。另外还有一点小技巧分享给刚开始搞WiFi驱动的朋友在驱动里加一个用于测试的特殊模块参数比如force_tx_error、skip_rx之类的这样可以在不修改代码的情况下模拟异常路径对验证驱动在错误条件下的行为非常有帮助。这个习惯我一直保留到现在每次开发新的驱动都会加上这类测试钩子省了不少事。WiFi驱动这条路很宽从SDIO到PCIe、从2.4GHz到6GHz、从SoftMAC到FullMAC每一个方向钻进去都有说不完的话题。希望这篇实战总结能给正在这条路上摸索的你一点帮助。如果你也在移植或者开发WiFi驱动欢迎把这篇文章当作一份参考手册踩到新的坑再一起交流。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进