2012年3月7日水曜日

Verilog-HDL 入門

verilog-HDL 入門

verilog-HDL 入門

1.始めに

本文書では verilog-HDL を用いてハードウエアを記述するために必要な 基礎を説明する。 とりあえず、なにがしかのハードウエアを記述し論理合成や テストを行えるようになることが目標である。verilog-HDL の詳細な文法については規格書や各種参考書を見て欲しい。

2. verilog-HDL の成り立ち

verilog-HDL は旧 Gateway Design Automation 社のシミュレータ専用言語であった。 元々はシミュレーション言語であるから、 ハードウエアを効率的に記述することは主目的では無かった。 しかし、これに論理合成ツールが加わることによって、HDL を用いて ハードウエアを記述し設計することが現実的になった。 このため大いに広まり、現在では IEEE の規格の一つになっている。

しかし、シミュレーション言語である verilog-HDL を 無理矢理ハードウエア記述に使っているため、 現実のハードウエアとの間に一部不整合が有る。 この点をふまえて、ハードウエア記述源として用いる場合には、 非効率なハードウエアを記述しないように注意する必要がある。

なお、より効率的な記述を取り入れると共に、 このような不整合を解消する試みとして Systemverilog が規格化されている。

3. verilog-HDL によるハードウエア記述の構造

図1.に verilog-HDL によってハードウエアを記述した場合の構造を示す。 verilog-HDL ではモジュール(module)と呼ばれる単位によって構造を記述する。 各部品をモジュールとして記述し、 このモジュールを繋ぎ合わせることによって全体を構成する。 繋ぎ合わせる時に実際に呼び出される実態をインスタンス(instance)と言う。 このように、モジュールはハードウエアの記述を行うだけで、実態としては働かない。 例えば、同一の構造をもった演算器を複数設置する場合、 その演算器の機能を記述するのがモジュールで、演算器1、演算器2と 名前を付けて設置されるものがインスタンスである。 インスタンス中でさらに別のインスタンスを呼び出す事もできる。 (再帰的呼び出しはできない)。

注意すべきは、ハードウエア記述言語は、一般的な 手続き型プログラミングとは全く違うということである。 古典的な手続き型プログラミングでは、 関数インスタンスが多少の内部状態を持つことが有っても、 各々の関数が呼び出されない限りその働きを気にする必要は無い。 これに対し、ハードウエアでは各々の構成要素は常に動いている。 全てが並列に動いていると言い換えても良い。 どちらかと言えば、イベント駆動型プログラミングにおいて、 そこらじゅうでイベントが起こっている状態に近い。

したがって、ハードウエア記述言語では常に各々の部品の動作に気を使う必要がある。 この点においては物理的なハードウエアの性質と何ら変わることはない。 結局ハードウエア記述言語というのは、 別々に動作する部品を設計しそれらを繋ぎ合わせる作業を、 文字を用いて行うための手段である。

4. モジュール

ここでは verilog-HDL の基本であるモジュールについて説明する。 まず、モジュールの構造の概要について述べ、 単純な 4 bit カウンタを例にモジュール内の構造を大まかに説明する。 細かい記述法については後続の節を参照して欲しい。

4.1 モジュールの基本構造

モジュールの構造の概要をリスト1に示す。 モジュールは大まかにいって、ポート記述、 変数記述、組み合わせ回路記述、順序回路記述から成る。 厳密にはこれは正しくないが、 ディジタル LSI を記述するという目的から見たときは、 このように考えて問題無い。

 リスト1 モジュールの構造の概要 module counter4(ポート名, ポート名,,, );    ポート記述     変数記述     組み合わせ回路記述     always 文      begin        順序回路記述      end endmodule 

4 bit カウンタを記述したモジュールをリスト2 に示す。"//"以降行末まではコメントである。 ここでは、細かい記述はともかく、モジュール内の構造に注目して欲しい。

 リスト2 モジュール記述の例 // 4 bit simple counter module counter4(                 //*************** port name list **************                 clk, // clock                 rst, // reset                 count // counter output                 //*********************************************                 );    //************** port definition **************    input clk, rst; // clock, reset    output [3:0] count; // counter output    //*********************************************     //*********** parameter declaration ***********    reg [3:0]         counter; // count register     wire [3:0]         count_tmp; // count up value    //*********************************************     //********* combination logic circuit *********    assign         count = counter; // connect to output    assign         count_tmp = counter + 1'b1; // count up    //*********************************************     //*************** state machine ***************    always @(posedge clk or negedge rst)      begin         if (~rst) // reset           begin              counter  

4.2 モジュール文

モジュールは module 文で始まり、endmodule 文で終わる。 module 文ではモジュール名を示し、つづく括弧内で引数として入出力ポート名を示す。

4.3 ポート属性記述部

module 文の直後にポートの属性とビット幅を記述する。 通常使うポートの属性は、input, output, inout の 3 つである。 ビット幅は通常 [ビット幅-1 :0] のように上位下位の順で指定する。 ビット幅が同じ場合は、"input clk, rst;" のように続けて記述できる。

4.4 変数定義部

言語仕様上は変数定義はモジュール内のどこでも記述できる。 モジュールが小さい場合はポート属性記述の次に変数定義を行うことが多い。 モジュールが大きい場合は、変数定義とその変数の使用場所があまり離れないように記述した方が良い。

4.5 組み合わせ回路記述部

変数定義部の後に組み合わせ回路を記述する事が多い。 リスト2 中では assign 文を用いて簡単な組み合わせ回路を表現している。 この assign 文を用いるか、function を用いて組み合わせ回路を記述する。 assign 文と function 文については後述する。

always 文を用いて組み合わせ回路を記述することもできるが、 記述が繁雑になる上に 論理合成で思わぬ結果が出ることもある。 always 文で組み合わせ回路を記述するのは避けた方が賢明である。

4.6 順序回路記述部

順序回路を含まないモジュールの場合は記述しない。順序回路を含む場合は、 多くの場合モジュールの最後に always 文を用いて順序回路を記述する。

always @( に続く信号名はセンシティビティリストと呼ばれる。 always 文はこのセンシティビティリストに記述された信号が変化したときのみ動作する。 posedge, negedge は各信号の立ち上がりで反応するか立ち下がりで反応するかを指定する。

論理合成をを用いて大規模なディジタル LSI を設計する事を考えると、 このセンシティビティリストにクロックとリセット 以外の信号を混ぜるのは避けた方が良い。

要するに普通にディジタル LSI を設計している場合には、 リセットする順序回路部分は always @(posedge clk or negedge rst) で始め、 リセットしない順序回路は always @(posedge clk) で始めると憶えれば良い。

(注)リセット信号は負論理であることが多い。

なお、一つのモジュール内にいくつでも always 文を記述することができるが、 繁雑さを防ぐために、できるだけ一モジュール内の always 文は 一つにまとめた方が良い。

4.7 リセット

リセット信号は実に厄介なしろものである。 現在では reset 信号は clock と同様に特別な配慮をする必要があるため、 論理合成時には理想的な信号線として扱い、 レイアウト時にリセットツリーを挿入することが一般的である。 このため、適切に遅延時間を算出するのが難しく、 リセット信号を論理に用いると静的タイミング解析時に問題が生じることがある。

例えば、リセット時に定数をレジスタ変数に代入する場合は良いが、 リスト3 のようにリセット時に変数の値を代入するような場合に問題となる。 これはチップの外から初期値を取り込むような場合に生じる。


IMはすべて一つの属性については、私は何
 リスト3 初期値の設定でリセット信号を用いる場合   always @(posedge clk or negedge rst)     begin        if (~rst) // reset          begin             counter  
この場合、リセット信号がセレクタの制御信号になってしまうので、 セレクタの遅延時間を計算するのが難しい。 これを防ぐ一つの方法は、リセット信号を論理に用いず、D-FF のセット・リセット 端子のみに繋がっている状態にすることである。 具体的にはリスト 4 のように初期状態を示すレジスタ変数を用い、 初期値の設定が終わったら初期状態では無いことを示すように書き換えれば良い。
 リスト4 リセット信号を論理から排除するための記述   reg init_s;   always @(posedge clk or negedge rst)     begin        if (~rst) // reset          begin             init_s  

4.8 モジュール呼び出しと、モジュール間接続

個々のモジュールを記述しても、それだけでは目的とするハードウエアを 記述したことにはならない。 各々のモジュールを配置し、 入出力信号を正しく接続する必要がある。 verilog-HDL はハードウエア記述言語であるため、 一般的なプログラミング言語の関数呼び出しとは全く違う概念でモジュール呼び出しを 行わなければならない。

verilog-HDL におけるモジュール呼び出しとは、 関数呼び出しのような結果を返すものでは無く、 単純に部品を配置すると言う意味である。 リスト5 にリスト4 のモジュール呼び出した例を示す。 モジュール呼び出しは、まずモジュール名を指定し、 次いでモジュールインスタンス名を指定し、 括弧内に引数リストを記述する。 リスト5 では counter4 がモジュール名で、count4_0, count4_1 がインスタンス名である。 このように、同一構造のモジュールを名前を変えて呼び出し、 同一の部品を複数配置することができる。 ここで、引数リストの記述法に二種類有り、単に引数を並べて書くものと、 ドット"."で呼び出すモジュールの内部変数を指定しさらに括弧で括って引数を指定する方式がある。 できるだけ内部変数を明示的に示す方式を用いた方が間違いが少ない。

 リスト5 モジュール呼び出し   モジュール呼び出しの形式   モジュール名 モジュールインスタンス名(引数リスト);    モジュール呼び出しの例   reg clk, rst;   wire [3:0] result0;   wire [3:0] result1;    counter4 count4_0(clk, rst, result0);   counter4 count4_1(.clk(clk), .rst(rst),                     .count(result1)); 
配置した部品は接続されねばならない。 verilog-HDL では reg と wire 変数を用いて接続する。 リスト6 に乗算機と加算器を結合した例を示す。 順序回路を持たない単純なモジュールだが、 接続の様子を知ることができる。

リスト6 では 4 bit乗算機と 8 bit 加算器を定義し、 mul_add モジュールでこの乗算機と加算器を呼び出して (opr0 * opr1) + opr2 を行っている。 mul4_0 と add8_0 へ同一のワイヤ products が mul4_0 の 出力と add8_0 の入力へ引き渡されている事が解る。 つまり、この信号線によって二つのモジュールのポート間が結合される。 この例の場合、呼び出されたモジュールのうち、 他のポートは上位モジュールの入出力ポートに直結されている。 モジュール間接続に使う信号は既に宣言されていなくてはならない。 上位モジュールのポートは既に宣言されたものとして扱われる。

このようにして呼び出されたモジュールの入力ポートには、 reg 変数と wire 変数が接続可能であり、出力ポートは wire 変数 のみが接続可能である。 上位モジュールの入出力ポートは wire と同等の扱いである。

 リスト6 モジュール間接続 モジュール記述 module mul4(oprA, oprB, result);   input [3:0] opr0, opr1;   output [7:0] result;    assign result = opr0 * opr1;  endmodule  module add8(oprA, oprB, result);   input [7:0] opr0, opr1;   output [8:0] result;    assign result = opr0 + opr1;  endmodule  上位モジュールでの呼び出し module mul_add(opr0, opr1, opr2, result);   input [3:0] opr0, opr1;   input [7:0] opr2;   output [8:0] result;    wire [8:0] product;    mul4 mul4_0(.oprA(opr0), .oprB(opr1),               .result(product));    add8 add8_0(.oprA(product), .oprB(opr2),                .result(result)); endmodule 

5. 定数、変数

この節では verilog-HDL の定数および変数について説明する。 ハードウエア記述言語における変数の特徴は、 プログラミング言語のような型を定義しにくい事である。 ハードウエア内部では特定のデータ型というものは無く、 自由にビット幅もフォーマットも変更できる必要があるからである。

そこで、verilog-HDL ではハードウエアに即した変数として、 配線に相当する wire 型と(これもまた厳密では無いが) 記憶素子に相当する reg 型を定義している。 以下、定数と変数について説明する。

なお、シミュレーション時に便利な変数については後述する。

5.1 定数

5.1.1 数値

リスト7 に数値記述の例を示す。verilog では 10 進, 2 進, 8 進, 16 進数をサポートしている。 まずビット幅指定を10進数で指定する。32 bit と言わず(処理系が許す限り) 何ビットでも記述することができる。 次に基数を b,o,d,h の何れかで示して、最後に数を記述する。 この数値を記述する時には不定値やハイインピーダンスも記述することができる。 なお、これらの数値を記述するときには、 読みやすいように数字の間にアンダースコア"_"を挟むことが できる。実行時にはアンダースコアは無視される。

 リスト7 数値表現     6    // 幅指定無し 10 進数の 6   'o76   // 幅指定無し 8 進数  4'd6    // 4 bit 10 進数 6  3'b110  // 3 bit 2 進数  6'o77   // 6 bit 8 進数  64'h00FF_00FF_00FF_00FF   // 64 bit 16 進数  4'b01xx // 不定値を含んだ 2 進数  4'bzzzz // 4 bit 全てがハイインピーダンス 
始めの 2 行の様に、ビット幅指定無しで記述することできるが、これは間違いの元である。 できるだけビット幅を指定して記述することを勧める。 ビット幅無しで記述しても間違う事が少ないのは、リセット時に 0 を設定するような場合である。

また、ビット幅を指定した場合も、できるだけ指定したビット幅全てを明示的に記述する方がよい。 もし、記述した数が指定したビット幅に満たなければ、 上位ビットは 0 で埋められる事になっているが、x や z が混じっている場合は煩雑である。

5.1.2 `define 文

`define 文を用いて、C 言語の #define マクロと同様の記述を行うことができる。 `define はモジュールの外、通常ファイルの先頭で記述される。 マクロであるから、式も記述できる。`undef も有り、マクロを解除できる。 行末のセミコロンが無いことに注意。

 リスト8 `define 文によるマクロ記述   `define BYTESIZE 8   `define WORDSIZE BYTESIZE * 4   `define INNER_PRODUCT (A0 * B0) + (A1 * B1) 

5.1.3 prameter 文

parameter 文を用いて、定数を記述できる。parameter 文は変数定義とよく似ており、 ビット幅指定を組み合わせることができる。parameter 文はセミコロンで終わる。 `define 文が有るのに何故 parameter 文が有るかと言えば、 やや用途が違い parameter の方が自由度が高いからである。 `define 文はソース中で一度しか有効でなく、モジュールの中で記述することもできない。 parameter 文はモジュールの外でも中でも記述することができ、 モジュール内で記述した parameter はモジュールにローカルになる。 また、parameter は defpram 文を用いて、呼び出し時に値を設定し直すこともできる。
 リスト9 prameter 文による定数定義   parameter byte_size = 8;   parameter byte_mask = byte_size - 1;   parameter [3:0] mux_selector = 0;   parameter newconst = 3'h4;  

5.2 変数

ハードウエアを記述するときに使える変数はレジスタ(reg)型とワイヤ(wire)型である。 大ざっぱに言って、reg 型は D-FF のような記憶素子、wire 型は配線である。 ポート属性記述と似た形で、型 ビット幅 変数名の順に記述する。 ビット幅は通常 [ビット幅-1 :0] のように上位下位の順で指定する。 変数名は、英文字もしくは "_" で始まり英数文字もしくは "_" が続く文字列でである。 1 ビットの信号の場合ビット幅指定を省略できる。 ビット幅が同じ場合は、ビット幅の指定の後に列記できる。 しかし、ビット幅が同じ信号をまとめて定義すると非常に読みにくくなるので、 ビット幅よりも関連する信号をまとめて書く方が良いであろう。
 リスト10 変数定義の例   reg [3:0]    counter1, counter2;   wire         g_clock;   wire [3:0]   count_tmp; 
このようにして定義した変数は単純に変数名で参照し、代入することもできるし、 明示的にビットを指定して参照、代入することもできる。 例えば counter1[3] と書けばcounter1 の 3 bit 目を意味する。 同様に counter2[2:1] のように信号の一部を取り出すこともできる。
表1。変数がとり得る値
意味
0 ゼロ、偽
1 1、真
x 全ビット不定値
X 一部のビットが不定値
z 全ビットハイインピーダンス
Z 一部のビットがハイインピーダンス

表1. に変数がとり得る値を示す。 0, 1 は単純に偽と真をあらわす、1 は条件式の中でも真として取り扱われる。 x と X は値が不定(unknown)であることを示し、z と Z はハイインピーダンス(high impedance)であることを示す。 x と X の違いは複数ビット幅の信号の時に意味を持つ。もし全ビット不定であるなら x、 一部のビットのみ不定であるなら X である。同様に全ビットハイインピーダンスならば z、 一部のビットのみハイインピーダンスならば Z 値を取る。 厳密には X や Z は値というわけでもなく、通常 X や Z を変数に設定することも無いが、 値を表示したときに意味を持つので、このように憶えておくと良い。


二次元配列を交換するには?

シミュレーション時に使用できる変数については後述する。

6. 代入

verilog-HDL には幾種類もの代入がある。 一般的なプログラミング言語であれば、代入とはある変数を示す メモリセルに値を設定することである。 これに対し verilog-HDL における代入とは、配線を結合するという意味であったり、 記憶素子にデータをセットするという意味であったりする。 これは verilog-HDL がシミュレーション用言語であるが故の繁雑さの一つである。 以下に各々の代入について示す。

6.1 継続的代入(continuous assignments)

継続的代入は常時信号線が繋がっていることを示す。単純に配線によって接続されていると思えばよい。 継続的代入には、変数宣言時に行う方法と assign 文を使う方法がある。 (他に force を使う継続的代入が有るが、これはシミュレーション時に例外的に使うものである)

6.1.1 wire 宣言時の代入

 リスト11 wire 宣言時の継続的代入   wire [3:0] count_out = counter;   wire [3:0] init_count = 4'b0000;   wire [3:0] result = a + b; 
リスト11 のように、wire を宣言するときに続けて代入を記述することができる。 右辺は式である。すなわち単一の変数も定数も式も代入することができる。

6.1.2 assign 文

 リスト12 assign 文を用いた継続的代入   wire [3:0] count_out;   assign count_out = counter;    wire [3:0] init_count;   assign init_count = 4'b0000; 
変数宣言時ではなく、assign 文を用いて明示的に継続的代入を行う方法である。 assign 文を使う以外は同様に記述する。 assign 文を用いれば、組み合わせ回路記述中の何処でも代入することができ、 自由度が高い。

6.2 手続き的代入(Procedural assignments)

手続き的代入は順序関係のある代入文である。手続き的記述を許すブロック、 always 部や inital 部(後述) や function(後述) や task(後述)で使用する。 verilog-HDL は元々シミュレーション用であったため、 この手続き的代入はハードウエアとの整合性が今ひとつで挙動が判りにくい。 ここでは簡略化して、頻繁に現れる状況に絞って説明する。 以下の手続き型代入は手続き記述ブロックで内で使用されることに注意して欲しい。

6.2.1 ブロッキング代入(blocking assignments)

 リスト13 ブロッキング代入   always @(a, b, c)     begin       b = a;       c = b;     end 
ブロッキング代入には等号 "=" を用いる。 ブロッキング代入では文の実行順が保存され、 各々の文は先行する文の実行が終了しない限り実行されない。 リスト13 の例では b = a; が実行されない限り c = b; は実行されない。 したがって、リスト13 を実行後は c と a は等しくなる。 各々の文がそれ以降をブロックしているので、ブロッキング代入文と呼ばれる。

6.2.2 ノンブロッキング代入(non-blocking assignments)

 リスト14 ノンブロッキング代入   always @(posedge clk)     begin       a  
ノンブロッキング代入は " まず、各々の代入文は、代入の準備だけを行う。 そして、センシティビティリストの信号が変化したときに、 あらかじめ準備しておいた値を代入する。 リスト14 の例では b に対して a の値が準備され c に対して b の値が準備されており、 クロックの立ち上がりで準備された値が代入される。 したがって、c と a は等しくはならず、 クロックが立ち上がった時点では c には一つ前のサイクルの b の値が 入っているはずである。つまり、リスト14 は 1 bit ずつシフトするレジスタを構成している。

このノンブロッキング代入はクロックに同期して動く回路の記述に適している。 D-FF を用いるときは、まず D-FF の入力信号を確定させ、 次いでクロックの立ち上がりで D-FF が値を取り込むからである。 always 文のセンシティビティリストでクロック信号を記述し、 全てノンブロッキング代入文を用いて always 文内を書けば、 自然に同期回路が記述できる。

6.3 データサイズの問題

代入するときには、特に理由がない限り左辺と右辺のビット幅を揃えるべきである。 例えば左辺が短い場合は上位ビットが捨てられてしまう。リスト 15 では、 c ではキャリーが捨てられ、d にはキャリーを含んだ値が代入される。 キャリーが必要ないと分かっている場合は問題ないが、 もしキャリーが必要であれば、きちんと 1 bit 長い変数に結果を代入すべきである。

 リスト15 代入時のデータサイズの不整合   wire [3:0] a, b, c;   wire [4:0] d;   assign c = a + b;   assign d = a + b; 
左辺の方が長い場合はさらに厄介な現象が生じうる。 右辺でなにがしか演算を行って代入する場合は上位ビットが 0 で補完され、 単に変数を代入した場合は上位から詰められる。 思わぬ結果にならないよう、全てのビットを明示的に記述するように心がけたい。

7. 演算子

表 2 に verilog-HDL の代表的な演算子を示す。
表2。演算子
+ - * / 算術演算(arithmetic)
~ & | ^ ~^ ビット演算(bit wise)
& ~& | ~| ^ ~^ リダクション演算(reduction) (注)単項演算子
! && || 論理演算(logical)
== != > >= 関係演算(relational)
> >>> シフト演算(shift)
? : 条件演算(conditional)
{} {{}} 連接(concatenation)、繰り返し(replication)
表2 のうち、算術演算と関係演算については説明の用は無いであろう。 ビット演算は各々のビット毎に演算を行い、論理演算は条件式に用いるものであると考えれば、 これらも理解できるであろう。 残る演算については以下に説明する。

7.1 リダクション演算

これは単項演算子である。対象となる変数の各々のビットに対して演算を行い、 結果を 1 bit で返す。
 リスト16 リダクション演算子   wire [3:0] data;   wire all_and;   assign all_and = &data; // 以下と等価   // assign all_and = data[3] & data[2] & data[1] & data[0]; 

7.2 シフト演算

シフト演算のうち > は符号無しシフトである。 シフトの結果空いた部分には 0 が埋められる。 >>> は算術シフトであり、シフトで空いた部分に符号ビットを拡張して補完する。 verilog-HDL では符号付き変数を論理合成することを考慮していないので、 演算子の方で明示的に指定する。

7.3 条件演算子

条件演算子 ? は条件によって式を選ぶ演算子である。次のような形をしている。

条件 ? 式1 : 式2

この式は条件が真であるときに式1 の値になり、条件が偽である時に式2 の値になる。 使用例をリスト17 に示す。ここでは select が真であれば a が、偽であれば b が x に代入される。この条件演算子は、組み合わせ回路を記述するときに、 単純な条件判定を記述するために多用される。

 リスト17 条件演算子   wire a, b;   wire select0, select1;   wire x, y;   assign x = select0 ? a: b;   assign y = select0 ? (select1 ? a: b): x; 
式であるからいくつでも入れ子にできるが、あまり階層を深くしすぎると 何が何やら判らなくなるので 2 〜 3 階層程度にとどめることを勧める。

7.4 連接演算子

連接演算子 {} は {} 内の変数を結合して一つの変数とみなす演算子である。 リスト18 では 8 bit の変数 a, b を結合し、16 bit の変数 r に 代入している。この演算子を利用して 桁揃えやビット幅の調整、パッキング等が容易に行える。
 リスト18 連接演算子   wire [7:0] a, b;   wire [15:0] r;   assign r = {a, b}; 
また、{}は繰り返し演算子としても使用できる。{}の前に数字(10進数)を 書けばその回数だけ {} の中を繰り返す。 リスト19 に例を示す。外側にもう一つ括弧が有るのに注意。 繰り返したモノを外側の連接演算子でくっつけている。
 リスト19 繰り返し演算子   wire [7:0] a = {8{1'b1}};  // a には 8'b1111_1111 が入る。   wire [7:0] b = {4{2'b01}}; // b には 8'b0101_0101 が入る。   wire [15:0] c = {2{a}};    // c には 16'b1111_1111_1111_1111 が入る 

8 条件分岐

条件分岐と言っても、ハードウエアであるから、できることは限られている。 ハードウエアで条件分岐に相当するものは、セレクタ(マルチプレクサ multiplexer)と デコーダー(decoder)である。 セレクタは条件によって接続する信号を選択するものであり、 デコーダーは条件に合致したとき一意の値を返すものである。 これらを記述するため、verilog では if 文と case 文を用いる。

8.1 if 文

リスト20 に if 文の例を示す。 この例ではノンブロッキング代入文を用いているが、ブロッキング代入文も使用できる。 残念なことにモジュール内に直に書くことができず、 always 部もしくは後述する function 内で記述しなくてはならない。
 リスト20 if 文 例 1   if (select)     begin       x 例 2   if (select1)     begin       x  
ほとんどの場合、if 文はセレクタとして論理合成される。 例 2 の様に if 〜 else if といくらでも 連続して記述することができるが、多段のセレクタが合成される可能性がある。 しかし、論理合成ツールの方で勝手に最適化してくれるので、 大して気にする必要はない。

8.2 case 文

多数の条件からの選択に便利な case 文は、デコーダに良く使用される。 リスト21 に case 文の例を示す。 case 文はある条件に適合したときの動作を記述することによって結果を選択する。 最後の default: は他の条件が満たされなかった場合に実行される。 リスト21 の例の場合には全条件を網羅してあるので必要無いように思えるかも知れないが、 これは主に論理合成ツールに情報を提供するためである。

例2 の様に casex 文を用いることによって x 値を含むドントケアを表すことができる。 同様に casez 文も定義されている。 例3 は複数の条件をまとめて記述した例である。


110ミリ秒はどのくらいですか
 リスト21 case 文 例1 単純なデコーダ   case (select) // synopsys parallel_case     2'b00: result = 4'b0001;     2'b01: result = 4'b0010;     2'b10: result = 4'b0100;     2'b11: result = 4'b1000;     default: result = 4'b0000;   endcase 例2 不定値を含む場合   casex (select) // synopsys parallel_case     2'b0x: result = 4'b0010;     2'b10: result = 4'b0100;     2'b11: result = 4'b1000;     default: result = 4'b0000;   endcase 例3 複数の条件をまとめて書いた例   case (select) // synopsys parallel_case     2'b00, 2'b01: result = 4'b0010;     2'b10: result = 4'b0100;     2'b11: result = 4'b1000;     default: result = 4'b0000;   endcase 
verilog-HDL の仕様上 case 文は順序を持つ記述である。 したがって case 文もモジュール内に直に書く事ができず、 always 部または function を用いて記述する。

順序を持つため、 論理合成において条件判定が多段になってしまうという、厄介な問題が生じる。 各々の条件は先頭の行から解釈されて実行される。 このため条件に重複する部分があっても、それだけでは破綻を生じない。 しかし、verilog-HDLの仕様通りに論理合成しようとすると、 先頭の文を表すセレクタを生成し、次の文を表すセレクタを生成しといった具合になる。 デコーダーは 32 bit 等かなり大きくなる場合があり得るのでこれは問題である。 もちろん、論理合成ツールが、 全ての条件が重複しないことを認識すれば、 並列に動作する論理回路を生成する事が可能である。 しかし、全ての場合にそれが可能であるとは限らない。

そこで、論理合成ツールによっては、あらかじめ並列に論理合成可能であることを 通知できるようになっている。 リスト21 中のコメント "// synopsys parallel_case" がそれである。 標準的な論理合成ツールである Synopsys 社の DesignCompiler は、このコメントが入っていると並列に合成可能な case 文であると認識する。 もちろん条件が重複している場合にこの指定を行うと問題が生じるが、 設計者側で並列に合成可能であると判っている場合には利用すべきである。

9 function

assign と条件演算子だけでは複雑な組み合わせ回路を記述することは難しい。 そこで、手続き的な記述が可能な function が儲けられている。 function 文を使って記述されたものは組み合わせ論理回路として合成される。 リスト22 に function の記述例を示す。

function では、まず返値のビット幅を指定して function 名を記述し、 次に input 文で 入力を指定する。 output の指定は無く、function 名に代入することによって返値が決定される。 従って、verilog-HDL における function は 1 出力である。 入力を指定した後、case 文や if 文を使用して組み合わせ回路を記述する。 リスト22 では、例1 で case 文を用いて 4 入力セレクタを、例2 で if 文を用いて 2 の補数表現の 2 入力加算器を記述している。 function 内で中間変数を用いる場合には例2 のように、 何故かレジスタ変数を用いる。

 リスト22 function の例 例1 4入力セレクタ   function [3:0] select4;     input [1:0] select;     input [3:0] a, b, c, d;     case (select) // synopsys parallel_case       2'b00: select4 = a;       2'b01: select4 = b;       2'b10: select4 = c;       2'b11: select4 = d;       default: select4 = 4'b0000;     endcase   endfunction  例2 2入力加算器   function [4:0] add4;     input [3:0] a, b;     input minus;          reg [3:0] tmp;      if (minus)       tmp = ~b;     else       tmp = b;      add4 = a + tmp + minus;   endfunction 
(注)
リスト22 例2 では 2 の補数を作るために、 まず入力 b を反転して、 加算時に同時に 1 を足すという構造を記述している。 これは、加算器が複製されてしまうのを防ぐためである。 if 文中に加算を記述すると、論理合成ツールが二つの加算器を生成し 結果を選択する回路を作ることがある。 現在は集積度が上がったため、数ビットの加算器が複製されても問題はないが、 大きな演算器を使うときには考慮した方が良い場合がある。

(注) verilog-HDL には function の他に複数の出力を可能とする task と呼ばれる構造がある。 しかし、現在の所 task は DesignCompiler 等で論理合成できない。 task はシミュレーション時に用いられるのが一般的である。

10 シミュレーション用記述

9 節までで、ハードウエアを記述する際の verilog-HDL の記法について説明した。 しかし、それだけではシミュレーションを行うのに極めて不便である。 ハードウエア記述時に混乱することを避けるため、 9 節までの説明では直接ハードウエアで実現しにくい表現については意識的に排除してきた。 しかし、元々シミュレーション用言語であった verilog-HDL は、 シミュレーション用記述が充実している。 以下に、シミュレーション時に有用な記述のうち、 必要最低限のものについて説明する。

10.1 テストモジュール

9 節までの説明では、最初にどうやってシミュレーションを開始するのか、 疑問に思うであろう。 このシミュレーションの挙動を記述するのがテスト用モジュールである。 テスト用モジュールで初期設定を行い、テスト用の信号を発生する。 リスト 23 に 4 bit 加算器をテストするモジュールの例を示す。

 リスト23 テスト用モジュール `timescale 1ns/100ps module test_top();    parameter  STEP = 10;     reg        clk, rst;  // clock & reset    integer    i,j;       // control val     reg [3:0]  a, b;      // input to add4    reg [4:0]  c;         // output from add4     add4 add4_i(a, b, c); // 4 bit adder    always begin          // clock generation       #(STEP/2) clk = ~clk;    end     initial begin       clk = 1'b0;        // initial value       rst = 1'b0;        // reset        a = 4'b0000;       b = 4'b0000;        #1.0;              // delay for avoiding race       #(STEP * 128);     // reset phase       rst = 1'b1;        // finish reset       #(STEP);        // generating test signal       for (i = 0; i  
シミュレーションを制御するモジュールであるから、 通常このテスト用モジュールが最上位モジュールになる。 したがって、テスト用モジュールは入出力ポートをもたない。 まず、モジュールの始めに必要なパラメータと変数を定義する。 ここでは clock の一サイクルを表す STEP と制御変数 i, j および 4 bit 加算器の入出力に接続するレジスタ変数を定義している。 次に全てのテスト対象モジュールを呼び出す。 続く always 文以降がシミュレーションの制御である。 以下、このシミュレーション制御について説明する。

10.2 クロック記述

リスト2.3 の 一つめの always 文は clock の記述用である。 この always 文はセンシティビティリストを持っていないため、 常に実行される。"#" で始まる行は clock の値を反転させ一定時間毎に clock を反転させている。 "#" の後に続く数字は遅延であり、 次の文の実行までまで一定時間待つことを示す。 遅延の単位は後述する `timescale ディレクティブで設定される。

verilog-HDL はイベントによって各処理が起動される言語であるから、 この遅延によって一定時間待つのは、遅延が記述されたブロックだけである。 他の部分は独立して動作する。

10.3 initial 文

シミュレーションを開始するには initial 文を使う。 initial 部は always 部と同様に手続き型記述を行うブロックであり、 always 部がセンシティビティリストに記されたイベントによって起動されるのに対し、 initial 部はシミュレーション開始時に一回だけ実行される。 この initial 部で各信号に値を設定し変化させることによってテスト用の信号の 変化(イベント)を生成する。

リスト23 ではまず initial 部の先頭でクロックとリセットの初期設定をしている。 次に"#" で一定期間待ち、リセットを解除している。 ここで、#1.0 に注目して欲しい。 これは信号の変化のタイミングをクロックからずらす為である。 この例では以下全ての遅延を 1 サイクルに相当するパラメータ STEP によって 表しているが、この #1.0 によってクロックからやや遅れたタイミングで信号が変化 するように制御している。

次にリセットを解除して 1 サイクル待ったのち、for 文で入力信号を生成している。 for 文の文法は特に説明の必要は無いであろう。 最内ループの先頭で 1 サイクル遅延を記述しているため、 これは各サイクル毎に新たな信号を生成する。 for 文が終了した後 initial 部の最後で $finish システムタスクによってシミュレーションを終了する。

initial 部はいくつでも記述することができ、各々が独立して動作する。 全く独立した入力出力が有るような場合に便利である。 各々の initial 部で書かれた遅延等は正確に反映されるが、 全く同じ時刻にイベントが起こった場合はどちらが先に処理されるか保証の限りでは無いので、 initial 部同士で相互作用するような記述は避けるべきである。

10.4 結果表示

リスト23 最後の always 部は結果を表示する部分である。 この always 部はセンシティビティリストに clock の立ち上がりを指定しているので、 クロック毎に結果を出力する。 ここでは後述する $write と $display システムタスクを用いている。 これらの表示用システムタスクは手続きブロックの何処にでも書けるが、 このようにテスト用トップモジュールにまとめて記述した方が間違いが少ないし、 多数のファイルを扱うような場合には手間が少なくて済む。

10.5 シミュレーション時に有効な変数

ここでは、ハードウエアとして考慮されていないが、 シミュレーションに使われる変数について説明する。 これには integer, real, time, realtime の 4 つが有る。 以下に簡単に特徴を述べる。


  • integer
    整数である。実装によってサイズは違うが、少なくとも 32 bit の長さである。 符号有る無し等微妙な部分が有るが、それが問題になる場合には使用しないこと。 厳密な表現が必要な場合は reg 変数で代用できる。
  • real
    実数である。あまり使用されない。
  • time
    シミュレーションタイムを整数で保持することができる変数。 少なくとも 64 bit の長さがあることが保証されている。 整数で表されたシミュレーションタイムは後述する $time システムタスクで得ることができる。
  • realtime
    シミュレーションタイムを実数で保持することができる変数。 実数で表されたシミュレーションタイムは後述する $realtime システムタスクで得ることができる。
なお、これらの宣言の方法はビット幅指定無しの wire や reg と同じである。

10.6 二次元配列

ここでは二次元配列について説明する。verilog-HDL では リスト24 (a)のように二次元配列を定義することができる。 まず始めに語長を指定し、次に配列名、語数の順で指定する。 通常語長は最上位ビット、最下位ビットの順で指定し、 語数は最下位アドレス最上位アドレスの順で指定する。

アクセスはリスト24 (b)のように行う。 アクセス時には変数名に続く[] 内に 定数もしくは変数でアドレスを指定する。 bit を指定することはできず、word 毎のアクセスのみ可能である。 読み出し時には継続的代入文と 手続き的代入文のどちらでも 使用することができる。 書き込み時には継続的代入文を使用することができず、 always 文や function (あるいは後述する task) などを使って手続き的代入文を用いなければならない。

 リスト24 二次元配列 (a)定義   reg [15:0] a_mem [0:63]; // 16 bit, 64 word   reg [7:0] b_mem [0:255]; // 8 bit, 256 word  (b)アクセス   wire [16:0] i_data; // input data   wire [16:0] a_data; // a data   wire [5:0]  a_address; // an address   wire        w; // write    assign a_data = a_mem[a_address]; // read    always @(negedge w) // write     begin       a_mem[a_address] = i_data;     end 
シミュレーション時には、 このような二次元配列は、 おおよそメモリのような動作をすると考えて差し支えない。 しかし、これがまともに論理合成されると考えてはいけない。 実際の物理的メモリには特別の配慮が必要である。 通常は製造者から提供されるメモリマクロを用いる。

10.7 system task and function

ここでは、よく使われるシステムタスクとファンクションについて説明する。

10.7.1 $write $display

$write と $display は表示に使われるシステムタスクである。 $write は改行を行わず、$display は表示後に改行を行う点のみが異なる。 書式は C 言語の printf に良く似ている。
 リスト25 $write, $display の書式   $write("フォーマット", 変数並び,,,);   $display("フォーマット", 変数並び,,,); 
フォーマットでは表示したい文字列と変数の表示形式を記述する。 特殊キャラクタを表示するために表3 のようにエスケープシーケンスを記述することができる。
表3. エスケープシーケンス
\n 改行
\t TAB
\\ バックスラッシュ
\&rdquo ダブルクォーテーション
%% %

表示形式は % に続く一文字で表される。 表4. によく使う表示形式指定を示す。 表示桁数はデータサイズによって自動的に最大桁に決定される。 十進数の場合は右詰で表示され、空いた桁にはスペースが出力される。 2進 8進 16進の場合は全ての桁が表示される。

表4. 表示形式指定
%d 十進数
%b 二進数
%o 八進数
%h 十六進数
%t シミュレーションタイム

不定値やハイインピーダンスの場合の表示は独特である。 全ての信号が不定もしくはハイインピーダンスであれば、x や z が表示される。 しかし、信号の一部が不定もしくはハイインピーダンスであれば大文字の X や Z が表示される。 これが全ての表示桁に対して独立して適用される。

この表示の時は、下位モジュールの変数も参照することができる。 記述方式は次の通りで、ドット "." で区切ることによって 下位モジュール内を参照することができる。 インスタンス名を連ねていくことによって、多階層のモジュール内を 参照することができる。 この参照はモジュールの入出力ポートに対しても有効である。

 リスト26 下層モジュール内の参照   インスタンス名.変数名 // 一つ下位のモジュール内   インスタンス名.インスタンス名.変数名 // 二階層下のモジュール内 

10.7.2 $finish

$finish システムタスクは、その時点でシミュレーションを終了する。 手続きブロックの何処にでも書けるが、 デバッグ等で臨時に挿入した $finish 文を忘れないようにしよう。

10.7.3 $readmemb, $readmemh

$readmemb と $readmemh はファイルから値を読み込んで、 verilog-HDL で 記述されたメモリにセットするタスクである。 リスト27 に $readmemh の例を示す。 $readmemh では初期値を示したファイル名を指定し、次に二次元配列を指定する。 初期値を記したファイルはリスト27(b) のように内容を記述する。 行頭 "@" で始まる16進数で示された数値がアドレスを示し、続く16進数で示された数値が設定する値である。 各々の数値には、見やすくするために "_" を挟むことができる。 また、verilog-HDL のソースと同様にコメントを記述することもできる。

$readmemb も同様に記述できるが、初期値を二進数で記述する事のみ異なる。 (注)$readmemb でもアドレスは16進数を用いて記述する。

 リスト27 $readmemh によるメモリ初期値設定 (a) $readmemh の使用法   reg [WORD -1: 0]    mem_bank [0: LEN];    initial begin     $readmemh("../mem/mem.dat", mem_bank);   end  (b) mem.dat の内容   @0000_0000 0000_0000 // comment   @0000_0001 0000_0001 // comment   @0000_0002 0000_0002   @0000_0003 0000_0003 
このような初期設定を行うハードウエアは存在しないので合成されない。 シミュレーション時のみ有効である。

10.7.4 $random

$random() システムファンクションは 32 bit の乱数を発生する。 () 内には乱数の種を記述する。 演算器の検証等に重宝する。

10.8 Compiler directives

コンパイラディレクティブ(Compiler directives)とは シミュレーターにシミュレーション条件を渡したり、 ファイルを読み込んだりするものである。 以下に必ず使う 2 つのコンパイラディレクティブを説明する。

10.8.1 `timescale

`timescale コンパイラディレクティブはシミュレーションタイムの 単位と精度を次のように設定する。
     `timescale time_unit/time_precision 
time_unitは遅延や $time で使用される値の単位を設定する。time_unit が 10 ns である時に、#2 と書けばこれは 20 nsec の遅延であることを示す。 time_precision はシミュレータがイベントを取り扱う精度を示す。 遅延は小数点以下いくらでも記述できるが、シミュレータに渡される前に time_presision に従って丸められる。 細かくすればするほど精度は上がるがシミュレーション速度が遅くなる。 シミュレーション対象を考えて適切な精度を設定する。

10.8.2 `include

`include コンパイラディレクティブはファイルからテキストを読み込んで `include が記述された場所に埋め込むものである。 何度も使用するものを読み込む場合、一部を取り替えたりする場合、 複数のファイルを一度にコンパイラに与えたりする場合に非常に有用である。 次のように `include の後にダブルクォーテーションで 括ってファイルを指定する。
     `include "test.v" 
この時、ファイルを相対パスで記述するように心がけると、 ディレクトリを移設した場合に作業が楽である。

10.9 テストベンチ記述

ハードウエアを設計するとき、検証に多くの労力を費やす。 中でも難渋するのがテストベンチの記述である。 基本的にはひたすら量を費やすしかないが、 いくつか手間を軽減する手法があるので紹介する。

10.9.1 for 文の利用

リスト23 に挙げたように、値の取り得る範囲が狭い場合は for 文を利用して全範囲を網羅すればよい。 また、ある程度のパターンが決まった信号が繰り返すような時も便利である。

10.9.2 乱数によるテスト

例えば 32 bit 演算器の検証のような場合は、 全てを網羅するのはとても無理である。 この場合は乱数による検証を行って検証を省略する事がある。 演算器のように、乱数で得た入力に対しあらかじめ出力が予想できるような 場合に有効である。 後述する task を用いて予想結果との一致を検証するとさらに便利である。

ただし、検証を省略すると言っても、十分な数だけパターンを作って 信頼性を挙げる必要がある。 また、キャリーが生じるような特殊な場合は、 前もって別に検証しておくことが必要である。

10.9.3 ファイルの利用

信号の変化とと遅延のみを記述したファイルを別に用意し、 `include で initial ブロック中に読み込んで実行する。 複数のテストでテストモジュールを使い回すことができる。

10.9.4 プログラムを動作させる

プロセッサの検証のように複雑な場合は、 とても全ての状態を記述できるものではない。 したがって、各々の信号を追っても、 検証としては意味をなさないことが多い。

そこで、このような場合は記述したプロセッサ上で プログラムを動作させ、結果が正しいかどうかをもって検証とする。 レジスタ等の値の変化を表示していけば、 何処でおかしくなったかを知ることができ、 そこから逆算してバグの存在するモジュールを推定することができる。

10.10 task の利用

task は function と同様に手続き記述を行うものであるが、以下の点で異なる。


  • function は出力を持たない、task は 出力を持つことができる。
  • function は 1 つ以上の入力を持つ必要がある。task は入力を持たなくても良い。
  • function は 1 単位時間で実行され終了するが task は時間の記述が可能である。
  • function は他の task を呼び出せない。task は function や task を呼び出せる。
  • function は必ず一つの返値を持つ。task は返値を持たない。
このような特徴を持つため、残念ながら task は論理合成されないが、 シミュレーションには非常に有用である。 たとえば task 中でテストパターンと予想値を作り、 結果と予想値を比較するような事ができる。 さらには結果が違っていた場合にメッセージを表示したり、 シミュレーションを終了したりする事もできる。

検証を自動化することができるので、大いに利用すべきである。

10.11 force と release

シミュレーション時にのみ利用できる代入として force と release が有る。 force は assign 文と同様に記述するが、 その信号に何が繋がっていようとも、 問答無用で強制的に値を設定することができる。 この設定した値は release で信号を指定することによって、 強制力が解除され、次のイベントでは通常の動作に復帰する。


     force signal_1 = 1'b1; // signal_1 を 1'b1 に強制的に設定      release signal_1; // signal_1 の強制的な値設定を解除 
実際にはこのようなハードウエアは存在しないので論理合成できないが、 検証で一時的に値を設定するには有用である。 スキャンパスのシミュレーション時間を短縮する場合等に用いられる。

11 その他

11.2 vcd dump

このテキストのバージョンアップ時に記述
バージョン情報
ver 1.00 2005/4/13  初版作成
var 1.01 2005/4/14 目次作成, 10 節のセクション番号修正。6.3 のセクション番号修正
var 1.02 2005/4/15 tab をスペースに変換, リスト21 モジュール文末を修正
var 1.03 2005/4/19 10.6.1 $write $display において、下層モジュールを参照する書式について記述を追加
var 1.04 2005/4/20 4.8 モジュール呼び出しと、モジュール間接続 を追加。 これに伴ってリスト番号変更。5.2 変数 において、変数名の形式を追加。
var 1.05 2005/4/21 7.4 連接演算子 リスト19繰り返し演算子を訂正
var 1.06 2005/4/27 10.6 二次元配列を追加。10.7.3 $readmemb, $readmemh の記述を追加。 これに伴って節番号、リスト番号整理。
var 1.07 2006/5/9 7.4 連接演算子 リスト19繰り返し演算子を訂正
var 1.

These are our most popular posts:

Verilog Introduction - 3

この章ではVerilog-HDLで利用できる演算子とオペランドの解説とその使用方法 について述べている。 一つの式 .... この4個の演算子にはビット・ビット間処理がされ、 ビット長が異なる場合には0が挿入されて比較される。 .... ベクトル・ネットやベクトル・ レジスタの連続したビット列から選択することが出来る。 ... ところが加算により発生した キャリーはどうすべきか不明である。17ビットとして処理するか、キャリーを無視するか どうかは機能の ... read more

テキスト処理プログラミングをマスタして LSIを効率的に設計しよう (基礎 ...

として取り上げる自作Verilog HDL-VHDL変換ツールの基本. 操作について述べた. ... で公開されており,無償で広く公開されているコンパイラ. 群を使って ... に強烈で, VHDLで頻繁に使用される列挙型がVerilog. HDLには存在 ... ったいどのような意味で 使われているのか」を調べること. が重要になってきます ... 注:図1(d)では,列挙型を ワンホット・エンコーディングによる論理ベクトル型へ置換する,ということを行っている. この変換 ... read more

verilog-HDL 入門

... な基礎を説明する。 とりあえず、なにがしかのハードウエアを記述し論理合成やテスト を行えるようになることが目標である。verilog-HDL の詳細な文法については規格書や 各種参考書を見て欲しい。 ... なお、より効率的な記述を取り入れると共に、 このような不 整合を解消する試みとして Systemverilog が規格化されている。 .... モジュールが 大きい場合は、変数定義とその変数の使用場所があまり離れないように記述した方が 良い。 read more

Verilog Introduction - 4

連続アサイメントの駆動強度の最初の値はネットが1になるときに使用され、ネットの値 が0になるときに使用される。 ネットのアサイ ... ベクトル・ネットの定数ビット部分 ベクトル・ネットの定数部分 3以上の要素連結. プロシジャー型アサイメント. ネット ( ベクトル又はスカラー値) ベクトル・レジスタのビット選択 ベクトル・レジスタの定数部分 メモリ素子 4以上の要素連結 ... 連続型はゲート間の結線に特別な指示をしなくても良い ように 用意されている。 ... 下記の規則はどの遅延がどのアサイメントを制御 するかを 示している。 read more

Related Posts



0 コメント:

コメントを投稿