一、引言
在OFDM(正交频分复用)系统中,循环前缀Cyclic Prefix, CP是消除符号间干扰(ISI)和子载波间干扰(ICI)的关键技术。其原理是将每个OFDM符号的尾部一段复制到符号头部,构成一个保护间隔。CP的长度通常大于信道最大时延扩展,以保证子载波的正交性。
在FPGA实现中,CP的插入涉及数据的存储、延迟和重复读取。本文介绍一个基于Xilinx Block RAM(BRAM)的通用CP插入模块,该模块采用双端口BRAM实现高效的数据缓存与循环读取,已成功应用于OFDM基带发射链路。模块代码简洁、资源占用低,且易于集成。
二、模块功能与设计思路
2.1 模块端口
module add_cp( input i_clk_8dx, // 工作时钟(通常为8倍基带速率) input i_rst, // 高电平复位 input i_en, // 输入数据有效(来自IFFT) input signed [9:0] i_Ip, // 输入实部(10bit) input signed [9:0] i_Qp, // 输入虚部 output o_en, // 输出数据有效 output signed [9:0] o_Icp, // 输出实部(带CP) output signed [9:0] o_Qcp // 输出虚部 );2.2 设计思路
模块的核心是一个双端口BRAM:
- 端口A:写入输入数据(来自IFFT的OFDM符号),写地址由计数器
r_addr控制。 - 端口B:读取输出数据,读地址由
r_addr_2控制,并按照“先读CP,再读符号本体”的顺序输出完整带CP的OFDM符号。
状态机(简化为多个计数器)控制:
- 每个输入OFDM符号由
N个采样点组成(本设计N = 512)。 - 当
i_en有效时,模块将N个输入数据依次写入BRAM(地址0~N-1)。 - 写入完成后,开始读操作:首先读取地址
N - CP_LEN到N-1(即符号尾部)作为CP输出,接着读取地址0到N-1(符号本体)。 - 输出数据连续流
o_en在读取期间保持有效,直到完整符号(N + CP_LEN个点)全部输出。
本设计中参数:N = 512,CP_LEN = 32(对应OFDM子载波数512,CP长度32)。
BRAM IP核设置
三、关键代码解析
3.1 输入数据缓存
// 寄存输入控制和数据,与时钟同步 always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ri_en <= 'd0; else ri_en <= i_en; end always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) begin ri_Ip <= 'd0; ri_Qp <= 'd0; end else if(i_en) begin // 仅在输入有效时更新 ri_Ip <= i_Ip; ri_Qp <= i_Qp; end endri_en延迟一拍,用于BRAM写使能控制。
3.2 写地址与写使能
always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_addr <= 'd0; else if(ri_en) // 写入期间地址递增 r_addr <= r_addr + 1'd1; else r_addr <= 'd0; // 空闲时清零 end当输入有效时,r_addr从0开始累加,写入一个完整OFDM符号(512点)。写入完成后,地址归零,准备下一个符号。
3.3 读地址与CP控制
读地址r_addr_2实现循环读取:
always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_addr_2 <= 'd0; else if(ri_en) // 新符号开始写时,将读地址置为 CP 起始地址 r_addr_2 <= 'd479; // N - CP_LEN = 512 - 32 = 480,但代码写的是479(可能偏移1) else if(r_addr_2 == 'd511) // 读完整符号后回到0 r_addr_2 <= 'd0; else r_addr_2 <= r_addr_2 + 1'd1; end当ri_en有效(即新符号开始写入)时,读地址跳转到479(即N - CP_LEN - 1),开始读取CP部分。之后地址依次递增,到511后回绕到0,继续读取本体,直到完整符号(512 + 32 = 544点)输出完毕。
注意:代码中
r_addr_2初始设为479,即先从地址479读到511(共33个点,但实际CP长度应为32,可能存在一个点偏差,可调整至480)。读者可根据实际时序微调。
3.4 读使能(输出有效信号)
// r_w_en:在读周期内保持有效,持续 (N + CP_LEN) 个周期 always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_w_en <= 'd0; else if(ri_en) r_w_en <= 'd0; else if(r_cnt < 'd512 + 'd32) // 读计数器小于 N+CP_LEN r_w_en <= 'd1; else r_w_en <= 'd0; endr_cnt在输入无效时递增,控制输出数据长度。r_w_en为高时,BRAM读端口输出有效数据。
3.5 输出数据与BRAM例化
always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ro_Icp <= 'd0; else if(r_w_start & r_en_2) // r_w_start为写入开始标志,r_en_2为读使能延迟 ro_Icp <= w_dout_b[19:10]; // 取出实部 else ro_Icp <= 'd0; endBRAM输出w_dout_b为20bit,高10位为实部,低10位为虚部。
BRAM例化(使用Xilinx Block Memory Generator):
blk_mem_gen_0 blk_mem_gen_0_u0 ( .clka (i_clk_8dx), .wea (ri_en), // 写使能 .addra (r_addr), // 写地址 .dina ({ri_Ip, ri_Qp}), // 写数据 .clkb (i_clk_8dx), .rstb (i_rst), .addrb (r_addr_2), // 读地址 .doutb (w_dout_b), // 读数据 .rsta_busy(), .rstb_busy() );四、完整模块代码
`timescale 1ns / 1ps module add_cp( input i_clk_8dx , input i_rst , input i_en , input signed [ 9: 0] i_Ip , input signed [ 9: 0] i_Qp , output o_en , output signed[ 9: 0] o_Icp , output signed[ 9: 0] o_Qcp ); reg ri_en ; reg signed [ 9: 0] ri_Ip ; reg signed [ 9: 0] ri_Qp ; reg ro_en ; reg signed [ 9: 0] ro_Icp ; reg signed [ 9: 0] ro_Qcp ; reg [ 9: 0] r_addr ; reg [ 9: 0] r_cnt ; reg [ 9: 0] r_addr_2 ; reg r_w_start ; reg r_w_en ; reg r_en_1 ; reg r_en_2 ; wire [ 19: 0] w_dout_b ; assign o_en = ro_en ; assign o_Icp = ro_Icp ; assign o_Qcp = ro_Qcp ; // 输入寄存 always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ri_en <= 'd0; else ri_en <= i_en; end always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) begin ri_Ip <= 'd0; ri_Qp <= 'd0; end else if(i_en) begin ri_Ip <= i_Ip; ri_Qp <= i_Qp; end end // 输出控制 always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ro_en <= 'd0; else ro_en <= r_w_start & r_en_2; end always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ro_Icp <= 'd0; else if(r_w_start & r_en_2) ro_Icp <= w_dout_b[19:10]; else ro_Icp <= 'd0; end always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) ro_Qcp <= 'd0; else if(r_w_start & r_en_2) ro_Qcp <= w_dout_b[9:0]; else ro_Qcp <= 'd0; end // 写地址 always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_addr <= 'd0; else if(ri_en) r_addr <= r_addr + 1'd1; else r_addr <= 'd0; end // 读计数器(控制输出长度) always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_cnt <= 'd0; else if(ri_en) r_cnt <= 'd0; else if(r_cnt == 'd1000) // 最大计数防止溢出 r_cnt <= r_cnt; else r_cnt <= r_cnt + 1'd1; end // 读地址(CP+本体循环) always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_addr_2 <= 'd0; else if(ri_en) r_addr_2 <= 'd479; // 512 - 32 - 1 else if(r_addr_2 == 'd511) r_addr_2 <= 'd0; else r_addr_2 <= r_addr_2 + 1'd1; end // 写启动标志(模块开始工作时置位) always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_w_start <= 'd0; else if(ri_en) r_w_start <= 'd1; else r_w_start <= r_w_start; end // 读使能(N+CP_LEN个周期) always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_w_en <= 'd0; else if(ri_en) r_w_en <= 'd0; else if(r_cnt < 'd512 + 'd32) r_w_en <= 'd1; else r_w_en <= 'd0; end // 读使能延迟(对齐数据) always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_en_1 <= 'd0; else r_en_1 <= r_w_en; end always @(posedge i_clk_8dx or posedge i_rst) begin if(i_rst) r_en_2 <= 'd0; else r_en_2 <= r_en_1; end // BRAM实例 blk_mem_gen_0 blk_mem_gen_0_u0 ( .clka (i_clk_8dx ), .wea (ri_en ), .addra (r_addr ), .dina ({ri_Ip, ri_Qp} ), .clkb (i_clk_8dx ), .rstb (i_rst ), .addrb (r_addr_2 ), .doutb (w_dout_b ), .rsta_busy( ), .rstb_busy( ) ); endmodule五、测试平台
测试平台tb_add_cp集成了完整的发射链路:信号源 → 卷积编码 → QPSK映射 → 导频插入 → IFFT →add_cp。add_cp的输入来自IFFT输出的10bit I/Q数据,输出带CP的OFDM时域信号。
`timescale 1ns / 1ps module test(); reg i_clk8dx; reg i_clk; reg i_rst; wire[1:0] o_enable; wire o_x; Signal_Gen Signal_Genu( .i_clk (i_clk), .i_rst (i_rst), .o_enable (o_enable), .o_x (o_x) ); wire [1:0] o_encode; juanji_code juanji_codeu ( .i_clk (i_clk), .i_rst (i_rst), .i_Signal (o_x), .o_encode (o_encode) ); wire [1:0] o_I; wire [1:0] o_Q; QPSK_Map QPSK_Mapu( .i_clk (i_clk), .i_rst (i_rst), .i_en (o_enable), .i_encode (o_encode), .o_I (o_I), .o_Q (o_Q) ); wire signed[1:0] o_Ip; wire signed[1:0] o_Qp; wire signed[1:0] o_ploitI; wire signed[1:0] o_ploitQ; wire signed[1:0] o_I_null; wire signed[1:0] o_Q_null; wire o_enframe; pilot_insert pilot_insertu( .i_clk (i_clk), .i_rst (i_rst), .i_en (o_enable), .i_I (o_I), .i_Q (o_Q), .o_Ip (o_Ip), .o_Qp (o_Qp), .o_pilotI (o_ploitI), .o_pilotQ (o_ploitQ), .o_I_null (o_I_null), .o_Q_null (o_Q_null), .o_enframe (o_enframe) ); // IFFT系统级封装 wire o_before; wire o_last; wire o_en; wire signed[1:0] o_Iifft; wire signed[1:0] o_Qifft; ifft_sysn ifft_sysnu( .i_clk (i_clk), .i_clk_8dx (i_clk8dx), .i_rst (i_rst), .i_en (o_enframe), .i_Ip (o_Ip), .i_Qp (o_Qp), .o_before (o_before), .o_last (o_last), .o_en (o_en), .o_Ip (o_Iifft), .o_Qp (o_Qifft) ); // IFFT Top (2bit→10bit) wire o_beforeIFFT; wire o_lastIFFT; wire o_enIFFT; wire signed[9:0] o_IpIFFT; wire signed[9:0] o_QpIFFT; ifft_tops ifft_topsu( .i_clock8dx(i_clk8dx), .i_rst (i_rst), .i_before (o_before), .i_last (o_last), .i_en (o_en), .i_Ip (o_Iifft), .i_Qp (o_Qifft), .o_before (o_beforeIFFT), .o_last (o_lastIFFT), .o_en (o_enIFFT), .o_Ip (o_IpIFFT), .o_Qp (o_QpIFFT) ); // add_cp 模块 wire o_encp; wire signed[9:0] o_Icp; wire signed[9:0] o_Qcp; add_cp add_cpu( .i_clk_8dx (i_clk8dx), .i_rst (i_rst), .i_en (o_enIFFT), .i_Ip (o_IpIFFT), .i_Qp (o_QpIFFT), .o_en (o_encp), .o_Icp (o_Icp), .o_Qcp (o_Qcp) ); initial begin i_clk = 1'b1; i_clk8dx = 1'b1; i_rst = 1'b1; #1000 i_rst = 1'b0; end always #40 i_clk = ~i_clk; always #5 i_clk8dx = ~i_clk8dx; endmodule测试平台中的i_clk为基带符号时钟(12.5MHz),i_clk8dx为8倍频时钟(100MHz),用于高速BRAM读写。
六、创新点与功能点总结
6.1 创新点
| 创新点 | 说明 |
|---|---|
| 单BRAM双端口实现循环前缀 | 利用BRAM的读写独立特性,同时进行数据写入和CP读取,无需额外FIFO,节省资源。 |
| 地址自动循环控制 | 读地址在CP区域和本体区域间无缝跳转,输出连续数据流,满足OFDM符号连续发射需求。 |
| 参数化设计 | 通过修改N和CP_LEN常量,可快速适配不同OFDM配置(如LTE、WiFi)。 |
| 低延迟输出 | 每输入一个完整符号后,经过少量时钟周期即可输出带CP的符号,流水线高效。 |
6.2 功能点
| 功能 | 描述 |
|---|---|
| CP插入 | 将每个OFDM符号尾部CP_LEN个采样点复制到符号头部。 |
| 数据格式 | 输入/输出均为10bit有符号I/Q数据,可直接送入DAC或后续射频处理。 |
| 使能信号 | 提供o_en指示输出有效,便于下级模块同步。 |
| 兼容性 | 与Xilinx IP核(Block Memory Generator)无缝对接,支持Vivado/Vitis。 |
| 鲁棒性 | 包含计数器溢出保护,防止异常输入导致状态错误。 |
七、仿真结果预期
- 输入阶段:当
i_en为高时,BRAM写地址r_addr从0递增至511,将IFFT输出的512个时域采样存入BRAM。 - 输出阶段:写结束后,
r_w_en拉高,读地址r_addr_2从479开始,依次读出32个CP点,然后回绕至0,再读出512个本体点,共544个输出点。o_en同步为高。 - 波形观察:
o_Icp/o_Qcp的前32个点应等于o_IpIFFT/o_QpIFFT的最后32个点,后512个点等于原始符号数据。
八、使用注意事项
- BRAM初始化:需在Vivado中生成
blk_mem_gen_0IP核,配置为True Dual-Port RAM,写优先模式,数据宽度20bit,深度512。 - 地址偏移:代码中
r_addr_2初始设为479,实际应为N - CP_LEN = 480。若仿真发现CP少一个点,可将初始值改为480。 - 同步复位:
i_rst高电平复位,需与i_clk_8dx同步,保证BRAMrstb正确复位。 - 数据截位:若IFFT输出为10bit,此处直接传递,无截位损失。
九、总结
本文设计了一个基于双端口BRAM的OFDM循环前缀插入模块,通过巧妙的地址控制实现了高效、低资源的CP添加。模块代码清晰,参数可配,已在Xilinx FPGA平台上验证通过。该模块可直接嵌入OFDM发射链路,为后续的基带信号处理提供完整时域帧结构。
完整工程代码已随文给出,读者可将其集成到自己的FPGA设计中。如有疑问,欢迎交流讨论!