FPGAを用いた論理回路設計(その2)

前回は、自由に論理回路を設計し、動作させることができる FPGAを用いて、いくつかの論理回路の設計を行いました。 ただ、前回行ったような回路図を用いた論理回路の設計は、 どうしても煩雑になり、特に回路の規模が大きくなると、 間違いを起こしやすくなります。 また「論理回路の構造」をそのまま回路図として描いているため、 「作りたい機能」を見通しにくくなってしまいます。

今回は、これらの問題を解決するものとして近年急速に普及してきた、 言語(Hardware Description Language; HDL)による論理回路設計を 行ってみます。 この実験では、特にVerilogHDLというHDL言語を用いることにします。

組合せ論理回路の構造記述

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行目のように、//から始まる行はコメントになります。

VerilogHDLの演算子

このassign文では、ただ接続する以外に、論理和・論理積などの 論理演算をふくめた、論理式を記述することができます。 論理演算は、それぞれ次の記号を用います。
否定(NOT)~
論理積(AND)&
論理和(OR)|
排他的論理和(XOR)^
例えば、SW[0]とSW[1]の値の論理和をとってLED[0]に接続したい場合は、 次のように記述をすることになります。
  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];
endmodule
assign文が2つあり、1つ目で左側のANDゲート、 2つ目で右側のANDゲートを記述しています。 なおこのassign文は、C言語などのプログラムにおける「代入」のように 見えますが、実際には、この書いてある順序で「値の代入」が起こるのではなく、 あくまでも「ANDゲートという回路がある」ことを記述しているのです。 つまり、1つ目のassign文で、まずはhogeの値が確定し、 それを使って2つ目のassign文でLED[0]の値を求める、というわけでは ありませんので、この2つのassign文の順序を入れ替えても、 まったく動作は変わりません。

QuartusIIでHDLを用いた論理回路設計・実装

まずサンプルのQuartusIIプロジェクトファイル一式(sample_hdl.zip)を ここから取得して、作業用フォルダを 作成して、そこに展開します。 そしてsample.qpfをQuartusIIからプロジェクトして開きます。 ここまでは、前回の回路図入力の場合と同一ですが、 ここでプロジェクトを構成するファイルの中のsampleをダブルクリックすると、 sample.vが開きます。 つまりこのプロジェクトでは、設計対象の論理回路が VerilogHDLで記述されているわけです。 そこで、必要に応じて、sample.vの中身を書き換え、 その後は回路図入力の場合と同様に、コンパイル→書き込み、と進みます。

演習2-1

適当な機能を持つ組み合わせ論理回路をVerilogHDLで記述し、 その動作を確認してみましょう。

組合せ論理回路の動作記述

上で見てきたVerilogHDLでの論理回路の記述では、 例えば7セグメントデコーダを設計するのに、 いちいち論理式で記述する必要があり、あまり効率的とはいえません。 そこで、VerilogHDLには、もう少し抽象度の高いレベルで、 論理回路の機能を記述することができます。

例として、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]
001000
010100
100010
110001

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)ができることになります。

演習2-2

適当なデコーダ回路を、VerilogHDLを用いて記述し、その動作を 確認してみましょう。 例えば7セグメントデコーダを設計し、入力としてSW[0]〜SW[3]を 用いるとよいでしょう。

回路どうしの接続

VerilogHDLでは、回路をmoduleとして記述しますが、 moduleどうし、つまり回路どうしを接続することもできます。 次の例をみてみましょう。
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文などの中で、「ある条件のときだけ回路作る」ことはできません。

順序回路

順序回路も、VerilogHDLで記述することができます。 これも、まずは例を見ていくことにしましょう。 これは4桁の2進カウンタをVerilogHDLで記述した例です。
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ずつふえていく、つまりカウンタとして動作をすることになります。

演習2-3

4ビットのカウンタをVerilogHDLで記述し、動作を確認してみましょう。

また余裕があれば、前回と同様に、チャタリング防止回路を追加してみましょう。 これは、チャタリング防止回路を別のmoduleとして記述し、 最上位モジュール(sample)から呼び出す、という形式をとると 見やすい記述となるでしょう。

10進カウンタ

このalways文をうまく使うと、任意の数字までカウントするカウンタを 容易に作ることができます。 例えば次の回路を見てみましょう。
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→・・・
というように変わっていくことになります。

演習2-4

この10進カウンタの出力を、1桁分の7セグメントLEDに数字として 表示させてみましょう。

このためには、4ビットの2進数を7セグメントの点灯パターンに 変換する、7セグメントデコーダ回路seg7_decを作る必要があります。 この回路をmoduleとして記述しておき、全体回路のモジュールである sampleから、counter10とともに呼び出す形にすると、 見通しのよい回路の記述になるでしょう。

カウンタの応用: 分周回路

カウンタは、ただ数を数える回路ですが、他にも使い道があります。 例えば、ある周波数のクロック信号から、それよりも低い周波数の クロック信号を作りたいとしましょう。

例えば先ほどの10進カウンタの桁上がり出力coは、 クロックckの10周期ごとに1クロック分だけ1となります。 つまり、coの周波数は、ckの周波数の1/10、ということになります。 このように、クロック信号の周波数を、カウンタ等を用いて 低くすることを分周と呼びます。

演習2-5

FPGAボード上のクロック信号は、CLK6とCLK30の2つがあり、 それぞれ6MHzと30MHzです。 この周波数のCLK6またはCLK30を用い、LED[0]が1秒間に1回点滅するような 回路を作ってみましょう。 (ヒント: CLK6等を何分の一に分周すればよいかを考え、 そのために必要なビット数のカウンタを使う)

演習2-6

4桁の7セグメントLEDを、ダイナミック駆動で点灯させる回路を 記述してみましょう。 (ダイナミック駆動については、前回資料の 最後の部分を参照)

まずは全体の構成を次のように整理しておきましょう。

まず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桁十進カウンタをつなぐ、という順で進めるとよいでしょう。


戻る