了解了計數器的規則,這一篇我們要來實作 Johnson Counter 和 Ring Counter ,其實兩者的實作差異不大,希望大家在學會 Johnson Counter 後,可以自己嘗試設計出 Ring Counter。
實作之前,我們再來看一次電路圖!(圖片來源:Javatpoint)
大致統整一下步驟:
建構出 Johnson Counter 其實很容易,畢竟我們已經取得電路圖的樣貌,只需要將其接起來即可。但是在第一次實作時,我花了數個小時,原因是我還當時還未熟悉 Verilog 語法。我所遇到的問題以線路該用 reg 還是 wire 為大宗。因此接下來我們會細細的去講解程式碼內部。
module D_ff(
input clk, rst, load, D,
output reg Q
);
always @ (posedge clk) begin
if (rst) Q <= load;
else Q <= D;
end
endmodule
過去雖然有實作過 D 型正反器,但是我們多了一個調整,就是加了 rst
輸入訊號。rst 全名應為 reset ,意思是重新設定,這邊我們新增一個輸入叫 load ,可以客製化我們的初始數值。
module johnson(
input clk, rst,
input [3:0] load,
output [3:0] out
);
D_ff d3(clk, rst, load[3], ~out[0], out[3]);
D_ff d2(clk, rst, load[2], out[3], out[2]);
D_ff d1(clk, rst, load[1], out[2], out[1]);
D_ff d0(clk, rst, load[0], out[1], out[0]);
endmodule
程式碼簡單明瞭,就是按著電路圖實作的。
但是當時我其實對這份程式碼不是很理解,因為我覺得我們沒有將前一個數值儲存起來。不過仔細一想,數值已經被儲存在正反器中,這也是為什麼正反器的 Q 必須指定為 reg 的原因。況且在實體化時,我們總共只有實體化 4 個正反器,而不是每每進到 Johnson Counter 就實體化一次,因此上一次的結果還儲存在正反器中。
還有一個問題是,到底要用 wire 還是 reg ?你可以這麼想,這筆數值要不要被儲存,又或者說他需不需要一直被修改。上述的例子是,正反器的輸出需要使用 reg 。一旦確認了 reg ,那麼這個輸出所連接的就必須是 wire ,如果將 reg 連接 reg ,那麼就會跳出以下訊息:
cannot be driven by primitives or continuous assignment.
實際來測試吧!
module Johnson_tb;
wire [3:0] in;
reg [3:0] load;
reg clk, rst;
initial begin
$dumpfile("js.vcd");
$dumpvars(0, Johnson_tb);
load = 4'b1000;
#1 clk = 1; rst = 1;
$display("[%2d s] Data = %4b", $time, in);
for (integer i=0; i<5; i=i+1) begin
#1 clk = 0; rst = 0;
#1 clk = 1;
$display("[%2d s] Data = %4b", $time, in);
end
clk = 0;
load = 4'b1010; rst = 1;
#1 clk = 1;
for (integer i=0; i<5; i=i+1) begin
#1 clk = 0; rst = 0;
#1 clk = 1;
$display("[%2d s] Data = %4b", $time, in);
end
#3 $finish;
end
johnson js(clk, rst, load, in);
endmodule
這個 testbench 中,我們使用到一個新語法叫做 for loop ,寫過 C 語言的同學肯定對這個語法不陌生。因為我要產生時脈波動,使用 for loop 我認為會相對好掌握。
讓大家想一下,在進入迴圈之前如果沒有將 rst 設為 1 ,會發生什麼事?如果 clk 設為 0 ,又會發生什麼事呢?
先從簡單的問題開始,在 DFF 的實作中,觸發功能的條件是 posedge clk
,如果沒有改變時脈,或是將它設為 0 ,後方的行為就形同虛設,無任何功用。那 rst 設為 1 帶來的影響其實就是要初始化計數器,讓他從指定初始值開始計數,這樣我們才能預測之後的行為。
看一下 testbench 的結果吧!先從 $display 的內容看起。
[ 1 s] Data = xxxx
[ 3 s] Data = 1000
[ 5 s] Data = 1100
[ 7 s] Data = 1110
[ 9 s] Data = 1111
[11 s] Data = 0111
[14 s] Data = 0000
[16 s] Data = 1000
[18 s] Data = 1100
[20 s] Data = 1110
[22 s] Data = 1111
確實就和我們理想結果一樣,因此大概可以確認計數器的正確性。
最後來看看波形圖吧!
大家可以比對上述的輸出資料和波形圖,檢查是否相同。
如果相同,就代表大家都成功時做出 Johnson Counter 了!
如果看懂電路圖,應該可以清楚發現 Johnson Counter 和 Ring Counter 的差異:一個 Not-Gate ! (圖片來源:Wikipedia)
大家可以試著實作看看這個計數器,做法和 Johnson Counter 一樣:
module ring(
input clk, rst,
input [3:0] load,
output [3:0] out
);
D_ff d3(clk, rst, load[3], out[0], out[3]);
D_ff d2(clk, rst, load[2], out[3], out[2]);
D_ff d1(clk, rst, load[1], out[2], out[1]);
D_ff d0(clk, rst, load[0], out[1], out[0]);
endmodule
module Ring_tb;
wire [3:0] in;
reg [3:0] load;
reg clk, rst;
initial begin
$dumpfile("ring.vcd");
$dumpvars(0, Ring_tb);
load = 4'b1000;
#1 clk = 1; rst = 1;
$display("[%2d s] Data = %4b", $time, in);
for (integer i=0; i<5; i=i+1) begin
#1 clk = 0; rst = 0;
#1 clk = 1;
$display("[%2d s] Data = %4b", $time, in);
end
clk = 0;
load = 4'b1010; rst = 1;
#1 clk = 1;
for (integer i=0; i<5; i=i+1) begin
#1 clk = 0; rst = 0;
#1 clk = 1;
$display("[%2d s] Data = %4b", $time, in);
end
#3 $finish;
end
ring r(clk, rst, load, in);
endmodule
這個測試檔案和 Johnson Counter 的測試檔案其實是相同的,除了 load 的數值不同。我想特別提一個地方,在兩個迴圈中間有一行 clk = 0
,如果不加上這一行會怎麼運作?我們有說到正反器的觸發是仰賴 posedge clk
,因此我們必須先製造一個低位的時脈,才有辦法產生一個 Rising Edge 。如果不加上這一行,後方的 reset 是不會有反應的。這件事情我們無法從 $display 的輸出看出來,必須要透過波形圖才又辦法得知。因此我們要善用波形圖,否則許多錯誤都無法被查找出。
[ 1 s] Data = xxxx
[ 3 s] Data = 1000
[ 5 s] Data = 0100
[ 7 s] Data = 0010
[ 9 s] Data = 0001
[11 s] Data = 1000
[14 s] Data = 1010
[16 s] Data = 0101
[18 s] Data = 1010
[20 s] Data = 0101
[22 s] Data = 1010