
简介这是一份面向无线通信与FPGA开发者的开源项目包基于软件定义无线电理念实现了与Linux mac80211兼容的全堆栈IEEE802.11/Wi-Fi设计。内容涵盖FPGA基带处理、Linux驱动与软件支持802.11a/g/n、20MHz带宽及70MHz至6GHz频率范围集成DCFCSMA/CA低MAC层、CSI信道状态信息提取、IQ捕获与实时AGC等功能适合嵌入式、SDR及无线协议栈学习者深入研究。资源包共140个文件以shell脚本、C/C头文件、Device Tree源文件、Makefile构建脚本及文档图片为主兼顾驱动编译、FPGA工程配置与说明文档整体大小8.7MB结构便于检索。目前已有3425人学习下载开源许可与第三方模块授权说明也已包含在内使用者可据此快速定位驱动入口、基带处理流程与收发通路实现。1. openwifi 本质一套能跑 Linux 原生 Wi-Fi 协议栈的开源 FPGA 基带传统 Wi-Fi 芯片的基带是个黑匣子OFDM 调制、CSMA 竞争接入、帧聚合这些细节都固化在硅片里想调一个退避窗口或换一种调制方式只能等芯片厂商发新版固件。openwifi 反着来在 Zynq 的 FPGA 逻辑里用 Verilog 实现 IEEE 802.11a/g/n 的物理层和关键 MAC 调度射频端接 AD9361 这种软件定义无线电收发芯片上层直接对接 Linux 原有的 mac80211 协议栈。于是我们可以用 ip、iw 命令驱动一块“自己写的 Wi-Fi 基带”也能在 Linux C 代码里看到每一帧的 PHY 状态。对研究 OFDM、CSMA、802.11 协议实现或做 SDR/FPGA 原型验证的开发者来说这是一份能直接上板复现的完整工程而不是停在仿真里的论文代码。2. 顶层架构拆解Zynq 的 PS/PL 如何配合 AD9361 发出 Wi-Fi 帧2.1 硬件组成为什么选 Zynq 而不是纯 FPGA 或纯 ARMopenwifi 选 Zynq 这类 SoC FPGA核心原因是它内部同时存在 ARM 处理器子系统PS和可编程逻辑PL。PS 侧跑完整 Linux处理网络协议栈、用户态配置和驱动PL 侧用硬件流水线做基带波形两者用 AXI 总线互联。纯 ARM 方案算不动实时基带纯 FPGA 方案又难跑复杂协议栈Zynq 的异构架构刚好两头都占。射频端最常搭配的是 AD9361一颗双通道射频敏捷收发器频率覆盖从几百 MHz 到 6 GHz 左右单通道带宽最大约 56 MHz正好覆盖 802.11a/g/n 常用的 20 MHz 和 40 MHz 信道。很多开发板把两者集成在一起常见形态是带 FMC 接口的 SDR 子板插在 Zynq 底板上。FPGA 与 AD9361 之间通过并行数据总线和同步时钟交换 I/Q 采样数据电平标准走 LVDS 或 CMOS。这意味着 FPGA 工程里必须做对应的引脚约束、时钟区域约束和延迟校准换一块底板或换一颗前端芯片这部分往往要跟着调。2.2 TX/RX 数据面路径一帧数据从协议栈到天线的完整旅程发送流程和普通网卡驱动的思路一致Linux 网络层把 802.11 帧交给 mac80211协议处理完调用驱动注册的发送回调驱动不逐字节搬数据而是先把帧放到 DMA 缓冲区把地址和长度写进 PL 侧 TX 描述符队列FPGA 的 DMA 引擎读走数据交给后级 MAC/PHY 做加前导、扰码、编码、OFDM 调制再送到 AD9361 变成射频信号发射。接收是逆过程。AD9361 下采样的 I/Q 数据进入 FPGA基带依次做帧同步、频偏估计、信道估计、均衡、解映射、解扰还原出 MAC 帧后用 DMA 写入内存触发中断通知驱动上送协议栈。这条链路里 DMA 是最关键的瓶颈点。驱动侧采用描述符环形队列一次提交多帧、按批次收中断如果退化成每帧一个中断加逐字节拷贝基带再快也会被 CPU 拖垮。openwifi 在这块的处理和很多高性能网卡驱动类似把批量搬运留给 DMA 引擎CPU 只管描述符和回收。2.3 目录结构导读拿到源码先看哪三块拿到源码不要立刻扎进 FPGA 细节。先按功能切成三块来看PL 工程、Linux 驱动、用户态 MAC 管理。PL 工程目录里是 Verilog 写的基带 IP 和顶层例化顶层文件能看到 AXI 接口、DMA 引擎、PHY 调制解调器、MAC 调度器这类实例整个数据通路在这个文件里画得一目了然。Linux 驱动目录是内核网络子系统侧的实现负责向 mac80211 注册无线设备、处理 TX/RX 和中断。用户态部分则跑一个管理 MAC 的进程承接 mac80211 不方便下沉到 FPGA 的逻辑比如某些管理帧处理和参数配置。调试时建议在这三个目录之间建立映射关系比如一帧断在哪个环节先定位它属于哪个目录再往下读具体 RTL 或 C 代码。2.4 时钟与前端接口和 AD9361 对齐的不只是引脚AD9361 的采样数据是连续流FPGA 的时钟必须跟着 ADC/DAC 走。常见做法是让 AD9361 给 FPGA 送来采样时钟FPGA 用这个时钟捕获 RX 数据并为 TX 数据对齐时序而不是各自用板载 PLL。移植平台时最容易遇到的玄学问题就在这里FPGA 内部仿真一切正常接上射频前端后收包就错位多半是 TX/RX 时钟没有和 AD9361 的转换时钟同步导致采样点落在错误的眼图上。PL 工程里通常有一个专门处理 AD9361 接口的模块负责时钟缓冲、延迟校准、通道映射和寄存器配置SPI。由于 Wi-Fi 是突发通信RX 路径还要在前导到来后快速完成增益调整和载波恢复这部分常被叫做快速 AGC 逻辑。想换 SDR 前端的开发者要注意openwifi 里改动成本最高、最需要理解的接口模块就是这个而不是基带主路径。2.5 为什么 CSMA 和 OFDM 一个放硬件、一个放软件802.11 里大量行为对时间要求极苛刻比如帧间间隔、退避计数、ACK 超时这些周期是微秒级的Linux 内核线程在这种尺度下不可控所以 openwifi 把 EDCA/CSMA 的竞争窗口和退避状态机放在 PL 侧硬件里实现。OFDM 是纯波形运算从 FFT 到星座映射都是固定算法硬件做又快又确定自然也在硬件流水线里。反过来mac80211 负责管理帧组装、认证关联、密钥管理等慢速逻辑。这个分工解释了为什么 openwifi 能对接标准 Linux 协议栈慢的交给 CPU快的留给 FPGA中间用 DMA 和寄存器把两边粘起来。理解这条线之后再看驱动代码会发现它其实不太关心调制解调只负责把帧从内存搬到硬件队列、把状态寄存器读回来真正的“Wi-Fi”发生在 PL 内部。3. 核心代码逐段拆解FPGA 基带与 Linux 驱动的关键实现3.1 PL 顶层从 AXI 接口到 PHY 的模块地图先看 FPGA 工程最顶层的结构。下面的代码不是 openwifi 的完整 RTL而是提炼出来的模块边界示意方便说明各块之间的连接关系。// 简化的 openwifi PL 顶层示意 module openwifi_top #( parameter C_AXI_DATA_WIDTH 64 )( input wire ad9361_rx_clk, input wire [11:0] ad9361_rx_i, input wire [11:0] ad9361_rx_q, output wire [11:0] ad9361_tx_i, output wire [11:0] ad9361_tx_q, // AXI 从接口给 ARM 配置寄存器用 AXI_BUS.S axi_ctrl, // AXI 高性能口DMA 读/写内存用 AXI_BUS.M axi_dma_mem ); wire tx_fifo_ren; wire rx_fifo_wen; wire [31:0] tx_fifo_data; wire [31:0] rx_fifo_data; // 1. MAC 发送引擎CSMA/EDCA 竞争中决定何时真正发送 mac_tx_engine u_mac_tx ( .clk (ad9361_rx_clk), .tx_fifo_ren (tx_fifo_ren), .tx_fifo_data (tx_fifo_data) ); // 2. PHY 发送侧加扰、编码、交织、OFDM 调制 phy_tx u_phy_tx ( .clk (ad9361_rx_clk), .data_in (tx_fifo_data), .data_out ({ad9361_tx_q, ad9361_tx_i}) ); // 3. PHY 接收侧同步、均衡、解映射 phy_rx u_phy_rx ( .clk (ad9361_rx_clk), .data_in ({ad9361_rx_q, ad9361_rx_i}), .data_out (rx_fifo_data), .fifo_wen (rx_fifo_wen) ); // 4. DMA 引擎把收发数据与 DDR 对接 dma_engine u_dma ( .axi_mem (axi_dma_mem), .tx_fifo ({tx_fifo_data, tx_fifo_ren}), .rx_fifo ({rx_fifo_data, rx_fifo_wen}) ); endmodule逻辑说明ad9361_rx_clk 是整个基带的唯一主时钟来源收发流水线都挂在它下面这样 I/Q 采样和基带处理天然同源。tx/rx 的 12 bit 数据宽度对应 AD9361 的典型转换精度。axi_ctrl 是 CPU 配置通道axi_dma_mem 才是数据通道两者分开后调 PHY 参数只动寄存器数据通路不用重新综合。参数方面重点理解 AXI_DATA_WIDTH64 bit 意味着 DMA 一次读取可以搬 8 字节配合 burst 传输才能支撑 40 MHz 带宽下的高吞吐改窄会直接限制峰值速率。新手最容易混淆 axi_ctrl 和 axi_dma_mem 这两条总线。配置寄存器写错地址通常表现为驱动加载时报错或 ioctl 失败数据总线接错则是驱动正常但收发完全不通并且没有明显报错。排查时先确认这两条总线各自接在 PL 的哪个 AXI 端口上。3.2 DMA 描述符与缓冲区管理内存和 FPGA 之间怎么交接FPGA 不能直接访问 Linux 的 sk_buff驱动必须先把数据放到一段物理地址连续的缓冲区再把地址告诉 FPGA。这个交接靠描述符完成。// openwifi Linux 驱动里常见的 DMA 描述符处理示意 struct hw_desc { uint32_t addr; // 物理地址FPGA 必须能直接访问 uint32_t len; // 帧长度 uint32_t flags; // 读/写标记完成位 uint32_t stat; // 状态如 CRC 是否通过 }; static int submit_tx(struct openwifi_priv *priv, struct sk_buff *skb, dma_addr_t dma_addr) { struct hw_desc *desc priv-tx_ring_head; desc-addr dma_addr; desc-len skb-len; desc-flags DESC_TX_READY; barrier(); // 确保描述符内容先写进内存 writel(1, priv-regs TX_TAIL); // 再通知 FPGA 队列新增一项 priv-tx_ring_head; return 0; }逻辑说明skb 是 Linux 网络栈的包结构dma_addr 由 dma_map_single 或 dma_alloc_coherent 得到。关键点在写描述符和更新 tail 指针的顺序——必须先让描述符内容对 FPGA 可见再写 TX_TAIL否则 FPGA 读到未就绪的数据会搬出半截帧。TX_TAIL 是硬件寄存器每次写入表示生产了一个新描述符FPGA 消费完之后要么轮询 desc-flags 里的完成位要么通过中断反馈openwifi 驱动两者都会用。参数说明addr 字段填的必须是物理地址很多人用 skb-data 的虚拟地址直接填结果 FPGA 访问到不存在的物理页表现为 TX 卡死或 RX 内存被写花。len 是帧总长度包括 802.11 头flags 里除了 DESC_TX_READY通常还有加密标记、重传标记具体位定义以驱动头文件为准。回收时别忘了 dma_unmap_single否则长期跑会出现 DMA 泄漏。3.3 驱动注册与收发骨架让 mac80211 认识这块 FPGA 网卡openwifi 在 Linux 侧本质是一个 mac80211 驱动下面这段代码是注册过程的骨架。static const struct ieee80211_ops openwifi_ops { .tx openwifi_tx, .start openwifi_start, .stop openwifi_stop, .config openwifi_config, .start_ap openwifi_start_ap, // 其余回调略 }; static int openwifi_probe(struct platform_device *pdev) { struct ieee80211_hw *hw; struct openwifi_priv *priv; hw ieee80211_alloc_hw(sizeof(*priv), openwifi_ops); priv hw-priv; priv-regs devm_platform_ioremap_resource(pdev, 0); priv-tx_ring dma_alloc_coherent(dev, TX_RING_SIZE, priv-tx_dma, GFP_KERNEL); ieee80211_hw_set(hw, SUPPORTS_HT_CCK_RATES); hw-wiphy-interface_modes BIT(NL80211_IFTYPE_STA) | BIT(NL80211_IFTYPE_AP); return ieee80211_register_hw(hw); }逻辑说明ieee80211_alloc_hw 分配 mac80211 驱动对象openwifi_ops 里的回调是协议栈和硬件之间的契约。.tx 是真正的发送入口.start/.stop 控制硬件上下电.config 负责信道、功率等参数下发.start_ap 在创建热点时被调用。dma_alloc_coherent 分配的内存同时给 CPU 一个虚拟地址、给 FPGA 一个物理地址避免后续每次都做一致性同步。注册接口时同时声明 STA 和 AP 模式所以同一套工程既能连别人的 AP也能自己开热点。参数说明interface_modes 里的 NL80211_IFTYPE_STA 和 NL80211_IFTYPE_AP 是 nl80211 定义的模式位缺 STA 则无法用 iw dev wlan0 connect 连网缺 AP 则无法 hostapd 开热点。probe 成功后再看 dmesg会看到类似 direct firmware load 和 ieee80211 phy0 的日志出现这些说明 mac80211 已经接管设备。4. 从源码到上板编译流程与运行验证4.1 环境准备Vivado、内核源码与交叉工具链首先要有一台能跑 Vivado 的 Linux 主机以及一块带 AD9361 的 Zynq 开发板。openwifi 源码从公开仓库 clone 下来后建议先读 README 和 boards 目录确认自己的板卡有没有现成工程脚本。板卡不同DDR 型号、MIO 配置、FMC 引脚约束都不一样不要直接用默认平台。工具链方面Vivado 版本要跟仓库要求的匹配。太新的 Vivado 打开老工程会提示升级 IP升级后综合能过但上板时序行为可能改变这是很多人栽过的地方。Linux 侧还需要交叉编译器通常用 arm-linux-gnueabihf内核版本也要和驱动代码匹配。准备阶段可以顺手验证一下板子能正常启动 Linux再做 FPGA 相关操作省得后面把系统问题和硬件问题混在一起。# 从公开仓库拉取 openwifi 源码地址以仓库 README 为准 git clone openwifi-remote-url cd openwifi ls boards # 查看支持的板卡目录 # 初始化 Vivado 环境版本号以你本机安装为准 source /opt/Xilinx/Vivado/2023.1/settings64.sh参数说明boards 目录里每个子目录对应一块具体板卡里面通常有约束文件、地址映射脚本和生成 bitstream 的 tcl。如果没有你的板卡型号就得从最接近的型号开始改改的东西主要是引脚约束和 DDR 配置。git clone 用浅克隆也可以但注意后面需要完整历史时再 fetch日常开发浅克隆够用。4.2 构建 bitstream 与内核驱动模块bitstream 构建是整个流程里最耗时的一步。进入对应板卡的 FPGA 工程目录用批处理模式跑构建脚本cd fpga/openwifi_xxx # 换成实际板卡对应目录 vivado -mode batch -source build.tcl这个脚本会创建工程、加载 IP、综合、实现并输出 bit 文件。时间取决于机器性能快则二三十分钟慢则数小时。中途报错通常集中在 IP 版本、许可证和时序收敛三类前两类看日志就能定位时序不收敛需要看 Tcl 里时序报告必要时降低时钟频率或调整约束。Linux 侧构建比较直接。先交叉编译内核再编译 openwifi 的内核模块export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make zynq_defconfig # 或用板卡厂商提供的配置 make -j8 make modules cd openwifi/driver # 内核驱动源码目录 make KDIR/path/to/kernel # KDIR 指向上一步的内核源码逻辑说明驱动模块必须以目标板同版本内核来编译模块内用到的内核符号如果版本不一致insmod 时会报 invalid module format。KDIR 指向的内核源码必须已经完成 make modules_prepare否则头文件生成不全。编译产物通常是 openwifi.ko 和配套的用户态管理程序加载顺序不能反。设备树也要提前准备。FPGA 基带映射在哪个地址、中断号是多少都要和 bitstream 里的实际配置一致。设备树写错不会影响 bitstream 加载但会导致驱动 probe 失败表现就是 /sys/bus/platform/devices 下找不到对应节点。4.3 上板加载与网络验证从 dmesg 到 ping 通把 boot.bin 或者 SD 卡里的启动镜像更新成包含新 bitstream 的版本后上电进入 Linux。先确认 FPGA 已经配置成功再加载驱动insmod openwifi.ko insmod openwifi_umac.ko # 或启动用户态管理程序 ip link set wlan0 up iw dev wlan0 scan # 扫描周边 AP如果 scan 能看到周围的 Wi-Fi 信号说明 RX 链路基本通了。接下来做回环自测用一根射频线把 AD9361 的 TX 输出直接接回 RX 输入这时候不开天线也能验证整个基带链路。回环通过后再拿两台 openwifi 节点组网一个配成 AP一个配成 STA最后 ping 包验证。# 节点 A配置为 AP iw dev wlan0 set type ap ip addr add 192.168.10.1/24 dev wlan0 # 节点 B连接 AP iw dev wlan0 connect openwifi-test ip addr add 192.168.10.2/24 dev wlan0 ping 192.168.10.1提示如果手头只有一套 openwifi可以用普通手机或电脑去连它开出的 AP。能连上但 ping 不通优先查 RX/TX 增益和天线端口而不是急着改代码。注意回环测试时一定要加足够的衰减AD9361 的 TX 功率直接灌进 RX很容易把接收前端打饱和出现“能收到但全是错包”的假象。5. 避坑手记openwifi 上板最容易翻车的五个细节5.1 DMA 描述符地址填错TX 队列直接卡死现象insmod 成功链路也能 up但只要一发包dmesg 就开始刷 DMA errorTX 计数器不动。原因描述符里的地址不是物理地址。skb 里的 data 指针是内核虚拟地址FPGA 侧的 DMA 引擎只认识物理地址填虚拟地址等于让硬件访问一个不存在的内存映射。解决所有交给 FPGA 的 buffer 一律走 dma_map_single 或者 dma_alloc_coherent描述符里保存 dma_addr_t回收完成帧时做对应的 unmap。我一般会在驱动里把地址打印成 %pad 格式化输出然后对照内存布局确认物理地址落在 DDR 范围内。5.2 能扫到别人家的 AP自己的 STA/AP 互连却不通现象iw scan 能看到周边大量 AP说明 RX 链路是好的但两个 openwifi 节点互相连不上或者连上后 ping 不通。原因最常见是 TX/RX 不对称。TX 增益设得太低信号出不去RX 增益设得太高前端饱和。另一种隐蔽情况是两个节点用了相同的 MAC 地址模板ARP 和关联流程直接错乱。解决先用 iw 读取双方的 MAC 地址确认不冲突然后把 TX 增益设在中间值比如 40 dBm 衰减档附近RX 增益从默认值开始逐档调再配一根射频线做回环确认单板自发自收正常最后再双板联调。5.3 仿真全对上板数据错位现象Vivado 仿真里波形完美上板后 RX 解调出来的数据全是错的BER 高到无法入网。原因AD9361 的 LVDS/CMOS 接口时序没对齐。仿真里理想时钟掩盖了真实链路的建立保持时间问题FPGA 采集 I/Q 数据的时刻落在数据翻转沿附近采样值不稳定。解决按芯片手册把 AD9361 送来的时钟引到全局时钟引脚用 set_input_delay 约束 RX 数据如果硬件支持用 IODELAY 做精细可调延迟上板后扫延迟值找到眼图中心。换板卡时这套约束必须重新调它跟 PCB 走线长度强相关。5.4 吞吐只有几 Mbps远低于 802.11n 预期现象ping 通iperf 测速惨不忍睹CPU 占用率却很高。原因驱动退化成“每帧一次中断 一次 DMA 拷贝”高帧率下大部分 CPU 都在响应中断和搬运数据。另一个常见原因是 TX ring 太小描述符提交速度跟不上硬件消费速度。解决打开 NAPI把多帧合并成一次中断处理加大 ring size让 FPGA 有足够的帧排队再检查 DMA burst 长度是否被配成了很小的值。改完重测吞吐往往会翻几倍。5.5 驱动加载后没有生成 wlan0现象openwifi.ko 加载无报错但 ifconfig 和 iw dev 都看不到无线网卡。原因设备树里 reg 地址或 interrupts 和 bitstream 不一致。FPGA 工程要是改过地址分配设备树没同步probe 阶段资源申请失败设备被内核直接丢弃。解决对比设备树节点里的 reg 和 PL 侧地址映射确认中断号一致。我一般会先 cat /proc/device-tree 里的节点内容再在驱动 probe 里加 dev_info 打印 platform_get_resource 的返回值定位是地址还是中断的问题。6. 二次开发技巧在 openwifi 上做你自己的 Wi-Fi 实验6.1 改频段和带宽从 AD9361 配置入手最常见的改动是切换信道带宽。20 MHz 和 40 MHz 不只是改一个寄存器AD9361 的采样率、FIR 滤波系数、FPGA 侧的处理时钟都要跟着变。建议先读前端接口模块里的配置表找到一组已验证过的参数组合复制一份再改不要从零拼参数。改完先用频谱仪看发射频谱是否落在目标带宽内再跑回环验证。6.2 注入自定义帧验证 MAC/PHY 行为想做协议实验可以在驱动 TX 路径上构造自己的 802.11 帧。RTL 稳定之后用户态可以通过 raw socket 发送管理帧或数据帧这比改 Verilog 快得多。建议先在 PL 的 RX 侧加一个统计寄存器记录收到的 CRC 错误帧数用来判断链路质量变化这比反复抓包直观。6.3 改 CSMA 退避参数真正动 FPGA如果动到竞争窗口、重传次数这类行为就要去 PL 工程里找 MAC 发送状态机了。这类改动的验证成本最高改完必须重新综合、生成 bitstream、上板。我会先把原状态机的关键参数抽成寄存器这样后续实验可以在运行时调不用每次重新编译。从那以后我每次改 PL 侧的代码都强制把“仿真验证、回环测试、双板联调”走一遍任何一个环节不过就不往下一步走。openwifi 的价值就在于它把基带细节摊开在眼前跑通它已经能学到不少东西改起来更是能把协议栈每层都摸一遍。希望这篇拆解能帮你在自己的板子上少走几步弯路。本文还有配套的精品资源点击获取