順序回路とか
順序回路
組み合わせ回路の出力は過去の状態に影響されないのに対し、順序回路は過去の状態によって出力が変わる。always文を使うやつ。
D-FF
フリップフロップにはD,SR,JK,Tがあるが、D-FFが最も基本的なFFなので、これの動作を復習しておく。 クロックの立ち上がりエッジでデータラッチする場合はこんな感じ。
module dff(clock, in, out); input clock; input in; output out; reg q; always @(posedge clock) begin q <= in; end assign out = q; endmodule
clockの立ち上がりエッジ(posede)でinをqに代入する。always @(***)のかっこの中身がセンシティビティリストで、複数信号、例えばclockと非同期リセットrst_nを入れる場合は、always @(posedge clock or negedge rst_n)みたいに書く。
メモリの記述
メモリはレジスタの配列として定義ができて、
reg [7:0] ram [0:1023]
と書くと、8bit x 1024 = 1024byteのメモリになる。[0:1023]は要素数であることに注意。
試しに同期式のSRAMとテストベンチを書いてみる。ポートは、
ポート | bit幅 | I/O | 極性 | 機能 |
---|---|---|---|---|
clock | - | I | ↑ | クロック |
addr | 10 | I | - | アドレス |
din | 8 | I | - | データ入力 |
cs | - | I | L | チップセレクト。Low-active |
wen | - | I | H | ライトイネーブル |
dout | 8 | O | - | データ出力 |
動作は、
- 同期式
- チップセレクトはLow-active
- ライトイネーブルHでアクセスすると書き込み、Lで読み出し
- データ入力ポートとデータ出力ポートは別々に持つ
とすると、
module singleport_ram(clock, addr, din, cs, wen, dout); input clock; input [9:0] addr; input [7:0] din; input cs; // chip select, low-active input wen; // write enable output [7:0] dout; reg [7:0] mem [0:1023]; reg [7:0] tmp_data; always @(posedge clock) begin if(!cs & wen) begin // write mem[addr] <= din; end else if(!cs & !wen) begin // read tmp_data <= mem[addr]; end end assign dout = tmp_data; endmodule
sim結果はこんな感じ。最低限の動作しか確認していないけど。
always文を使った組み合わせ回路とラッチ回路
組み合わせ回路はassignやfunctionで記述するが、always文を使って書くことができる。よく見る記述がalways @(*) begin ~~ end。XORをalwaysを使って書くと、
module xor_always(in0, in1, out); input in0; input in1; output out; reg tmp; always @(*) begin if(in0) tmp <= ~in1; else tmp <= in1; end assign out = tmp; endmodule
のように書ける。一見XORらしくないが、ちゃんとXORになっている。always @(in0 or in1)としても同じ。
さて、このコードでelse節をコメントアウトした、以下のRTLの動作を考える。
module xor_always(in0, in1, out); input in0; input in1; output out; reg tmp; always @(*) begin if(in0) tmp <= ~in1; // else tmp <= in1; end assign out = tmp; endmodule
in0=1の時の動作が規定されていない。真理値表を書くと、
in0 | in1 | out |
---|---|---|
0 | 0 | x |
0 | 1 | x |
1 | 0 | 1 |
1 | 1 | 0 |
こんな具合に、in0=1の時はx(不定)になる。simをやってみると、
in0 | in1 | out | 備考 |
---|---|---|---|
0 | 0 | x | |
0 | 1 | x | |
1 | 0 | 1 | |
1 | 1 | 0 | |
0 | 1 | 0 | 前回の出力値が保持されている |
0 | 0 | 0 | 前回の出力値が保持されている |
というように、in=0の時、前回の値を保持するような回路になってしまっていて、これをラッチという。実際にschemaを書かせてみると、以下の通り。
すべての入力の組み合わせにおける動作を定義しないと(今回のケースではin0=0が未定義)ラッチが生成されてしまう。例えばこのデザインをVivadoで合成すると以下のように警告が出る。
[Synth 8-327] inferring latch for variable 'tmp_reg' ["sandbox/verilog/xor/xor.v":9]
ブロッキング代入とノンブロッキング代入
"="がブロッキング代入、"<="がノンブロッキング代入。ブロッキング代入は記述順序に従って上からシーケンシャルに代入するプログラム的動作で、ノンブロッキング代入は右辺をすべて評価してから、左辺に代入する…という文章をよく見かける。で、あまり細かい意味を考えずに、ノンブロッキングで書いてきたが、改めて以下のようなコードで動作を確認してみる。
module tb(); reg a; reg b; initial begin // initialize a = 1; b = 0; #10 // blocking a = b; b = a; #10 $display("blocking: a = %d, b = %d", a, b); #10 // initialize a = 1; b = 0; #10 // non-blocking a <= b; b <= a; #10 $display("nonblocking: a = %d, b = %d", a, b); #30 $finish; end endmodule
結果は以下、
blocking: a = 0, b = 0 nonblocking: a = 0, b = 1
a,bをスワップする意図のコードだがブロッキング代入の場合、a=bを実行した時点で、a=0, b=0になってしまうので、その後にb=aを実行しても結果がa=0, b=0となってしまう。Cでスワップコードを書く場合は、ふつう退避用の変数を確保するが、同じことをしないといけない。
他方、ノンブロッキング代入の場合は一時変数を用意せずともスワップが成立している。a <= bでaにはb=0を代入する準備をして、さらにb <= aでbにa=1を代入する準備をして、それから時刻を進める(代入を行う)ため、スワップができる。右辺をすべて評価してから代入、という意味はこれ。
もう一つ、シフトレジスタの例を見ておく。2段のシフトレジスタ回路で、それぞれのFF出力を出力端子に接続している。b***はブロッキング代入の2段シフトレジスタ、nb***はノンブロッキング代入の2段シフトレジスタのつもり。
module shift(clock, in, b_out0, b_out1, nb_out0, nb_out1); input clock; input [1:0] in; output [1:0] b_out0, b_out1; output [1:0] nb_out0, nb_out1; reg [1:0] tmp_b0, tmp_b1; reg [1:0] tmp_nb0, tmp_nb1; always @(posedge clock) begin tmp_b0 = in; tmp_b1 = tmp_b0; tmp_nb0 <= in; tmp_nb1 <= tmp_nb0; end assign b_out0 = tmp_b0; assign b_out1 = tmp_b1; assign nb_out0 = tmp_nb0; assign nb_out1 = tmp_nb1; endmodule
sim結果は以下で、ノンブロッキング代入のほうは正しくシフトレジスタになっているが、ブロッキング代入のほうはb_out0=b_out1になっている。
schemaは以下の通りで、b_out0, b_out1ともに1段FFをたたいた結果が接続されていることがわかる。