論理回路は、インバータ、ANDゲートなどの論理ゲートを基本とし、 フリップフロップなどの記憶素子を組み合わせて設計します。 その設計手法としては、カルノー図による論理関数の簡略化、 順序回路の設計のための状態遷移図、など、多くの手法がありました。
それらの設計の結果を実際の回路として動作をさせるためには、 「回路図」を描き、「論理ゲート」が入っている部品(論理IC)を プリント基板の上に実装し、配線を行って回路を製作することになります。 しかしこの手法は、回路の「デバッグ」、つまり回路が期待した動作を しないときの間違い探しや、それに伴う回路の修正が大変面倒である、 という問題があります。 近年、情報機器がますます複雑化・高機能化してきて、この傾向は ますます顕著になりつつあります。
ところが近年、このような「古典的」な論理回路の設計手法とは別の、 全く新しいタイプの回路設計手法が現れ、ここ数年で急速に一般的に なってきました。 それは、「回路を自由にプログラムできる」論理素子、を 使うものです。 それは、インバータ、ANDゲート、フリップフロップのように、 機能が決まっている部品(論理IC)ではなく、あたかも白紙の設計図のように、 最初は機能が全く決まっていなくて、あとから「プログラムを書く」ように 機能を決められる論理ICです。 このタイプの論理ICを、総称してPLD (Programmable Logic Device)と 呼びます。 このPLDには、進化の歴史的経緯から、大きく分けて次の2つに 分類されます。
サンプルのファイル一式(sample_sch.zip)を ここから取得し、展開します。 すると sample.bdf, sample.qpf, sample.qsf の3つのファイルが 現れますので、それを作業用フォルダを作成してそこにコピーをします。 なお今後、いろいろな回路を作っていくことになりますが、その都度、 新しいフォルダを作り、この3つのファイルをコピーしてから 作業を開始します。 新しい回路を作り始めるとき、 回路図のファイル名を変更して保存するだけではいけません。 必ず、別のフォルダで作業を開始しましょう。
その後、sample.qpfをダブルクリックすると、Altera社の
FPGA設計用(CAD: Computer Aided Design)ソフトウエアQuartusIIが
起動します。
(あるいはデスクトップ上のQuartusIIのアイコンをダブルクリックし、
メニューのFile → Open Projectで、sample.qpfを選択します)
※下図のsample.qpfのアイコンを選択すること。別のアイコンに注意。
sample.qpfのアイコン
この回路図中の左側は "SW[0]"という名称がついた端子があります。 これは、実際にはFPGAボード上のSW0と書いてあるスイッチに接続されている 端子(入力)です。 同様に右側の"LED[0]"という名称の端子は、FPGAボード上の LED0と書いてあるLEDに接続されている端子(出力)です。 つまりこの回路は、SW[0]の値(押すと1、離していると0; 普通と逆(負論理)なので注意)を反転して LED[0]に出力(0で点灯、1で消灯)する、という回路です。
ではこれにもう1つ回路を追加してみましょう。
回路図の左側に並んでいるツールボタンの中から、
"Symbol tool"(ANDゲートのマーク)を選びます。
すると入力するべき論理素子などを選択する画面が現れます。
この左側のLibrariesの中のツリー構造を
開いていくと、入力することができる回路要素が現れます。
まずはこの中から、primitives → logic と選び、その中の
not(インバータ)を選択してOKを押しましょう。
するとインバータが回路図上に現れ、適当な場所でクリックすると
回路図上に配置されます。
同様に、入力端子(primitives → pin のinput)と
出力端子(primitives → pin のoutput)も配置しておきます。
そして入力端子、出力端子をそれぞれダブルクリックし、
Pin nameを、それぞれSW[1]、LED[1]に変更しておきます。
なお回路図中の配置済みの回路要素は、ドラッグして選択後、
コピー&ペーストも可能ですので活用すると便利でしょう。
まずは今回は、これで回路図入力を終わりにすることにします。
なお使用可能な回路要素は、これ以外にも論理ゲート、 フリップフロップや、論理IC(いわゆる74シリーズ)の機能ブロックなど 多数があります。 これらの名称と機能は、Help→Contentsの、「目次」の中の下のほうに あるPrimitivesの項にまとめられていますので、参考にしてください。 (英文ですが、要点だけであればそれほど苦労はしないはず)
ちなみに、いわゆる74シリーズの機能ブロックは、 回路要素を配置するときに、others → maxplus2の中から選べます。 74シリーズの型番と機能との対応は、 Google先生に聞くか、 Wikipediaを参考にするとよいでしょう。 例えば、4ビットの2進数(10進数で0〜9)を、7セグメントLEDの 点灯パターンに変換する機能を持つ論理IC(デコーダ)として、 7447というものがあります。
なお74シリーズの各論理ICの機能の詳細は、 それぞれのデータシート(仕様書)を読むことになります。 それらは、Web上(例えばTexas Instruments社のWebページ(英文)中の検索("Enter Part Number"))で 見つけられるでしょう。
まずは、コンパイルを行いましょう。
QuartusIIの画面の上のほうに並んでいるボタンの中から、
"Start Compilation"ボタンを押します。
すると、入力した回路をFPGAの設定情報に変換する作業が始まります。
右側中央の"Status"画面に、この作業の進捗状況が表示されますので、
すべてが"100%"になるまで、待つことにします。
すべての作業が無事終わると、"Full compilation was successful"の
ダイアログが現れます。
もし入力した回路図に、変換作業が不可能な間違いがある場合は
エラーが表示されて変換作業が中断しますので、適宜修正を加えます。
Warningが多く表示されますが、その多くは無視してかまいません。
なお当然ですが、回路図自体の間違い(論理機能の間違い)は、
この変換作業ではエラーとなりませんので、設計者が十分注意をして
設計をする必要があります。
書き込みが終了すると、FPGAは、直ちに動作を開始します。 すなわち、入力した回路図どおりの動作をするはずです。 スイッチSW[0], SW[1]を押し、LED[0], LED[1]の点灯の様子を確認しましょう。
このようにFPGAを用いた論理回路設計は、次の手順で行うことになります。
※スイッチ・LEDは、0〜3を、0から順に使う数の分だけ番号をつけないと、 うまくいかないようです(仕様)。 つまり、例えばSW[0], SW[2]の2個を使い、SW[1]を使わない、という 回路は避け、2個のスイッチを使うのであればSW[0], SW[1]を使うのが 無難です。
なおカウンタの入力(いわゆるクロック信号)に、スイッチを用いる場合、 スイッチの「チャタリング」という現象に留意する必要があります。 これは、スイッチを1回押したつもりでも、機械接点の動作上、 複数回のON/OFFが続いてしまう現象です。 このチャタリング現象の解消のためには、S-Rフリップフロップを用いた 回路を利用するのが効果的です。 余裕がある場合は、ぜひ試してみましょう。
今回は、これらの問題を解決するものとして近年急速に普及してきた、 言語(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は、上図のように接続されているので、
同時に別々の数字を表示することができません。
そこで、適当な周期で、SA[0]〜SA[3]を順に"0"として
1桁ずつ点灯させることにし、そのタイミングで、その桁に表示するべき
表示素子の点灯パターンをSG[0]〜SG[7]に与えることで、
すべての桁に所望の数字(などのパターン)を表示する方法をとります。
この方法では、ある瞬間に点灯しているのは1つの桁のみであるわけですが、
この切り替えを高速に行うことで、人間の目には、すべての桁が
点灯しているように見せることができます。
このような点灯方式を「ダイナミック駆動」と呼びます。
まず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を適当に分周して作成するか、
あるいはスイッチを用いるとよいでしょう。