☰
用Zynq FPGA与AD9361跑Linux Wi-Fi协议栈:openwifi架构与实践
2026/10/19 9:16:34 网站建设 项目流程

简介:这是一份面向无线通信与FPGA开发者的开源项目包,基于软件定义无线电理念,实现了与Linux mac80211兼容的全堆栈IEEE802.11/Wi-Fi设计。内容涵盖FPGA基带处理、Linux驱动与软件,支持802.11a/g/n、20MHz带宽及70MHz至6GHz频率范围,集成DCF(CSMA/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 或纯 ARM

openwifi 选 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_WIDTH:64 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 ARCH=arm export CROSS_COMPILE=arm-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 error,TX 计数器不动。

原因:描述符里的地址不是物理地址。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 的价值就在于它把基带细节摊开在眼前,跑通它已经能学到不少东西,改起来更是能把协议栈每层都摸一遍。希望这篇拆解能帮你在自己的板子上少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询