owned this note
owned this note
Published
Linked with GitHub
# 8. Verilog 避免 Latch
[參考網址:Verilog 中文詳細教程](https://www.runoob.com/w3cnote/verilog-tutorial.html)
###### tags: `verilog` `IC 設計`
---
[toc]
---
關鍵詞:觸發器,鎖存器
Latch 的含義 鎖存器(Latch),是電平觸發的存儲單元,數據存儲的動作取決於輸入時鐘(或者使能)信號的電平值。僅當鎖存器處於使能狀態時,輸出才會隨著數據輸入發生變化。 當電平信號無效時,輸出信號隨輸入信號變化,就像通過了緩衝器;當電平有效時,輸出信號被鎖存。激勵信號的任何變化,都將直接引起鎖存器輸出狀態的改變,很有可能會因為瞬態特性不穩定而產生振盪現象。 鎖存器示意圖如下:

觸發器(flip-flop),是邊沿敏感的存儲單元,數據存儲的動作(狀態轉換)由某一信號的上升沿或者下降沿進行同步的(限制存儲單元狀態轉換在一個很短的時間內)。 觸發器示意圖如下:

寄存器(register),在 Verilog 中用來暫時存放參與運算的數據和運算結果的變量。一個變量聲明為寄存器時,它既可以被綜合成觸發器,也可能被綜合成 Latch,甚至是 wire 型變量。但是大多數情況下我們希望它被綜合成觸發器,但是有時候由於代碼書寫問題,它會被綜合成不期望的 Latch 結構。
### :pushpin: Latch 的主要危害 (需要避免)
1)輸入狀態可能多次變化,容易產生毛刺,增加了下一級電路的不確定性;
2)在大部分 FPGA 的資源中,可能需要比觸發器更多的資源去實現 Latch 結構;
3)鎖存器的出現使得靜態時序分析變得更加複雜。
Latch 多用於門控時鐘(clock gating)的控制,一般設計時,我們應當避免 Latch 的產生。
### :pushpin: if 結構不完整
組合邏輯中,不完整的 if - else 結構,會產生 latch。 例如下面的模型,if 語句中缺少 else 結構,系統默認 else 的分支下寄存器 q 的值保持不變,即具有存儲數據的功能,所以寄存器 q 會被綜合成 latch 結構。
```verilog=
module module1_latch1(
input data,
input en ,
output reg q) ;
always @(*) begin
if (en) q = data ;
end
endmodule
```
避免此類 latch 的方法主要有 2 種,一種是補全 if-else 結構,或者對信號賦初值。 例如,上面模型中的always語句,可以改為以下兩種形式:
```verilog=
// 補全條件分支結構
always @(*) begin
if (en) q = data ;
else q = 1'b0 ;
end
//赋初值
always @(*) begin
q = 1'b0 ;
if (en) q = data ; //如果en有效,改寫q的值,否則q會保持為0
end
```
但是在時序邏輯中,不完整的 if - else 結構,不會產生 latch,例如下面模型。 這是因為,q 寄存器具有存儲功能,且其值在時鐘的邊沿下才會改變,這正是觸發器的特性。
```verilog=
module module1_ff(
input clk ,
input data,
input en ,
output reg q) ;
always @(posedge clk) begin
if (en) q <= data ;
end
endmodule
```
在組合邏輯中,當條件語句中有很多條賦值語句時,每個分支條件下賦值語句的不完整也是會產生 latch。
其實對每個信號的邏輯拆分來看,這也相當於是 if-else 結構不完整,相關寄存器信號缺少在其他條件下的賦值行為。例如:
```verilog=
module module1_latch11(
input data1,
input data2,
input en ,
output reg q1 ,
output reg q2) ;
always @(*) begin
if (en) q1 = data1 ;
else q2 = data2 ;
end
endmodule
```
這種情況也可以通過補充完整賦值語句或賦初值來避免 latch。例如:
```verilog=
always @(*) begin
//q1 = 0; q2 = 0 ; //或在这里对 q1/q2 赋初值
if (en) begin
q1 = data1 ;
q2 = 1'b0 ;
end
else begin
q1 = 1'b0 ;
q2 = data2 ;
end
end
```
### :pushpin: case 結構不完整
case 語句產生 Latch 的原理幾乎和 if 語句一致。在組合邏輯中,當 case 選項列表不全且沒有加 default 關鍵字,或有多個賦值語句不完整時,也會產生 Latch。例如:
```verilog=
module module1_latch2(
input data1,
input data2,
input [1:0] sel ,
output reg q ) ;
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
endcase
end
endmodule
```
當然,消除此種 latch 的方法也是 2 種,將 case 選項列表補充完整,或對信號賦初值。 補充完整 case 選項列表時,可以羅列所有的選項結果,也可以用 default 關鍵字來代替其他選項結果。 例如,上述 always 語句有以下 2 種修改方式。
```verilog=
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
default: q = 1'b0 ;
endcase
end
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
2'b10, 2'b11 :
q = 1'b0 ;
endcase
end
```
### :pushpin: 原信號賦值或判斷
在組合邏輯中,如果一個信號的賦值源頭有其信號本身,或者**判斷條件中有其信號本身的邏輯**,則也會產生 latch。因為此時信號也需要具有存儲功能,但是沒有時鐘驅動。此類問題在 if 語句、case 語句、問號表達式中都可能出現,例如:
```verilog=
//signal itself as a part of condition
reg a, b ;
always @(*) begin
if (a & b) a = 1'b1 ; //a -> latch
else a = 1'b0 ;
end
//signal itself are the assigment source
reg c;
wire [1:0] sel ;
always @(*) begin
case(sel)
2'b00: c = c ; //c -> latch
2'b01: c = 1'b1 ;
default: c = 1'b0 ;
endcase
end
//signal itself as a part of condition in "? expression"
wire d, sel2;
assign d = (sel2 && d) ? 1'b0 : 1'b1 ; //d -> latch
```
避免此類 Latch 的方法,就只有一種,即在組合邏輯中**避免這種寫法,信號不要給信號自己賦值**,**且不要用賦值信號本身參與判斷條件邏輯**。 例如,如果不要求立刻輸出,可以將信號進行一個時鐘週期的延時再進行相關邏輯的組合。上述第一個產生 Latch 的代碼可以描述為:
```verilog=
reg a, b ;
reg a_r ;
always (@posedge clk)
a_r <= a ;
always @(*) begin
if (a_r & b) a = 1'b1 ; //there is no latch
else a = 1'b0 ;
end
```
### :pushpin: 敏感信號列表不完整
如果組合邏輯中 always@() 塊內敏感列表沒有列全,該觸發的時候沒有觸發,那麼相關寄存器還是會保存之前的輸出結果,因而會生成鎖存器。 這種情況,把敏感信號補全或者直接用 always@(*) 即可消除 latch。
### :pushpin: 小結
總之,為避免 latch 的產生,在組合邏輯中,需要注意以下幾點:
1)if-else 或 case 語句,結構一定要完整
2)不要將賦值信號放在賦值源頭,或條件判斷中
3)敏感信號列表建議多用 always@(*)
# 6.6 Verilog 仿真激勵
關鍵詞:testbench,仿真,文件讀寫
- Verilog 代碼設計完成後,還需要進行重要的步驟,即邏輯功能仿真。仿真激勵文件稱之為 testbench,放在各設計模塊的頂層,以便對模塊進行系統性的例化調用進行仿真。
- 毫不誇張的說,對於稍微複雜的 Verilog 設計,如果不進行仿真,即便是經驗豐富的老手,99.9999% 以上的設計都不會正常的工作。不能說仿真比設計更加的重要,但是一般來說,仿真花費的時間會比設計花費的時間要多。有時候,考慮到各種應用場景,testbench 的編寫也會比 Verilog 設計更加的複雜。所以,數字電路行業會具體劃分設計工程師和驗證工程師。
- 下面,對 testbench 做一個簡單的學習。
### :pushpin: testbench 結構劃分
testbench 一般結構如下:

其實 testbench 最基本的結構包括信號聲明、激勵和模塊例化。 根據設計的複雜度,需要引入時鐘和復位部分。當然更為複雜的設計,激勵部分也會更加複雜。根據自己的驗證需求,選擇是否需要自校驗和停止仿真部分。 當然,復位和時鐘產生部分,也可以看做激勵,所以它們都可以在一個語句塊中實現。也可以拿自校驗的結果,作為結束仿真的條件。 實際仿真時,可以根據自己的個人習慣來編寫 testbench,這裡只是做一份個人的總結。
### :pushpin: testbench 仿真舉例
前面的章節中,已經寫過很多的 testbench。其實它們的結構也都大致相同。 下面,我們舉一個數據拼接的簡單例子,對 testbench 再做一個具體的分析。 一個 2bit 數據拼接成 8bit 數據的功能模塊描述如下:
```verilog=
module data_consolidation
(
input clk ,
input rstn ,
input [1:0] din , //data in
input din_en ,
output [7:0] dout ,
output dout_en //data out
);
// data shift and counter
reg [7:0] data_r ;
reg [1:0] state_cnt ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
state_cnt <= 'b0 ;
data_r <= 'b0 ;
end
else if (din_en) begin
state_cnt <= state_cnt + 1'b1 ; //数据计数
data_r <= {data_r[5:0], din} ; //数据拼接
end
else begin
state_cnt <= 'b0 ;
end
end
assign dout = data_r ;
// data output en
reg dout_en_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
dout_en_r <= 'b0 ;
end
//计数为 3 且第 4 个数据输入时,同步输出数据输出使能信号
else if (state_cnt == 2'd3 & din_en) begin
dout_en_r <= 1'b1 ;
end
else begin
dout_en_r <= 1'b0 ;
end
end
//这里不直接声明dout_en为reg变量,而是用相关寄存器对其进行assign赋值
assign dout_en = dout_en_r;
endmodule
```
對應的 testbench 描述如下,增加了文件讀寫的語句:
```verilog=
`timescale 1ns/1ps
//============== (1) ==================
//signals declaration
module test ;
reg clk;
reg rstn ;
reg [1:0] din ;
reg din_en ;
wire [7:0] dout ;
![Uploading file..._7e0oh9m34]()
wire dout_en ;
//============== (2) ==================
//clock generating
real CYCLE_200MHz = 5 ; //
always begin
clk = 0 ; #(CYCLE_200MHz/2) ;
clk = 1 ; #(CYCLE_200MHz/2) ;
end
//============== (3) ==================
//reset generating
initial begin
rstn = 1'b0 ;
#8 rstn = 1'b1 ;
end
//============== (4) ==================
//motivation
int fd_rd ;
reg [7:0] data_in_temp ; //for self check
reg [15:0] read_temp ; //8bit ascii data, 8bit \n
initial begin
din_en = 1'b0 ; //(4.1)
din = 'b0 ;
open_file("../tb/data_in.dat", "r", fd_rd); //(4.2)
wait (rstn) ; //(4.3)
# CYCLE_200MHz ;
//read data from file
while (! $feof(fd_rd) ) begin //(4.4)
@(negedge clk) ;
$fread(read_temp, fd_rd);
din = read_temp[9:8] ;
data_in_temp = {data_in_temp[5:0], din} ;
din_en = 1'b1 ;
end
//stop data
@(posedge clk) ; //(4.5)
#2 din_en = 1'b0 ;
end
//open task
task open_file;
input string file_dir_name ;
input string rw ;
output int fd ;
fd = $fopen(file_dir_name, rw);
if (! fd) begin
$display("--- iii --- Failed to open file: %s", file_dir_name);
end
else begin
$display("--- iii --- %s has been opened successfully.", file_dir_name);
end
endtask
//============== (5) ==================
//module instantiation
data_consolidation u_data_process
(
.clk (clk),
.rstn (rstn),
.din (din),
.din_en (din_en),
.dout (dout),
.dout_en (dout_en)
);
//============== (6) ==================
//auto check
reg [7:0] err_cnt ;
int fd_wr ;
initial begin
err_cnt = 'b0 ;
open_file("../tb/data_out.dat", "w", fd_wr);
forever begin
@(negedge clk) ;
if (dout_en) begin
$fdisplay(fd_wr, "%h", dout);
end
end
end
always @(posedge clk) begin
#1 ;
if (dout_en) begin
if (data_in_temp != dout) begin
err_cnt = err_cnt + 1'b1 ;
end
end
end
//============== (7) ==================
//simulation finish
always begin
#100;
if ($time >= 10000) begin
if (!err_cnt) begin
$display("-------------------------------------");
$display("Data process is OK!!!");
$display("-------------------------------------");
end
else begin
$display("-------------------------------------");
$display("Error occurs in data process!!!");
$display("-------------------------------------");
end
#1 ;
$finish ;
end
end
endmodule // test
```
仿真結果如下。由圖可知,數據整合功能的設計符合要求:

### :pushpin: testbench 具體分析
1)信號聲明 testbench 模塊聲明時,一般不需要聲明端口。因為激勵信號一般都在 testbench 模塊內部,沒有外部信號。 聲明的變量應該能全部對應被測試模塊的端口。當然,變量不一定要與被測試模塊端口名字一樣。但是被測試模塊輸入端對應的變量應該聲明為 reg 型,如 clk,rstn 等,輸出端對應的變量應該聲明為 wire 型,如 dout,dout_en。
2)時鐘生成 生成時鐘的方式有很多種,例如以下兩種生成方式也可以藉鑑。
```verilog=
initial clk = 0 ;
always #(CYCLE_200MHz/2) clk = ~clk;
initial begin
clk = 0 ;
forever begin
#(CYCLE_200MHz/2) clk = ~clk;
end
end
```
需要注意的是,利用取反方法產生時鐘時,一定要給 clk 寄存器賦初值。 利用參數的方法去指定時間延遲時,如果延時參數為浮點數,該參數不要聲明為 parameter 類型。例如實例中變量 CYCLE_200MHz 的值為 2.5。如果其變量類型為 parameter,最後生成的時鐘週期很可能就是 4ns。當然,timescale 的精度也需要提高,單位和精度不能一樣,否則小數部分的時間延遲賦值也將不起作用。
3)復位生成 復位邏輯比較簡單,一般賦初值為 0,再經過一段小延遲後,復位為 1 即可。 這里大多數的仿真都是用的低有效復位。
4)激勵部分 激勵部分該產生怎樣的輸入信號,是根據被測模塊的需要來設計的。 本次實例中:
- (4.1) 對被測模塊的輸入信號進行一個初始化,防止不確定值 X 的出現。激勵數據的產生,我們需要從數據文件內讀入。
- (4.2) 處利用一個 task 去打開一個文件,只要指定文件存在,就可以得到一個不為 0 的句柄信號 fp_rd。 fp_rd 指定了文件數據的起始地址。
- (4.3) 的操作是為了等待復位後,系統有一個安全穩定的可測試狀態。
- (4.4) 開始循環讀數據、給激勵。在時鐘下降沿送出數據,是為了被測試模塊能更好的在上升沿採樣數據。
利用系統任務 $fread ,通過句柄信號 fd_rd 將讀取的 16bit 數據變量送入到 read_temp 緩存。 輸入數據文件前幾個數據截圖如下。因為 $fread 只能讀取 2 進製文件,所以輸入文件的第一行對應的 ASCII 碼應該是 330a,所以我們想要得到文件裡的數據 3,應該取變量 read_temp 的第 9 到第 8bit 位的數據。

信號 data_in_temp 是對輸入數據信號的一個緊隨的整合,後面校驗模塊會以此為參考,來判斷仿真是否正常,模塊設計是否正確。
- (4.5) 選擇在時鐘上升沿延遲 2 個週期後停止輸入數據,是為了被測試模塊能夠正常的採樣到最後一個數據使能信號,並對數據進行正常的整合。
當數據量相對較少時,可以利用 Verilog 中的系統任務 $readmemh 來按行直接讀取 16 進制數據。保持文件 data_in.dat 內數據和格式不變,則該激勵部分可以描述為:
```verilog=
reg [1:0] data_mem [39:0] ;
reg [7:0] data_in_temp ; //for self check
integer k1 ;
initial begin
din_en = 1'b0 ;
din = 'b0 ;
$readmemh("../tb/data_in.dat", data_mem);
wait (rstn) ;
# CYCLE_200MHz ;
//read data from file
for(k1=0; k1<40; k1=k1+1) begin
@(negedge clk) ;
din = data_mem[k1] ;
data_in_temp = {data_in_temp[5:0], din} ;
din_en = 1'b1 ;
end
//stop data
@(posedge clk) ;
#2 din_en = 1'b0 ;
end
```
5)模塊例化 這裡利用 testbench 開始聲明的信號變量,對被測試模塊進行例化連接。
6)自校驗
如果設計比較簡單,完全可以通過輸入、輸出信號的波形來確定設計是否正確,此部分完全可以刪除。如果數據很多,有時候拿肉眼觀察並不能對設計的正確性進行一個有效判定。此時加入一個自校驗模塊,會大大增加仿真的效率。
實例中,我們會在數據輸出使能 dout_en 有效時,對輸出數據 dout 與參考數據 read_temp(激勵部分產生)做一個對比,並將對比結果置於信號 err_cnt 中。最後就可以通過觀察 err_cnt 信號是否為 0 來直觀的對設計的正確性進行判斷。 當然如實例中所示,我們也可以將數據寫入到對應文件中,利用其他方式做對比。
7)結束仿真
如果我們不加入結束仿真部分,仿真就會無限制的運行下去,波形太長有時候並不方便分析。 Verilog 中提供了系統任務 $finish 來停止仿真。 停止仿真之前,可以將自校驗的結果,通過系統任務 $display 在終端進行顯示。
### :pushpin: 文件讀寫選項
用於打開文件的系統任務 $fopen 格式如下:
```verilog=
fd = $fopen("<name_of_file>", "mode")
```
和 C 語言類似,打開方式的選項 "mode" 意義如下:

# 6.7 Verilog 流水線
關鍵詞:流水線,乘法器
硬件描述語言的一個突出優點就是指令執行的並行性。多條語句能夠在相同時鐘週期內並行處理多個信號數據。 但是當數據串行輸入時,指令執行的並行性並不能體現出其優勢。而且很多時候有些計算並不能在一個或兩個時鐘週期內執行完畢,如果每次輸入的串行數據都需要等待上一次計算執行完畢後才能開啟下一次的計算,那效率是相當低的。流水線就是解決多周期下串行數據計算效率低的問題。
### :pushpin: 流水線
流水線的基本思想是:把一個重複的過程分解為若干個子過程,每個子過程由專門的功能部件來實現。將多個處理過程在時間上錯開,依次通過各功能段,這樣每個子過程就可以與其他子過程並行進行。
假如一個洗衣店內洗衣服的過程分為 4 個階段:取衣、洗衣、烘乾、裝櫃。每個階段都需要半小時來完成,則洗一次衣服需要 2 小時。 考慮最差情況,洗衣店內只有一台洗衣機、一台烘乾機、一個衣櫃。如果每半小時送來一批要洗的衣服,每次等待上一批衣服洗完需要 2 小時,那麼洗完 4 批衣服需要的時間就是 8 小時。 圖示如下:

對這個洗衣店的裝備進行升級,一共引進 4 套洗衣服的裝備,工作人員也增加到 4 個,每個人負責一個洗衣階段。所以每批次的衣服,都能夠及時的被相同的人放入到不同的洗衣機內。由於時間上是錯開的,每批次的衣服都能被相同的人在不同的設備與時間段(半小時)內洗衣、烘乾和裝櫃。圖示如下。

可以看出,洗完 4 批衣服只需要 3 個半小時,效率明顯提高。 其實,在 2 小時後第一套洗衣裝備已經完成洗衣過程而處於空閒狀態,如果此時還有第 5 批衣服的送入,那麼第一套設備又可以開始工作。依次類推,只要衣服批次不停的輸入,4 台洗衣設備即可不間斷的完成對所有衣服的清洗過程。且除了第一批次洗衣時間需要 2 小時,後面每半小時都會有一批次衣服清洗完成。
衣服批次越多,節省的時間就越明顯。假如有 N 批次衣服,需要的時間為 (4+N) 個半小時。 當然,升級後洗衣流程也有缺點。設備和工作人員的增加導致了投入的成本增加,洗衣店內剩餘空間也被縮小,工作狀態看起來比較繁忙。
和洗衣服過程類似,數據的處理路徑也可以看作是一條生產線,路徑上的每個數字處理單元都可以看作是一個階段,會產生延時。 流水線設計就是將路徑系統的分割成一個個數字處理單元(階段),並在各個處理單元之間插入寄存器來暫存中間階段的數據。被分割的單元能夠按階段並行的執行,相互間沒有影響。所以最後流水線設計能夠提高數據的吞吐率,即提高數據的處理速度。
流水線設計的缺點就是,各個處理階段都需要增加寄存器保存中間計算狀態,而且多條指令並行執行,勢必會導致功耗增加。 下面,設計一個乘法器,並對是否採用流水線設計進行對比。
---
### :pushpin: 一般乘法器設計
前言 也許有人會問,直接用乘號 * 來完成 2 個數的相乘不是更快更簡單嗎? 如果你有這個疑問,說明你對硬件描述語言的認知還有所不足。就像之前所說,Verilog 描述的是硬件電路,直接用乘號完成相乘過程,編譯器在編譯的時候也會把這個乘法表達式映射成默認的乘法器,但其構造不得而知。
例如,在 FPGA 設計中,可以直接調用 IP 核來生成一個高性能的乘法器。在位寬較小的時候,一個週期內就可以輸出結果,位寬較大時也可以流水輸出。在能滿足要求的前提下,可以謹慎的用 * 或直接調用 IP 來完成乘法運算。 但乘法器 IP 也有很多的缺陷,例如位寬的限制,未知的時序等。尤其使用乘號,會為數字設計的不確定性埋下很大的隱瞞。 很多時候,常數的乘法都會用移位相加的形式實現,例如:
```verilog=
A = A<<1 ; //完成A * 2
A = (A<<1) + A ; //对应A * 3
A = (A<<3) + (A<<2) + (A<<1) + A ; //对应A * 15
```
用一個移位寄存器和一個加法器就能完成乘以 3 的操作。但是乘以 15 時就需要 3 個移位寄存器和 3 個加法器(當然乘以 15 可以用移位相減的方式)。 有時候數字電路在一個週期內並不能夠完成多個變量同時相加的操作。所以數字設計中,最保險的加法操作是同一時刻只對 2 個數據進行加法運算,最差設計是同一時刻對 4 個及以上的數據進行加法運算。
如果設計中有同時對 4 個數據進行加法運算的操作設計,那麼此部分設計就會有危險,可能導致時序不滿足。 此時,設計參數可配、時序可控的流水線式乘法器就顯得有必要了。
**設計原理**
和十進制乘法類似,計算 13 與 5 的相乘過程如下所示:

由此可知,被乘數按照乘數對應 bit 位進行移位累加,便可完成相乘的過程。 假設每個週期只能完成一次累加,那麼一次乘法計算時間最少的時鐘數恰好是乘數的位寬。所以建議,將位寬窄的數當做乘數,此時計算週期短。
**乘法器設計**
考慮每次乘法運算只能輸出一個結果(非流水線設計),設計代碼如下。
```verilog=
module mult_low
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input data_rdy , //数据输入使能
input [N-1:0] mult1, //被乘数
input [M-1:0] mult2, //乘数
output res_rdy , //数据输出使能
output [N+M-1:0] res //乘法结果
);
//calculate counter
reg [31:0] cnt ;
//乘法周期计数器
wire [31:0] cnt_temp = (cnt == M)? 'b0 : cnt + 1'b1 ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
cnt <= 'b0 ;
end
else if (data_rdy) begin //数据使能时开始计数
cnt <= cnt_temp ;
end
else if (cnt != 0 ) begin //防止输入使能端持续时间过短
cnt <= cnt_temp ;
end
else begin
cnt <= 'b0 ;
end
end
//multiply
reg [M-1:0] mult2_shift ;
reg [M+N-1:0] mult1_shift ;
reg [M+N-1:0] mult1_acc ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
mult2_shift <= 'b0 ;
mult1_shift <= 'b0 ;
mult1_acc <= 'b0 ;
end
else if (data_rdy && cnt=='b0) begin //初始化
mult1_shift <= {{(N){1'b0}}, mult1} << 1 ;
mult2_shift <= mult2 >> 1 ;
mult1_acc <= mult2[0] ? {{(N){1'b0}}, mult1} : 'b0 ;
end
else if (cnt != M) begin
mult1_shift <= mult1_shift << 1 ; //被乘数乘2
mult2_shift <= mult2_shift >> 1 ; //乘数右移,方便判断
//判断乘数对应为是否为1,为1则累加
mult1_acc <= mult2_shift[0] ? mult1_acc + mult1_shift : mult1_acc ;
end
else begin
mult2_shift <= 'b0 ;
mult1_shift <= 'b0 ;
mult1_acc <= 'b0 ;
end
end
//results
reg [M+N-1:0] res_r ;
reg res_rdy_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
res_r <= 'b0 ;
res_rdy_r <= 'b0 ;
end
else if (cnt == M) begin
res_r <= mult1_acc ; //乘法周期结束时输出结果
res_rdy_r <= 1'b1 ;
end
else begin
res_r <= 'b0 ;
res_rdy_r <= 'b0 ;
end
end
assign res_rdy = res_rdy_r;
assign res = res_r;
endmodule
```
testbench
```verilog=
`timescale 1ns/1ns
module test ;
parameter N = 8 ;
parameter M = 4 ;
reg clk, rstn;
//clock
always begin
clk = 0 ; #5 ;
clk = 1 ; #5 ;
end
//reset
initial begin
rstn = 1'b0 ;
#8 ; rstn = 1'b1 ;
end
//no pipeline
reg data_rdy_low ;
reg [N-1:0] mult1_low ;
reg [M-1:0] mult2_low ;
wire [M+N-1:0] res_low ;
wire res_rdy_low ;
//使用任务周期激励
task mult_data_in ;
input [M+N-1:0] mult1_task, mult2_task ;
begin
wait(!test.u_mult_low.res_rdy) ; //not output state
@(negedge clk ) ;
data_rdy_low = 1'b1 ;
mult1_low = mult1_task ;
mult2_low = mult2_task ;
@(negedge clk ) ;
data_rdy_low = 1'b0 ;
wait(test.u_mult_low.res_rdy) ; //test the output state
end
endtask
//driver
initial begin
#55 ;
mult_data_in(25, 5 ) ;
mult_data_in(16, 10 ) ;
mult_data_in(10, 4 ) ;
mult_data_in(15, 7) ;
mult_data_in(215, 9) ;
end
mult_low #(.N(N), .M(M))
u_mult_low
(
.clk (clk),
.rstn (rstn),
.data_rdy (data_rdy_low),
.mult1 (mult1_low),
.mult2 (mult2_low),
.res_rdy (res_rdy_low),
.res (res_low));
//simulation finish
initial begin
forever begin
#100;
if ($time >= 10000) $finish ;
end
end
endmodule // test
```
仿真結果如下。 由圖可知,輸入的 2 個數據在延遲 4 個週期後,得到了正確的相乘結果。算上中間送入數據的延遲時間,計算 4 次乘法大約需要 20 個時鐘週期。

### :pushpin: 流水線乘法器設計
下面對乘法執行過程的中間狀態進行保存,以便流水工作,設計代碼如下。 單次累加計算過程的代碼文件如下(mult_cell.v ):
```verilog=
module mult_cell
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input en,
input [M+N-1:0] mult1, //被乘数
input [M-1:0] mult2, //乘数
input [M+N-1:0] mult1_acci, //上次累加结果
output reg [M+N-1:0] mult1_o, //被乘数移位后保存值
output reg [M-1:0] mult2_shift, //乘数移位后保存值
output reg [N+M-1:0] mult1_acco, //当前累加结果
output reg rdy );
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
rdy <= 'b0 ;
mult1_o <= 'b0 ;
mult1_acco <= 'b0 ;
mult2_shift <= 'b0 ;
end
else if (en) begin
rdy <= 1'b1 ;
mult2_shift <= mult2 >> 1 ;
mult1_o <= mult1 << 1 ;
if (mult2[0]) begin
//乘数对应位为1则累加
mult1_acco <= mult1_acci + mult1 ;
end
else begin
mult1_acco <= mult1_acci ; //乘数对应位为1则保持
end
end
else begin
rdy <= 'b0 ;
mult1_o <= 'b0 ;
mult1_acco <= 'b0 ;
mult2_shift <= 'b0 ;
end
end
endmodule
```
**頂層例化**
多次模塊例化完成多次累加,代碼文件如下(mult_man.v ):
```verilog=
module mult_man
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input data_rdy ,
input [N-1:0] mult1,
input [M-1:0] mult2,
output res_rdy ,
output [N+M-1:0] res );
wire [N+M-1:0] mult1_t [M-1:0] ;
wire [M-1:0] mult2_t [M-1:0] ;
wire [N+M-1:0] mult1_acc_t [M-1:0] ;
wire [M-1:0] rdy_t ;
//第一次例化相当于初始化,不能用 generate 语句
mult_cell #(.N(N), .M(M))
u_mult_step0
(
.clk (clk),
.rstn (rstn),
.en (data_rdy),
.mult1 ({{(M){1'b0}}, mult1}),
.mult2 (mult2),
.mult1_acci ({(N+M){1'b0}}),
//output
.mult1_acco (mult1_acc_t[0]),
.mult2_shift (mult2_t[0]),
.mult1_o (mult1_t[0]),
.rdy (rdy_t[0]) );
//多次模块例化,用 generate 语句
genvar i ;
generate
for(i=1; i<=M-1; i=i+1) begin: mult_stepx
mult_cell #(.N(N), .M(M))
u_mult_step
(
.clk (clk),
.rstn (rstn),
.en (rdy_t[i-1]),
.mult1 (mult1_t[i-1]),
.mult2 (mult2_t[i-1]),
//上一次累加结果作为下一次累加输入
.mult1_acci (mult1_acc_t[i-1]),
//output
.mult1_acco (mult1_acc_t[i]),
.mult1_o (mult1_t[i]), //被乘数移位状态传递
.mult2_shift (mult2_t[i]), //乘数移位状态传递
.rdy (rdy_t[i]) );
end
endgenerate
assign res_rdy = rdy_t[M-1];
assign res = mult1_acc_t[M-1];
endmodule
```
testbench
將下述仿真描述添加到非流水乘法器設計例子的 testbench 中,即可得到流水式乘法運算的仿真結果。 2 路數據為不間斷串行輸入,且帶有自校驗模塊,可自動判斷乘法運算結果的正確性。
```verilog=
reg data_rdy ;
reg [N-1:0] mult1 ;
reg [M-1:0] mult2 ;
wire res_rdy ;
wire [N+M-1:0] res ;
//driver
initial begin
#55 ;
@(negedge clk ) ;
data_rdy = 1'b1 ;
mult1 = 25; mult2 = 5;
#10 ; mult1 = 16; mult2 = 10;
#10 ; mult1 = 10; mult2 = 4;
#10 ; mult1 = 15; mult2 = 7;
mult2 = 7; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 1; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 15; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 3; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 11; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 4; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 9; repeat(32) #10 mult1 = mult1 + 1 ;
end
//对输入数据进行移位,方便后续校验
reg [N-1:0] mult1_ref [M-1:0];
reg [M-1:0] mult2_ref [M-1:0];
always @(posedge clk) begin
mult1_ref[0] <= mult1 ;
mult2_ref[0] <= mult2 ;
end
genvar i ;
generate
for(i=1; i<=M-1; i=i+1) begin
always @(posedge clk) begin
mult1_ref[i] <= mult1_ref[i-1];
mult2_ref[i] <= mult2_ref[i-1];
end
end
endgenerate
//自校验
reg error_flag ;
always @(posedge clk) begin
# 1 ;
if (mult1_ref[M-1] * mult2_ref[M-1] != res && res_rdy) begin
error_flag <= 1'b1 ;
end
else begin
error_flag <= 1'b0 ;
end
end
//module instantiation
mult_man #(.N(N), .M(M))
u_mult
(
.clk (clk),
.rstn (rstn),
.data_rdy (data_rdy),
.mult1 (mult1),
.mult2 (mult2),
.res_rdy (res_rdy),
.res (res));
```
仿真結果
前幾十個時鐘週期的仿真結果如下。 由圖可知,仿真結果判斷信號 error_flag 一直為 0,表示乘法設計正確。 數據在時鐘驅動下不斷串行輸入,乘法輸出結果延遲了 4 個時鐘週期後,也源源不斷的在每個時鐘下無延時輸出,完成了流水線式的工作。

相對於一般不採用流水線的乘法器,乘法計算效率有了很大的改善。 但是,流水線式乘法器使用的寄存器資源也大約是之前不採用流水線式的 4 倍。 所以,一個數字設計,是否採用流水線設計,需要從資源和效率兩方面進行權衡。