搞数字信号处理的同学对FIR滤波器肯定不陌生,但真正把算法变成硬件可不容易。我当年第一次做FIR滤波器时,在Matlab里仿真好好的,一到FPGA上就跑偏,差点怀疑人生。后来才发现,从系数设计到硬件实现是个系统工程,每个环节都得抠细节。
FIR滤波器的核心就是那组系数,系数设计决定了滤波器的频率响应特性。在Matlab里用Filter Designer工具设计系数时,得特别注意通带波纹、阻带衰减这些参数。比如设计一个15阶低通滤波器,采样率50MHz,通带1MHz,阻带6MHz,用最小二乘法(Least-squares)实现,这些参数设置直接影响最终效果。
但Matlab里的浮点系数不能直接给FPGA用,得先量化。我常用12位定点数,量化时要注意动态范围。有次我偷懒直接用round函数,结果高频段失真严重,后来改用round(Num/max(abs(Num))*(2^(q_width-1)))这种归一化+量化的方式才解决。量化后的系数要保存为COE文件,这是Vivado FIR IP核的标配输入格式。
生成COE文件有两种主流方法,各有优劣。第一种是在Matlab里写脚本生成,灵活性强但容易出错。记得有次我忘记把最后一行的逗号改成分号,Vivado直接报错,查了半天才发现是格式问题。脚本大概长这样:
matlab复制q_width = 12; % 量化位宽
fid = fopen('FIR_coe.coe','w');
coe_data = round(Num/max(abs(Num))*(2^(q_width-1)));
fprintf(fid,'Radix = 16;\r\n'); % 十六进制
fprintf(fid,'Coefficient_Width = %d;\r\n',q_width);
fprintf(fid,'CoefData = \r\n');
fprintf(fid,'%x,\r\n',coe_data(1:end-1));
fprintf(fid,'%x;\r\n',coe_data(end)); % 最后一行用分号
fclose(fid);
第二种方法更傻瓜式,直接用Filter Designer的Xilinx Coefficient导出功能。在设置里选Fixed-point,指定字长,点几下就能生成。不过这种方法对量化方式控制不够精细,我对比过两种方法生成的系数,有时候会有1~2个LSB的误差。
实际项目中,我建议先用方法二快速验证,再用方法一精细调整。特别是对滤波器性能要求高的场景,比如医疗信号处理,每个系数的误差都可能影响最终结果。
有了COE文件,接下来就是在Vivado里配置FIR IP核了。打开FIR Compiler界面,在Select Source里选COE File导入刚生成的文件。这里有个坑:Vivado对文件路径特别敏感,最好把COE文件放在工程目录下,用相对路径引用。
系数量化方式的选择很关键:
我一般选Quantize Only,因为Matlab生成的系数已经做过归一化。有位宽设置时要留足余量,比如12位系数可以设成16位存储,避免运算溢出。
接口配置也有讲究。刚开始可以简单点,只保留数据接口。等功能验证通过了,再根据需要添加AXI4-Stream控制接口。记得勾选Show disabled ports看看所有可选接口,有时候异步复位之类的信号很有用。
仿真环节最容易翻车,分享几个实用技巧。首先,测试数据最好用Matlab生成并存成文本文件,这样可以和理论结果对比。比如用这个生成200个采样点的测试数据:
matlab复制t = 0:1/50e6:200/50e6-1/50e6;
x = cos(2*pi*1e6*t) + 0.5*cos(2*pi*8e6*t); % 1MHz信号+8MHz噪声
x_quant = round(x/max(abs(x))*(2^11)); % 12位有符号数量化
fid = fopen('test_data.txt','w');
fprintf(fid,'%x\n',mod(x_quant+2^12,2^12)); % 转16进制
fclose(fid);
在仿真文件里用$readmemh读取这个文件。注意Verilog里的数据位宽要和IP核配置一致,否则高位会被截断。仿真波形建议设置成Analog模式,选择Hold插值,这样看起来更直观。对比Matlab和Vivado的波形时,要特别注意时延——FIR滤波器会有固定的群延迟,大概等于阶数的一半。
最后的验证阶段,我习惯分三步走:
这里有个细节:FPGA输出往往是32位有符号数,需要先转成Matlab的double类型再做比较。我写了个常用脚本:
matlab复制fpga_out = load('fpga_output.txt'); % 读取FPGA输出
fpga_out = mod(fpga_out + 2^31, 2^32) - 2^31; % 转有符号数
fpga_out = fpga_out / 2^30; % 归一化到[-1,1]
% 时域对比
subplot(2,1,1);
plot(theory_out); hold on; plot(fpga_out);
legend('理论输出','FPGA输出');
% 频域对比
subplot(2,1,2);
f = linspace(0,50,1024);
plot(f,20*log10(abs(fft(theory_out,1024))));
hold on; plot(f,20*log10(abs(fft(fpga_out,1024))));
如果发现误差较大,先检查COE文件系数是否一致,再确认量化位宽设置。有时候仿真结果看起来很好,但烧写到FPGA后性能下降,这可能是时序问题,需要约束时钟或优化流水线。