最近在做一个宽带接收机的项目,把整个信号链路从模拟前端搬进了FPGA里,涉及到的正好是基带与中频的算法处理。这个领域挺有意思,但资料散,很多刚入门的同学容易被一堆概念绕晕——DDC、DUC、CIC、FIR、NCO,再加上各种定标和时序问题,真是各有各的坑。
这篇文章就把我在FPGA上实现基带与中频算法时的一些思路、参数选择、代码写法和踩坑记录整理出来。核心围绕“为什么要这么算”“系数怎么定”“逻辑怎么写”展开,尽量说人话,让做FPGA的朋友能直接参考。
1. 项目概述与核心思路拆解
1.1 基带与中频在FPGA里到底“管”什么
先统一概念。在无线通信或者雷达接收机里,天线收到的信号经过低噪声放大、混频、滤波之后,会落到一个适合ADC采样的频率上,这个频率可能是几十兆赫兹到几百兆赫兹的中频(IF)。ADC把模拟中频信号变成数字中频信号之后,后面的活基本就交给FPGA了。
FPGA在基带与中频处理链路上主要干三件事:频率搬移、滤波整形、速率变换。频率搬移就是把中频信号搬回基带(零中频)或者搬到一个较低的数字中频;滤波整形是为了滤除带外噪声和镜像分量;速率变换则是把ADC的高采样率数据降低到后端解调器能处理的较低速率,这个过程也就是抽取。反过来,发送链路要做插值和上变频。
为什么选FPGA而不是DSP或者ARM?核心原因是并行性和确定性延迟。一个ADC采样率200MSPS、16位的片子,意味着每秒要处理200M个采样点,每个采样点要做乘加运算。这种数据吞吐量下,DSP的串行指令流很难扛住,而FPGA天然是流水线结构,一拍处理一个数据,延迟基本固定,非常适合做实时信号处理。
1.2 为什么中频处理比纯基带更“折腾”
有些同学问,能不能直接把ADC采样率降到基带,省掉中频处理?理论上可以,但实践上有几个现实问题:
- 直流偏置和1/f噪声。基带信号在零频附近,但ADC、运放、混频器都会引入直流偏置,而且低频闪烁噪声很大,信号会被噪声淹没。
- 镜像抑制难度。直接变频到基带的接收机对I/Q两路的幅度和相位一致性要求极高,稍有失配,镜像抑制比就会掉到30dB以下,而模拟器件很难做到宽频带内的高一致性。
- 采样率和信号带宽的矛盾。窄带信号放在很低的频率采,采样率就得提到很高,否则抗混叠滤波器不好做。
所以传统架构通常是模拟电路先混频到一个不太高的中频,ADC采样后,FPGA负责从这个中频把信号搬到基带,顺带完成抽取滤波。这个过程就是数字下变频,DDC。
DDC的基本结构是:NCO产生正交本振 → 混频器做复数乘法 → CIC或FIR抽取滤波 → 增益和定标调整。发送链路则是对称的DUC:插值滤波 → 混频上变频 → 数据给DAC。
理解了这条主链路,后面所有算法模块都是围绕它展开的。
2. 核心细节解析与实操要点
2.1 NCO与DDS:本地振荡器的相位累加器实现
NCO(Numerically Controlled Oscillator)是DDC的“心脏”,它产生sin/cos两路正交本振信号。最常用的实现方式是DDS(Direct Digital Synthesis),核心就是一个相位累加器加上查找表。
相位累加器的位数N决定了频率分辨率。假设系统时钟频率是( f_{clk} ),那么频率分辨率是:
[ \Delta f = \frac{f_{clk}}{2^N} ]
比如系统时钟100MHz,相位累加器32位,频率分辨率就是( 100\times 10^6 / 2^{32} \approx 0.0233 )Hz。这个精度完全够用。
相位累加器的递推公式很简单:
phase_acc <= phase_acc + phase_increment;频率控制字(FTW,Frequency Tuning Word)的计算方式:
[ FTW = \frac{f_{target}}{f_{clk}} \times 2^N ]
比如要把100MHz采样率下的信号搬到-10MHz(即本振10MHz),N=32,那FTW = (10e6 / 100e6) * 2^32 ≈ 429496729。
Verilog实现大概是这样的:
// 相位累加器 always @(posedge clk) begin if (rst) begin phase_acc <= 32'd0; end else begin phase_acc <= phase_acc + phase_inc; end end // 取高14位做查找表地址(LUT深度16384) wire [13:0] phase_addr = phase_acc[31:18]; // 查表输出正弦、余弦值 // 可以使用ROM IP核,初始化sin/cos波形数据 wire [15:0] sin_data, cos_data; sin_rom u_sin ( .clk(clk), .addr(phase_addr), .dout(sin_data) ); cos_rom u_cos ( .clk(clk), .addr(phase_addr), .dout(cos_data) );实际工程里一般不自己用Verilog写查找表,而是用Xilinx的DDS Compiler IP核。IP核里可选相位抖动(Dithering)来改善无杂散动态范围(SFDR)。我之前测过,14位输出、无抖动的SFDR大概在84dBc左右,加了抖动之后能到100dBc以上,输出位宽减少到12位,效果反而更好。抖动其实就是在相位累加器的低位加一个伪随机序列,打破量化误差的周期性,代价是噪声基底抬升一点点,但换来的SFDR提升非常值。
2.2 混频器:为什么用复数乘法而不是实数乘法
DDC里混频那一步,本质是让输入信号乘以NCO的sin和cos。设输入信号是:
[ x[n] = A\cos(2\pi f_{IF}nT_s + \theta) ]
NCO输出本振:
[ \cos(2\pi f_{LO}nT_s), \quad \sin(2\pi f_{LO}nT_s) ]
混频后I路为 ( x[n] \cdot \cos(2\pi f_{LO}nT_s) ),Q路为 ( -x[n] \cdot \sin(2\pi f_{LO}nT_s) )。
用积化和差公式展开:
[ x[n]\cos(\omega_{LO}n) = \frac{A}{2}[\cos((\omega_{IF}-\omega_{LO})n + \theta) + \cos((\omega_{IF}+\omega_{LO})n + \theta)] ]
[ -x[n]\sin(\omega_{LO}n) = \frac{A}{2}[\sin((\omega_{IF}-\omega_{LO})n + \theta) - \sin((\omega_{IF}+\omega_{LO})n + \theta)] ]
注意这组正交结果里,差频部分在I路和Q路分别对应余弦和正弦,幅度相等、相位差90度,正好构成一个复指数信号 ( e^{j((\omega_{IF}-\omega_{LO})n+\theta)} ),这是单边带信号。如果把I路和Q路直接看成实信号,那确实是两个实信号,但它们合在一起表示的是只搬移正频率或负频率的复信号。这就是正交混频能区分“高于本振的信号”和“低于本振的信号”的原因。
后级抽取滤波只要滤掉和频分量,剩下的就是基带I/Q数据。这里有个关键点:必须使用复数乘法结构和后续复数滤波,否则无法区分正负频率。如果只是实数混频,那你无法判断镜像信号来自哪边,图省事的下场就是你辛辛苦苦做的接收机,镜像频率的信号也会掉进你的通带里,干扰根本滤不掉。
2.3 抽取与滤波:CIC + FIR 两级方案
抽取必然伴随抗混叠滤波。假如抽取因子是16,滤波器带宽外的信号如果不滤掉,会折叠回通带内,造成混叠。所以抽取前必须先做低通滤波。但如果在200MHz采样率下直接用FIR做1/16抽取,FIR阶数会非常夸张,乘法器资源吃紧。这里业界通行做法是CIC滤波器粗抽取 + FIR细滤波。
CIC滤波器的好处是没有乘法器,只用加法器和延迟器,适合做高速率、大抽取比的粗滤波。CIC的传递函数:
[ H(z) = \left( \frac{1 - z^{-RM}}{1 - z^{-1}} \right)^N ]
其中R是抽取率,M是差分延迟(通常取1或2),N是级数。实现上分成积分器(Integrator)和梳状器(Comb)两部分。积分器工作在采样率较高的前端,梳状器工作在抽取后的低速率端。
CIC的主要问题是通带内有通带跌落(Passband Droop)。比如32倍抽取、5级CIC,通带边缘的衰减可能有几个dB。这部分需要在后面的FIR里做补偿。
FIR滤波器用来做精细的通道选择和CIC跌落补偿。设计时直接用MATLAB的Filter Designer工具就能算系数,量化到16位后导出coe文件,在Vivado里用FIR Compiler IP核加载即可。
我的实际经验是:CIC负责砍掉绝大数据率,FIR负责精修频响。两级配合下,抽取因子可以做到几十甚至上百,乘法器消耗仍然能在几十个DSP48以内,逻辑资源占用很可控。
2.4 高速率下的多相分解技巧
如果ADC采样率做到500MSPS甚至1GSPS,FPGA内部时序可能只能跑到250MHz,这时候怎么办?答案是多相分解。
多相分解本质上是把一个高速率FIR滤波器拆成N个并行低速子滤波器,每个子滤波器处理一路数据,最后合路输出。每个子滤波器的系数是原始系数的N相抽取。
实现上通常由FIR Compiler IP核的Polyphase模式直接支持。设并行路数为4,每路数据率125MHz,那么IP内部会自动生成4个子滤波器,每个子滤波器处理1/4的系数,输出也是4路并行。
这里有个特别容易踩的坑:并行路数必须和抽取倍数匹配,或者至少是整数倍关系,否则IP核配置会报错。另外,多相滤波的数据输入顺序要跟通道映射对齐,每个通道的相位不能搞错,否则出来的频谱全是镜像。
3. 实操过程:一个完整数字中频链路的设计与实现
3.1 需求定义和参数预算:先算清楚再做
先以一个实例来说明整个设计过程。
假设项目需求是:ADC采样率 ( f_s = 200 )MSPS,中频信号中心频率 ( f_{IF} = 60 )MHz,信号带宽 ( BW = 2 )MHz,要求接收机输出IQ基带数据给后端解调器,输出数据率不低于4MSPS。
第一步,确定抽取倍数。总抽取倍数:
[ D = f_s / f_{out} = 200/4 = 50 ]
第二步,确定NCO频率。因为要把60MHz信号搬到基带,NCO本振频率就是60MHz。相位累加器位数N=32,( f_{clk}=200)MHz,FTW计算:
[ FTW = round(60e6 / 200e6 \times 2^{32}) = round(1288490188.8) = 1288490189 ]
第三步,抽取分解。50倍抽取拆成CIC抽取25倍,FIR抽取2倍。或者CIC抽50倍也行,但CIC抽取倍数太高时通带跌落会更严重,而且内部位宽增长大。工程上我倾向于CIC抽25倍,FIR再抽2倍。
ADC数据位宽16位。CIC内部位宽增长估算公式:
[ G = N \times \log_2(R \times M) + B_{in} ]
N=3级、R=25、M=1、输入16位:
[ G = 3 \times \log_2(25) + 16 \approx 3 \times 4.64 + 16 = 29.93 ]
取整为30位。CIC输出端要截位,最后保留16位给FIR输入。截位不能简单截掉低位,得考虑增益补偿,实际是把CIC的增益和截位统一处理。
第四步,FIR设计。FIR做2倍抽取的低通滤波,同时补偿CIC通带跌落。通带边缘设在1MHz,阻带起始设在2MHz,阶数限制在64阶以内。实际滤波器系数用MATLAB的fdesign.ciccomp函数计算。
3.2 完整链路代码框架:各模块怎么连
整个链路的顶层结构大致如下:
module ddc_top ( input wire clk_200m, input wire rst_n, input wire [15:0] adc_data, // 来自ADC的16位中频数据 output wire [15:0] i_out, // I路输出 output wire [15:0] q_out, // Q路输出 output wire out_valid ); // 1. NCO输出 wire [15:0] sin_w, cos_w; dds_compiler_0 u_dds ( .aclk(clk_200m), .s_axis_phase_tvalid(1'b1), .s_axis_phase_tdata(32'd1288490189), .m_axis_data_tvalid(), .m_axis_data_tdata({sin_w, cos_w}) ); // 2. 混频:复数乘法 wire [31:0] mult_i, mult_q; mult_gen_0 u_mix_i ( .CLK(clk_200m), .A(adc_data), .B(cos_w), .P(mult_i) ); mult_gen_0 u_mix_q ( .CLK(clk_200m), .A(adc_data), .B(-sin_w), .P(mult_q) ); // 3. CIC抽取25倍 wire [29:0] cic_i, cic_q; cic_compiler_0 u_cic_i ( .aclk(clk_200m), .s_axis_data_tvalid(1'b1), .s_axis_data_tdata(mult_i[30:15]), // 混频输出截位,防CIC溢出 .m_axis_data_tvalid(), .m_axis_data_tdata(cic_i) ); cic_compiler_0 u_cic_q ( .aclk(clk_200m), .s_axis_data_tvalid(1'b1), .s_axis_data_tdata(mult_q[30:15]), .m_axis_data_tvalid(), .m_axis_data_tdata(cic_q) ); // 4. FIR抽取2倍,同时补偿CIC跌落 wire [15:0] fir_i, fir_q; fir_compiler_0 u_fir_i ( .aclk(clk_200m), .s_axis_data_tvalid(cic_valid), .s_axis_data_tdata(cic_i[29:14]), .m_axis_data_tvalid(out_valid), .m_axis_data_tdata(fir_i) ); // 同理例化fir_q... assign i_out = fir_i; assign q_out = fir_q; endmodule注意几个接口细节:
- DDS的输入输出数据手性。Xilinx的DDS Compiler默认输出
{sin, cos}还是{cos, sin}取决于配置。最好在仿真里先看一眼波形再接,省得到时候I/Q接反,整个解调全废。 - 截位的时机。CIC是固定增益的,如果混频输出不截位直接进CIC,内部位宽会爆,30位也扛不住。混频器输出乘出来是32位,合理截到16位或20位再进CIC,截位用四舍五入(Round)而不是直接截断,否则直流偏置会很难看。
- CIC输出要截位到合适的位宽。我在这个项目里CIC输出是30位,但FIR输入是16位,所以做了
cic_i[29:14]这样的截位。这里的14不是随便定的,是根据信号有效位数和峰均比算出来的。
3.3 从仿真到板级的验证方法
仿真阶段,我用的测试激励是MATLAB生成的中频正弦波采样数据。先在MATLAB里生成60MHz正弦信号,采样率200MHz,加一点高斯白噪声,量化成16位整数,存成hex文件。Verilog测试平台里用$readmemh读进去,把DDS、混频、CIC、FIR整个链路跑一遍仿真,再把输出I/Q数据导出成文本,回灌到MATLAB做频谱分析。
对比仿真的输出频谱和理想输出频谱,主要看三件事:
- 信号是否搬到了0Hz附近。
- 杂散是否在预期水平。
- 通带内幅度是否符合预期(CIC补偿是否到位)。
板级验证阶段,通常用SignalTap或者Vivado的ILA抓取关键节点数据。抓DDS输出验证NCO频率对不对,再抓混频输出看有没有信号,最后抓CIC/FIR输出看频谱。抓数据之后导出来,用MATLAB按输出采样率重新画频谱,跟仿真结果对比。
这里有个心得:FPGA调试时,问题分层的思路非常重要,一定要从前往后逐级定位。先把NCO频谱抓出来,确认频率准、杂散低;再抓混频输出,确认信号确实被搬移了;最后抓滤波输出,确认抽取正常。一级一级确认,不要上来就看最终IQ数据,不然出了问题根本不知道是哪一级的锅。
4. 常见问题与排查技巧实录
4.1 混频后出现直流偏置,查半天原来是截位问题
这是我实际踩过的一个坑。混频器输出32位,我图省事直接截掉低16位送进CIC,结果I路和Q路都有明显的直流分量,大概占满刻度5%左右。仔细排查后,问题出在负数的截断处理上。
Verilog里的算术右移本质是向下取整。对于正数没有影响,但对于负数,直接右移会朝负无穷方向取整,导致均值偏移。比如-3用二进制补码表示,右移一位得到-2,而不是期望的-1.5取整到-1。
解决办法是四舍五入截位。加一个偏移量再截断:
// 四舍五入截低N位 wire [31:0] round_data = mult_i + 32'd32768; // 32768 = 2^15,即要保留低15位时加一半 wire [15:0] trunc_data = round_data[31:16];这个偏移量等于 ( 2^{N-1} ),其中N是去掉的位数。这样就把截断变成了四舍五入,直流偏置问题立竿见影地消失了。
记录一下这个教训:任何定点数截位,只要是负数可能出现的场合,必须用四舍五入,不能用直接截断。
4.2 CIC位宽增长没算够,最终输出全是噪声
第一次做CIC抽取时,我按公式算出来的位宽是N*log2(R*M)+B_in,但实际用IP核配置时,发现IP核计算的位宽比我算的多了一位。原因是Xilinx CIC Compiler对每级中间结果做了舍入处理,不是简单把所有位都保留。如果你不知道这个细节,你截位的位置可能就错了。
最好的做法是:在IP核配置界面里直接看一下它给出的输出位宽,然后在IP核内部配置成Full Precision,输出端再统一截位。不要自己在每级之间手动截位,很容易截错,导致每级都有量化噪声积累。
另外要注意,CIC的增益是非常大的。输入是幅度100的正弦波,CIC输出可能会达到几万甚至几十万。如果你发现CIC输出满偏,不要慌,这是正常的,关键是输出截位的时候要把有效位数理解清楚。
4.3 数据率太低,ILA采样时钟怎么选
板级调试时,如果输出数据率只有4MSPS,而系统时钟是200MHz,用ILA直接抓out_valid使能的数据,一次抓4096个点,对应的实际时间跨度只有1毫秒多,展示的频谱分辨率只有不到1kHz,可能根本看不到目标信号。
我常用的做法是:在ILA里用out_valid做触发条件,然后按有效数据个数存储,让ILA采样时钟用clk_200m,触发条件设为out_valid == 1,这样ILA抓到的数据是跨好多周期的有效数据点,再用MATLAB对有效数据做FFT,分辨率就够了。
另一个技巧:可以专门写一个仿真用的降速模块,把有效数据宽度打宽,用FIFO缓存后再以低速读出,这样ILA调试更轻松。但这会增加逻辑资源占用,一般只在调试阶段用。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| DDS输出频率不对 | FTW计算错误、相位累加器位宽不匹配 | 用仿真抓DDS时域波形,数一个周期个数验证频率 |
| I/Q输出存在直流偏置 | 负数截位未做四舍五入 | 检查所有截位逻辑,统一加偏移量 |
| 输出频谱有镜像信号 | I/Q幅相失配 | 检查NCO的sin/cos两条通路是否完全对称,乘法器位宽是否一致 |
| 抽取后带外信号混叠 | CIC/FIR滤波阶数不够 | 用MATLAB画滤波器频响,检查阻带衰减 |
| CIC输出溢出/满偏 | 位宽计算错误、输入增益过大 | 降低混频输出位宽,或增加CIC内部位宽配置 |
| FIR输出有毛刺 | 系数量化误差大、输入数据定标不对 | 重新量化系数,检查输入/输出定标参数 |
| 时序收敛不过 | 乘法器链路未打拍 | 在关键乘法器输出端增加寄存器流水级 |
5. 工具选型与开发流程心得
5.1 链路仿真先用MATLAB把算法“跑熟”
很多新手上来就直接写Verilog,结果一边仿真一边改算法,效率很低。我建议的流程是:先MATLAB浮点验证 → 再定点仿真 → 最后RTL实现。
浮点验证就是先把DDC/DUC链路用MATLAB的sps函数或者自己写脚本跑通,确认NCO频率参数、CIC抽取级数、FIR补偿量这些都能得到满意结果。定点仿真用MATLAB的Fixed-Point Designer,把各级数据位宽、截位策略先试一遍,看一下量化后的性能损失有多少。等定点方案满足了指标,再把它翻译成Verilog,这时候RTL基本一版就能跑通。
这样做的好处是很明显的:MATLAB里改参数只需要几分钟,而RTL里改一次位宽意味着工程重编译、时序重pa,一次就是半小时起步。而且很多逻辑错误(比如截位方向搞反)在定点仿真里就能暴露,不用等到板级调试才发现。
5.2 Vivado里IP核配置的几个关键点
- DDS Compiler:Output Width选16位,Phase Width选32位,频率分辨率自动变好;Spurious Free Dynamic Range选80dB以上,噪声整形选抖动。
- CIC Compiler:Filter Type选Decimation,Sample Rate Change设为25,Number Of Stages设为3,Differential Delay设为1。Output Data Width可以选Full Precision,后面自己截位。
- FIR Compiler:系数源选
coe文件,量化位数16位,输入/输出位宽16位,Decimation Rate设2。注意在Coefficient Type里选Signed,否则负系数会解析错误。 - Mult Gen:这个就俩乘数,位宽注意设为32×16,P端口选择32位即可。
IP核真没什么神秘的,很多设计失误其实出在IP核之外的连线,比如有效信号的时序、复位时序、数据对齐这些地方。
5.3 工程管理:参数化设计是长期维护的基础
算法参数化这一点值得多说一句。不要把所有参数硬编码在RTL里。我习惯在顶层定义一组localparam:
localparam ADC_WIDTH = 16; localparam NCO_WIDTH = 16; localparam NCO_PHASE = 32; localparam CIC_RATE = 25; localparam CIC_STAGES = 3; localparam FIR_RATE = 2; localparam OUTPUT_WIDTH = 16;所有模块的位宽、抽取倍数都从这组参数推导。如果以后项目改成ADC 250MSPS、带宽10MHz,只需要改几个localparam,各模块互连的地方也会跟着变,不容易出错。另外,每个IP核的配置界面里其实能看到一个“Summary”,记录了输入位宽、输出位宽、延迟周期等关键信息,把这些信息整理到一个Excel表里,做嵌入式工程师之间的交接时会非常有帮助。
6. 更进一步:DUC发射链路与多通道同步扩展
6.1 DUC发射链路的设计要点
接收链路是DDC,发射链路就是DUC(Digital Up Converter)。结构完全对称:基带I/Q数据先经过FIR插值滤波,再用CIC插值抬高数据率,最后和NCO混频上变频到中频,送给DAC。
DUC和DDC有个重要区别:插值滤波器的设计目标是抑制镜频,而不是抗混叠。插值过程中每个插入的零点都会在频谱上产生镜频分量,必须用低通滤波器滤除,否则DAC输出的频谱就会包含原始基带频谱和多个镜频副本,不仅浪费发射功率,还会违反频谱模板。
NCO频率参数和DDC几乎一样,只是FTW计算的符号可能反过来(上变频和下变频的相位旋转方向不同)。整套代码框架可以高度复用,这也是模块化设计最直观的价值。
6.2 多通道同步接收:相位对齐才是真功夫
如果你的接收机有多个通道,比如四通道测向,每个通道都要有自己的DDC。这时候最头疼的就是通道间相位对齐。
我在项目里踩过这样一个坑:四个通道的DDC明明用的是同一个NCO频率控制字,但通道2比通道1延迟了几个时钟周期,导致通道间的相位差完全是乱的。
原因出在复位信号的异步释放上。四个DDS IP核的复位信号如果释放时刻不一致,相位累加器的初相就不一致。解决办法是使用全局复位同步释放电路:
// 全局复位同步释放 reg [3:0] rst_sync; always @(posedge clk_200m or negedge rst_n) begin if (!rst_n) begin rst_sync <= 4'b0; end else begin rst_sync <= {rst_sync[2:0], 1'b1}; end end wire sys_rst_n = rst_sync[3];这样所有IP核的复位释放都在同一个时钟沿之后,相位就对齐了。
多通道还有个细节:NCO即使相位同步,不同通道的数据路径长度也必须一致。你在通道2里多加了一个流水线寄存器,那它的相位就滞后一拍。所以多通道设计时,每个通道的结构必须完全一样,不能有“优化只优化了一个通道”这种操作,否则后患无穷。
6.3 智能算法在FPGA上的嵌入式尝试
聊完了基带和中频处理,顺便提一句这个方向的新玩法。最近有不少团队尝试在FPGA里实现卡尔曼滤波、粒子滤波这些算法来替换传统环路滤波。传统DDC后面接的解调器里,环路滤波器加减速带宽都是固定的,遇到动态范围大、信号变化快的场景就很吃力。如果用卡尔曼滤波做载波跟踪,能根据信噪比动态调整滤波带宽,理论上能获得更好的动态性能。
我做过一个简单验证:用FPGA实现了2阶卡尔曼滤波,用来跟踪多普勒频移,整体资源消耗大约是2个DSP48和800个LUT,数据率4MSPS时完全跑得动。算法收敛后,跟踪精度比传统二阶环路在快变场景下有明显提升。不过卡尔曼滤波对矩阵运算有要求,在FPGA上要手动展开运算流程,矩阵求逆部分往往需要用Cholesky分解或QR分解来逼近,这块工作量不小,适合后续慢慢磨。
这个方向值得关注,但别一开始就冲着它去,先把DDC/DUC这些基本功做扎实,再考虑加智能算法才有意义。
7. FPGA资源与性能优化技巧
7.1 乘法器资源怎么省
中频高速信号处理最耗的就是DSP48乘法器。一个8阶FIR需要8个乘法器,一个级联滤波器组用几十个乘法器很正常。省乘法器的思路不外乎几种:
- 用CIC替代FIR做大抽取,CIC只用加法器。
- 利用对称系数。线性相位FIR的系数是对称的,可以先把对称的两路输入相加再乘系数,乘法器数量直接减半。
- 用时分复用做多通道。如果一个乘法器有空闲周期,可以让四路信号共用这一个乘法器,按时间片轮转计算。
最后一种最实用。比如四通道DDC的FIR,如果把数据率从200MHz降到50MHz,那一个DSP48可以在一个周期内处理四路数据,乘法器只剩四分之一。
7.2 时序优化:拆流水线、加寄存器
FPGA时序收敛的常规操作就是打拍。特别是乘法器输出到下一级加法器的路径,一级乘加运算在低频时没问题,但频率超过300MHz就会时序告急。
我的建议是:每一级乘法器输出后至少加一级寄存器,把组合逻辑延迟打散。有些编译器会自动插入流水线,但在手写代码时还是要主动加。
另外一个技巧是,把CIC的积分器部分改成多相结构来提升吞吐率。积分器是一个带反馈的累加器,本身就是一条长路。如果时序不过,可以考虑拆分积分器或者用多相CIC,这块在UG438里有详细说明,碰到再翻具体方案。
7.3 资源占用参考与选型建议
以我的这个项目为例,一个单通道DDC(16位ADC、200MSPS、抽取50倍、输出4MSPS)在Artix-7 35T上实现,大概消耗:
| 资源 | 使用量 | 占比 |
|---|---|---|
| LUT | 8500 | 40% |
| Flip-Flop | 7200 | 17% |
| DSP48E1 | 12 | 30% |
| BRAM | 8 | 20% |
| 时钟频率 | 200MHz | 满足时序 |
换成Zynq-7020也差不多,稍微紧一点。如果是多通道,或者数据率更高,建议直接上Kintex-7或Zynq UltraScale+。这里也提醒一下,不要只看FF和LUT的占用率,还要关注BRAM的占用,FIR滤波器系数、NCO查找表、FIFO缓存都会吃BRAM,资源预估时这一项最容易被忽略。
8. 最后分享一点个人体会
做基带和中频的FPGA算法实现,确实需要比较广的知识面:数字信号处理、通信原理、FPGA设计、板上调试,缺哪个环节都会走弯路。但核心的链路其实并不复杂,无非就是NCO、混频器、CIC/FIR滤波器来回组合。
我个人踩过最大的坑永远是数据定标和截位。看似简单,但只要一处处理不当,整条链路输出就会包含可见的杂散。所以在新项目启动时,我建议你先花半天时间把各级位宽、截位策略、尾数处理方式用文档固定下来。前期的这里多花一点时间,后期调试会轻松非常多。硬件工程师怕的不是算法复杂,而是问题藏在某个不起眼的细节里查不出来。