在學習FPGA使用Verilog HDL語言編程時,開始遇到時序邏輯和組合邏輯時概念一看就明白,但是實際使用時還是不清楚到底要用哪個。現在用就一個例子來體會一下這兩者的區別。
首先先看組合邏輯和時序邏輯的定義。
看完以後還是感覺雲裏霧裏搞不清楚,那麼就不用管它了,直接用例子來說明。
在這裏設計一個0---9計數器,clk爲輸入時鐘信號,cin爲計數有效信號,也就是說只有當cin爲高電平時,計數器才計數一次。cout爲計數進位信號,當計數值爲9時,計數值再加1的話,就輸出一個進位信號,同時計數值清零。q輸出計數值,輸出值的範圍是0--9。這個計數器類似於數碼管顯示數字時每一個數碼管的顯示範圍,每個數碼管顯示範圍爲0--9,當低位滿10之後,向前一位進1,同時低位清零。
下面開始編寫代碼
首先定義輸入輸出端口
module bcd_counter(
input clk, //時鐘
input rst_n, //復位
input cin, //計數使能
output cout, //進位輸出
output [3:0] q //計數輸出
);
endmodule
輸入信號有三個 時鐘 clk、復位 rst_n、計數使能 cin。
輸出信號有兩個 進位輸出cout、計數值輸出q,q的計數範圍是0--9,所以q設置爲4位計數器。
下面編寫計數代碼
reg [3:0] cnt;
//BCD碼計數
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cnt <= 4'd0;
else if(cin == 1'b1) begin //計數使能信號有效時 計數器加1
if(cnt < 4'd9)
cnt <= cnt + 1'b1;
else
cnt <= 4'd0;
end
else
cnt <= cnt;
end
cnt存儲計數值,復位後默認值爲0,每次當cin爲高電平時,計數值加1,當計數到9時,計數值清零。cin爲低電平時,計數值保持不變。這樣計數寄存器的值就在0-到9之間循環。
下面編寫進位代碼
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
當計數寄存器的值爲9,同時計數使能信號爲1時,說明已經計夠10次了,需要進位一次,這時cout輸出1。其餘情況下輸出爲0。
最後將寄存器的值連接到輸出端口上
assign q = cnt;
整體代碼如下
module bcd_counter(
input clk, //時鐘
input rst_n, //復位
input cin, //計數使能
output cout, //進位輸出
output [3:0] q //計數輸出
);
reg [3:0] cnt;
//BCD碼計數
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cnt <= 4'd0;
else if(cin == 1'b1) begin //計數使能信號有效時 計數器加1
if(cnt < 4'd9)
cnt <= cnt + 1'b1;
else
cnt <= 4'd0;
end
else
cnt <= cnt;
end
//進位信號輸出
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
assign q = cnt; //輸出計數值
endmodule
代碼的功能比較簡單,一個always語句產生計數信號,一個always語句產生進位信號。
下面編寫測試文件
`timescale 1ns/1ns
module bcd_counter_tb;
parameter T = 20;
reg sys_clk;
reg sys_rst_n;
reg cin;
wire cout;
wire [3:0] q;
bcd_counter bcd_counter0(
.clk (sys_clk), //時鐘
.rst_n (sys_rst_n), //復位
.cin (cin), //計數使能
.cout (cout), //進位輸出
.q (q) //計數輸出
);
initial begin
sys_rst_n = 1'b0;
sys_clk = 1'b1;
#200;
sys_rst_n = 1'b1;
repeat(100) begin
cin <= 1'b0; //輸出4個週期低電平
#(T * 4);
cin <= 1'b1; //輸出1個週期高電平
#(T);
end
cin <= 1'b0;
#(200 * T);
$stop;
end
always #(T/2) sys_clk = ~sys_clk;
endmodule
測試文件產生一個時鐘信號 sys_clk 和一個計數使能信號 cin,cin信號爲4個時鐘的低電平,然後1個時鐘的高電平.也是說沒5個時鐘週期計數器就會計數一次。
下來仿真一下,看看輸出波形。
放大波形看看cin和計數值關係
可以看到每個cin信號爲高時,計數值加1,當計數值爲9時,輸出cout信號輸出一個高脈衝。
再放大波形看看cout和cin的輸出時序
可以看到當計數值爲9時,cin信號出現高電平,cout延時一個時鐘週期才輸出的一個高電平。按照正常計數邏輯來說,當低位9再加1時,低位變爲0,同時向高位進1,計數和進位是同時發生的。而這個卻出現了計數和進位不同步的情況。出現這種情況是爲什麼呢?那就要分析分析代碼中的進位信號。
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
進位的always語句執行塊,是在時鐘的上升沿或者復位信號的下降沿纔會進入。
在波形中可以看出,在藍色光標處,時鐘的上升沿cin信號到來,計數值加1,此時計數值爲9,同時cin信號爲1,此時cout信號應該要輸出高電平了,但是由於cout是由時序邏輯控制的,只有在時鐘的上升邊沿always語句纔會執行,所以只有等到下一個時鐘上升沿cout纔有機會輸出高電平。
說明cout信號通過時序邏輯來實現的話,會有延時,不符合設計的實時變化要求,那麼就把cout的實現改成用組合邏輯來實現,看看是什麼效果。
在代碼中將cout改爲組合邏輯
/*
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
*/
將時序邏輯改爲組合邏輯
assign cout = (cnt == 4'd9 && cin == 1'b1) ? 1'b1 : 1'b0;
將cout改爲組合邏輯實現,當計數值cnt爲9,同時cin爲高電平時,cout值爲1,否則cout值爲0。
這樣當cin和cnt的值由任何變化時,cout值也跟着會變化,不受到時鐘上升沿的影響。
注意將cout由時序邏輯改爲組合邏輯時,要在初始化中將cout由寄存器類型改爲線網類型。
重新編譯代碼後,查看波形。
這時可以看到cout信號和cin信號會同時變爲高電平,不會有一個時鐘週期的延遲。
當計數值變爲9時,cin由低電平變爲高電平繼續計數時,計數值清0,同時進位輸出也變爲高電平。符合設計的要求。
通過上面的例子可以看到,當輸出信號需要實時跟隨輸入信號變化時,就必須用組合邏輯來實現。其餘情況下用時序邏輯來實現。