入門以前:Verilog-HDL基礎文法最速マスター

// 以前別のところに書いていたんですが、管理の都合上こちらに移動。
// ブックマーク、リンクなどしていただいていた奇特な方はつけなおしてもらえるとうれすぃです。

いろんなプログラミング言語の基礎文法最速マスターというのが流行っているようで、非常に助かっております。ありがとうございます。

f:id:mamiske:20160923103453p:plain:w300

見ていると、ハードウェア記述言語がなさそうだったので、ちょっとVerilog-HDLについて書いてみようと思います。入門書を読むときのハードルを下げるぐらいのイメージであちこち端折っていきますので、網羅的な記述やリファレンス的な使い方は期待しないでくださいませ。

はじめに

Verilog- HDLはHDL(Hardware Description Language:ハードウェア記述言語)のひとつです。ここでのハードウェアとはデジタル回路のことです。たとえばLSIであるとか FPGA/CPLDといったデバイスを設計するために使用します。がんばればCPUを設計することももちろんできます。

f:id:mamiske:20160922124944p:plain:w300

CPUも作れるということでふと思いつき、"CPUの創りかた"という本で製作されたTD4というCPU、この設計で使用する9種類のICを記述することで解説を進めてみようと思います。FPGAやCPLDでTD4を作る足がかりぐらいにはなるでしょう。なお、題材にするだけでTD4完成まではフォローできませんのであしからず。

ICのデータシートが必要なら、ぐぐるか例えば以下のところで検索して見つけてください。 http://www.datasheetcatalog.com/

 

反転回路(74HC14)

TD4では74HC14を使用しています。これは単なる反転ではなくヒステリシスを持たせたシュミットトリガ入力ですが、Verilog-HDLでは表現できません。ここでは入力信号INを単に反転してOUTに出力する74HC04と考えて記述してみます。74HC04は反転6個入りですがここでは1個入り。

/*****************
  反転回路(74HC04相当)
 *****************/
module inv ( IN, OUT );
  //// 宣言部
  // ポート宣言
  input  IN;   // 入力信号
  output OUT;  // 出力信号
  // 内部信号宣言
  wire   IN;
  wire   OUT;
  //// 回路記述部
  assign OUT = ~IN;
endmodule

Verilog-HDLはモジュールを設計の単位としています。モジュールの記述は、

module モジュール名 (ポートリスト);
endmodule

の形になり、このmoduleとendmoduleの間に回路を記述します。
ソースファイルは”モジュール名.v”とするのが一般的で、複数のモジュールから構成される場合同じ数だけファイルを作ります。 空白や改行の扱いは(たぶん)C言語と同じく自由に挿入できます。// から右側、 /* と */ に挟まれるとコメントになります。こちらもC言語(C++かな?)と同じでしょうか。

記述は大きく宣言部と記述部2つのパートに分かれています。
1つ目のパートが宣言部。ポート宣言は入出力信号を宣言します。ICで言えば、ピンの方向と名前を決めるようなイメージです。ここでポートリストにある信号をすべて宣言します。
続いて内部で使用する信号を宣言します。wireはそのまま信号線のイメージ。その他、フリップフロップとなるregがあります。ポート宣言されているwire信号は記述を省略しても問題ありません。上の例では内部信号宣言の2行は丸々省略可能。 2つ目のパートが回路記述部です。ここで回路の動作を決定します。

具体的な記述内容に進みますと、~ は反転を意味する演算子です。つまりINが1なら反転して0をOUTに入力します。組み合わせ回路を記述する場合、C言語のように単にイコールで結ぶだけではなくassignの記述が必要です。“INを反転してOUTに接続する”というイメージですね。

次の展開

機能として上記のような反転1つだけを持つFPGA/CPLDを作るのであれば、デバイスメーカーが出しているコンパイラやツールで「信号名とピンの対応を設定⇒上記のソースファイルをコンパイル⇒デバイスに書き込み」という流れです。詳細は省略。各メーカーがツールの無償版を用意していると思うのでやってみてもいいかもです。

シミュレーション

ここに書いちゃうと長くなるので省略。というか、いつも適当です。だからバグを出すのです、ごめんなさいごめんなさい。この記事の記述は一応ModelSimでシミュレーションしてるのでなんとなく動くはずです、たぶん、きっと。

3入力NAND(74HC10)

続いて3入力NANDを記述します。TD4で使用している74HC10は3個入りですが、ここでは1個入りのデバイスを作ることにします。3入力NANDは3ビットの入力INすべてが1の時に出力OUTが0、それ以外の時は1を出力します。

/**********************
  3入力NAND(74HC10相当)
 **********************/
module nand3 ( IN, OUT );
  //// 宣言部
  // ポート宣言
  input [2:0] IN;   // 3ビット入力信号
  output      OUT;  // 出力信号
  //// 回路記述
  assign OUT = ~( IN[0] & IN[1] & IN[2] );
endmodule

ここでは内部信号の記述を省略しています。
INは多ビットの信号です。多ビットの信号はC言語の配列のように幅を指定するのではなく、どこからどこまで使用するかを指定します。たとえば

input [5:2] IN;

のような書き方も可能です(が、ややこしいのでとくに理由なければ避けてください)。IN[0]のように個別に扱うこともできますし、INとすれば宣言したすべての信号(上記でいえば[2:0]の3ビット)を意味します。IN[1:0]のように一部を切り出して(この場合2ビット信号として)扱うこともできます。
回路の記述は“各信号をANDしたものを反転してOUTに接続する”というイメージですね。また、次のように記述することも可能です。知りたい人はリダクション演算子でググってください。

// 回路記述
assign OUT = ~& IN;

リダクション演算子 - Google 検索

その他にも、演算子の詳細が知りたければググるか入門書を読んでください、すみません。ちなみに2入力のOR(74HC32)は、もう作れますよね?よね?ということで省略。

デコーダ(74HC154)

入力されたアドレスAで16本の出力ピンOUTの中から1本指定します。指定されたピンだけが0(Low)、他の出力ピンは1(High)になる仕様です。ENの2本のイネーブル信号が両方ともイネーブル(負論理なので0)の時だけ有効で、それ以外の場合の出力はHigh固定です。普通はcase文で書くんでしょうけど、力技で論理演算を使って作ってみます。

/**********************
  デコーダ(74HC154相当)
 **********************/
module dec ( A, ENB, OUT );
  // ポート宣言
  input  [ 3:0] A;   // アドレス
  input  [ 1:0] ENB; // 負論理イネーブル
  output [15:0] OUT; // 出力
  // 回路記述
  assign OUT[ 0] = ~( ~ENB[0] & ~ENB[1] & ~A[3] & ~A[2] & ~A[1] & ~A[0] );
  assign OUT[ 1] = ~( ~ENB[0] & ~ENB[1] & ~A[3] & ~A[2] & ~A[1] &  A[0] );
  assign OUT[ 2] = ~( ~ENB[0] & ~ENB[1] & ~A[3] & ~A[2] &  A[1] & ~A[0] );
  assign OUT[ 3] = ~( ~ENB[0] & ~ENB[1] & ~A[3] & ~A[2] &  A[1] &  A[0] );
  assign OUT[ 4] = ~( ~ENB[0] & ~ENB[1] & ~A[3] &  A[2] & ~A[1] & ~A[0] );
  assign OUT[ 5] = ~( ~ENB[0] & ~ENB[1] & ~A[3] &  A[2] & ~A[1] &  A[0] );
  assign OUT[ 6] = ~( ~ENB[0] & ~ENB[1] & ~A[3] &  A[2] &  A[1] & ~A[0] );
  assign OUT[ 7] = ~( ~ENB[0] & ~ENB[1] & ~A[3] &  A[2] &  A[1] &  A[0] );
  assign OUT[ 8] = ~( ~ENB[0] & ~ENB[1] &  A[3] & ~A[2] & ~A[1] & ~A[0] );
  assign OUT[ 9] = ~( ~ENB[0] & ~ENB[1] &  A[3] & ~A[2] & ~A[1] &  A[0] );
  assign OUT[10] = ~( ~ENB[0] & ~ENB[1] &  A[3] & ~A[2] &  A[1] & ~A[0] );
  assign OUT[11] = ~( ~ENB[0] & ~ENB[1] &  A[3] & ~A[2] &  A[1] &  A[0] );
  assign OUT[12] = ~( ~ENB[0] & ~ENB[1] &  A[3] &  A[2] & ~A[1] & ~A[0] );
  assign OUT[13] = ~( ~ENB[0] & ~ENB[1] &  A[3] &  A[2] & ~A[1] &  A[0] ); 
  assign OUT[14] = ~( ~ENB[0] & ~ENB[1] &  A[3] &  A[2] &  A[1] & ~A[0] ); 
  assign OUT[15] = ~( ~ENB[0] & ~ENB[1] &  A[3] &  A[2] &  A[1] &  A[0] ); 
endmodule

論理圧縮はツールが勝手にやってくれるので、普通はあまり意識せずに人間がわかりやすいように書けば大丈夫です。なんとなく論理演算は記述できる気がしてきましたでしょうか。

セレクタ(74HC153)

マルチプレクサと呼ぶことも多いセレクタです。入力信号INの4ビットのうちどれかをSELで選んでOUTに出力します。

/*****************************
  セレクタ(74HC153相当)
 *****************************/
module sel4 ( IN, SEL, OUT );
  // ポート宣言
  input [3:0] IN;  // 入力信号
  input [1:0] SEL; // 選択信号
  output      OUT; // 出力信号
  // 回路記述
  assign OUT = ( SEL == 2'b00 ) ? IN[0] : // SELが0ならIN[0]を出力
               ( SEL == 2'b01 ) ? IN[1] : // SELが1ならIN[1]を出力
               ( SEL == 2'b10 ) ? IN[2] : // SELが2ならIN[2]を出力
                                  IN[3];  // それ以外ならIN[3]を出力
endmodule

条件演算子を使用して記述してみました。三項演算子という名前のほうが通りがいいかもしれないですね。比較演算と条件演算の説明は…とりあえずC言語系プログラミング言語とおおむね同じ感じ、ということでどうでしょう。 SELは2ビットの信号ですから、2ビットの数値と比較しています。"2'b00" と書いた部分の"2"はビット幅、"b"は2進数を表し、その右の"00"が数値を表します。ほかに10進数の"d"や16進数の"h"などが使えます。例えば ”4'b1010”は”4'ha”と同じです。 えーと、他の方法と比べて条件演算を連ねて書くと記述量が少ないので私は結構多用していますが、何でもかんでも条件演算を使っていると先輩や上司に怒られるかもしれません。怒られる理由は、動作速度で不利になることとネストの仕方でどんどん複雑にできちゃうことです。その他の方法としてif文やcase文を使えますが、always内以外で使う場合はfunctionを使用する必要があります。

バイナリフルアダー(74HC283)

IN1,IN2それぞれ4ビット同士の足し算器の記述です。例えば4'h5と4'h6を足すと4'hbとなります。これに下位の足し算器からキャリー入力CINがあれば、さらに1を足します。また、計算結果が5桁になった場合は、上位の足し算器に通知するためにキャリーCOUTを出力します。

/*****************************
  バイナリフルアダー(74HC283相当)
 *****************************/
module add ( IN1, IN2, CIN, OUT, COUT );
  // ポート宣言
  input  [3:0] IN1;  // 入力信号
  input  [3:0] IN2;  // 入力信号
  input        CIN;  // キャリー入力
  output [3:0] OUT;  // 出力信号
  output       COUT; // キャリー出力
  // 回路記述
  assign { COUT, OUT } = IN1 + IN2 + CIN;
endmodule

{ COUT, OUT }に着目すると、{ }で2つの信号を囲んでいます。これは連接演算子といいまして、ばらばらの信号をひとまとまりの多ビット信号として扱うのに使用します。ここでは合計5ビットの信号としてまとめて取り扱っています。IN1+IN2+CINの最大値は4'hf+4'hf+1'h1で5'h1fとなり5ビットでちょうど収まることがわかりますね。各項のビット数が違うところが私は微妙に気持ち悪いんですが、勝手に調節してくれるみたいです。ちなみに個人的には以下のように桁数を揃えたい気分です。

  // 回路記述は個人的にこう書きたい
  assign { COUT, OUT } = { 1'b0, IN1 } + { 1'b0, IN2 } + { 4'b0000, CIN };

足し算以外に引き算や掛け算も可能です。割り算もツールによっては可能みたいです。ビット幅とか符号がどうとか、算術演算はいかにもバグが出そうな所なので気をつけてください。

Dフリップフロップ(74HC74)

ここまでの回路は「組み合わせ回路」と呼ばれるもので、入力信号をリアルタイムで出力に反映する回路でした。フリップフロップは入力信号の変化をクロックのタイミングで出力に反映するものです。こういう回路を「順序回路」と呼びます。"CPUの創りかた"ではデジカメ例として説明していましたね。74HC74に取りかかる前に、次の記述を見てください。

/*************************
  シンプルフリップフロップ
 *************************/
module ff_s ( IN, CLK, OUT );
  // ポート宣言
  input  IN;   // 入力信号
  input  CLK;  // 入力クロック
  output OUT;  // 出力信号
  // 内部信号宣言
  reg    ff;   // reg信号
  // 回路記述
  always @ ( posedge CLK ) begin
    ff <= IN;
  end
  assign OUT = ff;
endmodule

always文は順序回路を書くときに使用するもので、( )内の信号に変化がある時にbegin~endの中身を実行します。posedgeを付けることで立ち上がりの変化のみを実行の条件にすることができます。上の例ではCLKの立ち上がりの時のみ実行するようになっています。立ち下がりを指定する場合はnegedgeです。
begin,endは処理が1行の場合省略が可能です。この例では省略可能です。C言語の{ }と似てますよね。
代入する際に<=を使っています。とりあえず順序回路では<=を使用すると思っていてください。reg信号であるffにwire信号であるOUTを接続して、外部に出力しています。

なお、つぎのように記述し、reg信号の値をそのまま出力することもできます。

/*************************
  シンプルフリップフロップ2
 *************************/
module ff_s2 ( IN, CLK, OUT );
  // ポート宣言
  input  IN;   // 入力信号
  input  CLK;  // 入力クロック
  output OUT;  // 出力信号
  //// 内部信号宣言
  reg    OUT;  // reg信号
  // 回路記述
  always @ ( posedge CLK ) begin
    OUT <= IN;
  end
endmodule

wire信号と違い、ポート宣言されている信号でもreg宣言は必要です。

ここまでの記述では最初のクロックが来るまで動作しないので出力の値が確定しません。つぎのようにパワーオンリセット(起動時に0→リセット解除で1の信号)を利用して初期化するのが一般的です。

/*****************************
  リセットつきフリップフロップ
 *****************************/
module ff_r ( IN, CLK, RSTB, OUT );
  // ポート宣言
  input  IN;   // 入力信号
  input  CLK;  // 入力クロック
  input  RSTB; // 負論理パワーオンリセット
  output OUT;  // 出力信号
  // 内部信号宣言
  reg    ff;   // reg信号
  // 回路記述
  always @ ( posedge CLK or negedge RSTB ) begin
    if( !RSTB ) begin // パワーオンリセット
      ff <= 1'b0;
    end else begin    // 入力信号をラッチ
      ff <= IN;
    end
  end
  assign OUT = ff;
endmodule

always文の条件が2つになりました。条件をorで結ぶことで、どちらか一方の条件を満たせば処理を実行するということになります。最初からRSTが0だったらnegedgeじゃないような気もしてしまいますが、生成される回路は非同期リセットになり、起動時ffに0を代入することになるみたいです。if文はC言語とだいたい同じです。処理が1行なのでbegin,endは省略可能です。alwaysの条件(CLKの立ち上がりとRSTBの立ち下がり)以外のとき、reg信号は値を保持します。

では、ようやく74HC74に相当する記述をしてみます。例によって1個入りです。データシートを見ると、セットとリセットが同時にイネーブル(Lowアクティブなので0)になるとき2つの出力が両方Highになるみたいですが、そこは無視してみます。

/*******************************************************
  非同期セット・リセット付Dフリップフロップ(74HC74相当)
 *******************************************************/
module d_ff ( IN, SDB, RDB, CLK, OUT, OUT_N );
  // IO宣言
  input   IN;          // 入力信号
  input   SDB;         // セット信号(負論理)
  input   RDB;         // リセット信号(負論理)
  input   CLK;         // 入力クロック
  output  OUT, OUT_N;  // 出力信号(両者は反転)
  //// 内部信号宣言
  reg     dff;         // reg信号
  // 回路記述
  always @ ( posedge CLK or negedge SDB or negedge RDB ) begin
    if ( !RDB ) begin         // リセットが0でdffを0にする
      dff <= 1'b0;
    end else if( !SDB ) begin // セットでが0でdffを1にする
      dff <= 1'b1;
    end else begin            // それ以外はdffにINを代入する
      dff <= IN;
    end     
  end
  assign OUT   =  dff;
  assign OUT_N = ~dff;
endmodule

SDBとRDBが同時にイネーブルになるときは、RDBが優先としました。

バイナリカウンタ(74HC161)

カウンタの記述です。下位のカウンタが桁あふれして、CINにキャリー信号の入力があるときにカウントアップし、カウンタが桁あふれしたら上位のカウンタにキャリー信号COUTを出力するような動きをします。

/******************************
  バイナリカウンタ(74HC161相当)
 ******************************/
module counter ( DATA, CLK, RST, EN, PENB, CIN, OUT, COUT );
  // IO宣言
  input [3:0]  DATA; // セット値
  input        CLK;  // クロック
  input        RST;  // 負論理非同期リセット
  input        EN;   // カウンタイネーブル
  input        PENB; // パラレルイネーブル(負論理)
  input        CIN;  // 下からキャリー
  output [3:0] OUT;  // 出力データ
  output       COUT; // 上にキャリー
  //// 内部信号宣言
  reg    [3:0] cnt;  // 内部カウンター
  // 回路記述
  always @ ( posedge CLK or negedge RST ) begin
    if ( !RST ) begin               // リセットで0
      cnt <= 4'h0;
    end else if ( !PENB ) begin     // !PENでデータセット
      cnt[3:0] <= DATA;
    end else if ( EN & CIN ) begin  // カウントアップ
      cnt      <= cnt + 4'h1;
    end else begin                  // 保持
      cnt      <= cnt;
    end
  end
  assign COUT = & { cnt, COUT };    // 上にキャリー
  assign OUT  = cnt;
endmodule

やや複雑になってきましたが大丈夫でしょうか。
cnt <= cnt + 4'h1; の足し算について、 4'b1111+4'b0001を計算する場合、4ビット信号cntに代入すると桁あふれして4'b0000となることに注意してください。このとき assign COUT = & { cnt, COUT }; の記述によりキャリー出力しています。 reg信号で、クロックがきても値を保持する条件がある場合、 記述なしでも保持しますが、cnt <= cnt; のように その旨記述する方が多いと思います。

3ステート反転バッファ(74HC540)

少し趣きが変わります。こいつは反転なんですが、3ステート出力と言うことでHigh,Lowの他に3つめの状態であるハイインピーダンスを出力する機能があります。

/*******************************************************
  3ステート反転バッファ(74HC540相当)
 *******************************************************/
module inv_3st ( IN, ENB, OUT );
  // ポート宣言
  input  [7:0] IN;   // 入力信号
  input  [1:0] ENB;  // バッファのイネーブル
  output [7:0] OUT;  // 出力信号
  // 回路記述
  assign OUT = ( ENB == 2'b00 ) ? ~IN : 8'hz;
endmodule

条件演算ですから、ENB == 2'b00のときはINを反転して出力することが分かります。それ以外のときがハイインピーダンス出力(z)となります。ハイインピーダンス出力というのは、むしろ何も出力しないということですね。例えば複数基板を接続する際、各基板からの警告を1本の信号でまとめる場合などがあります。通常状態にハイインピーダンス、警告をLow出力として、受け側の基板上でプルアップしておくと、信号がHighからLowに変化することでどれかの基板から警告が上がってきたと判断することができます。

モジュール呼び出し

大規模な開発をする場合は、モジュールを階層化して記述するのが一般的です。4ビットの加算器を上の方で作りましたので、それを呼び出して8ビットの加算を記述してみます。

/*************
  8ビット加算
 *************/
module add8( IN1, IN2, OUT );
  // ポート宣言
  input  [7:0] IN1; // 入力データ1
  input  [7:0] IN2; // 入力データ2
  output [7:0] OUT; // 出力データ
  // 内部信号宣言
  wire         carr;
  // 回路記述
  add  add_0( .IN1( IN1[3:0] ), .IN2( IN2[3:0] ),
              .CIN( 1'b0 ), .OUT( OUT[3:0] ), .COUT( carr ) );
  add  add_1( .IN1( IN1[7:4] ), .IN2( IN2[7:4] ),
              .CIN( carr ), .OUT( OUT[7:4] ), .COUT(      ) );
endmodule

モジュールの呼び出しは、

モジュール名 インスタンス名 (ポートリスト);

の形で行います。上の例では同じモジュールを2回呼び出していますが、1回しか呼ばない場合はモジュール名とインスタンス名を同じにしても大丈夫です。 ポートリストは .ポート名(接続信号) の形で記述します。加算器のICを2つ用意してピン(ポート名)と信号線(wire信号)をハンダ付けしているような印象を私は感じるんですが、みなさんどうでしょう。 inputのポートには必ず信号の接続が必要です。outputのポートは使用しないなら信号の接続は不要です。この辺もICの一般的な扱いと似ている気がします。

書いてみた感想

正確な用語を覚えてないことに愕然としました。最初ブロック図を描こうかと思ってたんですが、手間がかかりすぎるのでパス。あと、シミュレーションで確認するのが面倒。そんなこというからバグを出すんですね。すみませんすみません。

それはともかく、最低限必要なことが結構網羅できた気がします。あとは本を見ながら、検索しながらでそこそこ記述できると思います。デジタル回路がある程度わかっている人がこれぐらいの記述ができるなら、まぁ、ちょっとVerilogできますと言ってもそんなに嘘ではないんじゃないでしょうか。

触れたほうがいいかもだけど触れていないこと(文法以外も含む)

命名規則
function文
case文
alwaysで組み合わせ回路
inout
parameterとかintegerとか`defineとか
ブロッキング/ノンブロッキング代入
不正なラッチとか
シミュレーション
タイミング設計
状態遷移
他の算術演算・符号付きとか
FIFOとかシリパラ/パラシリ変換とかの書き方
TD4にはROMもあったな…

おそまつさまでした。

参考にした本

Amazon.co.jp: CPUの創りかた: 渡波 郁: 本
Amazon.co.jp: 入門Verilog HDL記述―ハードウェア記述言語の速習&実践 (Design wave basic): 小林 優: 本
Amazon.co.jp: FPGAボードで学ぶVerilog HDL: 井倉 将実: 本
書籍購入:RTL設計スタイルガイド