去年秋天我把 IEEE 802.11a 的 OFDM 物理层在 FPGA 上完整实现了一遍从加扰、卷积编码、交织、映射到 IFFT、加循环前缀再到接收端的帧检测、频偏估计、信道估计、维特比译码整个链路跑通的那一刻确实挺有成就感。之所以挑 802.11a而不是更新的 802.11ax是因为它足够简单但又足够完整64 点 FFT、52 个有效子载波、前导序列、导频跟踪、卷积码所有 OFDM 系统该有的关键点一个不少而且计算规模对 FPGA 非常友好。这篇文章把我的实现思路、参数选择、RTL 设计要点和调试过程整理出来适合正在入门 FPGA 数字信号处理、想做通信物理层原型、或者准备把 OFDM 作为毕设/竞赛项目的朋友参考。1. 为什么选 802.11a协议参数与 OFDM 核心逻辑1.1 OFDM 到底解决了一个什么问题OFDM 说起来一句话就能概括把一列高速串行数据拆成很多路低速并行数据分别调制到不同频率的子载波上同时发送。但这句话背后对应的是一个非常实际的工程痛点——无线信道的多径效应。信号在真实环境里传播会经过墙壁、地面、桌椅反射接收端收到的是多条路径信号的叠加每条路径的时延、幅度、相位都不一样。在窄带系统里多径体现为符号间干扰在宽带系统里它体现为频率选择性衰落也就是信道的频响在某些频点深衰落、某些频点正常。如果采用单载波方案想对抗这种信道接收端往往要做很复杂的时域均衡——均衡器阶数可能高达几十甚至上百阶。OFDM 的思路是“打不过就把它拆小”。20 MHz 带宽被分成 64 个子载波后每个子载波只占 312.5 kHz在这么窄的带宽内信道频响近似是平坦的。换句话说OFDM 把一个复杂的宽带均衡问题变成了几十个独立的简单复数乘法问题。这也是为什么接收机做信道估计和均衡时每个子载波只需要乘一个复数补偿系数就够了。循环前缀Cyclic PrefixCP则是配合 OFDM 的另一个关键设计。多径时延会让上一个符号拖尾到下一个符号造成符号间干扰。通过在每个 OFDM 符号前插入一段 CP只要多径时延不超过 CP 长度上一个符号的拖尾就被 CP 吸收掉了。而且 CP 复制的是 IFFT 输出末尾的一段数据这让线性卷积近似变成了循环卷积接收端 FFT 后每个子载波上依然只乘一个复增益均衡依然简单。1.2 802.11a 的参数为什么刚好适合 FPGA 实现802.11a 是 OFDM 系统的绝佳“教学标本”因为它的所有参数都非常规整和 FPGA 的时钟、存储、FFT IP Core 的天然结构匹配度极高。核心参数如下表所示。参数数值对 FPGA 实现的意义信道带宽20 MHz基带采样率 20 MSps采样周期 50 nsFFT 点数64FFT IP Core 规模很小资源开销极低子载波间隔312.5 kHz20 MHz / 64频率分辨率清晰有效子载波52 个48 个数据子载波 4 个导频子载波循环前缀0.8 us即 16 个采样点非常容易在 RTL 里处理OFDM 符号长度4 us3.2 us IFFT 0.8 us CP共 80 个采样点前导序列16 us短训练序列 8 us 长训练序列 8 us数据速率6 ~ 54 Mbps由调制方式和卷积码率组合决定采样率 20 MSps 意味着每秒钟要处理 2000 万个复数采样但每个采样点只有 50 ns 间隔。FPGA 内部逻辑跑到 100 MHz 甚至 200 MHz 很轻松所以如果采用“快时钟域处理、慢数据率输入”的结构每个数据样本在时间上都有 10 倍的余量供算法模块使用时序压力小很多。64 点 FFT 是个非常舒服的尺寸。Xilinx 的 FFT IP Core 直接支持 64 点流水线模式资源占用不大即使你自己写一个 radix-2 蝶形单元状态机也不复杂。如果换成 802.11n 的 64 点还带 MIMO或者 802.11ax 的 1024 点 FFT实现的复杂度就陡增了。1.3 为什么用 FPGA 而不是 MATLAB 或者 DSPMATLAB/Python 仿真当然能验证算法但仿真通过和实际电路跑通之间有相当大的鸿沟。定点量化、有限字长、时序控制、数据反压、FFT 输出顺序、pipeline 延迟这些问题是纯仿真环境里根本体会不到的。DSP 处理器虽然灵活但处理 64 点 FFT 加上同步算法指令周期开销大而且并行性受限于处理器架构想验证硬件流水线思维也不够直接。FPGA 做 OFDM 调制解调最大的价值在于把“数据通路”变成一种物理直觉。当你看到 tvalid/tready 握手信号贯穿整个链路看到 FFT IP 的 s_axis_data_tlast 和 m_axis_data_tlast 如何对齐成帧看到频偏补偿模块里的相位累加器每来一个采样递增一次你对“实时信号处理”的理解是完全不一样的。这个经历对后面做 5G 物理层、卫星通信、软件无线电都有直接帮助。2. 发端设计从 MAC 数据到 OFDM 时域波形2.1 PPDU 帧结构先把时域轮廓画出来802.11a 的物理层协议数据单元PPDU从时间上看分三段前导序列、SIGNAL 字段、DATA 字段。短训练序列 L-STF持续 8 us由 10 个重复的短训练符号组成每个符号 0.8 us共 160 个采样点。接收机靠它做自动增益控制、粗频偏估计和分组检测。长训练序列 L-LTF前面有 1.6 us 的保护间隔之后是两个重复的长训练符号共 8 us160 个采样点。接收机靠它做精频偏估计和信道估计。SIGNAL 字段一个 OFDM 符号4 us承载速率和长度信息用 BPSK 1/2 编码保证低信噪比下也能解出来。DATA 字段若干个 OFDM 符号每个 4 us承载真正的 PSDU 数据。RTL 实现时最直接的做法是先把整个 PPDU 的时域采样点算好写进 ROM/RAM然后按地址顺序读出交给 DAC。但 DATA 部分长度不固定所以 DATA 必须实时生成数据比特进入编码/交织/映射/IFFT 流水线在控制状态机的调度下依次输出。前导序列可以离线生成。短训练序列和长训练序列在频域有固定定义用 MATLAB 算好 IFFT 之后把 160160 个复数采样存成初始化文件在需要输出时从 ROM 里按顺序读出来即可。这样省掉了前导生成逻辑也避免了一次次重新仿真的麻烦。2.2 数据比特链路加扰、卷积编码、交织、打孔DATA 部分的数据流处理和 OFDM 调制之间隔着一段经典的比特级处理链。顺序是加扰 - 卷积编码 - 打孔 - 交织 - 映射到星座点 - 插入导频和空子载波 - IFFT。加扰的目的是把输入数据随机化避免出现长串的 0 或 1。802.11a 用的扰码生成器是 7 位 LFSR生成多项式是 1 z^-4 z^-7发送端初始化一个非零状态然后把生成序列与数据异或。如果数据里有大段重复比特直接调制会在频谱上产生离散谱线可能干扰其它设备加扰后数据近似白噪声。卷积编码是 802.11a 的纠错核心约束长度 7生成多项式是 133 和 171八进制码率 1/2。在 FPGA 上实现卷积编码非常直观本质上就是两个移位寄存器异或网络接上打孔逻辑就能输出不同码率。打孔就是把 1/2 母码的某些校验位按固定规则丢弃达到 2/3 或 3/4 码率这是牺牲纠错能力换吞吐量的手法。数据速率表如下调制方式编码码率编码后比特/符号数据速率 (Mbps)BPSK1/2246BPSK3/4369QPSK1/24812QPSK3/4721816QAM1/2962416QAM3/41443664QAM2/31924864QAM3/421654交织的作用是打散连续比特错误。无线信道里的深衰落通常会连续干掉几个相邻子载波如果不交织一个 OFDM 符号里相邻比特全错维特比译码基本无力回天。交织后错误比特被分散到不同子载波和不同比特位置上维特比译码就能通过卷积码的约束关系把错误纠正回来。在 RTL 里交织器实现为一块双口 RAM按自然顺序写入、按交织后地址读出即可。2.3 星座映射子载波编号与 FFT 输入顺序的对应关系完成交织后的数据比特进入映射器。BPSK、QPSK、16QAM、64QAM 把每 1/2/4/6 个比特映射成一个复数星座点。这里要注意功率归一化不同调制阶数的星座点平均功率不同理论上应该按 802.11 标准把星座点归一化到单位平均功率。不过实际工程里如果整个链路幅度都是一个尺度功率归一化可以整体折算到发射增益或者接收 AGC不在映射器里乘。映射后的数据要放进 64 个 FFT bin 里。让不少人翻车的地方就在这里——802.11a 的 48 个数据子载波分布在正负频率上而 Xilinx FFT IP 输入的 bin 0 到 bin 63 对应频率 0 到 63/64 * Fs其中 bin 1 到 bin 31 是正频率bin 32 是 Nyquist 点通常不用bin 33 到 bin 63 实际上是负频率部分。所以数据子载波索引 -26 到 -1 应该放到 bin 38 到 bin 63数据子载波索引 1 到 26 放到 bin 1 到 bin 26。我最初实现时把负频率子载波直接放到了 bin 27 到 bin 52结果发射出去的信号频谱乱成一团。后来在 MATLAB 里和理论频谱比对才定位到问题。这个问题在纯仿真里可能因为收发都是同一套映射逻辑而完全被掩盖但一旦要和标准设备互通就必然会出错。推荐的做法是写一个频率映射表把 48 个数据子载波和 4 个导频子载波的位置全部列清楚再对照 FFT IP 输入接口。2.4 IFFT、循环前缀和输出时序Xilinx FFT IP Core 配置成 64 点、流水线结构输出是自然顺序的时域采样点。关键配置点是选择 inverse也就是做 IFFT。FFT IP 的输入是 AXI4-Stream 接口每 64 个复数采样打包成一帧用 tlast 标记帧尾。输出端同样每 64 个点一帧。循环前缀的实现有两种常见方式。一种是在 FFT IP 输出端接一个 64 深度双口 RAM等 64 个时域点全部写完后先读地址 48 到 63 的 16 个点再读地址 0 到 63 的全部点拼成 80 个点的 OFDM 符号。另一种做法更节约延迟让输出数据同时过一个延迟 FIFO但整体逻辑会复杂一些不推荐新手上来就这么做。实际代码里CP 插入模块需要维护一个输出计数器和读地址计数器。写入端 tready 拉低时暂停读出端跟着上层模块的握手信号走。由于每个 OFDM 符号之间还要拼接前导序列所以控制状态机要能区分当前是在输出前导 ROM、SIGNAL 字段还是 DATA 字段并在符号边界处插入干净的时钟周期。这里最重要的一点是保证 tlast 信号的周期严格等于 80也就是 16 个 CP 采样 64 个 IFFT 采样。3. 收端设计同步、信道估计与均衡的完整链路3.1 接收机整体结构先明确要解决什么问题接收机是整个项目里真正有挑战性的部分。发端是开环的、确定性的写完基本就能工作。收端要面对的是信号什么时候到、频率偏移了多少、FFT 窗口从哪里开始、信道对每个子载波造成了多大衰减和相位旋转、残余频偏怎么跟踪。我的接收链路顺序是输入缓存 - 分组检测 - 粗频偏估计 - 频偏补偿 - 精频偏估计 - 符号定时 - FFT - 信道估计 - 均衡 - 解映射 - 解交织 - 维特比译码 - 解扰。在实际 RTL 里粗频偏估计和分组检测常常合并在一起因为它们使用的是同一个短训练序列的自相关结果。接收数据进 FPGA 时先要过一级异步 FIFO因为 ADC 采样时钟域和 FPGA 内部处理时钟域不是同一个。如果 ADC 采样率是 20 MSps数据进入 100 MHz 逻辑时钟域后每个采样点依然保持 50 ns 间隔这一点可以通过插入空闲时钟周期来保证。整个接收链路可以在 100 MHz 下工作因为数据率低时间余量足够大。3.2 帧检测与符号定时用短训练序列找信号起点分组检测用的是延迟相关算法。因为短训练序列每 0.8 us 重复一次也就是每 16 个采样重复一次接收信号与其延迟 16 个采样后的信号之间具有强相关性。定义相关值c[n] Σ r[nm] * conj(r[nmD])其中 D 16累加长度 L 取 16 或 32。再把相关值用接收能量归一化得到 m[n] |c[n]|^2 / (E1 * E2)。当没有信号时 m[n] 接近 0当短训练序列到达后 m[n] 迅速抬升并出现一个平台。门限通常取 0.5 到 0.75并加一点滞回防止在噪声毛刺处误触发。在 FPGA 里实现这个检测器不需要乘 16 次加法器。维护一个深度 32 的滑动窗每个新采样进来时做一次乘加同时把窗尾最老的采样贡献减掉这样每个时钟周期只需要一次复数乘法和两次累加资源非常省。我当时用的是 Xilinx 的 Complex Multiplier IP宽度配成 16 bit加法器累加位宽扩到 32 bit 防溢出。符号定时可以在这个平台上继续做。归一化相关值平台开始的位置大约对应短训练序列开始平台长度约 9 个短符号周期。从平台起点往后推 160 个采样左右就能大致找到长训练序列的位置。不过更稳妥的方式是用长训练序列的已知时域波形做互相关峰值位置就是长训练的精确起点。互相关的峰值非常尖锐不像延迟相关那样是平台定位精度更好。3.3 载波频偏估计与补偿先粗后精两段式载波频偏是 OFDM 系统最致命的问题之一。发射机和接收机的本地振荡器频率不可能完全一致假设中心频率 5 GHz两端晶振误差合计 20 ppm频偏就达到 100 kHz 量级。而 OFDM 子载波间隔只有 312.5 kHz100 kHz 频偏会造成子载波间干扰ICI星座点会持续旋转即使做信道估计也补偿不干净。802.11a 的前导结构天然支持两段式频偏估计。短训练序列周期 D 16用延迟相关的相位角估计频偏Δf angle(c[n]) / (2π * D * Ts)其中 Ts 50 ns。因为 angle 的取值范围是 [-π, π]所以 D 16 时可估计的最大频偏是 ±1/(21650ns) ±625 kHz。这个范围远超实际晶振偏差也超过了子载波间隔312.5 kHz说明短训练序列能捕获“整数倍加小数倍”的频偏。但是短训练序列的估计精度不够高。16 个采样周期的相位差太小噪声对角度估计的影响较大。所以精频偏估计用长训练序列来做长