緒論
不起眼的 FIR 濾波器是 FPGA 數(shù)字信號處理中最基本的模塊之一,因此了解如何將具有給定抽頭數(shù)及其相應(yīng)系數(shù)值的基本模塊組合在一起非常重要。因此,在這個關(guān)于 FPGA 上 DSP 基礎(chǔ)實用入門的教程中,將從一個簡單的 15 抽頭低通濾波器 FIR 開始,在 Matlab 中為其生成初始系數(shù)值,然后轉(zhuǎn)換這些值用于編寫 Verilog 模塊。
有限脈沖響應(yīng)或 FIR 濾波器定義為脈沖響應(yīng)在特定時間段內(nèi)穩(wěn)定為零值的濾波器。脈沖響應(yīng)穩(wěn)定到零所花費的時間與濾波器階數(shù)(抽頭數(shù))直接相關(guān),濾波器階數(shù)是 FIR 的基礎(chǔ)傳遞函數(shù)多項式的階數(shù)。FIR 的傳遞函數(shù)不包含反饋,因此如果輸入一個值為 1 的脈沖,然后輸入一串零值,輸出將只是濾波器的系數(shù)值。
濾波器的作用基本都是用于信號調(diào)節(jié),主要集中在選擇濾除或允許通過哪些頻率。最簡單的例子之一是低通濾波器,它允許低于某個閾值(截止頻率)的頻率通過,同時大大衰減高于該閾值的頻率,如下圖所示。
該項目的主要重點是在 HDL(具體為 Verilog)中實現(xiàn) FIR,它可以分解為三個主要邏輯組件:一個循環(huán)緩沖器,用于將每個樣本計時到適當?shù)乜紤]了串行輸入的延遲、每個抽頭系數(shù)值的乘法器以及每個抽頭輸出的求和結(jié)果的累加器。
由于本項目專注于 FPGA 邏輯中 FIR 的設(shè)計機制,所以只是使用 Simulink 中的 FDA 工具和 Matlab 為低通濾波器插入一些簡單參數(shù),然后使用生成的系數(shù)值放到 Verilog 模塊中完成濾波器的設(shè)計(在后面的步驟中完成)。
選擇實現(xiàn)一個簡單的 15 抽頭低通濾波器 FIR,采樣率為 1Ms/s,通帶頻率為 200kHz,阻帶頻率為 355kHz,得到以下系數(shù):
-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 模塊創(chuàng)建設(shè)計文件
在 Vivado 項目中添加源文件。
在確定 FIR 的順序(抽頭數(shù))并獲得系數(shù)值后,接下來需要定義的下一組參數(shù)就是輸入樣本、輸出樣本和系數(shù)本身的位寬。
對于這個 FIR,選擇將輸入樣本和系數(shù)寄存器設(shè)置為 16 位寬,并將輸出樣本寄存器設(shè)置為 32 位,因為兩個 16 位值的乘積是一個 32 位值(兩個值的寬度相乘得到乘積的寬度,所以如果選擇了 8 位抽頭的 16 位輸入樣本,那么輸出樣本將為 24 位寬)。
這些值也都是帶符號的,因此 MSB 用作符號位,在選擇輸入樣本寄存器的初始寬度時一定要記住這一點。要在 Verilog 中將這些值設(shè)置為有符號數(shù)據(jù)類型,使用關(guān)鍵字signed :
`reg signed [15:0] register_name;
`
接下來要解決的是如何在 Verilog 中處理系數(shù)值,小數(shù)點值需要轉(zhuǎn)換為定點值。由于所有系數(shù)值都小于 1,因此寄存器的所有 15 位(總共 16 位,MSB 是有符號位)都可以用于小數(shù)位。通常,必須決定要將寄存器中的多少位用于數(shù)字的整數(shù)部分與數(shù)字的小數(shù)部分。因此,轉(zhuǎn)換分數(shù)值抽頭的數(shù)學(xué)是:(fractional coefficient value)*(2^(15))該乘積的小數(shù)值被四舍五入,并且如果系數(shù)為負,則計算該值的二進制補碼:
`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
`
現(xiàn)在我們終于準備好關(guān)注 FIR 模塊的邏輯,第一個是循環(huán)緩沖區(qū),它引入串行輸入樣本流并為濾波器的 15 個抽頭創(chuàng)建一個包含 15 個輸入樣本的數(shù)組。
` 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
`
接下來,乘法階段將每個樣本乘以每個系數(shù)值:
` /* 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
`
乘法階段的結(jié)果值通過加法累加到寄存器中,最終成為濾波器的輸出數(shù)據(jù)流。
` /* 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
`
最后,邏輯的最后一部分是將數(shù)據(jù)流進和流出 FIR 模塊的接口。AXI Stream 接口是最常見的接口之一。關(guān)鍵方面是允許控制上游和下游設(shè)備之間的數(shù)據(jù)流的tready和tvalid信號。這意味著 FIR 模塊需要向其下游設(shè)備提供tvalid信號以指示其輸出是有效數(shù)據(jù),并且如果下游設(shè)備解除其tready信號,則能夠暫停(但仍保留)其輸出。FIR 模塊還必須能夠與其主端接口上的上游設(shè)備以同樣的方式運行。
以下是 FIR 模塊的邏輯設(shè)計概述:
請注意tready和tvalid信號如何設(shè)置輸入循環(huán)緩沖器的使能值和 FIR 的乘法級以及數(shù)據(jù)或系數(shù)通過的每個寄存器都被聲明為有符號的。
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 ==