FPGA/Verilog 設(shè)計FIR濾波器
[TOC]
前言
這應(yīng)該是第一次的FPGA(DSP方向)的實戰(zhàn)(也算不上)分享.也算是小班教學的其中一節(jié)課吧.
話不多說,先給大家介紹一下這次要干啥先:學過信號與系統(tǒng)的可以直接跳過基礎(chǔ)知識...
系統(tǒng)環(huán)境:
matlab2018a
quartus16.0
Multisim
VScode
ubuntu18.04
EP4CE15F23C8
設(shè)計目標
設(shè)計一個10階的FIR低通濾波器,濾波器的通帶截止頻率是2MHz,阻帶截止頻率是4MHz.
基礎(chǔ)知識
數(shù)字濾波器
濾波器是一種對信號有處理作用的器件或電路,其主要作用是讓有用信號盡可能無衰減地通過,對無用信號盡可能大地衰減.而數(shù)字濾波器就是一個按預定的有限精度算法實現(xiàn)的,將輸入的數(shù)字信號轉(zhuǎn)換為所要求的輸出數(shù)字信號的線性時不變系統(tǒng).
如果對上面加粗的莫得認識的話,那你就直接想成是用數(shù)字電路實現(xiàn)的濾波器得了.
FIR濾波器
FIR(Finite Impulse Response,FIR)濾波器,中文名叫有限沖激響應(yīng)濾波器,在講沖激響應(yīng)之前,我們不妨預習/回顧一下信號與系統(tǒng):

就是一個輸入信號,經(jīng)過一個系統(tǒng),每個系統(tǒng)會有他對應(yīng)的系統(tǒng)函數(shù)來處理輸入信號,最后得到輸出信號.對常見的濾波器來說,他們的理想系統(tǒng)函數(shù)往往是這些:
-
低通濾波器(紅線部分),也是這篇blog要實現(xiàn)的東西
2 -
others
3
注意看橫坐標,我們可以看到,輸入信號是時域進來的,但是系統(tǒng)函數(shù)是頻域來的.而他們的命名也是跟頻率有關(guān)系的.
這個時候我們就需要用到傅里葉變換了,至于詳情,請看:
這是一個通俗易懂的傅里葉變換簡介/教程
我們可以看到頻域上的乘積就是時域上的卷積:
用Z變換來就是:
突然想起并不是在教DSP,所以我們了解到這里就夠了,也就是說,我們只要把h(k)求出來,然后知道他可以有濾波的效果就完事了.
這里用的是FIR的橫向結(jié)構(gòu):

我們可以看到主要有三個部分,第一部分是對輸入信號的延時,第二部分是輸入信號和抽頭系數(shù)的相乘,第三部分是相乘結(jié)果的累加.這也是fpga的基本結(jié)構(gòu)假設(shè)
具體實現(xiàn)
matlab-獲取FIR抽頭系數(shù)
在matlab命令行窗口打入filterDesigner(善用tab鍵):

大家大概可以看見面板下面有很多東西,分別是:
- 指定濾波器類型,是IIR還是FIR,用什么算法生成
- 濾波器階數(shù),是特定階數(shù)還是自動生成最小階數(shù)的
- 采樣頻率,信號頻率,截止頻率
-
衰減倍數(shù)
當中的折衷關(guān)系不是這里的重點,根據(jù)設(shè)計要求,最后要改成這樣:
5
記得按下方的design filter.
右上的圖就是系統(tǒng)函數(shù)的形狀了.
接下來點[file]->[Export]導出,快捷鍵ctrl+E,直接導出到workspace就可以了
然后在命令行窗口打:round(Num*512),意思是放大512倍并取整,方便我們后面在fpga中做定點數(shù)乘法

保存下來即可
matlab-產(chǎn)生波形數(shù)據(jù)
因為懶得接信號源和寫ad模塊驅(qū)動,這次實驗直接在fpga中用一個rom的ip核儲存波形數(shù)據(jù)
matlab代碼如下:
- 生成波形
%產(chǎn)生兩個正弦信號sin(x)和sin(8x)疊加后的信號,取128個點,將信號放大,
%轉(zhuǎn)換成無符號數(shù)據(jù),存入ROM中作為信號源
clear all
clc
depth = 128;
width = 16;
x = 0 : 2*pi/(depth-1) :2*pi;
y = sin(x)+sin(8*x);
plot(x,sin(x),'r') %紅色為sin(x)函數(shù)
hold on
plot(x,sin(8*x),'g') %綠色為sin(8x)函數(shù)
hold on
plot(x,y,'b') %藍色為生成的混合信號
grid
y = (y/2) * 32768;%將信號放大32768倍
b = signed2unsigned(y,width); %轉(zhuǎn)換為無符號數(shù)輸入
%下面函數(shù)重新新建一個腳本文件
%需要調(diào)用了如下函數(shù),將有符號數(shù)轉(zhuǎn)換成無符號數(shù)
function b = signed2unsigned(a,wl)
%This function covert an signed integer number into an unsinged integer
%number. a is the input vector while wl means the width of input number;
%Example: a = [-2,-1,0,1];
%signed2unsigned(a,3); THEN return [2,3,0,1]
k = 2^(wl)*(a<0);
b = k + a;;
b = fix(b+0.5);
for i = 1:length(a)
if (b(i) == 65536)
b(i) = 0;
end
end
- 編寫mif文件
%編寫mif文件
fid = fopen('sinx.mif','wt'); %將信號寫入一個.mif文件中
fprintf(fid,'WIDTH=%d;\n',width);%寫入存儲位寬8位
fprintf(fid,'DEPTH=%d;\n',depth);%寫入存儲深度1024
fprintf(fid,'ADDRESS_RADIX=UNS;\n');%寫入地址類型為無符號整型
fprintf(fid,'DATA_RADIX=UNS;');%寫入數(shù)據(jù)類型為無符號整型
fprintf(fid,'CONTENT BEGIN\n');%起始內(nèi)容
for num=0 : 127
fprintf(fid,'%d:%16.0f;\n',num,b(num+1));
end
fclose(fid);
運行整套代碼我們也可以看見:

紅色是2MHz的正弦波,綠色是8MHz的正弦波,藍色是混合信號.
FPGA-導入波形數(shù)據(jù)
這里我們需要例化一個ROM模塊來在fpga上預存前面生成的波形文件,打開quartus,[Tools]->[IP Catalog]
查找ROM,會找到一個在[Basic Function]->[On Chip Memory]下面的ROM:1-PORT(其實只要看見這個名字就可以了)雙擊例化
-
設(shè)置好位寬和數(shù)據(jù)長度
8 -
由于對ROM沒有別的要求,所以看直接next到下圖這個位置:
9
填進剛剛在matlab里面生成的數(shù)據(jù)文件.
然后直接finish就可以了 例化這個新建的ROM,順便加時鐘
//例化ROM模塊
data_rom ROM_Init
(
.address (address_rom ), //rom的地址端口
.clock (CLK_50M ), //rom的時鐘端口
.q (data_rom ) //rom的數(shù)據(jù)端口
);
//時序電路,用來給rom_addr寄存器賦值
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N) //判斷復位
address_rom <= 7'd0; //初始化time_cnt值
else
address_rom <= address_rom + 2 ; //用來給time_cnt賦值
end
//這里為什么是 +2 后面再討論
這樣子,每一個時鐘周期下面我們在data_rom下面都可以得到一個波形數(shù)據(jù).
- 順道把抽頭系數(shù)定義一下
localparam COEFF1 = 63;
localparam COEFF2 = 39;
localparam COEFF3 = 48;
localparam COEFF4 = 54;
localparam COEFF5 = 59;
localparam COEFF6 = 60;
localparam COEFF7 = 59;
localparam COEFF8 = 54;
localparam COEFF9 = 48;
localparam COEFF10 = 39;
localparam COEFF11 = 63;
FPGA - FIR結(jié)構(gòu)設(shè)計
由前面我們講過,FIR有三個部分,有三大部分,第一部分是對輸入信號的延時,第二部分是輸入信號和抽頭系數(shù)的相乘,第三部分是相乘結(jié)果的累加.所以我們也可以分三部分來寫這個東西
part1-移位寄存器
我們可以用移位寄存器來達到輸入信號的延時.
//pipeline 1
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)begin
data_shift[0] <= 0;
data_shift[1] <= 0;
data_shift[2] <= 0;
data_shift[3] <= 0;
data_shift[4] <= 0;
data_shift[5] <= 0;
data_shift[6] <= 0;
data_shift[7] <= 0;
data_shift[8] <= 0;
data_shift[9] <= 0;
data_shift[10] <= 0;
end
else begin
data_shift[10] <= data_shift[9];
data_shift[9] <= data_shift[8];
data_shift[8] <= data_shift[7];
data_shift[7] <= data_shift[6];
data_shift[6] <= data_shift[5];
data_shift[5] <= data_shift[4];
data_shift[4] <= data_shift[3];
data_shift[3] <= data_shift[2];
data_shift[2] <= data_shift[1];
data_shift[1] <= data_shift[0];
data_shift[0] <= data_rom;
end
end
part2-乘法器
這里本來想例化乘法器ip來寫的,但是鑒于很懶,所以就直接在verilog上面的乘號來代替,將優(yōu)化丟給了編譯器.(所以下面的代碼框架是別人家的)
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
mul_data[0] <= 0;
else
mul_data[0] <= data_shift[0] * COEFF1;
end
always @(posedge CLK_50M or negedge RST_N)begin
if(!RST_N)
mul_data[1] <= 0;
else
mul_data[1] <= data_shift[1] * COEFF2;
end
………………………………………………………………………
always @(posedge CLK_50M or negedge RST_N)begin
if(!RST_N)
mul_data[9] <= 0;
else
mul_data[9] <= data_shift[9] * COEFF10;
end
always @(posedge CLK_50M or negedge RST_N)begin
if(!RST_N)
mul_data[10] <= 0;
else
mul_data[10] <= data_shift[10] * COEFF11;
end
part3-加法器
這里給出兩種寫法,順道說明一下fpga里面的些許技巧
- 類c寫法
always @(posedge CLK_50M or negedge RST_N)begin
if(!RST_N)begin
dout_r <= 0;
end
else
dout_r <= mul_data[0]+mul_data[1]+mul_data[2]+mul_data[3]+mul_data[4]+mul_data[5]+mul_data[6]+mul_data[7]+mul_data[8]+mul_data[9]+mul_data[10];
end
- 例化ip寫法
用的是parallel_add的ip,其實只要自己手動優(yōu)化一下,也可以不用ip.因為他只能做8的倍數(shù)的,但是我的濾波器只有10階.雖然有點浪費資源,但是簡單演示的話,把其他位置0就完事了.例化過程較為簡單,就不介紹了
wire [19:0] add_result;
add unit_add(
.clock (CLK_50M),
.data0x (mul_data[0][21:6]),
.data10x (mul_data[1][21:6]),
.data11x (mul_data[2][21:6]),
.data12x (mul_data[3][21:6]),
.data13x (mul_data[4][21:6]),
.data14x (mul_data[5][21:6]),
.data15x (mul_data[6][21:6]),
.data1x (mul_data[7][21:6]),
.data2x (mul_data[8][21:6]),
.data3x (mul_data[9][21:6]),
.data4x (mul_data[10][21:6]),
.data5x (16'b0),
.data6x (16'b0),
.data7x (16'b0),
.data8x (16'b0),
.data9x (16'b0),
.result (add_result)
);
至此,整個系統(tǒng)的各個關(guān)鍵部件就搭建完成了.
仿真結(jié)果
仿真波形

第一個波形是輸入信號,
第二個波形是加法器的輸出,
第三個波形是用上面第一種加法器寫出來的波形,
第四個波形是用上面第二種加法器寫出來的波形.
可以看出基本有那么一點點的濾波的作用,然而噪聲還是非常的大,其中最主要的原因就在于,這個FIR濾波器只有十階,再加上FIR本身的濾波特性,莫得辦法啦
而事實上由于階數(shù)太少了,以至于濾波效果還沒有收到字長效應(yīng)的影響,這也是讓我始料未及的...
方案對比
寫這個最主要的原因是為了回答一下那些說用類c寫法來直接寫fpga的大兄弟們...因為真的很多人學了之后都這樣子...
方案一:類c寫法
我們來看一下他的資源占用和最高運行速率:


總共用了857個邏輯單元,最高運行速率是59.2MHz
方案二:例化ip寫法


總共用了797個邏輯單元,最高運行速率是190.59MHz.
還千萬不要忘記了,這個ip是16個輸入位的,而我們只用了其中的11個!
所以結(jié)果就是,我用的資源比你少,速度可以比你快,出來的波形還一樣.雖然類C寫法確實帶來了不少的方便,但是他確實敵不過速度的制約,速度的制約出問題之后,顯然時序約束就是天荒夜談了.
結(jié)語
這一個應(yīng)用或許是fpga在dsp上面較為簡單的一個應(yīng)用了,用fpga寫dsp,還是要有扎扎實實的dsp基礎(chǔ)才行.
這個也志在跑通一個流程吧,FIR其實有很多東西,考慮到篇幅,我都沒有寫出來.不過也莫得關(guān)系了.
期末愉快




