FPGA实战(50):Gardner定时同步算法的FPGA实现与MATLAB联合仿真

FPGA实战(50):Gardner定时同步算法的FPGA实现与MATLAB联合仿真
1. 引言在数字通信系统中,接收端需要从连续信号中恢复出符号的最佳判决时刻,即定时同步。Gardner定时误差检测算法是一种非数据辅助(NDA)的定时恢复方法,它利用每个符号周期内两个采样点的幅值关系来估计定时偏差,具有计算简单、对载波相位不敏感等优点,广泛应用于QPSK、QAM等调制方式的接收机中。本文将以一个完整的QPSK基带接收机为例,从算法原理、MATLAB浮点建模、Verilog RTL设计、到FPGA仿真验证,全面展示Gardner定时同步环的实现流程。所有代码均可直接运行,适合作为数字通信FPGA开发的参考。2. Gardner定时同步算法原理2.1 信号模型假设发送端采用根升余弦(RRC)脉冲成形,接收端以符号速率 ( T_s ) 的整数倍 ( M ) 进行过采样(通常 ( M=4 ))。接收信号 ( r(t) ) 包含未知的定时偏差 ( \tau ),采样序列为:r[n]=r(nTs+τ) r[n] = r(nT_s + \tau)r[n]=r(nTs​+τ)定时同步的目的就是估计 ( \tau ),从而在最佳时刻对信号进行重采样。2.2 Gardner误差检测公式Gardner算法在每个符号周期内取两个样点:一个位于最佳判决时刻(符号中点),一个位于两个最佳时刻的中间位置(即 ( T/2 ) 处)。误差信号由下式计算:e(k)=Ik((k−1)T+T2)⋅[Ik(kT)−Ik((k−2)T)]+Qk((k−1)T+T2)⋅[Qk(kT)−Qk((k−2)T)] e(k) = I_k \left( (k-1)T + \frac{T}{2} \right) \cdot \left[ I_k(kT) - I_k((k-2)T) \right] + Q_k \left( (k-1)T + \frac{T}{2} \right) \cdot \left[ Q_k(kT) - Q_k((k-2)T) \right]e(k)=Ik​((k−1)T+2T​)⋅[Ik​(kT)−Ik​((k−2)T)]+Qk​((k−1)T+2T​)⋅[Qk​(kT)−Qk​((k−2)T)]其中 ( I_k(\cdot) ) 和 ( Q_k(\cdot) ) 为插值得到的同相和正交分量,( T ) 为符号周期。该误差信号反映了定时偏差的大小和方向,当锁定时 ( e(k) ) 的均值为零。需要说明的是,Gardner算法要求每个符号周期内恰好有两个采样点,因此过采样倍数必须为2的倍数(如4),且输入采样率须为符号速率的两倍以上。2.3 环路结构定时恢复环路由三个主要部分组成:插值滤波器:根据分数间隔 ( \mu_k ) 从输入采样点中内插出最佳时刻的符号值。定时误差检测器(TED):按Gardner公式计算误差。环路滤波器:通常为二阶(比例+积分),将误差信号滤波后产生频率控制字。数控振荡器(NCO):累加频率控制字,产生插值使能信号和分数间隔 ( \mu_k )。闭环结构使NCO不断调整插值相位,最终使误差均值为零,完成同步。3. MATLAB浮点模型在HDL实现之前,我们先用MATLAB搭建浮点算法模型,验证环路的收敛性和性能。相关代码请见下文。3.1 参数设置len=300000;% 符号数Nsmps=4;% 过采样倍数Ts=3.125e-8;% 采样周期 (32 MHz)T=Nsmps*Ts;% 符号周期Time_err=2e-5;% 固定定时偏差(秒)SNRs=20;% 信噪比 (dB)3.2 发送端信号生成随机生成QPSK符号(I/Q两路独立BPSK),经过根升余弦脉冲成形滤波器。这里采用分段时域卷积的方式模拟基带成形,并且人为加入定时偏差(通过改变采样时刻的偏移)。该偏差在接收端通过Gardner环进行补偿。3.3 接收端信号将成形后的信号叠加AWGN,得到接收采样序列 ( I_{in} ) 和 ( Q_{in} )。3.4 Gardner环实现我们按照前面所述的公式,对每个符号周期进行迭代:NCO根据频率控制字 ( w ) 产生分数间隔 ( u ) 和插值使能。插值器采用线性插值:( y = u \cdot x_2 + (1-u) \cdot x_1 )。误差检测计算 ( e(k) ),需要用到前后多个插值输出。环路滤波器更新 ( w ),采用比例系数 ( C_1=0.001 ),积分系数 ( C_2 = C_1 \times 0.001 )。最终将插值输出的I/Q信号用于星座图显示和误码率评估。3.5 数据量化与保存为了进行FPGA仿真,我们将浮点信号量化为16位有符号整数,并保存为文本文件,供Vivado仿真读取。4. FPGA RTL设计4.1 顶层架构顶层模块Gardner_tops包含四个子模块,连接关系如下:i_I/i_Q → insert(插值) → error_check → looper → NCO_mod → 控制插值其中looper输出频率控制字给NCO_mod,NCO_mod产生插值使能o_sels和分数间隔o_uk反馈给insert,形成闭环。4.2 插值器insert该模块采用线性插值,利用一个移位寄存器链存储连续4个输入采样点。当i_sel == 1'b0时(即NCO指示需要插值),根据分数间隔i_u(Q15格式,16384代表1)计算:out=i_u×data1+(16384−i_u)×data2 \text{out} = \text{i\_u} \times \text{data1} + (16384 - \text{i\_u}) \times \text{data2}out=i_u×data1+(16384−i_u)×data2结果右移2位(因为乘数是16位,积为32位,取高14位再截断为16位),最终输出16位有符号数。4.3 误差检测error_check该模块实现Gardner误差公式。它需要四个插值输出:当前符号的I/Q值、前一符号的I/Q值、以及中间时刻的I/Q值。由于插值输出是以符号速率更新的(即每个符号产生一个新输出),但输入采样是4倍过采样,所以误差检测在符号速率下工作。模块内部使用两个移位寄存器链分别对I和Q进行延迟,以满足公式中所需的不同时刻的数据。时钟分频产生一个每2个时钟周期有效一次的脉冲,用于决定误差更新的时刻(因为NCO可能每个符号周期产生多次插值使能,但误差检测只需在每个符号周期计算一次)。4.4 环路滤波器looper二阶环路滤波器,包含比例路径和积分路径。误差信号i_err为32位有符号,比例系数 ( C_1 ) 近似为 ( 1/1024 ),积分系数 ( C_2 ) 近似为 ( 1/1048576 )。由于实际系数很小,我们通过移位和加法来实现乘法,避免使用DSP单元。滤波输出w为32位有符号,其中高16位作为NCO的频率控制字。额外添加了一个补偿网络,对w的高16位进行加权求和(系数为 ( 1/2+1/4+1/32+1/64+1/256 )),目的是平滑输出,提高环路稳定性。积分项寄存器r_w初始化为32'd670757376,该值对应NCO的初始频率接近符号速率,可加快锁定过程。4.5 数控振荡器NCO_modNCO采用相位累加结构,频率控制字i_wk为16位有符号。相位寄存器r_q_0初始化为16'd14745(即 ( 0.9 \times 2^{14} ))。每个时钟周期,相位减去i_wk(注意此处是减法,因为定时误差反馈是相减)。当相位变为负数时,表示发生溢出,此时:产生一个插值使能脉冲(o_sels = 1)。将分数间隔o_uk更新为 ( 2 \times ) 溢出前的相位(即2 * w_q)。相位加上16384(即1)进行模归一化。o_uk也用于判断是否产生复位脉冲:当o_uk = 50时,产生一个单周期复位信号o_reset,用于重置误差检测和环路滤波器的内部状态,以防止数值溢出。5. Verilog代码详解以下给出每个模块的完整Verilog代码,均已通过Vivado 2020.2综合与仿真。5.1 误差检测模块my_error_check.v`timescale 1ns / 1ps module error_check ( input i_clk, input i_rst, input signed [15:0] i_data1, input signed [15:0] i_data2, output wire signed [31:0] o_Err_dects ); //============================================================================= // reg //============================================================================= reg [1:0] r_cnt; reg r_clk_div; reg signed [15:0] r_data11; reg signed [15:0] r_data12; reg signed [15:0] r_data13; reg signed [15:0] r_data14; reg signed [15:0] r_data15; reg signed [15:0] r_data16; reg signed [15:0] r_data21; reg signed [15:0] r_data22; reg signed [15:0] r_data23; reg signed [15:0] r_data24; reg signed [15:0] r_data25; reg signed [15:0] r_data26; reg signed [31:0] ro_Err_dects; //============================================================================= // wire //============================================================================= wire signed [15:0] w_diff1; wire signed [31:0] w_rr1; wire signed [15:0] w_diff2; wire signed [31:0] w_rr2; //============================================================================= // assign //============================================================================= assign w_diff1 = i_data1 - r_data14; assign w_rr1 = $signed(r_data12) * $signed(w_diff1); assign w_diff2 = i_data2 - r_data24; assign w_rr2 = $signed(r_data22) * $signed(w_diff2); assign o_Err_dects = ro_Err_dects; //============================================================================= // always //============================================================================= // 时钟分频,产生2时钟周期的脉冲 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin r_cnt = 2'd0; r_clk_div = 1'd0; end else begin r_cnt = r_cnt + 2'd1; if (r_cnt == 2'd0) r_clk_div = 1'd1; else r_clk_div = 1'd0; end end // I路延时链 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin r_data11 = 16'd0; r_data12 = 16'd0; r_data13 = 16'd0; r_data14 = 16'd0; r_data15 = 16'd0; r_data16 = 16'd0; end else begin r_data11 = i_data1; r_data12 = r_data11; r_data13 = r_data12; r_data14 = r_data13; r_data15 = r_data14; r_data16 = r_data15; end end // Q路延时链 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin r_data21 = 16'd0; r_data22 = 16'd0; r_data23 = 16'd0; r_data24 = 16'd0; r_data25 = 16'd0; r_data26 = 16'd0; end else begin r_data21 = i_data2; r_data22 = r_data21; r_data23 = r_data22; r_data24 = r_data23; r_data25 = r_data24; r_data26 = r_data25; end end // 误差输出寄存器 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin ro_Err_dects = 32'd0; end else begin if (r_clk_div == 1'd1) ro_Err_dects = w_rr1 + w_rr2; else ro_Err_dects = ro_Err_dects; end end endmodule5.2 插值模块my_insert.v`timescale 1ns / 1ps module insert ( input i_clk, input i_rst, input i_sel, input signed [15:0] i_data, input signed [15:0] i_u, output wire signed [15:0] o_insert ); //============================================================================= // reg //============================================================================= reg signed [15:0] r_data1; reg signed [15:0] r_data2; reg signed [15:0] r_data3; reg signed [15:0] r_data4; reg signed [15:0] ro_insert; //============================================================================= // wire //============================================================================= wire signed [31:0] w_rr1; wire signed [31:0] w_rr2; //============================================================================= // assign //============================================================================= assign w_rr1 = $signed(i_u) * $signed(r_data1); assign w_rr2 = $signed(16'd16384 - i_u) * $signed(r_data2); assign o_insert = ro_insert; //============================================================================= // always //============================================================================= // 输入延时链 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin r_data1 = 16'd0; r_data2 = 16'd0; r_data3 = 16'd0; r_data4 = 16'd0; end else begin r_data1 = i_data; r_data2 = r_data1; r_data3 = r_data2; r_data4 = r_data3; end end // 插值输出,i_sel=0时更新 always @(posedge i_clk or posedge i_rst) begin if (i_rst) begin ro_insert = 16'd0; end else begin if (i_sel == 1'b0) ro_insert = w_rr1[31-2:16-2] + w_rr2[31-2:16-2]; else ro_insert = ro_insert; end end endmodule5.3 环路滤波器my_looper.v`timescale 1ns / 1ps module looper ( input i_clk, input i_rst, input signed [31:0] i_err, output wire signed [15:0] o_w ); //============================================================================= // reg //=======================