ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux WiFi设备驱动开发全链路:从总线probe到mac80211注册

Linux WiFi设备驱动开发全链路:从总线probe到mac80211注册 第一次碰 WiFi 驱动的人几乎都会在同一个地方卡住lspci能看到设备、dmesg里有枚举日志、/sys/bus/mmc/devices里节点也在但ifconfig -a死活不出现wlan0。更难受的是翻遍网上的 Linux 驱动开发资料字符设备驱动、I2C 设备驱动、平台设备驱动都讲得很清楚一到 WiFi 就好像断层了——因为 WiFi 驱动不是一类设备驱动它是总线驱动 网络设备 无线协议栈适配层三件事叠在一起缺任何一层都跑不起来。这篇内容就是我把自己折腾 Linux WiFi 设备驱动开发的路径整理出来的一次记录面向的是已经写过最简单的字符设备驱动、看得懂file_operations和 probe/remove 那套模型但还没摸过 mac80211 的开发者。核心想解决三件事搞清楚内核里WiFi 驱动到底指哪一部分代码把从总线枚举到能扫描、能连接这条链路拆开看以及把那些官方文档里几乎不会写、但实践里必踩的坑提前摆出来。全文按概念边界 → 子系统骨架 → 环境搭建 → 注册时序 → 监管域与信道 → 排查链路 → 设备树与电源 → 性能调优的顺序推进你可以按需跳读。1. 先搞清楚WiFi驱动在内核里到底指哪一部分代码1.1 一个 net_device 背后站着四个不同的东西很多人第一次看wlan0会以为它和eth0差不多都是网卡驱动注册出来的。这个理解只对了一半。Linux 里一个无线接口确实最终表现为一个struct net_device但支撑它的代码分成四块各自归不同的子系统管层面归谁管你写不写总线枚举与资源分配PCIe / USB / SDIO / SPI 子系统不写但要会读日志硬件具体操作寄存器、DMA、固件你写的驱动模块必须写无线协议栈适配管理帧、扫描、密钥mac80211 / cfg80211二选一看硬件能力用户态配置与连接wpa_supplicant、iw、NetworkManager不写但要会调换句话说一个完整的WiFi 驱动开发任务里真正需要你从零写的只有第二层和第三层的一部分。第一层内核已经做完了第四层有成熟工具。新手最容易犯的错是把第四层的事也揽过来比如自己写扫描逻辑、自己在驱动里解析 SSID 列表——这些在 mac80211 体系下都是框架做掉的事情。1.2 为什么内核里没有WiFi驱动这个目录打开drivers/net/wireless/你会看到的是按厂商分的目录intel/、ath/、broadcom/、realtek/、mediatek/而不是一个统一的wifi/。这个组织方式本身就说明了问题无线驱动是按芯片家族划分的因为不同芯片的固件接口、队列模型、射频校准方式完全不同没有抽象的余地。真正起到统一作用的是net/wireless/和net/mac80211/这两个目录。前者就是 cfg80211后者就是 mac80211。你写的驱动模块本质上是在这两个框架提供的回调表里填空。所以我通常建议读源码先从include/net/cfg80211.h和include/net/mac80211.h的头文件注释入手而不是先去看某个具体厂商驱动的几千行代码——后者会让你淹没在硬件细节里看不到骨架。1.3 一个反直觉的结论先别急着看自己的芯片我带过几个刚入门的同学他们的第一反应都是我要开发某某型号的 WiFi 驱动那我先把厂商给的 SDK 看一遍。结果往往是看了一周连哪些函数是必须实现的、调用顺序是什么都说不清。更有效的路径是反过来的先拿一个内核里已经支持的、结构干净的驱动当模板比如drivers/net/wireless/ath/ath9k/或者drivers/net/wireless/mediatek/mt7601u/把它的probe、ieee80211_ops、cfg80211_ops这三处抄出来对照头文件看一遍建立起我的驱动需要提供哪些能力的清单再回去翻芯片手册。顺序对了效率差好几倍。2. cfg80211与mac80211一上一下的两条控制路径2.1 分层不是摆设选错层等于白干先把关系说清楚。cfg80211 是配置面它是 nl80211 netlink 接口的实现者负责处理用户态发来的扫描一下连到这个 BSS设置监管域这类请求向上暴露的是一个叫wiphy的对象。mac80211 是数据面调度器 软件 MAC它处理管理帧的收发与解析、块确认聚合、报文重排、队列调度、密钥管理等一大堆协议细节向下调用你实现的ieee80211_ops。这两者的关系不是并列的而是 mac80211 向 cfg80211 注册自己你的驱动再向 mac80211 注册硬件。调用链大概是iw dev wlan0 scan - netlink - cfg80211 (net/wireless/) - rdev_scan 回调 - mac80211 (net/mac80211/) - ieee80211_ops.hw_scan 或 sw_scan_start你的代码看清楚这条链你就知道为什么调试的时候既要开 cfg80211 的 tracepoint又要开 mac80211 的 tracepoint——问题可能卡在任何一段。2.2 fullmac 还是 softmac这个决定影响后面所有工作这是开发初期最关键的选型判断判断错了后面全部推倒重来类型硬件做什么你要实现什么典型场景fullmac硬件自己完成 802.11 MAC包括扫描、关联、加密cfg80211_ops部分 SDIO/USB 低成本模块softmac硬件只做射频和基带MAC 交给主机ieee80211_opsPCIe 高性能网卡、嵌入式常见方案判断方法很直接看芯片手册里有没有自主扫描自主关联这类描述或者看固件接口是不是一个命令 事件的邮箱模型。如果固件把完整的 MAC 状态机都包了那就是 fullmac 路线你只需要实现cfg80211_ops里的scan、connect、disconnect、add_key等回调工作量小但灵活性差很多统计信息拿不到。反之 softmac 路线你需要实现ieee80211_ops工作量翻倍但能控制到每一个重传、每一个聚合窗口。2.3 ieee80211_ops 里哪些回调是真必须的struct ieee80211_ops成员很多第一次看容易慌。按我的经验跑通最基本的扫描 连接 收发需要先实现这些static const struct ieee80211_ops my_hw_ops { .tx my_tx, /* 发数据帧入口 */ .start my_start, /* 启动硬件配置队列 */ .stop my_stop, .add_interface my_add_interface, /* 创建 vif分配硬件上下文 */ .remove_interface my_remove_interface, .config my_config, /* 信道、类型、功率等 */ .configure_filter my_configure_filter,/* 决定哪些帧上报主机 */ .bss_info_changed my_bss_info_changed,/* BSSID、速率集、beacon 丢失阈值 */ .sta_add my_sta_add, /* 对端站信息 */ .sta_remove my_sta_remove, .set_key my_set_key, /* 密钥下发到硬件 */ .ampdu_action my_ampdu_action, /* 聚合开关 */ .sw_scan_start my_sw_scan_start, /* 没有硬件扫描能力时用软件扫描 */ .sw_scan_complete my_sw_scan_complete, .get_tsf my_get_tsf, /* 时间同步很多上层依赖它 */ };这里面configure_filter和bss_info_changed是新手最容易忽略的两个。前者决定你能否收到 probe request、beacon 这类管理帧后者是连接状态变化的唯一通知点很多连上了但收不到包的问题就出在这里没处理BSS_CHANGED_BSSID。.get_tsf如果没有某些依赖时间戳的功能会直接报错不是可选项。3. 搭建一个能反复验证的开发环境3.1 内核源码与配置别用发行版自带的头文件凑合开发无线驱动必须要有和目标内核版本完全一致的内核源码树而且要能编译出模块。发行版的linux-headers包只包含部分头文件net/mac80211这类内部头是不会给你的。所以标准做法是# 拿到与目标板一致的源码和配置 git clone --depth 1 -b v6.1 内核源码地址 linux-6.1 cd linux-6.1 cp /path/to/board/.config .config make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules_preparemodules_prepare这一步很多人省掉结果编译自己模块时提示找不到Module.symvers或者更隐蔽地出现符号能编过但加载时 unknown symbol。原因是modules_prepare会生成模块版本校验所需的信息跳过它会让你在insmod时才踩坑。驱动模块的 Makefile 用最朴素的写法就够了obj-m my_wifi.o my_wifi-y : main.o txrx.o fw.o debugfs.o KDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean3.2 调试环境把 debugfs 和 trace 提前打开无线部分的调试信息基本都在 debugfs 里但很多发行版默认不挂载。开发前先确认mount -t debugfs none /sys/kernel/debug ls /sys/kernel/debug/ieee80211/同时内核配置里这几个选项要打开不然后面排查会毫无抓手CONFIG_MAC80211_DEBUGFSyCONFIG_MAC80211_MESHy做 mesh 才需要CONFIG_CFG80211_DEBUGFSyCONFIG_DYNAMIC_DEBUGyCONFIG_FUNCTION_TRACERy与CONFIG_FTRACEy3.3 一个容易被忽略的准备接口命名策略新内核默认启用了可预测接口命名WiFi 接口可能叫wlp3s0或者带 MAC 后缀的名字做脚本化测试时很烦。开发阶段我一般临时改回传统命名避免脚本到处写死# 内核启动参数加 net.ifnames0这不是技术难点但能省下大量为什么脚本找不到 wlan0的时间。4. 从 probe 到 register让设备先活过来的完整时序4.1 三种总线入口的差别决定了 probe 的第一段怎么写总线资源获取方式中断方式数据搬运常见坑PCIepci_iomap映射 BARMSI/MSI-X描述符环 DMABAR 映射失败多为固件未加载或电源域未开USBusb_set_intfdata中断 urbbulk in/out urb 池urb 提交失败通常是端点类型判断错SDIOsdio_enable_func数据线中断或外部 GPIO块传输3.3V/1.8V 电压切换时序错误导致初始化随机失败SPIspi_setup外部 GPIO单向半双工吞吐低不适合高带宽但成本极低以 SDIO 为例probe 的骨架大概长这样每一步的顺序都不建议改static int my_probe(struct sdio_func *func, const struct sdio_device_id *id) { int ret; struct my_priv *priv; sdio_claim_host(func); ret sdio_enable_func(func); if (ret) goto err_release; ret sdio_set_block_size(func, 512); if (ret) goto err_disable; sdio_release_host(func); /* 1) 时钟、电源、复位 GPIO先让芯片能响应寄存器读 */ ret my_hw_power_on(func); if (ret) goto err_disable; /* 2) 下载固件并等待就绪事件 */ ret my_fw_download(func); if (ret) goto err_power_off; /* 3) 申请中断必须在硬件 ready 之后否则可能收不到首次中断 */ ret my_request_irq(func); if (ret) goto err_power_off; /* 4) 注册到 mac80211这一句之后 wlan0 就会出现 */ ret ieee80211_register_hw(priv-hw); if (ret) goto err_free_irq; return 0; /* ... 错误路径逐层回滚 ... */ }4.2 固件加载的时机与方式差一步症状完全不同固件加载用request_firmware文件放在/lib/firmware/下。常见的两种写法区别很大同步加载在 probe 里直接request_firmware。简单但 probe 会阻塞几百毫秒启动阶段可能触发超时告警。异步加载probe 里用request_firmware_nowait加载完成后再继续初始化。启动快但代码复杂度上升而且必须处理好加载完成前用户态就试图 up 接口的竞态。我踩过的一个坑值得单独说某次固件下载用了异步方式结果ieee80211_register_hw在固件回执之前就执行了返回成功、wlan0也出现了但第一次扫描永远超时。dmesg里没有任何错误只是一个孤零零的scan request failed。后来加了事件序列打印才发现是硬件还在复位状态。结论是注册框架的时机必须严格在硬件就绪之后顺序不对不会报错只会表现为各种超时。4.3 中断、DMA 与收发队列的初始配置my_start被调用时硬件才真正开始工作。这里要做三件事初始化 DMA 描述符环、配置收发队列、把硬件中断使能打开。关于队列mac80211 会通过ieee80211_wake_queue/ieee80211_stop_queue管理流控硬件一般提供 4~8 个队列对应不同的 AC语音、视频、尽力而为、背景。如果你的硬件队列数少于 mac80211 期望的数量需要在add_interface里调整vif-hw_queue映射否则会出现某个优先级的报文全部发不出去。另一个细节是ieee80211_stop_queue之后的恢复。写驱动时非常容易漏掉发送完成中断里检查队列是否需要 wake这一步症状是跑一段大流量后吞吐突然掉到零iw dev wlan0 station dump却显示一切正常。这类问题不查队列状态是看不出来的。5. 监管域、信道表与扫描请求被内核拦住的常见原因5.1 监管域不是可选项配错连扫描都发不出去很多新手第一次看到regulatory_hint和wiphy-regulatory_flags会直接跳过觉得和自己的硬件初始化没关系。实际上一旦监管域没设置好cfg80211 会把一部分信道标记为 disabled扫描请求直接返回-EINVAL或者扫描结果里看不到任何 AP。驱动的责任是把芯片支持的频段和信道如实填进wiphy-bands[NL80211_BAND_2GHZ]和bands[NL80211_BAND_5GHZ]并在拿到固件里携带的国家码信息后调用regulatory_hint。填信道表时几个字段必须给对struct ieee80211_channel ch { .band NL80211_BAND_2GHZ, .hw_value 3, .center_freq 2412, .max_power 20, /* 单位 dBm不是 mW */ .flags IEEE80211_CHAN_NO_IR, /* 某些信道不允许主动发送 */ };max_power的单位错误是最常见的低级失误写成 dBm 才对写成 mW 会导致功率超标或者弱到连不上。IEEE80211_CHAN_NO_IR这类标志位漏掉会让设备在不该主动发包的信道上发探测请求属于合规问题。5.2 一次扫描请求在内核里的完整旅程搞清这条链排查扫描问题就不会乱用户态iw dev wlan0 scan通过 netlink 发出NL80211_CMD_TRIGGER_SCAN。cfg80211 检查当前接口状态、监管域、是否有正在进行的扫描。有硬件扫描能力时调用rdev_scan最终进到你的hw_scan没有则走sw_scan_start。你的驱动把信道列表配置给硬件逐个信道发起探测。硬件把收到的探测响应通过接收路径上报你在中断里把它们封装成 skb 交给 mac80211。mac80211 解析出 BSS 信息通过 cfg80211 缓存到scan_request。扫描完成NL80211_CMD_NEW_SCAN_RESULTS返回给用户态。链路上的每一环都可能断。链路上没有硬件扫描能力却实现了hw_scan会让框架认为你可以扫全信道结果永远等不到完成事件——这个坑我见过不止一次正确做法是能力不具备时老老实实只实现sw_scan_start/sw_scan_complete。5.3 连接建立过程中驱动该做什么连接流程里大部分协议细节由 mac80211 处理驱动要做的是把状态告诉硬件、把密钥交给硬件。具体几个点bss_info_changed里收到BSS_CHANGED_BSSID时把 BSSID 写进硬件过滤寄存器否则硬件的地址过滤会把该 BSS 的帧全丢掉。收到BSS_CHANGED_ASSOC时更新关联状态标志并在未关联时清空对端站信息。sta_add/sta_remove对应硬件里加解密表项的增删漏掉sta_remove会导致硬件表项泄漏长时间跑下来会出现新站加不进去。四次握手阶段用户态的 wpa_supplicant 完成 EAPOL 交换后把协商出的密钥通过 nl80211 下发内核侧最终调用你的set_key。如果有硬件加密卸载在这里写密钥表没有的话保持IEEE80211_KEY_FLAG_*标志指示由软件加解密即可。这里有一个经验加密相关的 bug 往往表现为能连上但 ping 不通或能 ping 通但传大文件就断。区别在于单播和组播走的表项不同如果只配了单播密钥忘了组播密钥症状就是你 ping 网关单播好得很一旦有广播流量就崩。6. 问题排查链路从 dmesg 到 tracepoint 的六步定位法6.1 先分层看日志别一上来就翻代码无线驱动的报错信息经常很含糊我的习惯是按固定顺序过一遍绝大多数问题在前三步就能定位总线层lspci -vv、lsusb -t、dmesg | grep -i mmc确认设备被枚举、资源分配成功。驱动 probe 层dmesg里看你自己的打印确认 probe 是否被调用、返回什么值。框架注册层cat /sys/kernel/debug/ieee80211/有没有对应phyN目录iw dev能不能列出接口。协议层iw dev wlan0 scan能不能扫到东西、dmesg里有没有 cfg80211 的告警。数据层ip -s link show wlan0、cat /proc/net/dev看重传、错误计数。硬件层这时候才去读寄存器、看频谱。跳过前三步直接去查协议是新手最常见的时间黑洞。6.2 用 tracepoint 看到函数之间的真实调用顺序printk打点的问题是它不告诉你谁调用了谁、耗时多少。mac80211 和 cfg80211 自带了一批 tracepoint把它打开能省下大量时间cd /sys/kernel/debug/tracing echo 1 events/mac80211/enable echo 1 events/cfg80211/enable cat trace_pipe输出里能看到drv_tx、drv_return_int、rdev_scan这类事件函数名、参数、调用时序一目了然。如果还想看耗时可以切到 function_graphecho function_graph current_tracer echo my_* set_ftrace_filter cat trace_pipe这个组合特别适合查某个回调被调用了几十次或者某个耗时操作在中断上下文里做了很久这类问题。6.3 我把踩过的坑列成一张对照表下面这些是我在实际调试中反复遇到的症状和根因往往不直观症状可能根因验证方式probe 返回 -ENOENT固件文件缺失或路径不对dmesg搜 firmware检查/lib/firmware/wlan0 不出现ieee80211_register_hw失败或未调用在注册前后各加一条打印扫描永远超时注册时机早于硬件就绪信道表为空打印固件事件序列能连上但 ping 不通组播密钥未配置接收过滤把帧丢了查configure_filter的入参位掩码大流量下突然静默队列 stop 后没 wakeiw dev wlan0 station dump看队列状态休眠唤醒后中断失效中断唤醒源未配置或寄存器未恢复cat /proc/interrupts看计数是否增长随机初始化失败时钟/电压切换时序不对加大上电延时后复测6.4 一个真实的排查过程印象最深的一次是某模块在高温环境下连续跑两小时后丢包率飙升。dmesg 干净iw station dump的 RSSI 正常看起来毫无头绪。按六步法走到第五步时/proc/net/dev的tx dropped一直在涨于是打开events/mac80211的 trace发现drv_tx被调用的频率远高于硬件实际发出量。顺着查下去问题是收发描述符环的大小只有 32而 mac80211 在高负载时会在一个调度周期里塞进上百个 skb环满之后驱动返回NETDEV_TX_BUSY但上层没有正确处理重试导致报文被静默丢弃。把描述符环扩到 128并把NETDEV_TX_BUSY的处理改成停队列 发送完成后 wake问题消失。这个案例说明很多无线不稳定的锅最后都落在队列和流控上而不是射频。7. 设备树与电源管理嵌入式平台最容易翻车的两处配置7.1 设备树里那几行看起来无害的配置在 ARM 平台上WiFi 模块能不能稳定工作一半取决于设备树写得对不对。以 SDIO 接口的模块为例一个能用的节点大概是这样mmc1 { status okay; bus-width 4; non-removable; keep-power-in-suspend; cap-power-off-card; vmmc-supply wifi_vmmc; vqmmc-supply wifi_vqmmc; mmc-pwrseq wifi_pwrseq; }; wifi_pwrseq { compatible mmc-pwrseq-simple; reset-gpios gpio3 12 GPIO_ACTIVE_LOW; post-power-on-delay-ms 200; };几个关键点non-removable必须加。不加会被当成可插拔卡控制器会周期性重新探测表现为运行中偶发掉线。cap-power-off-card影响休眠时的断电策略配上keep-power-in-suspend才能保证唤醒后不断连。post-power-on-delay-ms是给芯片上电稳定留的时间。这个值太小是冷启动随机失败的头号原因加大到 200ms 往往直接解决。vqmmc-supply对应 IO 电压。如果芯片支持 1.8V 高速模式但 regulator 只给了 3.3V会表现为协商失败或者速率上不去。7.2 休眠唤醒中断为什么在 resume 之后就没了带电池的设备必然要做低功耗。WiFi 这边的核心是两点把中断配置成唤醒源以及在suspend/resume回调里完整保存和恢复寄存器状态。static int my_suspend(struct device *dev) { struct my_priv *priv dev_get_drvdata(dev); if (!device_may_wakeup(dev)) my_hw_enter_low_power(priv); else enable_irq_wake(priv-irq); return 0; }一个容易忽略的细节是如果硬件在休眠时断电唤醒后固件需要重新下载。有些方案为了唤醒快选择让芯片保持供电只关射频这就必须在设备树里配keep-power-in-suspend否则控制器断电后芯片状态全丢唤醒回来就是一块砖。这个取舍没有标准答案取决于唤醒延迟要求要求秒连就用保持供电要求极致省电就重新初始化。7.3 唤醒源的验证方法配置完之后不要只看能不能唤醒要确认是 WiFi 中断唤醒的cat /sys/power/wakeup_count cat /proc/interrupts | grep my_wifi休眠前后对比中断计数如果唤醒后计数增长了说明确实是 WiFi 侧的中断把系统叫醒的如果计数没变但系统也醒了那是别的设备唤醒的WiFi 的唤醒配置其实是失效的只是被掩盖了。8. 吞吐与稳定性调优队列、聚合与中断的三个观察点8.1 先把收发的瓶颈位置量出来调优之前不要猜。我一般用这条路子定位瓶颈# 打流并观察 iperf3 -c 对端地址 -t 60 -P 4 # 同时看队列与错误计数 watch -n1 cat /proc/net/dev | grep wlan0 # 看中断分布是否集中在某一个核 watch -n1 cat /proc/interrupts | grep my_wifi如果tx dropped持续增长瓶颈在发送队列或者 DMA 环如果重传计数高但丢包不多瓶颈可能在射频环境或者速率自适应如果只有一个 CPU 核的中断计数在涨、softirq占用很高那就是中断聚合没做好。三种情况对应的优化方向完全不同。8.2 聚合参数不是越大越好AMPDU 聚合和 A-MSDU 聚合是提升吞吐最直接的手段但参数设错会反过来伤害延迟和小包性能参数偏大偏小建议AMPDU 长度吞吐高重传代价大延迟抖动吞吐上不去大文件传输为主时用大值AMPDU 数量缓冲区压力大内存吃紧流水线不足结合描述符环大小定A-MSDU头开销小但单包出错整块重传开销大小包多时谨慎开启中断聚合阈值CPU 占用低延迟升高延迟低CPU 占用高交互场景调小吞吐场景调大我的经验是先在实验室把参数拉到极端值分别测吞吐和延迟画出两条曲线再根据实际业务决定落点。拍脑袋填一个中间值往往两头都不讨好。8.3 长时间跑流的观察点稳定性问题和性能问题经常混在一起。跑长时间压力测试时除了吞吐我固定观察四个量重传率iw dev wlan0 station dump里的tx retries与tx packets比值持续高于 20% 说明链路质量有问题或者速率自适应没生效。描述符环水位驱动里打个周期性日志看环的使用峰值长期贴满就是环太小。中断合并命中率硬件一般有统计寄存器命中率低说明聚合阈值设得不合适。内存水位无线驱动频繁分配 skb长时间跑下来如果slabtop里skbuff_head_cache一直不回落检查是不是有 skb 泄漏通常出现在错误路径里漏了dev_kfree_skb。最后分享一个小技巧调试阶段在驱动的收包路径上加一个可开关的计数器统计每个队列每小时处理了多少包、丢弃了多少包比临时加打印有用得多也不会打乱中断时序。这套计数器后来成了我所有无线驱动模块的标配很多时候问题都是它先报警的。至于接下来还能往哪走我个人的路线是先把一个 fullmac 风格的简单模块跑通把注册、扫描、连接这条链完全走顺再去啃 softmac 下的聚合和速率自适应。前者大概一两周能出成果后者往往要几个月才能调稳别想着一步到位。
返回列表