
我断断续续做了两个多月终于把 802.11a 物理层里最硬核的 OFDM 调制解调链路在 FPGA 上完整跑通了。从最初对着协议咬文嚼字到后来在仿真波形里看到星座点聚拢再到把数据从发射端编码送出去、接收端解码还原出来整个过程跟剥洋葱一样每一层都有让头发变少的东西。这个项目包括发射链路和接收链路两大部分涉及加扰、卷积编码、交织、QAM 映射、导频插入、IFFT/FFT、自动增益控制、符号定时同步、频偏估计补偿、信道均衡、维特比译码等等。做一遍之后最大的感受是协议标准只是把“什么”说清楚了而“怎么在硬件上实现”才是真正的修行。这篇东西就是记录这次实现的完整过程把我踩过坑、反复验证过的细节都摊开来讲给同样想做无线物理层 FPGA 的人一个参考。1. 为什么是 OFDM协议层面的硬约束1.1 802.11a 物理层那些绕不开的参数802.11a 把 20MHz 带宽分配成 64 个频率槽位实际只用了其中 52 个子载波48 个用来传数据4 个用来传导频。子载波间隔是 312.5kHz因此一个 OFDM 符号的 FFT 积分时间是 3.2us前面加 0.8us 的循环前缀CP合起来一个符号正好 4us。这些数字不是拍脑袋定的如果子载波间隔太大频谱效率就低间隔太小多普勒频移和本振偏差很容易把子载波间的正交性破坏掉。312.5kHz 的间隔能在典型室内信道下保持正交性同时给接收端留下足够的频偏容忍空间这是 IEEE 标准委员会在频谱效率和实现复杂度之间做的一次妥协。调制方式支持 BPSK、QPSK、16QAM、64QAM卷积编码码率有 1/2、2/3、3/4。不同的调制编码组合对应不同的数据速率最低 6Mbps最高 54Mbps。这些参数在 FPGA 实现时直接影响数据通路的位宽和控制逻辑。比如做 64QAM 时每个子载波要传 6 个比特48 个数据子载波一口气就是 288 比特卷积编码后码率 3/4 时还要再往前推 384 个编码比特交织深度也跟着变大。硬件设计上不能写一个只适配某一种速率的死逻辑而是要把“每符号比特数”和“码率”设计成参数可配的状态这样才能在实现时动态切换。除了数据字段物理层前导符PLCP preamble也非常重要。标准里定义了短训练序列和长训练序列用来做自动增益控制、粗频偏估计、符号定时和信道估计。短训练序列有 10 个长度为 16 的重复段长训练序列有 2 个长度为 64 的重复段中间还插了保护间隔。这些序列在设计接收机同步算法时就是天然的“尺子”我在实现帧检测时就死死盯着它们的自相关性靠这个在时域上确定一帧从哪里开始。可以说前端同步能不能锁住一大半功劳要归功于这个前导符结构。1.2 从协议到 FPGA比特流到 IQ 样值的全过程协议里描述的是一整套数学运算但 FPGA 工程师拿到手的第一件事就是把它拆成一个个可以流水的硬件模块。我习惯先把收发链路的处理流程画成信号流图标出每个模块的输入输出位宽、时钟频率和接口握手信号。发射端从 MAC 层拿到 PSDU物理层服务数据单元先加扰然后做卷积编码再交织接着做星座映射、插入导频再拼接导频和数据子载波到 64 点 IFFT 的输入做完 IFFT 加 CP最后打窗、上变频输出。数据速率虽然是可变的但因为前面所有模块都是每 4us 处理一个 OFDM 符号所以整个基带处理在 20MHz 采样率下实际上是一个平滑的流水线每个符号的 16 个 CP 样值期间刚好可以做发射端的控制调度和接收端的重配置。接收端则是相反的收到的数字基带信号先做帧检测和粗频偏估计然后做符号定时同步找到 FFT 窗口起点去掉 CP做 64 点 FFT接下来进行细频偏估计和信道估计用已知导频和长训练序列算出每个子载波的复数增益做均衡再解映射得到软信息或硬比特解交织最后送进维特比译码器还原原始比特流。这个过程中每个模块都有多种算法选择而从协议到硬件的关键就在于把浮点数学转成定点运算同时保证链路端到端的性能损失在可接受的范围内。2. 发射通路设计把比特“堆”成波形2.1 也不算复杂的加扰、卷积编码与交织发射端第一个让我真正动手写代码的是加扰模块。802.11a 规定的加扰多项式是 S(x) x^7 x^4 1本质上是一个 7 位的线性反馈移位寄存器LFSR。加扰的目的不是加密而是把数据随机化避免长串的 0 或 1 导致星座点上出现过大的直流偏置或频谱线谱。实现时先给 7 个寄存器赋初值然后每一比特都和反馈抽头异或电路非常简单几行 Verilog 就能写完。但要注意一点PLCP 头里的速率和长度字段在加扰时也要参与运算而且接收端解扰必须用同样的种子同步启动如果一个帧的起始同步错了后面所有比特全部错乱。我在这里吃过苦头后面调试小节会详细说。卷积编码用的是标准的 (2,1,7) 卷积码生成多项式是 133_octal 和 171_octal约束长度 7。由于 802.11a 支持打孔puncturing来获得 2/3 和 3/4 码率所以编码器本身只需要按 1/2 码率输出再根据码率丢弃部分校验比特。硬件上这个模块是一组移位寄存器和两个异或门关键是要把时序控制精确因为打孔模式是周期性的。我实现时把打孔模式做成一个参数可配置的查找表调度逻辑根据当前码率选择每个比特周期内哪些编码比特保留。如果你用 3/4 码率会看到每 6 个输入比特产生 4 个编码比特后再打掉其中 2 个这个节奏必须和交织器输入长度对齐否则数据错位。交织模块看似简单但里面藏了不少位宽计算的坑。它做两层置换第一层把编码比特按行写入一个矩阵再按列读出第二层对相邻比特做交换每个比特与相隔 13 的比特交换位置。这样做的目的是把卷积编码前的突发错误尽量分散开让维特比译码更鲁棒。由于调制阶数不同交织器的列数也不同行数是 13 固定。所以每个 OFDM 符号的数据比特数除以 13就是交织深度。我最开始没认真算写了个固定深度的仿真模型结果换到 64QAM 时直接数组越界。后来老老实实把交织器深度做成 N_CBPS/13 的可配置参数才把整个链路跑顺。2.2 星座映射与导频插入星座映射实现起来并不难本质就是查表。BPSK、QPSK、16QAM、64QAM 各有独立的 IQ 映射表但要注意点位顺序必须符合规范里的 Gray 编码规则。特别在 64QAM 中相邻星座点只差一个比特这样在低信噪比下常见的单比特错误不会引发大片误码爆点。FPGA 里我用一个 ROM 存映射表输入索引是每 6 个比特组合输出是复数形式的 I、Q 值。这里有个容易被忽略的问题映射输出需要做归一化。标准里的星座映射能量是归一化的每种调制方式都有一个归一化因子比如 64QAM 是 1/sqrt(42)。如果不乘这个因子后面 IFFT 输出的幅度会出现系统性偏差接收端做信道估计时也会跟着抖。导频插入需要特别留心。四个导频子载波索引分别是 -21、-7、7、21导频值是固定的 ±1而且有一个 127 位的伪随机序列控制正负号。这个序列和加扰器用的是同一个多项式但相位不同。导频的作用不只是帮接收端估计剩余频偏还能在信道估计的时候提供频域插值的参考点。我在发射端用了一个计数器产生导频符号的序号通过查表得到当前 OFDM 符号的导频极性。这里很容易把导频极性表跟数据加扰的伪随机序列搞混因为它们的多项式一样但生成时刻不同一定要独立写逻辑。2.3 IFFT 与加保护间隔核心运算模块是 64 点 IFFT。FPGA 上我用了 Xilinx 的 FFT IP 核配置成逆变换模式输出采用定点数。如果不用 IP 核自己写 64 点基 2 IFFT 其实也是可行的但要做多级蝶形运算和旋转因子表写起来工作量大得多而且时序收敛要花很多时间。IP 核的好处是它内部已经做了流水线输入输出接口都很标准可以轻松对接上下游模块。我这次用的是 Pipelined Streaming 模式可以让连续符号无缝处理避免帧间等待时长超过协议规定的帧间隔。加循环前缀其实就是把 IFFT 输出的最后 16 个采样点复制到符号最前面形成一个 80 点的符号波形。这里要控制好 RAM 的读写时序先把 64 点数据全部写进缓冲区再从第 48 个点读起读完整个 64 点形成一个 80 点输出。如果直接顺序写再顺序读是没办法复制尾部的。我设计里用了双端口 RAM一个口写 FFT 输出另一个口按循环顺序读这样输出数据流连续不中断也不用额外缓存。还有一个细节是加窗协议允许发送端做适当的频谱整形来抑制带外辐射但在 FPGA 实现时很多人图省事直接发矩形窗结果邻道泄漏超标。严格做的话需要在符号边界做升余弦窗把前后的样值过渡拼接起来。我这次为了先跑通链路没做窗函数如果以后要做成产品这步还得补上。2.4 上变频与波形输出基带 OFDM 信号是复数基带需要上变频到 5GHz 频段才能发射。不过 FPGA 里的 802.11a 通常只做基带处理射频部分交给外部的射频前端芯片。因此 FPGA 输出的是 20MHz 采样率下的正交 IQ 基带数据可以送到 DAC 再经射频前端上变频。有些开发板上直接集成了射频芯片但数字基带和模拟射频的接口还需要配置比如 IQ 的直流偏置校正、正交性校准等。这些模拟域的校准在调试中比数字部分还让人头疼经常出现星座图旋转或镜像。我在项目里先用仿真生成了基带 IQ 数据再经 RF 前端输出到频谱仪能够看到标准的 OFDM 频谱。为了验证发射基带的正确性我同时把 IQ 数据存到文件里用 MATLAB 做接收处理把结果和 FPGA 接收链路的输出对比。这种交叉验证的方式帮我隔离了很多问题比如后来发现频谱方向反了是因为上变频器的本振相位设置问题跟基带无关。3. 接收通路设计在噪声和频偏里把波形“捞”回来3.1 帧检测与自动增益控制接收端最先碰到的就是“这一帧数据从哪里开始”的问题。我用的是短训练序列的周期自相关法。思路很简单短训练序列每 16 个采样点重复一次把接收信号延迟 16 个采样点并计算共轭相关如果当前采样点落在短训练序列区域内相关值会明显出现一个峰值平台再用相关幅值跟一个自适应门限比较。门限不能设死要根据接收信号功率做归一化否则高增益下噪声也会触发误检。我在实现里用滑动窗计算信号平均能量然后把相关值除以能量得到归一化度量再跟固定门限比较。自动增益控制AGC通常和帧检测协同工作。射频前端的增益需要在一个动态范围内调整让 ADC 输入信号幅度既不饱和也不太弱。由于短训练序列长达 16us完全有时间在帧检测的同时完成 AGC 收敛。我在 FPGA 里做的是先统计短训练序列前几段的峰值调整一个数字增益因子让信号幅度稳定在 ADC 满量程的 70% 左右。由于 FPGA 内部处理是定点数数字 AGC 本质上就是固定增益移位和乘法配合模拟 VGA 的粗调。注意 AGC 的更新必须在帧检测触发后关住环路避免调整完的符号后又被噪声翻转。3.2 符号定时同步与频率同步符号定时同步的目标是找到 FFT 窗口的起始点即数据符号中第一个有用采样点的位置。我采用的是一种“延迟相关 峰值搜索”的方法对短训练序列做延迟相关得到定时度量曲线在曲线峰值处前移几个点通常可以恢复出最佳 FFT 窗口。定时偏差如果只是整数采样点会影响信道估计的相位如果是小数采样点会产生子载波间干扰。为了把小数延时补偿掉还需要一个插值滤波器。考虑到项目时间我直接让定时偏差控制在整数点范围内然后用信道均衡去吸收残余相位。频率同步分为粗同步和细同步两步。粗频偏利用短训练序列的相关峰值相位计算出整个信号带宽内的整数倍频偏相对子载波间隔细频偏利用长训练序列的两个重复段算出精确到子载波间隔几分之一的残余频偏。FPGA 里做相位估计最常用的是 CORDIC 算法或者查表反正切。我因为时间约束用了查表逼近把相位值查出来再通过一个数字振荡器生成正弦/余弦波对信号做混频。频偏补偿的混频频点需要对每个采样点更新这个模块要求乘法器数量不少但好在可以流水化处理。3.3 信道估计与均衡信道估计是 OFDM 接收机的灵魂。从时域看长训练序列提供两个已知的 OFDM 符号它们经过信道后包含了信道在 52 个子载波上的频率响应。我在接收端先对长训练序列做 FFT然后与已知长训练序列的频域值相除得到每个子载波上的信道估计值 H[k]。为了减小噪声影响可以把两个长训练符号的信道响应进行平均。这个计算很简单难点主要在于相除的硬件实现除数是复数FPGA 里直接做复数除法不划算我改成了共轭相乘后除以模平方这样一来只需要乘法和一次倒数用查找表算出模平方倒数再相乘即可。信道均衡是在频域对每个数据子载波乘以 H[k] 的共轭并除以 |H[k]|^2。这里有个增强噪声的问题当某个子载波落在深度衰落点|H[k]|^2 很小直接除以它会把噪声放大很多倍。为了抑制这个问题可以用 ZF 均衡或 MMSE 均衡。ZF 实现简单但会产生噪声增强MMSE 需要一个噪声方差估计效果在低信噪比下好一些。我最后实现的是简化的 MMSE在 H[k] 的模平方上加一个小的正则项让分母不会趋近于零。这个正则项我用固定的经验值没有做自适应的噪声估计虽然不完美但链路还能接受。3.4 解映射、解交织与维特比译码均衡之后得到的是带有残余偏差的复数星座点。如果发的是硬比特的信息只需要按星座图划分区域做判决但为了配合卷积码的维特比译码尽可能保留软信息因为软判决能多出约 2dB 的增益。对于 BPSK 和 QPSK直接取 I、Q 的实数作为软信息即可对于 16QAM/64QAM需要按比特映射规则计算每个比特的近似对数似然比。为了省硬件我用的是“距离度量近似法”将星座点划分成多个小区间用最小距离差作为近似软信息。这种方式比完整对数似然比简单很多性能损失也不大。解交织是发射端交织的逆过程因为交织器是可逆置换只要把地址计算逻辑改为逆映射就行。这里最担心的是地址计算的位宽错误我在仿真阶段反复核对映射表确保解交织后的比特顺序和编码器的输出完全一致。维特比译码我没有自己从零写而是调用了 Xilinx 的 Viterbi IP 核。它支持约束长度 7 和多种码率但需要提供打孔指示。起初我没搞明白 IP 核的打孔输入时序导致 3/4 码率下译码后误码率不正常。后来仔细看了 IP 核文档发现打孔位置需要在输入数据上打上 “valid” 标记打掉的位置输出零。把打孔操作放在解交织之前做译码器就听话了。4. FPGA 实现中的定点化与资源权衡4.1 定点量化比特宽度怎么定用 FPGA 处理信号首先要回答一个问题每个节点用多少位宽。位宽太窄量化噪声会吞掉 OFDM 本身带来的性能优势位宽太宽资源消耗大、时序难收敛。我做了一个简单的误差仿真用 MATLAB 的浮点链路作为基准将各个模块分别定点化观察最终误码率的变化。结论是接收 ADC 输入如果用 12 位FFT 中间节点建议 16 位QAM 映射后的 I/Q 用 12 位均衡器的乘法器输出保留 18 位再截位到 12 位。截位时不能简单直接截断最好带随机抖动或者至少用四舍五入否则会出现直流偏置和低幅度信号失真。具体到 FFT IP 核它的输入、输出、中间级位宽都是可配置的而且能自动做块浮点缩放。我刚开始图省事设置了全精度模拟发现 DSP 片子的数量浪费很多时序也跑不到 50MHz 以上。后来改成块浮点动态范围没问题资源降了不少。块浮点的原理是所有蝶形级共享一个指数缩放相当于每级都自动归一化对 OFDM 这种信号很合适。不过要注意块浮点的缩放因子必须要和信道估计模块对齐否则均衡出来的幅度会偏。我在这里折腾了整整一个下午就是因为缩放因子没同步。4.2 时序收敛与流水线设计FPGA 实现不仅能跑通功能还要让时序收敛到期望时钟频率。802.11a 基带处理在 20MHz 采样率下运行理论上很轻松但我把发射和接收链路整合到一个单时钟域里要跑 50MHz 才能让流水线有足够裕量。遇到的问题是维特比译码器的回溯深度比较长而且它的输入数据来自解交织器解交织器又依赖一个循环地址发生器形成了较长的组合逻辑路径。我通过把解交织输出加寄存器打拍切断过长路径最终在 Xilinx 7 系列上跑到 100MHz 以上完全满足带宽需求。流水线设计里最忌讳的是组合逻辑环。比如导频极性表和符号计数器之间如果存在循环依赖综合器虽然会费很大劲去平衡时序但实际电路行为不可控。我在设计里强制所有跨模块控制信号都经过至少一个寄存器同步并且用 valid/ready 握手信号跨模块传输避免数据竞争。调试握手信号会费不少时间但这属于必不可少的结构开销。状态机设计尽量采用一段式把所有输出在状态转移的同一时刻更新这样逻辑清晰时序也更稳定。相比之下二段式三段式在复杂控制下很容易出现毛刺。5. 调试实录那些年我们踩过的坑5.1 常见问题速查表做这个项目我踩过的坑总结成了一句话链路提示“正常”和链路真正“正确”之间差着十万八千里。下面是我实际遇到的高频问题和排查手段列成表格方便参考。现象可能原因排查方法帧检测经常误触发门限太灵敏或信号功率估计不准检查能量归一化逻辑用仿真对比相关值峰值FFT 后星座点整体旋转频偏未完全补偿用导频计算残余相位偏移调整细同步环路某些子载波上星座点散开信道估计不准检查长训练序列接收路径位宽对齐确认 FFT 缩放因子解交织后顺序错误交织地址生成与实际不符用发射端交织器输出作为接收端测试向量做闭环比对维特比译码输出错误率极高打孔时序或软信息符号极性错误检查打孔模式确认 BPSK 软信息是否有符号翻转频谱仪看到的频谱不对称上变频 IQ 镜像检查 IQ 修正值确认本振频率和相位配置5.2 仿真与上板联调的经验我强烈建议不要上来就接射频前端调板子先在仿真环境里把基带链路闭环。具体做法是用 MATLAB 或仿真器生成一组随机比特经过发射链路仿真得到基带波形再在仿真环境中加入高斯白噪声和已知频偏最后把波形喂给接收链路解出比特流和原始比特比对统计误码率。只有在仿真闭环完全正确的情况下才能上板联调。上板后最先观察的是频谱仪上的发射波形母要确认频谱形状符合预期再来调接收。我在联调过程中遇到的最诡异问题是从频谱仪上看一切都正常但接收端 QPSK 星座老是绞着转圈。后来发现是接收端 FFT 窗口启始位置偏移了若干个采样点导致每个子载波上引入一个线性相位偏差信道均衡虽然能补偿一部分但符号间还有残余偏转。把定时同步窗口往前微调了几个采样点之后星座点立刻聚拢。这个案例说明接收机的同步算法不能只看单模块仿真一定要和信道响应一起看。还有一次是误码率在某几个子载波上特别高其他地方都很好。我查了半天找不到原因最后发现是导频极性序列索引跟 FFT 输出子载波顺序对不齐。原来我的 IFFT 输入是按频率索引排列的但 FFT 输出是顺序索引需要做一次重排而我的重排ROM有一处写反了。这种情况如果只观察星座图根本看不见必须逐个载波打印误码统计才能暴露。5.3 小技巧与避坑心得先把收发链路的所有参数表交织映射、导频序列、卷积打孔模式全部写成只读 ROM集中管理避免散落各处。我自己最初把交织表放在一个大 case 语句里每次需要改参数都要翻代码找半天最后改成 ROM 后后期调试效率一下子就上来了。还有就是时钟域。基带处理虽然用的同一个主时钟但有些参考信号比如随机数种子、导频极性需要和“符号索引”严格同步。我建议所有跨模块的符号计数都在一个统一的 counter 上派生模块内部不要自己偷偷数这样一旦出错马上能在波形窗口里看出来。另外仿真时可别只测高信噪比低信噪比下很多硬件细节问题才会暴露出来。特别是定点截位导致的平坦底噪在 Eb/N0 只有 6dB 时会让误码率曲线出现一个平台。如果发现误码率随着 SNR 提高不下降多半是量化噪声或同步残留导致的问题需要回过去削弱截位误差。6. 扩展思考与这个项目带给我的体会6.1 往后还能怎么做做完 802.11a 的 OFDM 调制解调链路之后后面其实有好多可以继续挖的方向。一个是把链路做成多入多出MIMO版本这需要加空时编码、更多的通道同步和信道估计复杂度会上一个新台阶。还有一个是把这个基带当作一个低速、低功耗的私有协议处理引擎配合外设接口实现一个简单的小数据传输系统这在非常多物联网、自组网场景里能用上。另外802.11a 的 OFDM 参数和后续的 802.11n/ac/ax 有很多相似之处但 802.11ax 增加了 OFDMA 和多路用户复用FFT 点数也更大。如果把这个基础链路搞懂再看那些新协议就相当于在框架上往不同方向发展不会觉得陌生。对我来说这个项目的意义在于把教科书上的公式变成了手里能调参数、能看波形的实物系统这种成就感不是光做仿真可以给的。6.2 做这个项目后我的真实体会现在回顾这个“FPGA 实现基于 802.11a 协议的 OFDM 调制解调之旅”我最深的感受是协议实现最重要的不是把某个算法做得多花哨而是把数据流和控制流的关系理清楚。OFDM 看起来是一堆数学变换真正在 FPGA 上跑起来后你会发现它本质上是一连串的“排队”问题什么时候来数据什么时候输出哪些计算可以并行哪些必须串行。很多人上来就闷头写 IFFT 和维特比结果整体架构没理顺最后联调被各种时序和字节对齐问题折磨。我还记得第一次在示波器上看到稳定收敛的星座图时的场景。那个时刻我盯着屏幕愣了几秒不是因为星座图多漂亮而是心里清楚那背后是一整套环环相扣的同步和均衡逻辑在同时工作。那个瞬间突然觉得前面那些改代码、调门限、焊线抓包的日子全都值了。如果你也在啃类似的无线物理层项目别急着追求高指标先把一条干净的数据通路跑出来再从链路性能倒推去优化各个模块。这条路看着绕反而是实际走得最快的。