しかし、シミュレーション言語である Verilog を 論理合成を前提としたハードウエア記述に使っているため、 現実のハードウエアとの間に一部不整合が有る。 この点をふまえて、ハードウエア記述源として用いる場合には、 非効率なハードウエアを記述しないように注意する必要がある。
なお、より効率的な記述を取り入れると共に、
このような不整合を解消する試みとして
Systemverilog
が規格化されている。
3. Verilog によるハードウエア記述の構造
図1.に Verilog によってハードウエアを記述した場合の構造を示す。
Verilog ではモジュール(module)と呼ばれる単位によって構造を記述する。
各部品をモジュールとして記述し、
このモジュールを繋ぎ合わせることによって全体を構成する。
繋ぎ合わせる時に実際に呼び出される実態をインスタンス(instance)と言う。
このように、モジュールはハードウエアの記述を行うだけで、実態としては働かない。
例えば、同一の構造をもった演算器を複数設置する場合、
その演算器の機能を記述するのがモジュールで、演算器1、演算器2と
名前を付けて設置されるものがインスタンスである。
インスタンス中でさらに別のインスタンスを呼び出す事もできる。
(再帰的呼び出しはできない)。
注意すべきは、ハードウエア記述言語は、一般的な 手続き型プログラミングとは全く違うということである。 古典的な手続き型プログラミングでは、 関数インスタンスが多少の内部状態を持つことが有っても、 各々の関数が呼び出されない限りその働きを気にする必要は無い。 これに対し、ハードウエアでは各々の構成要素は常に動いている。 全てが並列に動いていると言い換えても良い。 どちらかと言えば、イベント駆動型プログラミングにおいて、 そこらじゅうでイベントが起こっている状態に近い。
したがって、ハードウエア記述言語では常に各々の部品の動作に気を使う必要がある。
この点においては物理的なハードウエアの性質と何ら変わることはない。
結局ハードウエア記述言語というのは、
別々に動作する部品を設計しそれらを繋ぎ合わせる作業を、
文字を用いて行うための手段である。
4. モジュール
ここでは Verilog の基本であるモジュールについて説明する。
まず、モジュールの構造の概要について述べ、
単純な 4 bit カウンタを例にモジュール内の構造を大まかに説明する。
細かい記述法については後続の節を参照して欲しい。
4.1 モジュールの基本構造
Verilog のモジュールは部品の定義を記述するものである。
モジュールの構造の概要をリスト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 [4: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'b0000; end else begin // count up counter <= count_tmp[3:0]; end end // always @ (posedge clk or negedge rst) //********************************************* endmodule // counter4
always 文を用いて組み合わせ回路を記述することもできるが、
記述が繁雑になる上に
論理合成で思わぬ結果が出ることもある。
always 文で組み合わせ回路を記述する場合は十分注意する必要が有る。
4.6 順序回路記述部
順序回路を含まないモジュールの場合は記述しない。順序回路を含む場合は、
多くの場合モジュールの最後に always 文を用いて順序回路を記述する。
always @( に続く信号名はセンシティビティリストと呼ばれる。 always 文はこのセンシティビティリストに記述された信号が変化したときのみ動作する。 posedge, negedge は各信号の立ち上がりで反応するか立ち下がりで反応するかを指定する。
always 文はセンシティビティリストに書かれた信号が変化するときに動作する文であるため、 このリストにクロックだけを書いておけば、クロックを基準に動作する順序回路を記述している事になる。 センシティビティリストにリセットも追加すれば、 クロックを基準に動作し、クロックとは非同期にリセットする事ができる順序回路になる。 論理合成を用いて大規模なディジタル LSI を設計する場合は、 順序回路を記述するセンシティビティリストにクロックとリセットのみを書いておくことが多い。
要するに普通にディジタル LSI を設計している場合には、 リセットする順序回路部分は always @(posedge clk or negedge rst) で始め、 リセットしない順序回路は always @(posedge clk) で始めると憶えれば良い。
(注)リセット信号は負論理であることが多い。
なお、一つのモジュール内にいくつでも always 文を記述することができるが、 繁雑さを防ぐために、できるだけ一モジュール内の always 文は 一つにまとめた方が良い。
(注)もし always 文のセンシティビティリストに、内部で使う全ての信号を記述するか、
アスタリスク"∗"で指定すれば、これは全ての信号に反応するので、
組み合わせ論理回路になる事が多い。
しかし、注意して書かないとラッチを生成することがあるので、
よほど慣れている人で無ければお勧めしない。
4.7 リセット
リセット信号は実に厄介なしろものである。
現在では reset 信号は clock と同様に特別な配慮をする必要があるため、
論理合成時には理想的な信号線として扱い、
レイアウト時にリセットツリーを挿入することが一般的である。
このため、適切に遅延時間を算出するのが難しく、
リセット信号を論理に用いると静的タイミング解析時に問題が生じることがある。
例えば、リセット時に定数をレジスタ変数に代入する場合は良いが、 リスト3 のようにリセット時に変数の値を代入するような場合に問題となる。 これはチップの外から初期値を取り込むような場合に生じる。
リスト3 初期値の設定でリセット信号を用いる場合 always @(posedge clk or negedge rst) begin if (~rst) // reset begin counter <= A; end else if (hoge) begin counter <= B; end end // always @ (posedge clk or negedge rst)
リスト4 リセット信号を論理から排除するための記述 reg init_s; always @(posedge clk or negedge rst) begin if (~rst) // reset begin init_s <= 1'b1; counter <= 4'b0; end else begin if (init_s) begin counter <= A; init_s <= 1'b0; end else if (hoge) begin counter <= B; end end // if (~rst) end // always @ (posedge clk or negedge rst)
Verilog におけるモジュール呼び出しとは、 関数呼び出しのような結果を返すものでは無く、 単純に部品を配置すると言う意味である。 リスト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));
リスト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
そこで、Verilog ではハードウエアに即した変数として、 配線に相当する 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 全てがハイインピーダンス
また、ビット幅を指定した場合も、できるだけ指定したビット幅全てを明示的に記述する方がよい。
もし、記述した数が指定したビット幅に満たなければ、
上位ビットは 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)
リスト9 prameter 文による定数定義 parameter byte_size = 8; parameter byte_mask = byte_size - 1; parameter [3:0] mux_selector = 0; parameter newconst = 3'h4;
リスト10 変数定義の例 reg [3:0] counter1, counter2; wire g_clock; wire [3:0] count_tmp;
値 | 意味 |
---|---|
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 を変数に設定することも無いが、 値を表示したときに意味を持つので、このように憶えておくと良い。
シミュレーション時に使用できる変数については10章の シミュレーション用記述で後述する。
リスト11 wire 宣言時の継続的代入 wire [3:0] count_out = counter; wire [3:0] init_count = 4'b0000; wire [3:0] result = a + b;
リスト12 assign 文を用いた継続的代入 wire [3:0] count_out; assign count_out = counter; wire [3:0] init_count; assign init_count = 4'b0000;
リスト13 ブロッキング代入 always @(a, b, c) begin b = a; c = b; end
リスト14 ノンブロッキング代入 always @(posedge clk) begin a <= in_data; b <= a; c <= b; end
まず、各々の代入文は、代入の準備だけを行う。 そして、センシティビティリストの信号が変化したときに、 あらかじめ準備しておいた値を代入する。 リスト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;
+ - * / | 算術演算(arithmetic) |
~ & | ^ ~^ | ビット演算(bit wise) |
& ~& | ~| ^ ~^ | リダクション演算(reduction) (注)単項演算子 |
! && || | 論理演算(logical) |
== != > >= < <= | 関係演算(relational) |
<< >> >>> | シフト演算(shift) |
? : | 条件演算(conditional) |
{} {{}} | 連接(concatenation)、繰り返し(replication) |
リスト16 リダクション演算子 wire [3:0] data; wire all_and; assign all_and = &data; // 以下と等価 // assign all_and = data[3] & data[2] & data[1] & data[0];
条件 ? 式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;
リスト18 連接演算子 wire [7:0] a, b; wire [15:0] r; assign r = {a, b};
リスト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 が入る
リスト20 if 文 例 1 if (select) begin x <= a; end else begin x <= b; end 例 2 if (select1) begin x <= a; end else if (select2) begin x <= b; end else begin x <= c; end
リスト21 に case 文の例を示す。 case 文はある条件に適合したときの動作を記述することによって結果を選択する。 ここでは各条件に付き、単に代入文一つが記述されているが、 begin ~ end で括って複数の文を記述することもできる。 最後の default: は他の条件が満たされなかった場合に実行される。 リスト21 の例の場合には全条件を網羅してあるので必要無いように思えるかも知れないが、 これは主に論理合成ツールに情報を提供するためである。
例2 の様に casex 文を用いることによって x 値を含むドントケアを表すことができる。 同様に casez 文も定義されている。 例3 は複数の条件をまとめて記述した例である。
リスト21 case 文 例1 単純なデコーダ case (select) 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) 2'b0x: result = 4'b0010; 2'b10: result = 4'b0100; 2'b11: result = 4'b1000; default: result = 4'b0000; endcase 例3 複数の条件をまとめて書いた例 case (select) 2'b00, 2'b01: result = 4'b0010; 2'b10: result = 4'b0100; 2'b11: result = 4'b1000; default: result = 4'b0000; endcase
case 文は順序を持つため、 論理合成において条件判定が多段になってしまうという厄介な問題が生じる。 各々の条件は先頭の行から解釈されて実行される。 このため条件に重複する部分があっても、それだけでは破綻を生じない。 しかし、Verilogの仕様通りに論理合成しようとすると、 先頭の文を表すセレクタを生成し、次の文を表すセレクタを生成しといった具合になる。 デコーダーは 16 bit 等かなり大きくなる場合があり得るのでこれは問題である。 もちろん、論理合成ツールが全ての条件が重複しないことを認識すれば、 並列に動作する論理回路を生成する事が可能である。 しかし、全ての条件を記述していない場合には、 並列に動作可能であるかどうかを判定できない場合がある。
そこで、すべての条件を網羅するために default の記述を必ず書くようにし、
条件も重複しないように注意した方が良い。
論理合成ツールによっては、あらかじめ並列に論理合成可能であることを
通知できるようになっているが、
これは真に並列動作可能である場合以外に使うと問題を起こす。
case 文では順序を前提とした動作はなるべく記述せず、
順序関係が存在する場合は if 文を使った方が問題が生じにくい。
なお、default の動作を記述する時結果に x 値を指定するとドントケアとなり、
回路を小さくできることが多い。
しかしこれは不定値の伝搬のような厄介な問題を引き起こし得るので、
絶対に default の部分が実行されないような場合にのみ使うべきである。
9 function
assign と条件演算子だけでは複雑な組み合わせ回路を記述することは難しい。
そこで、手続き的な記述が可能な function が設けられている。
function 文を使って記述されたものは組み合わせ論理回路として合成される。
リスト22 に function の記述例を示す。
function では、まず返り値のビット幅を指定して function 名を記述し、 次に input 文で 入力を指定する。 output の指定は無く、function 名に代入することによって返値が決定される。 従って、Verilog における function は 1 つの値しか出力できない。 入力を指定した後、case 文や if 文を使用して組み合わせ回路を記述する。
リスト22 では、例1 で case 文を用いて 4 入力セレクタを、例2 で if 文を用いて 2 の補数表現の 2 入力加算器を記述している。 function 内で中間変数を用いる場合には例2 のように、 何故かレジスタ変数を用いる。この場合のレジスタ変数は明らかに D-FF では無く、 記憶装置としては働かない。 function 内で宣言した変数は function 内ローカルになる。 同一モジュールであれば、なぜか function 内からも function 外の変数を参照できるが、 大変に判りにくい書き方なのでこれはやらない方が良い。
function を呼び出すときには、単純に a_function(引数0, 引数1, …) の様に記述すれば良い。
リスト22 function の例 例1 4入力セレクタ function [3:0] select4; input [1:0] select; input [3:0] a, b, c, d; case (select) 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
(注)
Verilog には function の他に複数の出力を可能とする
task と呼ばれる構造がある。
しかし、現在の所 task は DesignCompiler 等で論理合成できない。
task はシミュレーション時に用いられるのが一般的である。
10 シミュレーション用記述
9 節までで、ハードウエアを記述する際の Verilog の記法について説明した。
しかし、それだけではシミュレーションを行うのに極めて不便である。
ハードウエア記述時に混乱することを避けるため、
9 節までの説明では直接ハードウエアで実現しにくい表現については意識的に排除してきた。
しかし、元々シミュレーション用言語であった Verilog は、
シミュレーション用記述が充実している。
以下に、シミュレーション時に有用な記述のうち、
必要最低限のものについて説明する。
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 <= 15; i = i + 1) begin for (j = 0; j <= 15; j = j + 1) begin #(STEP); a = i; b = j; end end #(STEP * 2); $finish; end // initial always @(posedge clk) begin $write("a %h ", a); $write("b %h ", b); $display("c %h", c); $write("a %h ", add4_i.a_input); $write("b %h ", add4_i.b_input); $display("c %h", add4_i.result); end // always @ (posedge clk) endmodule // test_top
Verilog はイベントによって各処理が起動される言語であるから、
この遅延によって一定時間待つのは、遅延が記述されたブロックだけである。
他の部分は独立して動作する。
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 つが有る。
以下に簡単に特徴を述べる。
アクセスはリスト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
リスト25 $write, $display の書式 $write("フォーマット", 変数並び,,,); $display("フォーマット", 変数並び,,,);
\n | 改行 |
\t | TAB |
\\ | バックスラッシュ |
\&rdquo | ダブルクォーテーション |
%% | % |
表示形式は % に続く一文字で表される。 表4. によく使う表示形式指定を示す。 表示桁数はデータサイズによって自動的に最大桁に決定される。 十進数の場合は右詰で表示され、空いた桁にはスペースが出力される。 2進 8進 16進の場合は全ての桁が表示される。
%d | 十進数 |
%b | 二進数 |
%o | 八進数 |
%h | 十六進数 |
%t | シミュレーションタイム |
不定値やハイインピーダンスの場合の表示は独特である。 全ての信号が不定もしくはハイインピーダンスであれば、x や z が表示される。 しかし、信号の一部が不定もしくはハイインピーダンスであれば大文字の X や Z が表示される。 これが全ての表示桁に対して独立して適用される。
この表示の時は、下位モジュールの変数も参照することができる。 記述方式は次の通りで、ドット "." で区切ることによって 下位モジュール内を参照することができる。 インスタンス名を連ねていくことによって、多階層のモジュール内を 参照することができる。 この参照はモジュールの入出力ポートに対しても有効である。
リスト26 下層モジュール内の参照 インスタンス名.変数名 // 一つ下位のモジュール内 インスタンス名.インスタンス名.変数名 // 二階層下のモジュール内
$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
`timescale time_unit/time_precisiontime_unitは遅延や $time で使用される値の単位を設定する。time_unit が 10 ns である時に、#2 と書けばこれは 20 nsec の遅延であることを示す。 time_precision はシミュレータがイベントを取り扱う精度を示す。 遅延は小数点以下いくらでも記述できるが、シミュレータに渡される前に time_presision に従って丸められる。 細かくすればするほど精度は上がるがシミュレーション速度が遅くなる。 シミュレーション対象を考えて適切な精度を設定する。
`include "test.v"この時、ファイルを相対パスで記述するように心がけると、 ディレクトリを移設した場合に作業が楽である。
ただし、検証を省略すると言っても、十分な数だけパターンを作って
信頼性を上げる必要がある。
また、キャリーが生じるような特殊な場合は、
前もって別に検証しておくことが必要である。
10.9.3 ファイルの利用
信号の変化とと遅延のみを記述したファイルを別に用意し、
`include で initial ブロック中に読み込んで実行する。
複数のテストでテストモジュールを使い回すことができる。
10.9.4 プログラムを動作させる
プロセッサの検証のように複雑な場合は、
とても全ての状態を記述できるものではない。
したがって、各々の信号を追っても、
検証としては意味をなさないことが多い。
そこで、このような場合は記述したプロセッサ上で
プログラムを動作させ、結果が正しいかどうかをもって検証とする。
レジスタ等の値の変化を表示していけば、
何処でおかしくなったかを知ることができ、
そこから逆算してバグの存在するモジュールを推定することができる。
10.10 task の利用
task は function と同様に手続き記述を行うものであるが、以下の点で異なる。
検証を自動化することができるので、大いに利用すべきである。
10.11 force と release
シミュレーション時にのみ利用できる代入として force と release が有る。
force は assign 文と同様に記述するが、
その信号に何が繋がっていようとも、
問答無用で強制的に値を設定することができる。
この設定した値は release で信号を指定することによって、
強制力が解除され、次のイベントでは通常の動作に復帰する。
force signal_1 = 1'b1; // signal_1 を 1'b1 に強制的に設定 release signal_1; // signal_1 の強制的な値設定を解除実際にはこのようなハードウエアは存在しないので論理合成できないが、 検証で一時的に値を設定するには有用である。 スキャンパスのシミュレーション時間を短縮する場合等に用いられる。
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.08 2007/8/22 | リストの表示法を変更。 |
var 1.09 2020/9/19 | 一部誤解を招く表現を修正。wire 変数が代入し直せない事を追記。 |