碎碎思 · 2023年06月06日

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

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

image.png

绪论

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

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

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

image.png

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

image.png

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

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

`-0.0265 

0.0441 

-0.0934 

0.3139 
0.5000 
0.3139 

-0.0934 

0.0441 

-0.0265
`

为 FIR 模块创建设计文件

在 Vivado 项目中添加源文件。

image.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 模块还必须能够与其主端接口上的上游设备以同样的方式运行。
image.png

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

image.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 模块,需要创建一个测试平台作为其仿真源:

image.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 选项。

image.png

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

image.png

总结

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

image.png

原文:OpenFPGA
作者:碎碎思

相关文章推荐

更多FPGA干货请关注FPGA的逻辑技术专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
10614
内容数
577
FPGA Logic 二三事
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息