(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核

上一文 討論了FIR濾波器的結構以及使用Python從兩個方面(循環運算和矩陣運算)實現FIR,而文中提到的單片機,只需要按照循環運算的方法就可以實現FIR濾波器。

所以,單片機實現FIR濾波器並不複雜;奈何我手癢了,想捨棄掉FIR IP核,用Verilog自己寫一個FIR。不知道大家有沒有這樣手癢的感覺,如果有,跟隨這篇文章一起來,看完記得點贊。

本文內容涉及Verilog的語法:function,generate for,generate if, readmemb,readmemh;部分Python語法以及學習Verilog)及計算機數值表示規則。


設計思想

首先需要把FIR最基本的結構實現,也就是每個FIR抽頭的數據與其抽頭係數相乘這個操作。由頂層文件對這個基本模塊進行多次調用。

(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核

由於FIR抽頭係數是中心對稱的,為了減少乘法在FPGA內的出現,每個基本結構同時會輸入兩個信號,也是關於中心對稱的。

(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


此外,為了防止後續相加的過程引起符號位溢出,FIR基本模塊需要對乘法結果進行符號位擴展。

擴展完成後,如果同時對這些(71個)加權結果相加,肯定會使得系統運行速率上不去,而且設計的比較死板。這裡需要引入流水線操作;何為流水線操作,簡單地說,本來你一個人承擔一桌菜,你需要洗菜,切菜,炒菜,裝盤,上桌,不僅十分麻煩而且很耽誤時間;這個時候有人過來幫你,一個人洗菜,一個人切菜,一個人炒菜,一個人裝盤,你負責上桌,雖然費了些人,但是每個人的任務都比較輕鬆所以做事速度也很快,這就是流水線操作,把一件很複雜的事情劃分成N個小事,雖然犧牲了面積但換取了系統運行時鐘的提升。

前期準備

除了Verilog模塊,我們還有幾樣東西需要準備。首先,需要將FIR抽頭係數定點化,上一文使用的FIR抽頭係數都是很小的浮點數,為此,我們直接對每個係數乘以2的15次冪,然後取整數,捨去小數位,設定FIR抽頭係數位寬為16bit;因為係數本身比較小,不擔心會溢出。注意,這裡抽頭係數的位寬儘量不超過信號位寬,否則可能會有問題。

為了方便多個模塊同時調用FIR係數,這裡使用Python直接將定點化的係數生成為function,輸入為index,需要第N階的FIR係數,就調用function,輸入參數為N,輸出為定點化的係數。

所謂定點化,這裡使用的方法十分粗暴,直接對所有浮點數,乘以一個2的n次冪。然後對參數向下取整,捨棄小數位。

FIR浮點係數轉化為定點數並生成function的代碼如下:

<code>def coef2function(filename, exp, gain):    
# :param filename: FIR抽頭係數文件名
# :param exp: 浮點數轉定點數的位寬
# :param gain: 浮點數整體的增益,增益為power(2, gain)
# :return:
\tcoef = set_coef(filename)
\twith open('fir_coef.v', 'w') as f:
\t\tf.write('function [{}:0] get_coef;\\n'.format(exp-1))
\t\tf.write('input [7:0] index;\\n')
\t\tf.write('case (index)\\n')
\t\tfor i in range(len(coef)):
\tf.write('{}: get_coef = {};\\n'.format(i,int(np.floor(coef[i] * np.power(2,gain)))))
\t\tf.write('default: get_coef = 0;\\n')
\t\tf.write('endcase\\nendfunction')/<code>

轉換生成的function示例如下:

<code>function [15:0] get_coef;
input [7:0] index;
case (index)
0: get_coef = 0;
1: get_coef = 0;
2: get_coef = 2;
3: get_coef = 10;

...
69: get_coef = 10;
70: get_coef = 2;
71: get_coef = 0;
72: get_coef = 0;
default: get_coef = 0;
endcase
endfunction/<code>

這樣,當多個基本模塊並行運行時,每個模塊的係數可以通過調用function獲取對應的參數。


仿真需要有信號源供FIR濾波,所以直接將仿真用的信號源定點化;因為Testbench中使用readmemh或者readmemb讀取txt文檔數據,只能讀取二進制或16進制數據,所以需要對數據進行二進制或16進制轉換。

信號源選取上一文的信號源,由於該信號源最大值為3,設定信號源的位寬為16位,為防止數據溢出,信號源整體乘以2的12次冪,然後取整捨去小數位。為了方便後續轉二進制,這裡需要將數據由16bit有符號轉為16bit無符號;轉換的過程為,如果data[i]小於0,直接設定data[i] = 2^16 + data[i]。然後使用“{{:0>16b}}”.format(data[i])轉換為16bit二進制,存入cos.txt。

浮點數轉換定點數並轉換二進制數據存入txt轉換代碼如下:

<code>def float2fix_point(data, exp, gain, size):    
# '''

# :param data: 信號源數據
# :param exp: 浮點數轉定點數的位寬
# :param gain: 浮點數整體乘以增益,增益為power(2,15)
# :param size: 轉換多少點數
# :return:
# '''
\tif size > len(data):
\tprint("error, size > len(data)")
\t\treturn
\tdata = [int(np.floor(data[i] * np.power(2, gain) )) for i in range(size)]
fmt = '{{:0>{}b}}'.format(exp)
\tn = np.power(2, exp)
\tfor i in range(size):
\tif data[i] > (n //2 - 1):
\tprint("error")
\t\tif data[i] < 0:
\td = n + data[i]
\t\telse:
\td = data[i]
\t\tdata[i] = fmt.format(d)
\tnp.savetxt('cos.txt', data, fmt='%s')/<code>


實現方法

為了方便看示例代碼,這裡假定信號位寬DATA_BITS為16,係數位寬為COEF_BITS為16,擴展符號位寬EXTEND_BITS為5, 階數FIR_ORDER為72。

設計思路還是從底層開始設計,首先需要實現FIR的基本模塊。前面提到,為了節省乘法器,每個模塊輸入兩個信號和一個FIR抽頭係數,兩個參數相加,相加結果直接乘以係數,最後做符號位擴展,防止後續操作導致符號位溢出。

fir_base.v 主要代碼:

<code>reg signed [DATA_BITS + COEF_BITS - 1:0]  data_mult;
// 因為FIR係數是中心對稱的,所以直接把中心對稱的數據相加乘以係數
// 相加符號位擴展一位
wire signed [DATA_BITS:0] data_in ;
assign data_in = {data_in_A[DATA_BITS-1], data_in_A} + {data_in_B[DATA_BITS-1], data_in_B};
// 為了防止後續操作導致符號位溢出,這裡擴展符號位,設計位操作知識
assign data_out = {{EXTEND_BITS{data_mult[DATA_BITS + COEF_BITS - 1]}},data_mult };

always @(posedge clk or posedge rst) begin
\tif (rst) begin
\t// reset
\tfir_busy <= 1'b0;
\t\tdata_mult <= 0 ;
\t\toutput_vld <= 1'b0;
\tend
else if (en) begin
//如果coef為0,不需要計算直接得0
\tdata_mult <= coef != 0 ? data_in * coef : 0;
\t\toutput_vld <= 1'b1;
\tend
else begin
\tdata_mult <= 'd0;
\t\toutput_vld <= 1'b0;
\tend
end/<code>


完成了基本模塊後,頂層模塊就是調用基本模塊,然後對運算結果進行相加操作。但這裡需要注意,頂層首先需要73個16bit的寄存器,用來保存傳入的信號並實現每時鐘週期上升沿,73個數據整體前移;學過數據結構的同學可以把這個想象成隊列結構,每次信號上升沿時,隊首信號出隊,隊尾補進新的信號。

(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


實現方法如下:

<code>// FIR輸入數據暫存寄存器組
reg signed \t[DATA_BITS-1:0]\tdata_tmp [FIR_ORDER:0] ;
always @(posedge clk or posedge rst) begin
\tif (rst) begin
\t\t// reset
\t\tdata_tmp[0] <=\t0;
\tend
\telse if (data_in_vld) begin
\t\tdata_tmp[0] <=\tdata_in;
\tend
end
generate

\tgenvar j;
for (j = 1; j <= FIR_ORDER; j = j + 1)
\tbegin: fir_base
\t//這裡無法兼顧0,FIR_HALF_ORDER
always @(posedge clk or posedge rst) begin
if (rst) begin
// reset
data_tmp[j] <=\t0;
end
else if (data_in_vld) begin
data_tmp[j] <=\tdata_tmp[j-1];
end
\tend
endgenerate/<code>


這裡實現了從0-72共73個寄存器,使用了Verilog的類似二維數組的寄存器定義用法。可以從代碼看到,0號data_tmp過於特殊,需要保存輸入的信號,而其他data_tmp直接使用generate for語法實現前面提到的“隊列”功能。generate for語法是可以綜合的,其中for循環的參數必須是常數,其作用就是直接在電路上覆制循環體的內容。對於像這樣需要規律性地賦值操作很方便,下面還會出現generate for語法。


寄存器組的問題解決後,需要與FIR參數進行乘加,這裡同樣適用generate for語句簡化設計:

<code>localparam FIR_HALF_ORDER = FIR_ORDER / 2;  //36
wire signed [OUT_BITS-1:0]\tdata_out_tmp [FIR_HALF_ORDER:0] ;
// FIR輸出數據後流水線相加的中間變量,多出部分變量,防止下一級相加過程中index越界

reg signed \t[OUT_BITS-1:0]\tdat_out_reg [FIR_HALF_ORDER+4:0] ; \t//40-0
always @(posedge clk or posedge rst) begin
\tif (rst) begin
\t\t// reset
\t\tdat_out_reg[FIR_HALF_ORDER] <= 0;
\tend
\telse if (output_vld_tmp[FIR_HALF_ORDER]) begin
\t\tdat_out_reg[FIR_HALF_ORDER] <= data_out_tmp[FIR_HALF_ORDER];
\tend
end
fir_base
#(
\t.DATA_BITS(DATA_BITS),
\t.COEF_BITS(COEF_BITS),
\t.EXTEND_BITS(EXTEND_BITS)
\t)
fir_inst_FIR_HALF_ORDER(
\t.clk\t\t(clk),
\t.rst\t\t(rst),
.en\t\t\t(data_in_vld),
.data_in_A\t(data_tmp[FIR_HALF_ORDER]),
.data_in_B\t(12'd0),
.coef\t\t(get_coef(FIR_HALF_ORDER)),
.fir_busy\t(),
.data_out\t(data_out_tmp[FIR_HALF_ORDER]),
.output_vld\t(output_vld_tmp[FIR_HALF_ORDER])
);

generate
\tgenvar j;
\tfor (j = 1; j < FIR_HALF_ORDER; j = j + 1)
\tbegin: fir_base
\tfir_base
\t#(
\t.DATA_BITS(DATA_BITS),
\t.COEF_BITS(COEF_BITS),
\t.EXTEND_BITS(EXTEND_BITS)
\t)
\tfir_inst_NORMAL
\t(
\t\t.clk\t\t(clk),
\t\t.rst\t\t(rst),
\t\t
\t\t.en\t\t\t(data_in_vld),
\t\t.data_in_A\t(data_tmp[j]),
\t\t.data_in_B\t(data_tmp[FIR_ORDER-j]),
\t\t.coef\t\t(get_coef(j)),
\t\t
\t\t.fir_busy\t(),
\t\t.data_out\t(data_out_tmp[j]),

\t\t.output_vld\t(output_vld_tmp[j])
\t);
\talways @(posedge clk or posedge rst) begin
\t\tif (rst) begin
\t\t\t// reset
\t\t\tdat_out_reg[j] <= 0;
\t\tend
\t\telse if (output_vld_tmp[j]) begin
\t\t\tdat_out_reg[j] <= data_out_tmp[j];
\t\tend
\tend
endgenerate/<code>

首先由於中心點(第36階)的係數是隻乘中心點,並不像其他係數可以傳入關於中心對稱的兩個信號。所以FIR_HALF_ORDER需要單獨例化。同樣,dat_out_reg也需要單獨複製;其他的信號在generate for循環體完成操作,由於0號係數在階數為偶數的情況下為0,這裡跳過0號係數直接從1號係數開始,所以for循環是從1 - FIR_HALF_ORDER。


加權結果出來後,需要對結果相加,為了提升系統運行速率,這裡採用三級流水線操作。每次進行4位數據相加傳遞給下一級流水線,所以示例代碼裡FIR最高階數為4 * 4 * 4 * 2 = 128。

流水線操作過程如下:

<code>// 流水線第一級相加,計算公式ceil(N/4)
localparam FIR_ADD_ORDER_ONE = (FIR_HALF_ORDER + 3) / 4; //
// 流水線第二級相加,計算公式ceil(N/4)
localparam FIR_ADD_ORDER_TWO = (FIR_ADD_ORDER_ONE + 3) / 4; //3
reg signed [OUT_BITS-1:0]\tdat_out_A [FIR_ADD_ORDER_ONE+3:0] ;\t//12-0
reg signed [OUT_BITS-1:0]\tdat_out_B [FIR_ADD_ORDER_TWO+3:0] ;\t//6-0

// 這些多餘的reg直接設為0就可以了
always @ (posedge clk) begin
\tdat_out_reg[FIR_HALF_ORDER+1] = 0;
\tdat_out_reg[FIR_HALF_ORDER+2] = 0;
\tdat_out_reg[FIR_HALF_ORDER+3] = 0;
\tdat_out_reg[FIR_HALF_ORDER+4] = 0;
\tdat_out_A[FIR_ADD_ORDER_ONE] = 0;
\tdat_out_A[FIR_ADD_ORDER_ONE+1] = 0;
\tdat_out_A[FIR_ADD_ORDER_ONE+2] = 0;
\tdat_out_A[FIR_ADD_ORDER_ONE+3] = 0;
\tdat_out_B[FIR_ADD_ORDER_TWO] = 0;
\tdat_out_B[FIR_ADD_ORDER_TWO + 1] = 0;
\tdat_out_B[FIR_ADD_ORDER_TWO + 2] = 0;
\tdat_out_B[FIR_ADD_ORDER_TWO + 3] = 0;
end
// 判定所有FIR_BASE模塊完成轉換
assign data_out_vld = (&output_vld_tmp[FIR_HALF_ORDER:1] == 1'b1) ? 1'b1 : 1'b0;
//最後一級流水線
always @(posedge clk or posedge rst) begin
\tif (rst) begin
\t\t// reset
\t\tdata_out \t<=\t0;
\tend
\telse if (data_out_vld) begin
\t\tdata_out \t<= dat_out_B[0] + dat_out_B[1] + dat_out_B[2] + dat_out_B[3];
\tend
end
generate
\tgenvar j;
\tfor (j = 1; j < FIR_HALF_ORDER; j = j + 1)
\tif (j <= FIR_ADD_ORDER_ONE)
\tbegin
\t//流水線相加 第一級
\t//注意j 的範圍是[1,FIR_HALF_ORDER]
\t//所以dat_out_A[j-1]
\t\talways @(posedge clk or posedge rst) begin
\t\t\tif (rst) begin
\t\t\t\t// reset
\t\t\t\tdat_out_A[j-1] <= 0;
\t\t\tend
\t\t\telse begin
\t\t\t\tdat_out_A[j-1] <= dat_out_reg[4*j-3] + dat_out_reg[4*j-2] + dat_out_reg[4*j-1] + dat_out_reg[4*j];
\t\t\tend
\t\tend
\tend

if (j <= FIR_ADD_ORDER_TWO)
\tbegin
\t// 流水線相加 第二級
\t\talways @(posedge clk or posedge rst) begin
\t\t\tif (rst) begin
\t\t\t\t// reset
\t\t\t\tdat_out_B[j-1] <= 0;
\t\t\tend
\t\t\telse begin
\t\t\t\tdat_out_B[j-1] <= dat_out_A[4*j - 4] + dat_out_A[4*j- 3] + dat_out_A[4*j - 2] + dat_out_A[4*j - 1];
\t\t\tend
\t\tend
\tend
end\t
endgenerate/<code>

這裡第一級,第二級流水線的循環次數採用ceil(N/4)的計算方式,也就是取比N/4大的最小整數。比如5/4 = 1.25,則ceil(1.25) = 2, 而ceil(1) = 1;

定義每級寄存器組時,會多定義4個寄存器組。並且這些寄存器永遠為0,這樣做的原因以第一級流水線相加舉例:

看第一級流水線,假定FIR_ORDER為70,FIR_HALF_ORDER為35,FIR_ADD_ORDER_ONE為9,當j為9時,dat_out_A[8] <= dat_out_reg[33] + dat_out_reg[34] + dat_out_reg[35] + dat_out_reg[36];

而我們在前面設計中正好定義了dat_out_reg[36],並且它永遠為0,不影響最終結果。

可以看到,第一級,第二級流水線使用generate for, if語句。如果if條件成立,if內部的電路會被描述。最後71個數據經過三級流水線相加,結果輸出。

如果想要提升FIR的最高運行頻率,可以把流水線級數增加,每級流水線相加改為2個或者3個。

這個結構只適合FIR階數為偶數的情況,由於最近比較忙,沒有做更大的兼容性。

仿真結果與資源佔用對比

(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


VCS仿真結果


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR濾波效果,Python


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR濾波效果,Verilog


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR_IMPLE的資源佔用 Quartus II 13.1


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR IP核資源佔用,參數相同情況下,Quartus II 13.1


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR_IMPLE的Fmax Quartus II 13.1


(探討濾波器)2. 手把手用Verilog實現FIR濾波器,非IP核


FIR IP核,Fmax, Quartus II 13.1


提升建議:

1. 提升最高運行速率,可以增多流水線操作

2. 可以修改部分代碼適配階數為奇數的情況

歡迎關注留言點贊收藏我,一同探討FPGA/電子/硬件/軟件。關於本文所用的資源會在評論區給出


分享到:


相關文章: