这几天尝试了一下通信题要的fir滤波器设计,以及在vivado平台上进行IP核实现,最后实现了个1M的60dB衰减的Lowpass,效果OK
下面就讲讲实现过程😁
1. 利用Matlab的firpm函数设计filter 进行滤波器设计的话其实也可以利用APP拓展中 filterDesigner
进行快速设计
不过由于方案是自选的,所以很难是相应要求下的最优滤波器,故这边直接使用 Matlab 中的 firpm 函数来设计最优滤波器,详细的使用方案和原理可以康康 help 文档
设定相关滤波器参数、利用kaiserord函数获取满足要求的最小滤波器阶数
1 2 3 4 5 6 7 8 9 10 11 %lowpass - 37_order_60dB damping Fs=10*10^6; k=12; %滤波器系数量化位数-配合FPGA设计 Fc=[1*10^6 2*10^6]; %过渡带 mag=[1 0]; %窗函数的理想滤波器幅度 %通带衰减:Ap=-20*log10(1-a1)=0.915dB %阻带衰减:As=-20*log10(a2)=60dB a1=0.1;a2=0.001; dev=[a1 a2]; %采用凯塞窗函数获取满足要求的最小滤波器阶数 - 自动计算 [n,~,~,~] = kaiserord(Fc,mag,dev,Fs);
这边可以根据需求调整相关的参数,不过就是不能为了一度追求滤波器的理想效果而过度地调整过渡带和阻带衰减(因为我就是这样过来的),这样的结果就是filter
阶数跃升,滤波器阶数过高就会引发一系列问题如 复杂度太大、可以还会出现串扰的效应,最终就会导致实际运用中其实效果一般
所以这边调整了下搞了个37阶的窗函数滤波器
这边还需要说明的就是等波纹法和窗函数法设计的区别
由于窗函数使用的是最小平方积分来设计而等波纹法是通过最大加权误差来设计,故窗函数得到的滤波系数更接近理想滤波器。而此结果是因为窗函数的滤波系数将多于等波纹法,两者在相位响应方面也有不同
利用 firpm 函数进行设计 - 量化滤波器系数(配合FPGA设计) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 %采用firpm函数自动设计最优滤波器 fpm=[0 Fc(1)*2/Fs Fc(2)*2/Fs 1]; %firpm函数的频段向量 magpm=[1 1 0 0]; %firpm函数的幅值向量 h_pm=firpm(n,fpm,magpm); %量化滤波系数 - 处理方法与输入信号类似 h_pm = h_pm/max(abs(h_pm)); q_pm=round(h_pm*(2^(k-1)-1)); fid=fopen('你所要导出的文件地址','w'); fprintf(fid, "RADIX=10;\n"); %生成索引 fprintf(fid, "COEFDATA = \n"); for i = 1:n+1 if i == n+1 fprintf(fid,'%d;', q_pm(i)); else fprintf(fid,'%d,', q_pm(i)); end end fclose(fid);
在Matlab运行后可以在指定的路径下找到相关的COE文件,这是用来导入vivado
的,具体的代码可以参照上面的,如果担心量化误差我也写了相关的代码进行查看 - 可以去我的github上面
量化误差基本不会影响最终的设计效果不过越接近理想的滤波器性能时,量化误差会变大的(比如调整阻带衰减)
经过上面的步骤,即生成COE文件后其实就得到了相应的FPGA可使用的滤波器参数,下面进行 vivado 设计
2. 新建 vivado 工程 - 进行IP核设计
点开 IP Catalog
搜索fir
进入 FIR Compiler
Filter Options 界面导入COE文件
Channel Specification 界面设计数据通道 1 和采样频率(Fs)和IP核时钟,这边设定为MHz
Implementation 界面设定数据宽度 - 需要和Matlab的量化系数相同
补充:Implementation 界面设定Quantization
还是应该为Integer Coefficients
而不是Maximize Dynamic Range
这主要是因为我们前面在MATLAB会议已经将设计生成的滤波器向量h_pm
量化为12bit位的q_pm
,这时候动态范围实际上已经被限制了,如果这时候再选择最大动态范围就不适合这种运用情况,今晚和hui-shao进行FIR测试的时候也发现选择Maximize Dynamic Range
后DA输出直接变成直流(这是因为在FIR滤波器的加权和累加运算中,如果有任何一个系数被缩放了,那么乘法器将会乘以一个不等于1的系数值,这将导致系统的增益发生变化,使得输出信号中出现直流分量),关于这边的选择以及优势我这几天会写篇博客
上述设定完后可以在Summary里面康康有没有修改完全然后OK就行
在Source里新建一个空白文件就行,可以参考下面的例化代码,书写格式可以参考veo
文件中的描述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 `timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: Kevin // // Create Date: 2023/01/02 21:20:31 // Module Name: project_fir ////////////////////////////////////////////////////////////////////////////////// module project_fir( input clk, input reset_n, input s_axis_data_tvalid, output s_axis_data_tready, input [15:0] s_axis_data_tdata, output m_axis_data_tvalid, output [31:0] m_axis_data_tdata ); fir_compiler_0 your_instance_name ( // removed .aresetn(reset_n) !Error! .aclk(clk), // input wire aclk .s_axis_data_tvalid(s_axis_data_tvalid), // input wire s_axis_data_tvalid .s_axis_data_tready(s_axis_data_tready), // output wire s_axis_data_tready .s_axis_data_tdata(s_axis_data_tdata), // input wire [15 : 0] s_axis_data_tdata .m_axis_data_tvalid(m_axis_data_tvalid), // output wire m_axis_data_tvalid .m_axis_data_tdata(m_axis_data_tdata) // output wire [31 : 0] m_axis_data_tdata ); endmodule
这边需要注意的就是不需要声明aresetn端,应该是相应的IP核自动声明,不然后续的Simulation就跑不动
新建 testbench 文件 - 生成复位信号、50M的 clock等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: Kevin // // Create Date: 2023/01/02 21:21:19 ////////////////////////////////////////////////////////////////////////////////// `timescale 1ns / 1ps module tb_fir(); parameter LEN = 1000; reg clk; reg reset_n; reg [15:0] signal[LEN-1:0]; reg [15:0] data_tdata; reg data_tvalid; reg [3:0] cnt; wire s_axis_data_tvalid; wire s_axis_data_tready; wire [15:0] s_axis_data_tdata; wire m_axis_data_tvalid; wire [31:0] m_axis_data_tdata; initial begin clk = 1'b0; reset_n = 1'b0; data_tvalid = 1'b0; $readmemh("signal_in.txt", signal);//从signal_in.txt中读入采样数据,注意输入信号文件的存放路径 #200 reset_n = 1'b1; #100 repeat(5) begin //filter task重复执行5次 filter; end end always #10 clk = ~clk; // 50MHz always @(posedge clk or negedge reset_n) begin if(!reset_n) begin cnt <= 4'd0; data_tvalid <= 1'b0; end else begin cnt <= cnt +4'd1; if(cnt == 4) begin cnt <= 4'd0; data_tvalid <= 1'b1; end else data_tvalid <= 1'b0; end end assign s_axis_data_tvalid = data_tvalid; //生成 s_axis_data_tvalid 脉冲信号 assign s_axis_data_tdata = data_tdata; integer k; task filter; begin for(k = 0; k < LEN; k = k+1) #100 data_tdata = signal[k]; //间隔100ns(10MHz)读入一个采样数据 end endtask project_fir fir( //例化第3节写的模块 .clk(clk), .reset_n(reset_n), .s_axis_data_tvalid(s_axis_data_tvalid), .s_axis_data_tready(s_axis_data_tready), .s_axis_data_tdata(s_axis_data_tdata), .m_axis_data_tvalid(m_axis_data_tvalid), .m_axis_data_tdata(m_axis_data_tdata) ); endmodule
这边需要两点注意,都是仿真跑不出来的 Debug 结果
正确读取注意输入信号文件的存放路径 - xsim文件夹下
1 D:\ele_match\FPGA_filter\filter_lowpass\filter_lowpass.sim\sim_1\behav\xsim
这边的输入信号当然是需要我们自己来生成,借助万能的Matlab - 当然也需要进行量化处理,处理之后再导出成txt形式就可以。代码在我的 github - share_daily里有,也可以先在 Matlab 中进行滤波效果查看然后与 vivado 仿真波形对比
3. 功能仿真 进行完上述的操作后就可以进行最后的仿真波形实现
虽然有超多warning但是基本是未用端口未处理的警告 - 经典不管他
虽然已完成了大部分的工作,但是最后显示也要注意显示参数的设定,不然还是看不到最终的信号
设定 Radix - Signed Demical
设定 Fir 输出信号的显示范围 - 不然就只有小幅度
Analog Settings 取消 Auto 模式,设定 Fixed 下的显示范围
Simulation Settings 设定仿真时间 - 100us
可以看出滤波效果不错,不过这时候信号比较简单,因为我就是把 1 M 和 3 M 的正弦信号叠加,后面会增加游戏难度试试滤波器的真 · 效果,不过这只要修改下生成输入信号的文件,Matlab修改下,重跑下仿真就行🥳🥳🥳
下面和文章头是增加输入信号复杂度的仿真波形,效果还是杠杠的😉
IP核调用 - 配合ADC 实际使用当中可以将滤波器看成是封装好的定制器件,只要将处理好的数据输入设定端口,然后引出相应的输出端口就可以,下面这个就是前面设计的IP核
aresetn - 复位引脚,低电平有效
aclk - 时钟引脚
s_axis_data_tdata - 输入采样数据
s_axis_data_tready - 高电平 表示IP核已准备接收采样数据
s_axis_data_tvaid - 高电平 表示采样数据有效
m_axis_data_tdata - 输出滤波数据
m_axis_data_tvalid - 高电平 表示输出数据有效
调用的语法格式可以参考VEO
文件下
1 2 3 4 5 6 7 8 fir_compiler_0 your_instance_name ( .aclk(aclk), // input wire aclk .s_axis_data_tvalid(s_axis_data_tvalid), // input wire s_axis_data_tvalid .s_axis_data_tready(s_axis_data_tready), // output wire s_axis_data_tready .s_axis_data_tdata(s_axis_data_tdata), // input wire [15 : 0] s_axis_data_tdata .m_axis_data_tvalid(m_axis_data_tvalid), // output wire m_axis_data_tvalid .m_axis_data_tdata(m_axis_data_tdata) // output wire [31 : 0] m_axis_data_tdata );