FPGA 的数字信号处理:Verilog 实现简单的 FIR 滤波器

avatar
作者
筋斗云
阅读量:0

c6417f58cbac91a3d14176b8df9b8854.png

该项目介绍了如何使用 Verilog 实现具有预生成系数的简单 FIR 滤波器。

76a999d1e68cea49aaa3fe48972bd0ca.png

绪论

不起眼的 FIR 滤波器是 FPGA 数字信号处理中最基本的模块之一,因此了解如何将具有给定抽头数及其相应系数值的基本模块组合在一起非常重要。因此,在这个关于 FPGA 上 DSP 基础实用入门的教程中,将从一个简单的 15 抽头低通滤波器 FIR 开始,在 Matlab 中为其生成初始系数值,然后转换这些值用于编写 Verilog 模块。

有限脉冲响应或 FIR 滤波器定义为脉冲响应在特定时间段内稳定为零值的滤波器。脉冲响应稳定到零所花费的时间与滤波器阶数(抽头数)直接相关,滤波器阶数是 FIR 的基础传递函数多项式的阶数。FIR 的传递函数不包含反馈,因此如果输入一个值为 1 的脉冲,然后输入一串零值,输出将只是滤波器的系数值。

滤波器的作用基本都是用于信号调节,主要集中在选择滤除或允许通过哪些频率。最简单的例子之一是低通滤波器,它允许低于某个阈值(截止频率)的频率通过,同时大大衰减高于该阈值的频率,如下图所示。

7a2d3a5f49ef1d1acd6f622c33ef348b.png

该项目的主要重点是在 HDL(具体为 Verilog)中实现 FIR,它可以分解为三个主要逻辑组件:一个循环缓冲器,用于将每个样本计时到适当地考虑了串行输入的延迟、每个抽头系数值的乘法器以及每个抽头输出的求和结果的累加器。

39ab59d221fa437ca54ed1cc456eac1f.png

由于本项目专注于 FPGA 逻辑中 FIR 的设计机制,所以只是使用 Simulink 中的 FDA 工具和 Matlab 为低通滤波器插入一些简单参数,然后使用生成的系数值放到 Verilog 模块中完成滤波器的设计(在后面的步骤中完成)。

dbb451b344f2df421639d707fc7020d1.png

选择实现一个简单的 15 抽头低通滤波器 FIR,采样率为 1Ms/s,通带频率为 200kHz,阻带频率为 355kHz,得到以下系数:

-0.0265  0  0.0441  0  -0.0934  0  0.3139  0.5000  0.3139  0  -0.0934  0  0.0441  0  -0.0265

为 FIR 模块创建设计文件

在 Vivado 项目中添加源文件。

dce2efd47b557898c3efda49058735a4.png

在确定 FIR 的顺序(抽头数)并获得系数值后,接下来需要定义的下一组参数就是输入样本、输出样本和系数本身的位宽。

对于这个 FIR,选择将输入样本和系数寄存器设置为 16 位宽,并将输出样本寄存器设置为 32 位,因为两个 16 位值的乘积是一个 32 位值(两个值的宽度相乘得到乘积的宽度,所以如果选择了 8 位抽头的 16 位输入样本,那么输出样本将为 24 位宽)。

这些值也都是带符号的,因此 MSB 用作符号位,在选择输入样本寄存器的初始宽度时一定要记住这一点。要在 Verilog 中将这些值设置为有符号数据类型,使用关键字signed :

reg signed [15:0] register_name;

接下来要解决的是如何在 Verilog 中处理系数值,小数点值需要转换为定点值。由于所有系数值都小于 1,因此寄存器的所有 15 位(总共 16 位,MSB 是有符号位)都可以用于小数位。通常,必须决定要将寄存器中的多少位用于数字的整数部分与数字的小数部分。因此,转换分数值抽头的数学是:(fractional coefficient value)*(2^(15))该乘积的小数值被四舍五入,并且如果系数为负,则计算该值的二进制补码:

tap0 = twos(-0.0265 * 32768) = 0xFC9C tap1 = 0 tap2 = 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5 tap3 = 0 tap4 = twos(-0.0934 * 32768) = 0xF40C tap5 = 0 tap6 = 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D tap7 = 0.5000 * 32768 = 16384 = 0x4000 tap8 = 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D tap9 =  0 tap10 = twos(-0.0934 * 32768) = 0xF40C tap11 = 0 tap12 = 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5 tap13 = 0 tap14 = twos(-0.0265 * 32768) = 0xFC9C

现在我们终于准备好关注 FIR 模块的逻辑,第一个是循环缓冲区,它引入串行输入样本流并为滤波器的 15 个抽头创建一个包含 15 个输入样本的数组。

always @ (posedge clk)         begin             if(enable_buff == 1'b1)                 begin                     buff0 <= in_sample;                     buff1 <= buff0;                             buff2 <= buff1;                              buff3 <= buff2;                           buff4 <= buff3;                           buff5 <= buff4;                            buff6 <= buff5;                         buff7 <= buff6;                            buff8 <= buff7;                            buff9 <= buff8;                            buff10 <= buff9;                             buff11 <= buff10;                            buff12 <= buff11;                            buff13 <= buff12;                            buff14 <= buff13;                     end         end

接下来,乘法阶段将每个样本乘以每个系数值:

/* Multiply stage of FIR */     always @ (posedge clk)         begin             if (enable_fir == 1'b1)                 begin                     acc0 <= tap0 * buff0;                     acc1 <= tap1 * buff1;                     acc2 <= tap2 * buff2;                     acc3 <= tap3 * buff3;                     acc4 <= tap4 * buff4;                     acc5 <= tap5 * buff5;                     acc6 <= tap6 * buff6;                     acc7 <= tap7 * buff7;                     acc8 <= tap8 * buff8;                     acc9 <= tap9 * buff9;                     acc10 <= tap10 * buff10;                     acc11 <= tap11 * buff11;                     acc12 <= tap12 * buff12;                     acc13 <= tap13 * buff13;                     acc14 <= tap14 * buff14;                 end         end

乘法阶段的结果值通过加法累加到寄存器中,最终成为滤波器的输出数据流。

/* Accumulate stage of FIR */        always @ (posedge clk)          begin             if (enable_fir == 1'b1)                 begin                     m_axis_fir_tdata <= acc0 + acc1 + acc2 + acc3 + acc4 + acc5 + acc6 + acc7 + acc8 + acc9 + acc10 + acc11 + acc12 + acc13 + acc14;                 end         end

最后,逻辑的最后一部分是将数据流进和流出 FIR 模块的接口。AXI Stream 接口是最常见的接口之一。关键方面是允许控制上游和下游设备之间的数据流的tready和tvalid信号。这意味着 FIR 模块需要向其下游设备提供tvalid信号以指示其输出是有效数据,并且如果下游设备解除其tready信号,则能够暂停(但仍保留)其输出。FIR 模块还必须能够与其主端接口上的上游设备以同样的方式运行。

5074f17104d4886628ee02db551971d1.png

以下是 FIR 模块的逻辑设计概述:

322828b1ab5d72fcda50c82f9084be6e.png

请注意tready和tvalid信号如何设置输入循环缓冲器的使能值和 FIR 的乘法级以及数据或系数通过的每个寄存器都被声明为有符号的。

FIR模块Verilog代码:

`timescale 1ns / 1ps  module FIR(     input clk,     input reset,     input signed [15:0] s_axis_fir_tdata,      input [3:0] s_axis_fir_tkeep,     input s_axis_fir_tlast,     input s_axis_fir_tvalid,     input m_axis_fir_tready,     output reg m_axis_fir_tvalid,     output reg s_axis_fir_tready,     output reg m_axis_fir_tlast,     output reg [3:0] m_axis_fir_tkeep,     output reg signed [31:0] m_axis_fir_tdata     );       always @ (posedge clk)         begin             m_axis_fir_tkeep <= 4'hf;         end              always @ (posedge clk)         begin             if (s_axis_fir_tlast == 1'b1)                 begin                     m_axis_fir_tlast <= 1'b1;                 end             else                 begin                     m_axis_fir_tlast <= 1'b0;                 end         end          // 15-tap FIR      reg enable_fir, enable_buff;     reg [3:0] buff_cnt;     reg signed [15:0] in_sample;      reg signed [15:0] buff0, buff1, buff2, buff3, buff4, buff5, buff6, buff7, buff8, buff9, buff10, buff11, buff12, buff13, buff14;      wire signed [15:0] tap0, tap1, tap2, tap3, tap4, tap5, tap6, tap7, tap8, tap9, tap10, tap11, tap12, tap13, tap14;      reg signed [31:0] acc0, acc1, acc2, acc3, acc4, acc5, acc6, acc7, acc8, acc9, acc10, acc11, acc12, acc13, acc14;            /* Taps for LPF running @ 1MSps with a cutoff freq of 400kHz*/     assign tap0 = 16'hFC9C;  // twos(-0.0265 * 32768) = 0xFC9C     assign tap1 = 16'h0000;  // 0     assign tap2 = 16'h05A5;  // 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5     assign tap3 = 16'h0000;  // 0     assign tap4 = 16'hF40C;  // twos(-0.0934 * 32768) = 0xF40C     assign tap5 = 16'h0000;  // 0     assign tap6 = 16'h282D;  // 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D     assign tap7 = 16'h4000;  // 0.5000 * 32768 = 16384 = 0x4000     assign tap8 = 16'h282D;  // 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D     assign tap9 = 16'h0000;  // 0     assign tap10 = 16'hF40C; // twos(-0.0934 * 32768) = 0xF40C     assign tap11 = 16'h0000; // 0     assign tap12 = 16'h05A5; // 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5     assign tap13 = 16'h0000; // 0     assign tap14 = 16'hFC9C; // twos(-0.0265 * 32768) = 0xFC9C          /* This loop sets the tvalid flag on the output of the FIR high once       * the circular buffer has been filled with input samples for the       * first time after a reset condition. */     always @ (posedge clk or negedge reset)         begin             if (reset == 1'b0) //if (reset == 1'b0 || tvalid_in == 1'b0)                 begin                     buff_cnt <= 4'd0;                     enable_fir <= 1'b0;                     in_sample <= 8'd0;                 end             else if (m_axis_fir_tready == 1'b0 || s_axis_fir_tvalid == 1'b0)                 begin                     enable_fir <= 1'b0;                     buff_cnt <= 4'd15;                     in_sample <= in_sample;                 end             else if (buff_cnt == 4'd15)                 begin                     buff_cnt <= 4'd0;                     enable_fir <= 1'b1;                     in_sample <= s_axis_fir_tdata;                 end             else                 begin                     buff_cnt <= buff_cnt + 1;                     in_sample <= s_axis_fir_tdata;                 end         end         always @ (posedge clk)         begin             if(reset == 1'b0 || m_axis_fir_tready == 1'b0 || s_axis_fir_tvalid == 1'b0)                 begin                     s_axis_fir_tready <= 1'b0;                     m_axis_fir_tvalid <= 1'b0;                     enable_buff <= 1'b0;                 end             else                 begin                     s_axis_fir_tready <= 1'b1;                     m_axis_fir_tvalid <= 1'b1;                     enable_buff <= 1'b1;                 end         end          /* Circular buffer bring in a serial input sample stream that       * creates an array of 15 input samples for the 15 taps of the filter. */     always @ (posedge clk)         begin             if(enable_buff == 1'b1)                 begin                     buff0 <= in_sample;                     buff1 <= buff0;                             buff2 <= buff1;                              buff3 <= buff2;                           buff4 <= buff3;                           buff5 <= buff4;                            buff6 <= buff5;                         buff7 <= buff6;                            buff8 <= buff7;                            buff9 <= buff8;                            buff10 <= buff9;                             buff11 <= buff10;                            buff12 <= buff11;                            buff13 <= buff12;                            buff14 <= buff13;                     end             else                 begin                     buff0 <= buff0;                     buff1 <= buff1;                             buff2 <= buff2;                              buff3 <= buff3;                           buff4 <= buff4;                           buff5 <= buff5;                            buff6 <= buff6;                         buff7 <= buff7;                            buff8 <= buff8;                            buff9 <= buff9;                            buff10 <= buff10;                             buff11 <= buff11;                            buff12 <= buff12;                            buff13 <= buff13;                            buff14 <= buff14;                 end         end              /* Multiply stage of FIR */     always @ (posedge clk)         begin             if (enable_fir == 1'b1)                 begin                     acc0 <= tap0 * buff0;                     acc1 <= tap1 * buff1;                     acc2 <= tap2 * buff2;                     acc3 <= tap3 * buff3;                     acc4 <= tap4 * buff4;                     acc5 <= tap5 * buff5;                     acc6 <= tap6 * buff6;                     acc7 <= tap7 * buff7;                     acc8 <= tap8 * buff8;                     acc9 <= tap9 * buff9;                     acc10 <= tap10 * buff10;                     acc11 <= tap11 * buff11;                     acc12 <= tap12 * buff12;                     acc13 <= tap13 * buff13;                     acc14 <= tap14 * buff14;                 end         end                   /* Accumulate stage of FIR */        always @ (posedge clk)          begin             if (enable_fir == 1'b1)                 begin                     m_axis_fir_tdata <= acc0 + acc1 + acc2 + acc3 + acc4 + acc5 + acc6 + acc7 + acc8 + acc9 + acc10 + acc11 + acc12 + acc13 + acc14;                 end         end                 endmodule

创建仿真文件

要测试 FIR 模块,需要创建一个测试平台作为其仿真源:

c1cd20f7f71d7f77fedf8f2676096df2.png

在 FIR 模块中有两个主要的东西需要测试:滤波器算法和 AXI 流接口。为实现这一点,测试台中创建了一个状态机,它生成一个简单的 200kHz 正弦波,并切换从属端的有效信号和 FIR 接口主控端的tready信号。

FIR 模块的测试平台:

`timescale 1ns / 1ps  module tb_FIR;      reg clk, reset, s_axis_fir_tvalid, m_axis_fir_tready;     reg signed [15:0] s_axis_fir_tdata;     wire m_axis_fir_tvalid;     wire [3:0] m_axis_fir_tkeep;     wire [31:0] m_axis_fir_tdata;          /*      * 100Mhz (10ns) clock       */     always begin         clk = 1; #5;         clk = 0; #5;     end          always begin         reset = 1; #20;         reset = 0; #50;         reset = 1; #1000000;     end          always begin         s_axis_fir_tvalid = 0; #100;         s_axis_fir_tvalid = 1; #1000;         s_axis_fir_tvalid = 0; #50;         s_axis_fir_tvalid = 1; #998920;     end          always begin         m_axis_fir_tready = 1; #1500;         m_axis_fir_tready = 0; #100;         m_axis_fir_tready = 1; #998400;     end          /* Instantiate FIR module to test. */     FIR FIR_i(         .clk(clk),         .reset(reset),         .s_axis_fir_tdata(s_axis_fir_tdata),            .s_axis_fir_tkeep(s_axis_fir_tkeep),            .s_axis_fir_tlast(s_axis_fir_tlast),            .s_axis_fir_tvalid(s_axis_fir_tvalid),          .m_axis_fir_tready(m_axis_fir_tready),         .m_axis_fir_tvalid(m_axis_fir_tvalid),          .s_axis_fir_tready(s_axis_fir_tready),          .m_axis_fir_tlast(m_axis_fir_tlast),            .m_axis_fir_tkeep(m_axis_fir_tkeep),            .m_axis_fir_tdata(m_axis_fir_tdata));                reg [4:0] state_reg;     reg [3:0] cntr;          parameter wvfm_period = 4'd4;          parameter init               = 5'd0;     parameter sendSample0        = 5'd1;     parameter sendSample1        = 5'd2;     parameter sendSample2        = 5'd3;     parameter sendSample3        = 5'd4;     parameter sendSample4        = 5'd5;     parameter sendSample5        = 5'd6;     parameter sendSample6        = 5'd7;     parameter sendSample7        = 5'd8;          /* This state machine generates a 200kHz sinusoid. */     always @ (posedge clk or posedge reset)         begin             if (reset == 1'b0)                 begin                     cntr <= 4'd0;                     s_axis_fir_tdata <= 16'd0;                     state_reg <= init;                 end             else                 begin                     case (state_reg)                         init : //0                             begin                                 cntr <= 4'd0;                                 s_axis_fir_tdata <= 16'h0000;                                 state_reg <= sendSample0;                             end                                                      sendSample0 : //1                             begin                                 s_axis_fir_tdata <= 16'h0000;                                                                  if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample1;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample0;                                     end                             end                                                   sendSample1 : //2                             begin                                 s_axis_fir_tdata <= 16'h5A7E;                                                                   if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample2;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample1;                                     end                             end                                                   sendSample2 : //3                             begin                                 s_axis_fir_tdata <= 16'h7FFF;                                                                  if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample3;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample2;                                     end                             end                                                   sendSample3 : //4                             begin                                 s_axis_fir_tdata <= 16'h5A7E;                                                                  if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample4;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample3;                                     end                             end                                                   sendSample4 : //5                             begin                                 s_axis_fir_tdata <= 16'h0000;                                                                  if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample5;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample4;                                     end                             end                                                   sendSample5 : //6                             begin                                 s_axis_fir_tdata <= 16'hA582;                                                                   if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample6;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample5;                                     end                             end                                                   sendSample6 : //6                             begin                                 s_axis_fir_tdata <= 16'h8000;                                                                   if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample7;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample6;                                     end                             end                                                   sendSample7 : //6                             begin                                 s_axis_fir_tdata <= 16'hA582;                                                                   if (cntr == wvfm_period)                                     begin                                         cntr <= 4'd0;                                         state_reg <= sendSample0;                                     end                                 else                                     begin                                          cntr <= cntr + 1;                                         state_reg <= sendSample7;                                     end                             end                                                               endcase                 end         end      endmodule

运行行为仿真

FIR 模块及其测试平台文件就位后,从 Flow Navigator 窗口启动 Vivado 中的仿真器,选择 Run Behavioral Simulation 选项。

59568d4dc6b26b5f2332b1f68b771f25.png

如行为仿真所示,FIR 正确过滤信号并正确响应 AXI 流信号。

d0ac89678dd49fc8284a31e328f49425.png

总结

代码都在上面大家有兴趣可以自行运行,但是大家可能会注意到,这个 FIR 模块在设计上运行综合和实现时时序应该是不能通过的。我们将在下一篇文章中详细介绍如何在无法满足时序要求时重新设计你的设计~

297bfa037f7e67437a996ec3b8991360.png

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!