今回は、これらの問題を解決するものとして近年急速に普及してきた、 言語(Hardware Description Language; HDL)による論理回路設計を 行ってみます。 この実験では、特にVerilogHDLというHDL言語を用いることにします。
// sample module module sample(SW, LED); input [1:0] SW; output [1:0] LED; assign LED[0] = SW[0]; assign LED[1] = SW[1]; endmoduleこの例では、"sample"という名前の回路(module)を定義しています。 最初のmoduleの後に回路の名称を書き、そのあとの括弧内に、 その回路の入力と出力の名称を記述します。 ここでは、FPGAボード上にある、前回も用いたLED[0]〜LED[1]と SW[0]〜SW[1]を用いることを記述しています。
続いて、回路の記述に入りますが、まずは入力と出力の名称と、 それらが入力と出力のいずれかなのか、を記述しています。 この例では、SW[0]〜SW[1]を入力(input)、LED[0]〜LED[1]を出力(output)と 記述しています。
その後がいよいよ回路自体の記述になります。 この場合は、assgin(割り当て)文という記述方法を用いて、 出力であるLED[0]に、入力であるSW[0]を割り当て、すなわち 接続しています。 同様にLED[1]にはSW[1]を接続しています。
最後は、endmodule文で、回路の記述が終わることを示します。 ちなみに1行目のように、//から始まる行はコメントになります。
否定(NOT) | ~ |
論理積(AND) | & |
論理和(OR) | | |
排他的論理和(XOR) | ^ |
assign LED[0] = SW[0] | SW[1];このように、VerilogHDLでは、ほとんど論理式をそのまま書くだけで 回路として記述が完了することになります。 さらに、論理式の簡略化は、自動的に行ってくれますので、 カルノー図などとにらめっこをする必要は、ほとんどありません。
なお回路の中には、inputやoutputで定義される入出力以外にも、
たとえば次のように、回路の途中の「ノード(信号)」が
あることもあります。
このような、途中のノード(信号)は、wire文で宣言をすることができます。
たとえばこの回路中の、赤矢印の信号線に hoge という名前をつけて、
この回路全体を記述すると、次のようになります、
module sample(SW, LED); input [3:0] SW; output [3:0] LED; wire hoge; assign hoge = SW[0] & SW[1]; assign LED[0] = hoge & SW[2]; endmoduleassign文が2つあり、1つ目で左側のANDゲート、 2つ目で右側のANDゲートを記述しています。 なおこのassign文は、C言語などのプログラムにおける「代入」のように 見えますが、実際には、この書いてある順序で「値の代入」が起こるのではなく、 あくまでも「ANDゲートという回路がある」ことを記述しているのです。 つまり、1つ目のassign文で、まずはhogeの値が確定し、 それを使って2つ目のassign文でLED[0]の値を求める、というわけでは ありませんので、この2つのassign文の順序を入れ替えても、 まったく動作は変わりません。
例として、2ビットの入力(d[0], d[1])に応じて、4本の出力(q[0]〜q[3]) のうちの1つのみが1となるような「2 to 4デコーダ」をみてみましょう。 この回路の真理値表は次のようになります。
d[1] | d[0] | q[0] | q[1] | q[2] | q[3] | |
0 | 0 | 1 | 0 | 0 | 0 | |
0 | 1 | 0 | 1 | 0 | 0 | |
1 | 0 | 0 | 0 | 1 | 0 | |
1 | 1 | 0 | 0 | 0 | 1 |
module sample(SW, LED); input [1:0] SW; output [3:0] LED; wire [1:0] d; reg [3:0] q; assign d = {SW[1], SW[0]}; assign LED = q; always @(d) begin case (d) 2'b00 : q <= 4'b0001; 2'b01 : q <= 4'b0010; 2'b10 : q <= 4'b0100; 2'b11 : q <= 4'b1000; endcase end endmodule一気に複雑度が増しましたが、順を追ってみていきましょう。 最初のmodule文、input文、output文は、先ほどと同じです。
続くwire文では、回路の中で用いる「ノード」(信号)として"d"を 宣言しています。 しかも"wire [1:0]"と記述することで、実は「1番」から「0番」の 2本をまとめて、配列のように取り扱うことができます。 この例では、実際にはd[0]とd[1]を宣言していることになります。
続くreg文では、wire文と同様に、回路の中で用いるノードとして、 4ビット幅のqを宣言していますが、wire文と異なり、 その値が、後で出てくるalways文の中で変更(定義)することができる、 という性質を持ちます。 このwireとregの使い分けを説明するのはなかなか難しいので、 実例をいろいろ見るのが有効でしょう。 とりあえずは
続いて、wire文で宣言した2ビット幅のノードdに、SW[1]とSW[0]を 接続しています。 この例では、中括弧{}を用いていますが、これにより、 2つ以上の信号線をまとめて取り扱うことができます。 もちろん両辺の幅が同じ場合は、次のように記述してもかまいません。 具体的には、この記述は、実際には次のものと等価になります。
assign d = SW;続くassign文でも、出力であるLED[0]〜LED[3]に、ノードq[0]〜q[3]を 接続しています。
続いて、この回路の記述の中心部である、always文になります。 これは、英文のように記述を読むと意味がわかりやすいでしょう。 "always @(=at) d"、つまり、「dでいつも」という意味になりますが、 これは、「dが変化するときはいつも」と読み替えます。
その後のbegin〜endではさまれた部分で、 そのdが変化するときに、実際に行う動作を記述しています。 ここでは、case文を用いて、dの値に応じて、場合分けをしています。 具体的には、dが2'b00(「2桁の2進数(binary)の00」 の意味)のときには、qに4'b0001(「4桁の2進数の0001」)を 代入する、と定義しています。 (4'b0001は、最下位のみが1で、この1はq[3]ではなく、q[0]に代入されることに 注意しましょう。つまり最上位がq[3]、最下位がq[0]です。逆ではありません。) 同様に、dが2'b01、2'b10、2'b11の場合も、qに代入されるべき 値を定義しています。 最後に、end文でbegin文を閉じ、さらにendcase文でcase文を閉じています。
これにより、2桁の2進数であるdの値に応じて、 q[0]〜q[3]のいずれかのみが1となる回路、すなわち デコーダ(2 to 4 decoder)ができることになります。
module sample(SW, LED); input [1:0] SW; output [1:0] LED; inv i0(SW[0], LED[0]); inv i1(SW[1], LED[1]); endmodule module inv(a, x); input a; output x; assign x = ~a; endmoduleこの例では、後半で記述している"inv"という名前のモジュール(実体は インバータ)を、前半のモジュールsample内で用いています。 まず最初に、i0という名称でモジュールinvの機能を持つ回路を作り、 その入力(この場合は第1引数)と出力(この場合は第2引数)に、 それぞれSW[0]とLED[0]を接続しています。 同様に、もう1つのインバータi1を作って、その入力にSW[1]を、出力にLED[1]を 接続しています。
この回路をコンパイルすると、最上位モジュール(他から
呼び出されていないモジュール)が、全体回路として扱われることになります。
結果として、次のような回路が作られることになります。
なおここでは、回路を「呼び出す」という表現を使いましたが、
実際には、C言語などの関数を呼び出して値の代入を実行する、
というわけではなく、この回路図のように、
「インバータが2つ作られる」(「インバータが2つあることを記述している」)
ことに注意しましょう。
このような回路の呼び出しで接続されるノードは、必ずwire型を 用います。 たとえば先ほどの2つのANDゲートからなる回路(途中ノードはhoge)は 次のように書くこともできます。
module sample(SW, LED); input [3:0] SW; output [3:0] LED; wire hoge; and_2 i0(SW[0], SW[1], hoge); and_2 i1(hoge, SW[2], LED[0]); endmodule module and_2(a, b, x); input a, b; output x; assign x = a & b; endmoduleなおこのような回路の呼び出しでも、実際には「ANDゲートが2つ作られる」 (「ANDゲートが2つある回路を記述している」)わけですから、 2つのANDゲートを呼び出す記述の順序を逆にしても、 まったく同じ回路が作られます。 またこれは、あくまでも「回路を作る」記述ですので、 先ほどのalways文の中で、次のように使うこともできません。
always @(d) begin case (d) 2'b00 : begin q <= 4'b0000; and_2 i0(d[0], d[1], hoge); end ...つまり、あくまでも「回路(この例ではi0という名前のand_2)を作る」 ことを書くのは1回だけしかできず、それはalways文の外で書くべきもので、 always文などの中で、「ある条件のときだけ回路作る」ことはできません。
module sample(SW, LED); input [3:0] SW; output [3:0] LED; reg [3:0] q; assign LED = q; always @(negedge SW[0] or negedge SW[1]) begin if (SW[0] == 1'b0) begin q <= 4'b0000; end else begin q <= q + 1; end end endmoduleこの例では、always文の括弧内に、negedgeという書いてあります。 これは、信号の立ち下がり(negative edge)を示すもので、 "negedge SW0"とかくと、「信号SW0の立ち下がり」という意味になります。 この例では、2つの条件を"or"で並べていますので、 「SW[0]の立ち下がり、あるいはSW[1]の立ち下がり」のときに、 always文内の記述の動作が行われることになります。
その動作は、if文で2つの場合に分かれていて、 「SW[0]が0のとき」(つまりSW[0]の立ち下がり、のとき)は、 qに4桁の0を代入しています。 この代入は、「<=」という演算子を用いていますが、 これは、とりあえずは「フリップフロップが入る回路のとき」に 使うと理解しておいてください。 つまりSW[0]の立ち下がり、すなわちSW[0]を押したときに、 qが0にリセットされます。
それ以外(else)、すなわち、もう1つの条件である 「SW[1]が0のとき」(つまりSW[1]の立ち下がり、のとき)は、 qに1を加えたものを、次のqに代入しています。 すなわちSW[1]の立ち下がり、すなわちSW[1]を押すたびに、 qの値は1ずつふえていく、つまりカウンタとして動作をすることになります。
また余裕があれば、前回と同様に、チャタリング防止回路を追加してみましょう。 これは、チャタリング防止回路を別のmoduleとして記述し、 最上位モジュール(sample)から呼び出す、という形式をとると 見やすい記述となるでしょう。
module sample(SW, LED); input [3:0] SW; output [3:0] LED; wire co; counter10 i0(SW[0], LED, co); endmodule module counter10(ck, q, co); input ck; output [3:0] q; // counter output output co; // carry out reg [3:0] q; reg co; always @(posedge ck) begin if (q == 9) begin q <= 0; co <= 1; end else begin q <= q + 1; co <= 0; end end endmodule後半でcounter10という名称の回路を記述し、それを前半の全体回路である sampleで呼び出して使っています。 ここでcoという、LEDなどには接続されていない信号線がありますが、 これはあとで使いますので、とりあえずいまは無視しておきましょう。
このcounter10では、クロック信号ckの立ち上がりごとに
if文を使って、qの値に応じて、次にqをどのような値にするかを
定義しています。
具体的には、q(4ビットの数)が9であれば、qの値を0に更新しますが、
それ以外のときは、次にはqの値をq+1に更新するように
記述しています。
これにより、qの値は、ckの立ち上がりごとに、
0→1→2→・・・→8→9→0→1→・・・
というように変わっていくことになります。
このためには、4ビットの2進数を7セグメントの点灯パターンに 変換する、7セグメントデコーダ回路seg7_decを作る必要があります。 この回路をmoduleとして記述しておき、全体回路のモジュールである sampleから、counter10とともに呼び出す形にすると、 見通しのよい回路の記述になるでしょう。
例えば先ほどの10進カウンタの桁上がり出力coは、 クロックckの10周期ごとに1クロック分だけ1となります。 つまり、coの周波数は、ckの周波数の1/10、ということになります。 このように、クロック信号の周波数を、カウンタ等を用いて 低くすることを分周と呼びます。
まずは全体の構成を次のように整理しておきましょう。
まず7セグメントLEDの各桁をダイナミック駆動で順次点灯させるために、
4進カウンタcounter4を用いることにします。
この出力qs[1:0]は、(1)表示するべき7セグメントLEDの桁の選択SA[3:0]、 (2)その7セグメントLEDに表示するべき値として、該当する桁のカウンタの 出力を選択するselect4、の2つに使われます。
7セグメントデコーダdecode_7segは、select4によって 選ばれたカウンタの値(q0〜q3のいずれか)に応じて、 7セグメントLEDの点灯パターンを作り、数字を表示します。
全体のカウンタは4桁の10進カウンタですから、 10進カウンタcounter10を4個、呼び出して接続します。 なお、各桁の接続方法には、 カウンタ用クロックがすべての桁のカウンタに入る「同期式」(この図)と、 各桁のカウンタのクロックを、前の桁の桁上げ信号coから与える 「非同期式」があります。 今回は、構成がシンプルな「非同期式」で十分ですが、 前の桁のcoから、次の桁のクロックをどのように作成すればよいか、 十分考えましょう。
なおダイナミック駆動用クロックはCLK6またはCLK30を 適当に分周して作成することとし、 その周波数は1kHz程度がよいでしょう。 (あまり周波数が低いとチラツキが目立ちます) またカウンタ用クロックは、同じくCLK6またはCLK30を適当に分周して作成するか、 あるいはスイッチを用いるとよいでしょう。
どこから手をつけたらよいか見当がつかない場合は、まずは適当なSGを定数で与え、 SAを順次切り替える、という回路を作るところからはじめるとよいでしょう。 (この切り替えも、最初はスイッチSW[0]で切り替える、などでもよい) その後、そのSAにあわせて与えるべきSGを7セグメントデコーダから作るようにして、 最後に4桁十進カウンタをつなぐ、という順で進めるとよいでしょう。