
简介基于高云FPGA的IP设计的FIR低通滤波器工程面向毕业设计、课程设计、工程实训与FPGA竞赛场景完整覆盖从IP核配置、滤波器系数生成、硬件设计到仿真验证的流程尤其适合需要快速搭建可运行项目的学生和开发者。工程已实测可复现代码结构清晰设计文档亦可借鉴便于二次扩展资源内含完整源码、约束文件、比特流与系数查找表可辅助理解高云FPGA下FIR滤波器的实现细节。资源包共124个文件、33.77MB主要包含Verilog/VHDL源码、工程与IP配置文件、仿真脚本和波形记录、滤波器系数与测试数据以及docx/md/txt说明文档。其中HDL源码与工程配置用于综合实现仿真脚本与波形记录帮助功能验证系数与测试数据可直接复用目录结构分级明确便于按模块定位与二次开发。当前已有154人学习下载可作为FPGA数字信号处理方向的起步模板。1. 从课设到竞赛高云FPGA的IP设计做FIR低通滤波器FIR低通滤波器是数字信号处理里最基础的单元也是FPGA课程设计和竞赛题里的常客。常见做法是调一个现成FIR IP但高云FPGA的入门级器件上很多场景看不到完整FIR专用IP更多要靠乘法器IP、移位寄存器IP和自定义状态机把这个滤波器拼出来这正是“IP设计”这道题真正要练的部分。这篇文章按一条能跑通的完整主线讲滤波器参数怎么定、Gowin软件里怎么配乘法器IP、串行MAC结构RTL怎么写、仿真怎么验证最后用Python做量化误差和频谱验收。毕设、课设、实训、大作业都能直接参考对想了解国产FPGA工具链差异的工程师也有实际信息量。2. FIR低通滤波器的原理与高云FPGA的IP选型2.1 FIR低通滤波器参数怎么定抽头、截止频率和窗函数数字FIR低通滤波器的输出是输入信号与有限冲激响应的卷积y[n] Σ h[k]x[n-k]其中N是抽头数h[k]是滤波器系数。实际设计时三个参数最先确定采样率fs、截止频率fc、抽头数N。fs由ADC或数据链路决定fc根据信号频带定N由过渡带宽度和阻带衰减要求倒推。Hamming窗的过渡带约为4/N个归一化周期工程上在过渡带不算太窄、不需要极高衰减的场景最常用要做到-80dB阻带就得换Kaiser窗并增大N。FIR还有一个对硬件设计影响很大的性质当系数满足 h[k] h[N-1-k] 时滤波器是线性相位不会破坏信号的相位关系这在通信和图像处理链路里非常关键。同时对称系数可以把乘法次数减半把对称位置的两个延迟数据先相加再乘同一个系数。FPGA里DSP乘法器是稀缺资源这个折法对IP选型和资源评估影响很大。2.2 高云FPGA的IP资源乘法器、RAM和PLL怎么分工高云提供的基础IP已经覆盖了搭建FIR所需的大部分模块。常见分工是输入延迟链用移位寄存器IP或分布式RAM乘法用乘法器IP累加用普通LUT逻辑或DSP输出端的加法器。中低密度器件上LUT资源通常比DSP块宽裕所以“时间换资源”的串行MAC结构很划算如果选带大容量DSP块的GW2A系列展开成并行乘加树也有余量。IP/资源在FIR里的作用配置要点乘法器IP计算 h[k]*x[n-k]有符号数输入位宽、输出位宽、流水级数移位寄存器IP构造N级延迟链深度等于抽头数位宽等于数据位宽块RAM/分布式RAM存滤波器系数或样本块双口RAM可同时读两个对称系数PLL给系统时钟倍频串行MAC时系统时钟要远高于采样率这里的IP设计顺序不要反先定采样率和数据通路位宽再决定用哪个IP。数据位宽由ADC/DAC和滤波指标共同决定先把16位有符号这种常用配置定下来再生成乘法器IP不然中途改位宽所有模块例化都要跟着返工。2.3 乘累加结构和分布式算法怎么选FIR在FPGA里有两条主流实现路线乘累加MAC和分布式算法DA。MAC思路直观用乘法器IP每时钟算一个抽头TAPS个时钟后出一个结果DA不调用DSP乘法器把固定系数做成LUT查找表适合系数固定、乘法器不够用的大抽头场景。对毕设和竞赛这类项目我一般建议先做MAC乘法器IP例化最快调试成本低答辩时也容易把“几个时钟出一个点、流水级数怎么影响吞吐”讲清楚。DA省乘法器但改系数要重新生成查找表可读性差初学阶段容易被时序和表项折磨。3. 在高云FPGA上用乘法器IP搭出FIR低通滤波器3.1 先用Python算系数再转成16位定点FIR滤波器设计第一步不是写Verilog而是把系数算对。用scipy.signal的firwin即可完成。下面这组参数贯穿全文采样率500kHz、截止频率100kHz、64抽头、Hamming窗。import numpy as np from scipy.signal import firwin fs 500e3 fc 100e3 taps 64 # 计算64阶低通系数fs必须显式传入 b firwin(taps, fc, fsfs, windowhamming) # 量化到16位有符号定点 b16 np.round(b * 32767).astype(np.int16) # 写补码十六进制给Verilog的$readmemh用 with open(coeffs.mem, w) as f: for c in b16: f.write(f{(c 0xffff):04x}\n)firwin第一个参数是抽头数第二个是截止频率fs不传的话函数会按归一化频率处理出来的系数完全不对。乘32767是把浮点1.0映射到16位定点最大值量化后再除以32767可以换回浮点做误差对比。写文件时转成补码十六进制每行4个字符对应16位方便后面用$readmemh直接灌进数组。3.2 在Gowin软件里生成乘法器IP并例化到工程打开高云Gowin Software在IP核生成器里新建乘法器IP。需要填的参数主要是A位宽、B位宽和是否有符号。FIR数据通路两边都选有符号输入16位输出32位。流水级数按系统时钟定50MHz以下1级够用我习惯设成2级时序更稳。IP生成后会给出可例化模板顶层模块名按模板里的名字使用然后把乘法器例化进自己的FIR模块wire signed [15:0] mul_a; wire signed [15:0] mul_b; wire signed [31:0] mul_p; mult_16x16_signed u_mul ( .clk(clk), .a(mul_a), .b(mul_b), .p(mul_p) );这个IP的输出p比输入晚两个时钟周期也就是流水延迟2。后续状态机的累加路径必须按这个延迟对齐否则每个抽头都会错位。不同器件或不同IP版本生成的延迟可能不一样生成后看IP报告里的Pipeline Latency通常就是2或3。3.3 用一个乘法器分时处理64个抽头串行MAC状态机资源不够时就要换思路抽头数大于乘法器数量是常态。用“每时钟处理一个抽头”的串行MAC系统时钟50MHz、采样率500kHz每个样本点之间有100个时钟64抽头加状态机完全来得及。下面是一个可综合的串行MAC框架module fir_serial #( parameter DATA_WIDTH 16, parameter COEFF_WIDTH 16, parameter TAPS 64, parameter LATENCY 2 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] din, input wire din_valid, output reg signed [DATA_WIDTHCOEFF_WIDTH-1:0] dout, output reg dout_valid ); reg [DATA_WIDTH-1:0] shift [0:TAPS-1]; reg signed [COEFF_WIDTH-1:0] coeff [0:TAPS-1]; reg [7:0] cnt; reg pending; reg busy; reg signed [31:0] acc; reg signed [DATA_WIDTH-1:0] mul_a; reg signed [COEFF_WIDTH-1:0] mul_b; wire signed [31:0] mul_p; integer k; // 仿真时用系数文件初始化板上改用RAM初始化文件 initial $readmemh(coeffs.mem, coeff); mult_16x16_signed u_mul(.clk(clk), .a(mul_a), .b(mul_b), .p(mul_p)); always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; busy 0; pending 0; acc 0; mul_a 0; mul_b 0; dout_valid 0; end else begin dout_valid 1b0; if (din_valid !busy !pending) begin // 先把新样本移入延迟链 shift[0] din; for (k 1; k TAPS; k k 1) shift[k] shift[k-1]; pending 1; end else if (pending !busy) begin // 延迟一拍再启动MAC保证shift[0]是新样本 mul_a shift[0]; mul_b coeff[0]; acc 0; cnt 1; busy 1; pending 0; end else if (busy) begin if (cnt 1 cnt TAPS) begin mul_a shift[cnt]; mul_b coeff[cnt]; end // 乘法器输出晚LATENCY拍够延迟才累加 if (cnt LATENCY) acc acc mul_p; if (cnt TAPS LATENCY - 1) begin // 把最后一拍乘积补进去 dout acc mul_p; dout_valid 1b1; busy 0; end cnt cnt 1; end end end endmodule这段代码里有两个容易写错的地方。第一个是pending状态如果din_valid到达的同一拍就去读shift[0]读到的还是上一拍的数据所以要让移位链先更新下一拍再启动MAC。第二个是LATENCY对齐乘法器输出晚两拍acc不能从启动后的第一拍就累加否则会把历史残值算进去。cnt TAPS LATENCY - 1时最后一拍乘积刚好出现在乘法器输出上acc mul_p把它补进结果这是串行MAC最精妙也最容易漏的一行。如果乘法器流水延迟不是2比如配置成3状态机无需改结构LATENCY参数直接改成3结束条件会自动对齐。验证时先看这个参数和IP报告是否一致不一致查多久都白费。3.4 一套能直接复现的参数配置表参数数值说明系统时钟 clk50 MHz高云入门级开发板常用采样率 fs500 kHz受串行MAC处理周期和数据源上限约束抽头数 TAPS64可改32或128coeffs.mem同步更新数据位宽16 bit signed输入输出基准系数位宽16 bit signed阻带衰减的主要瓶颈截止频率 fc100 kHz必须低于fs/2实际取0.4倍以下乘法器流水延迟2决定状态机的结束对齐条件这套参数的资源占用很小一个乘法器IP加一小片分布式RAM就能跑。想更快看到滤波效果可以把采样率降到100kHz、截止频率设为20kHz系数文件重新生成即可RTL不用动。4. 从仿真到板级验证FIR低通滤波器怎么算通过4.1 用SystemVerilog testbench生成20kHz和150kHz叠加信号验证FIR不是看有没有输出而是确认低通真的把高频分量滤掉了。仿真中最好直接叠两个正弦一个低于截止频率100kHz一个高于它。这里选20kHz和150kHz采样率还是500kHz。module fir_tb; logic clk 0, rst_n 0; logic signed [15:0] din 0; logic din_valid 0; logic signed [31:0] dout; logic dout_valid; int cnt 0; real t 0.0, raw; fir_serial #(.TAPS(64)) dut( .clk(clk), .rst_n(rst_n), .din(din), .din_valid(din_valid), .dout(dout), .dout_valid(dout_valid) ); always #10 clk ~clk; always (posedge clk) begin if (cnt 0) begin t t 1.0/500e3; // 20kHz通带分量 150kHz阻带分量 raw 12000.0 * $sin(2 * 3.14159 * 20e3 * t) 12000.0 * $sin(2 * 3.14159 * 150e3 * t); din $rtoi(raw); din_valid 1; end else begin din_valid 0; end cnt (cnt 99) ? 0 : cnt 1; end initial begin #30 rst_n 1; #500_000 $finish; end endmodulecnt每100个时钟跳一次等效采样率500kHz。$sin是SystemVerilog仿真器普遍支持的数学函数少数旧版仿真器不认时换成预填的正弦查找表在initial里加载也一样。幅度取12000而不是32767是为了给两路正弦叠加留出余量避免累加溢出。跑完看dout波形如果输出基本是干净正弦周期约50us对应20kHz且150kHz幅度明显消失滤波功能就通过了。更严格的做法是把输出数据导出成文本交给Python做FFT这个放在第5章。4.2 群延迟和启动瞬态两个看波形前要搞清的点FIR有固定群延迟输出和输入在时间轴上错位是正常的。64抽头的对称FIR群延迟是31.5个采样周期在500kHz采样率下等于63us。仿真波形刚启动时延迟链还没填满那一段输出不能用来判断滤波效果要等dout_valid连续输出几百个样本后再选稳定段观察。如果后续接的是PID或闭环控制这63us会进环路影响相位裕量。竞赛项目最好把群延迟写进设计文档答辩时主动提出来比被评委追问要强得多。4.3 板级调试没有示波器怎么办板级验证常见做法是信号发生器输出混合频率经过ADC进FPGA滤波后DAC输出接示波器。不过课设和实训不一定有ADC/DAC子卡。没有时就在FPGA内部用DDS生成两路正弦叠加滤波结果暂存进片上RAM通过串口或JTAG调试口读出交给Python画图。这套流程在只有一块高云开发板的条件下能完整跑通。注意不要拿GPIO直接推一个RC低通去和FPGA滤波结果对比。FPGA I/O的推挽输出和模拟RC电路的阻抗匹配完全是两回事示波器探头电容会改变RC截止频率很容易得到“滤波器没生效”的错误结论。5. 用Python做量化误差和频谱验收给FIR项目留一份可信证据仿真通过只能说明RTL逻辑对说明不了16位系数够不够。课设答辩时“系数位宽够不够”是必问题要用数据说话。把浮点系数和16位定点系数分别求频响再和设计指标逐点对比即可import numpy as np from scipy.signal import firwin, freqz b firwin(64, 100e3, fs500e3, windowhamming) # 量化后一定要除回32767才能和浮点系数对比 bq np.round(b * 32767).astype(np.int16) / 32767.0 w, h1 freqz(b, worN1024, fs500e3) _, h2 freqz(bq, worN1024, fs500e3) stop (w 130e3) print(浮点阻带最大增益: %.2f dB % (20*np.log10(np.max(np.abs(h1[stop]))))) print(定点阻带最大增益: %.2f dB % (20*np.log10(np.max(np.abs(h2[stop])))))Hamming窗的理论阻带衰减约53dB。如果定点后的阻带比浮点差2到3dB属于系数舍入的正常范围如果差6dB以上先检查bq是不是忘了除以32767再考虑系数位宽是不是降到了12bit。对毕设项目16位系数下阻带保住50dB以上答辩基本说得过去。第二个验收手法是抓RTL仿真输出做FFT。testbench里用$fwrite把dout写进文本文件Python读入后加窗再变换fs 500e3 data np.loadtxt(fir_out.txt) spec np.abs(np.fft.rfft(data * np.hanning(len(data)))) freq np.fft.rfftfreq(len(data), 1/fs)FFT里20kHz的峰应该保持150kHz的峰应低于-40dB这个数值和freqz预测的阻带衰减对上就说明RTL实现确实满足设计指标。取数据时只取稳态段前几百个点属于延迟链填充过程会拉高低频假峰。最后一个工程技巧是系数文件单一来源Python生成coeffs.mem后testbench用$readmemh加载它板上RAM初始化也用同一份文件不要手工改Verilog数组。仿真和板级用同一份系数能避免查半天最终发现两边系数不一致的尴尬。本文还有配套的精品资源点击获取