前阵子刚啃完802.11a的OFDM FPGA实现,踩了一堆坑,今天整理点干货出来,不会扯太多太玄的理论,都是实际写代码时遇到的破事

发布时间:2026/7/25 5:21:39

前阵子刚啃完802.11a的OFDM FPGA实现,踩了一堆坑,今天整理点干货出来,不会扯太多太玄的理论,都是实际写代码时遇到的破事 FPGA实现ofdm调制解调基于802.11a协议包含qpsk调制解调循环前缀添加去除。首先先交代下背景802.11a就是最早的5G WiFi标准核心就是用了OFDM调制每个OFDM符号带64个子载波其中48个传数据4个放导频剩下12个是空子载波不用管。整个流程拆下来就是比特流→QPSK映射→串并转→IFFT→加循环前缀→发出去接收端反过来去CP→FFT→QPSK解调→串转并输出比特。先从最基础的QPSK映射开始说吧毕竟每个QPSK符号能扛2bit数据是整个调制的基础。我一开始写的版本特别糙就是直接把00/01/10/11对应±1的I/Q值结果后续IFFT出来的信号幅度太小FPGA定点数处理的时候丢了好多精度后来改成把幅度拉到8位有符号数的最大值127刚好不会溢出代码大概长这样// QPSK映射模块输入是2bit一组的串行比特输出I/Q两路并行符号 module qpsk_mapper( input clk, input rst_n, input din_valid, input [1:0] din, // 2bit对应一个QPSK符号 output reg dout_valid, output reg signed [7:0] I_out, output reg signed [7:0] Q_out ); always (posedge clk or negedge rst_n) begin if(!rst_n) begin I_out 8sd0; Q_out 8sd0; dout_valid 1b0; end else if(din_valid) begin // QPSK的映射规则00→(1,1), 01→(1,-1), 10→(-1,1), 11→(-1,-1) // 这里做了量化把幅度缩到127避免后续IFFT溢出 case(din) 2b00: begin I_out 8sd127; Q_out 8sd127; end 2b01: begin I_out 8sd127; Q_out -8sd127; end 2b10: begin I_out -8sd127; Q_out 8sd127; end 2b11: begin I_out -8sd127; Q_out -8sd127; end default: begin I_out 8sd0; Q_out 8sd0; end endcase dout_valid 1b1; end else begin dout_valid 1b0; end end endmodule这段代码没啥花活但是当时踩了个坑一开始没考虑串并转换的时序直接把串行比特送进来结果每个符号的比特错位了后来加了valid信号同步才把这个问题搞定。接下来是IFFT部分802.11a用的是64点IFFT把频域的子载波信号转成时域的OFDM符号。我直接用了Xilinx的xfft IP核但是官方文档真的太绕了一开始把I和Q的输入顺序搞反了出来的波形完全不对后来翻了IP核的用户指南才知道Xilinx的FFT IP输入是先IM再RE代码例化大概是这样// 例化Xilinx的64点IFFT IP配置为定点数、浮点转定点模式输出是时域OFDM符号 wire [63:0] ifft_din_re; // 64路并行I路数据每个是16bit定点 wire [63:0] ifft_din_im; // 64路并行Q路数据 wire ifft_din_valid; wire [63:0] ifft_dout_re; wire [63:0] ifft_dout_im; wire ifft_dout_valid; xfft_64_point u_ifft( .aclk(clk), .aresetn(rst_n), .s_axis_data_tdata({ifft_din_im, ifft_din_re}), // 划重点IP输入是IM在前RE在后 .s_axis_data_tvalid(ifft_din_valid), .m_axis_data_tdata({ifft_dout_im, ifft_dout_re}), .m_axis_data_tvalid(ifft_dout_valid) );这里还要提一下串并转换的部分我们前面QPSK输出的是串行的I/Q符号需要先把48个数据4个导频子载波按顺序填到64路并行的总线里导频的位置是固定的-21、-7、7、21这个地方我一开始直接把数据塞到所有子载波结果接收端根本同步不了后来查了标准文档才知道导频是用来做信道估计的必须插对位置。然后就是循环前缀添加了这个是我调最久的部分。802.11a要求把IFFT输出的最后16个采样点复制到符号前面用来对抗多径时延也就是总共有641680个采样点每个OFDM符号。一开始我写了个偷懒的版本直接把前16个周期输出后面的采样结果接收端同步的时候完全对不上后来改成用双端口RAM把64个采样点存下来先读最后16个作为CP再读全部64个采样才搞定// 改进版CP添加用双端口RAM存IFFT输出的64个采样避免采样错位 reg [15:0] ram_re[63:0]; reg [15:0] ram_im[63:0]; reg [5:0] wr_addr; reg [5:0] rd_addr; reg wr_en; always (posedge clk or negedge rst_n) begin if(!rst_n) begin wr_addr 6d0; wr_en 1b0; end else if(ifft_dout_valid wr_addr 6d63) begin wr_en 1b1; ram_re[wr_addr] ifft_dout_re[15:0]; ram_im[wr_addr] ifft_dout_im[15:0]; wr_addr wr_addr 1b1; end else begin wr_en 1b0; end end // 读的时候先读最后16个地址48到63作为CP再读0到47的有效数据 reg [3:0] cp_cnt; always (posedge clk or negedge rst_n) begin if(!rst_n) begin rd_addr 6d48; cp_cnt 4d0; dout_valid 1b0; end else begin if(cp_cnt 4d15) begin // 输出CP部分 tx_ofdm_re ram_re[rd_addr]; tx_ofdm_im ram_im[rd_addr]; rd_addr rd_addr 1b1; cp_cnt cp_cnt 1b1; dout_valid 1b1; end else if(cp_cnt 4d15) begin // 输出最后一个CP采样然后切换到读0地址 tx_ofdm_re ram_re[rd_addr]; tx_ofdm_im ram_im[rd_addr]; rd_addr 6d0; cp_cnt cp_cnt 1b1; dout_valid 1b1; end else begin // 输出有效数据部分 tx_ofdm_re ram_re[rd_addr]; tx_ofdm_im ram_im[rd_addr]; rd_addr rd_addr 1b1; if(rd_addr 6d63) begin rd_addr 6d48; cp_cnt 4d0; end dout_valid 1b1; end end end当时为了验证这个CP有没有加对我用Matlab生成了一个标准的OFDM符号然后用ILA抓了FPGA输出的波形对比下来几乎一模一样才放心了。FPGA实现ofdm调制解调基于802.11a协议包含qpsk调制解调循环前缀添加去除。然后说接收端的部分其实就是发射端的逆过程先去掉循环前缀也就是把前16个采样丢掉只取后面64个时域采样代码非常直观// 循环前缀去除输入是带CP的80采样点输出64个时域采样给FFT module cp_remove( input clk, input rst_n, input din_valid, input signed [15:0] din_re, input signed [15:0] din_im, output reg dout_valid, output reg signed [15:0] dout_re, output reg signed [15:0] dout_im ); reg [6:0] sample_cnt; // 0~79刚好80个采样点 always (posedge clk or negedge rst_n) begin if(!rst_n) begin sample_cnt 7d0; dout_valid 1b0; end else if(din_valid) begin if(sample_cnt 7d16 sample_cnt 7d80) begin // 丢掉前16个CP从16到79共64个采样 dout_re din_re; dout_im din_im; dout_valid 1b1; sample_cnt sample_cnt 1b1; end else if(sample_cnt 7d80) begin sample_cnt 7d0; dout_valid 1b0; end else begin sample_cnt sample_cnt 1b1; dout_valid 1b0; end end end endmodule接下来是FFT模块和IFFT几乎一样只是把IP核的配置改成FFT模式就行然后取出频域的48个数据子载波和4个导频送到QPSK解调模块。QPSK解调的代码其实和映射是对称的不过要注意接收端的I/Q信号可能因为信道噪声稍微有点偏移所以不要硬比较±127而是用大于0小于0来判断象限这样容错性更好// QPSK解调模块输入I/Q两路符号输出2bit的串行比特 module qpsk_demapper( input clk, input rst_n, input din_valid, input signed [7:0] I_in, input signed [7:0] Q_in, output reg dout_valid, output reg [1:0] dout ); always (posedge clk or negedge rst_n) begin if(!rst_n) begin dout 2b00; dout_valid 1b0; end else if(din_valid) begin // 根据I和Q的正负判断象限映射回2bit比特 if(I_in 0 Q_in 0) begin dout 2b00; end else if(I_in 0 Q_in 0) begin dout 2b01; end else if(I_in 0 Q_in 0) begin dout 2b10; end else begin dout 2b11; end dout_valid 1b1; end else begin dout_valid 1b0; end end endmodule最后把所有模块串起来的顶层就不多说了主要就是时序同步的问题每个模块的valid信号一定要对齐不然就会出现数据错位的问题。整个项目下来我觉得最难的不是写代码而是搞清楚802.11a的帧结构和各个模块之间的时序配合还有调IP核的时候的各种配置细节比如FFT的输出位宽、输入输出顺序这些稍微错一点就完全不对。不过当最后用ILA抓到正确的OFDM符号波形并且接收端能正确解出发送的比特流的时候还是挺有成就感的。对了还有个小细节802.11a的OFDM符号采样率是20MHz所以每个采样点的时间是50ns64个时域采样点就是3.2us加上16个CP的0.8us每个OFDM符号的总时长是4us这个在写时序约束的时候要注意一下。

相关新闻