システムタスクで結果を出力する

(1)ここで学ぶ内容

概要

  • さまざまな処理ができるシステムタスクについて理解する
  • システムタスクを使ったテストベンチ記述を学ぶ

目標

  • システムタスクを理解し説明できる
  • 代表的なシステムタスクの動作を理解し説明できる
  • 代表的なシステムタスクを使ってテストベンチを記述できる

修了判定1

4ビットカウンタのテストベンチを作成する
条件:システムタスクを用いて、シミュレーション結果をファイルに出力する

⚠️ 修了判定:波形ダンプとシミュレーション制御

設問:テストベンチのトップモジュール TESTBENCH の中で、シミュレーション開始時に を用いてすべての信号を波形ファイル "wave.vcd" にダンプし、シミュレーション時間 10,000ns で して終了する initial ブロックを記述せよ。

解答例

initial begin
    // 波形ダンプの設定
    ("wave.vcd");
    (0, TESTBENCH);
    
    // 10000ns 後にシミュレーション終了
    #10000;
    ("Simulation Finished.");
    ;
end

修了判定2

4ビットカウンタをシミュレーションして、出力ファイルの内容を確認する
条件:修了判定1を先に実施する

(2)システムタスクとは

テストベンチを記述しているともっと複雑な処理も試してみたくなると思います。VerilogHDLでは、システムタスクという処理ルーチンが備わっているので複雑な処理にはこれを利用するととても便利です。シミュレータによって多くのシステムタスクがありますので、探す楽しみもあります。

ファイル読み込みの
システムタスク
$readmemh



入力ファイル
テストベンチ
入力印加 ➔
検証対象
➔ 出力観測
ファイル書き出しの
システムタスク
$fstrobe



シミュレーション結果
  • シミュレータに組み込まれているタスク
  • '$' で始まる
  • initial 文や、always 文などから呼び出す
  • ファイルアクセス、画面表示、シミュレーションの実行制御などがある
  • 細かい動作やシステムタスクの存在がシミュレータごとに異なる
  • 基本的なシステムタスクは、シミュレータ間での互換性がある

シミュレーションの実行中に外部ファイルをアクセスしたり、自由にメッセージを表示したい場合があります。システムタスクはシミュレータに組み込まれたサブルーチン集でもあります。

たとえばテストベンチの中で検証対象に与える信号があらかじめディスク上のファイルになっていたとします。

ファイル読み込みのシステムタスク $readmemh を用いてディスク上のファイルを読み込み、検証対象へ1クロックごとに与えることもできます。

また検証対象の出力のシミュレーション結果をやはりディスク上のファイルとして出力することも可能です。

ファイル書き出しのシステムタスクには $fstrobe などがあります。

このユニットで紹介するシステムタスクは基本的なものです。どのシミュレータでも同じように動作します。

(3)システムタスクの呼び出し

ここでは二つのシステムタスクを例にシステムタスクの動作とテストベンチからの呼び出しを説明します。

$readmemh ・・ メモリ(レジスタ配列)へのファイル読み込み
reg [15:0] rom_mem[0:1023];

initial begin
    $readmemh( "rom_data.hex", rom_mem );
    ...
end

rom_data.hex
( 16進数で記述された
データファイル )
rom_mem
0005C00
0015C01
0025C02
00301FF
......

$readmemh は指定したファイルをあらかじめ宣言したレジスタ配列に読み込みます。読み込むファイルは16進数で表現された文字型のファイルです。

まず16ビット幅のレジスタ配列 rom_mem を宣言します。 initial文内の先頭で $readmemh を呼び出し、レジスタ配列の rom_mem に読み込んでいます。読み込むファイルはファイル名を " でくくった、rom_data.hex です。

レジスタ配列 rom_mem には0番地から順番にデータが格納されます。

次にシミュレーション結果の画面表示です。

$display ・・ シミュレーション結果の画面表示
always
    #STEP $display( "addr=%h", addr );
画面
addr=fe00
addr=fe01
addr=fe02
addr=fe03
...

$display は画面表示のシステムタスクです。シミュレータには起動メッセージやログを表示するウィンドウがあります。このウィンドウにメッセージを表示するのは $display です。

ここではalways文をつかって、1周期ごとに $display を呼び出し、信号 addr の値を16進数で表示しています。

(4)画面表示のシステムタスク

$display( <信号名>, <信号名>, ・・ );
$display( "任意の文字列と表示フォーマット指定", <信号名>, <信号名>, ・・ );

シミュレータのメッセージ表示ウィンドウにシミュレーション結果を表示させるシステムタスクです。
代表的なシステムタスクの $display を例に説明します。

$display の後の括弧内に信号を記述すると $display が呼び出された時点での信号の値を10進数で表示します。" で囲えば任意の文字列を表示することが出来ます。

$display( "A=%b B=%h C=%d", A, B, C );

%bA に、%hB に、%dC にそれぞれ順番に対応します。

さらにこの中に表示のためのフォーマット指定を行うことが出来ます。
" の文字列の中に % と一文字でフォーマットを指定します。

ここでは A= に続いて A の値を2進数で表示します。同様に B= に続いて B を16進数で、C= に続いて C を10進数で表示します。フォーマット指定と表示する信号名は図のように順番で対応しています。順番を間違えたり過不足があると正しく表示できません。

wire [3:0] A, B, C;
A=13, B=14, C=15  のとき
画面
A=1101 B=e C=15

ABC は4ビットでそれぞれ13、14、15としたとき、この記述の実行結果はこのようになります。

フォーマット

%h16進数
%d10進数
%o8進数
%b2進数
%c文字
%s文字列

特殊文字

改行
タブ
\バックスラッシュ ( \ )
"ダブルクォート ( " )
\ddd任意文字、dは8進数

画面表示システムタスク

$display行末に改行あり
$write行末に改行なし
$monitor指定した信号に変化があれば表示する
一度呼び出されると継続的に実行する
$strobeすべてのイベント処理が終了してから表示

指定できるフォーマットには次のものがあります。フォーマット指定の文字は大文字でも小文字でも構いません。

" で囲った文字列の中には様々な特殊文字含めることが出来ます。たとえばバックスラッシュnで改行をバックスラッシュtでタブを表示できます。このほかにも表のような特殊文字を表示することが出来ます。

$display の他にもこの表に示す画面表示システムタスクがあります。
$monitor は指定した信号に変化があったときだけ表示します。また、一度呼び出されると継続的に実行されますので、繰り返し実行される記述は不要です。
$strobe はすべてのイベント処理がすんでから表示します。動作が確定してから表示しますので、表示結果が確実です。

(5)メモリ内容を読み/書きするシステムタスク

graph TD subgraph ファイルI/Oシステムタスク IN_FILE[入力ファイル
data.hex] -->|| MEM(テストベンチ内の
配列メモリ) MEM -->|データ印加| DUT[設計回路] DUT -->|出力データ| OUT(フォーマット処理
) OUT -->|| OUT_FILE[出力ファイル
result.log] end style IN_FILE fill:#e3f2fd,stroke:#1976d2; style OUT_FILE fill:#fff3e0,stroke:#f57c00; style DUT fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;

ファイル読み込み
$readmemh
$readmemb
reg [15:0] mem[0:255];

レジスタ配列
005C00
015C01
025C02
0301FF
......

ファイル書き出し
$writememh
$writememb
ファイルの読み込み
$readmemh( "filename", memname, begin_addr, end_addr ); // 16進数
$readmemb( "filename", memname, begin_addr, end_addr ); // 2進数

ファイルへの書き出し
$writememh( "filename", memname, begin_addr, end_addr ); // 16進数
$writememb( "filename", memname, begin_addr, end_addr ); // 2進数

レジスタ配列を使うと大容量のメモリを記述できます。ディスクファイルをメモリに読み込んだり、メモリ内容をディスクファイルへ書き出すことができます。
ここでは16ビット幅で256ワードのレジスタ配列 mem を宣言しています。
このレジスタ配列にデータを読み込むのが $readmemh です。これは16進数の文字で記述されたファイルを読み込むシステムタスクです。2進数で記述されたファイルを読み込む $readmemb もあります。
読み込むファイルは16進数もしくは2進数値を連続して記述したものです。コメントやアンダースコアを含んでいてもかまいません。改行やスペースが数値の区切りになります。
メモリの内容をファイルに書き出すのが $writememh です。16進数の文字ファイルで出力します。2進数での出力には $writememb を使います。

$readmemh$writememh は括弧に続いて、" で囲ったファイル名、レジスタ配列名、ビギンアドレス、エンドアドレスを記述します。 ビギンアドレス、エンドアドレスを記述すると配列の読み込みや書き出しを行うアドレスを指定できます。ビギンアドレスやエンドアドレスを省略することもできます。

(6)ファイル出力のシステムタスク

$fdisplay( <ファイル変数>, "任意の文字列と表示フォーマット", <信号名>, <信号名>, ・・ );
integer fd, i;
wire [3:0] A;

initial begin
    fd = $fopen( "data.hex" );
    for ( i=0; i<256; i=i+1 )
        #STEP $fdisplay( fd, "A=%b", A );
    $fclose( fd );
end
fd = $fopen( "ファイル名" );ファイルのオープン
$fdisplay( fd, ... );$display と同一形式で出力
$fwrite( fd, ... );$write と同一形式で出力
$fmonitor( fd, ... );$monitor と同一形式で出力
$fstrobe( fd, ... );$strobe と同一形式で出力
$fclose( fd );ファイルのクローズ

前のページで紹介したシステムタスクはメモリ内容を一括して読み書きするものでした。
ここでは画面表示のシステムタスクのようにきめ細かいフォーマット指定のできるファイル出力方法を紹介します。

画面表示システムタスクには $display がありました。これと同じ動作でそのままファイル出力できるのが $fdisplay です。これを利用するにはいくつかの手順が必要です。

まず出力するファイルを開きます。$fopen で出力するファイル名を指定します。$fopen は開いたファイルに対し固有の値をかえします。
この値をintegerか32ビットのレジスタで宣言しておいた変数に格納しておきます。この変数をファイル変数とよびます。

次に $fdisplay を使ってファイルへの書き出しを行います。$fdisplay の最初の引数にファイル変数を記述します。そのあとの引数は $display と同じです。フォーマットの指定方法も同じです。
ファイルへの出力が終了したら $fclose を使ってファイルを閉じます。

(7)値を返すシステムタスク

$timeシミュレーション時刻(64ビット)
$stimeシミュレーション時刻(32ビット)
$random乱数(32ビット)
// シミュレーション時刻
initial $monitor( $stime, " addr=%h", addr );

// 乱数
initial begin
    for ( i=0; i<100; i=i+1 )
        // 1クロックごとにdinに乱数を入力
        #STEP din = $random;
    ...
end

システムタスクには処理を行うだけでなく、値を返すものもあります。代表的なものをいくつか紹介します。
$time$stime はシミュレーション時刻を返すシステムタスクです。$time は64ビット、$stime は32ビット幅です。長時間のシミュレーションを行った場合、時刻の情報が32ビットの範囲を超えることあります。
この場合64ビット幅の $time を使うことで正確な時刻表示ができます。

$random は32ビットの乱数を返します。これらのシステムタスクは式の中で使うことが出来ます。したがってそのまま画面表示したり、代入の右辺に使うことが出来ます。

記述例を示します。
$time$stime はシミュレーション結果の表示とともに使うと便利です。
$random を検証対象への入力にそのまま使った例です。100クロックの間検証対象の入力 din に乱数を入力しています。

(8)中断/終了するシステムタスク

$stopシミュレーションを中断し対話モードへ
$finishシミュレーション終了
initial begin
    ...
    // DOUTが不定値になったらシミュレーション中断
    if ( DOUT===1'bx )
        $stop;
    
    ...
    #STEP ...
    // シミュレーション終了
    #STEP $finish;
end

シミュレータの動作を制御するシステムタスクです。シミュレーションを一時中断させる $stop と完全に終了させる $finish があります。
$stop を実行するとシミュレーションを中断して中間結果を表示したり、信号への値を設定できる対話モードになります。
プログラム言語のデバック環境で使うブレークポイントのような使い方ができます。

記述例を示します。
$stop をブレークポイントとして使っています。このため検証対象の出力 DOUT が不定値になったら $stop を実行するように if 文で判断しています。
記述の最後には $finish を実行するように記述しています。

シミュレータは信号の変化がある限りシミュレーションを続行しますので、クロックが別に記述されているとシミュレーションが終了しません。そこで $finish により強制的にシミュレーションを終了させています。

(9)ワンポイント・アドバイス

$monitorの特徴
  • 一度呼び出されると継続的に実行する
    initial $monitor( "%d A=%h", $stime, A );
  • 指定した信号に変化があれば表示する

$display と $strobe の違い

          1000                1001
            |                   |
CK _________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾
            |
 A _________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾
            |
 B ‾‾‾‾‾‾‾‾‾|___________________|____
            |
 C _________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾
            |  ↑        ↑       |
            | $display $strobe|
                            

出力例

   0  A=xxxx
1000  A=0000
2000  A=0010
3000  A=0018
7000  A=01c0
9000  A=03fe

$monitorの特徴
$monitor は一度呼び出されると継続的に実行します。通常のシステムタスクは呼び出されたときのみ実行しますが、$monitor は呼び出された後も動作が持続します。したがってinitial文で1行で記述してしまうことが出来ます。
$monitor は指定した信号に変化があったときだけ表示されます。変化がなければ表示されません。
例に示したように信号 A に変化があったときにだけ表示されますので、シミュレーション時刻がとびとびになることがあります。

$displayと$strobeの違い
論理シミュレーションでは同時に変化する信号を扱うことが出来ます。しかし、コンピュータで処理する時には処理の順番が存在します。例えばクロック CK の立ち上がりで同時に変化するする信号 ABC があったとします。
回路記述では同時変化するはずでも処理上は ABC の順に変化したとします。このときシステムタスク $display が信号 A の変化の後に呼び出されたとすると信号 BC では変化前の値を表示してしまうことになります。
一方 $strobe は呼び出される場所に関係なく、その時刻でのすべての信号が安定してから処理(表示)します。したがって信号表示につかう場合は $display より $strobe の方がよいでしょう。

(10)修了判定1

設問 記述を埋めて、4ビットカウンタのテストベンチを完成させる。
たいへん
よくでき
ました!!
4ビットカウンタ
counter
res
──
ck
──
q
──►
/
4

シミュレーション結果を、以下に示すフォーマットでファイルに出力する。
ファイル名: "counter.out"

    0 ck=0 res=0 q=x
  500 ck=1 res=0 q=x
 1000 ck=0 res=1 q=0
 10進  2進   2進  16進

module counter_test;

reg        ck, res;
wire [3:0] q;
integer    logfile;        // ファイル変数
parameter  STEP=1000;      // 1クロック周期

counter C1( ck, res, q );  // 検証対象接続

always begin               // クロックの記述
    ck = 0; #(STEP/2);
    ck = 1; #(STEP/2);
end

initial begin
               res = 0;
    #STEP      res = 1;
    #STEP      res = 0;
    #(STEP*20) $fclose( logfile ); // ファイルを閉じる
               $finish ;           // シミュレーション終了
end

initial begin
    logfile = $fopen ( "counter.out" ); // ファイルを開く
    $fmonitor( logfile, "%d ck=%b res=%b q=%h",
               $stime, ck, res, q );

end

endmodule

(11)修了判定2

設問
  • シミュレータを用いて、4ビットカウンタの動作確認を行う。
  • 波形でカウンタの動作を確認する。
  • シミュレーション後、出力ファイルの内容を確認する。
    ファイルの確認は、more コマンドを使う。
    more counter.out
counter
res
──
ck
──
q
──►
/
4
counter.out
回路記述ファイル:counter.v
テストベンチ・ファイル:counter_test.v
シミュレーション結果:counter.out

Tiny-Waves での波形表示確認

※ ツール名: Tiny-Waves for HDL Endeavor [ D:\exam\T_unit\T3_11\counter.log ]

波形表示内容 (0 〜 20000 以上の時間軸)

  • ck: 一定周期のクロック信号
  • res: 初期 (0〜1000付近) で1回だけ HIGH になり、その後 LOW
  • q[3:0] (16進数表示): x0123456789abcdef01 ... とカウントアップ。
  • q[3], q[2], q[1], q[0] (各ビット表示): 2進数として適切にカウントアップしている様子が確認できる。

ファイル出力結果 (counter.out)

    0 ck=0 res=0 q=x
  500 ck=1 res=0 q=x
 1000 ck=0 res=1 q=0
 1500 ck=1 res=1 q=0
 2000 ck=0 res=0 q=0
 2500 ck=1 res=0 q=1
 3000 ck=0 res=0 q=1
 3500 ck=1 res=0 q=2
 4000 ck=0 res=0 q=2
 4500 ck=1 res=0 q=3
 5000 ck=0 res=0 q=3
 5500 ck=1 res=0 q=4
 6000 ck=0 res=0 q=4
 6500 ck=1 res=0 q=5
 7000 ck=0 res=0 q=5
 7500 ck=1 res=0 q=6
 8000 ck=0 res=0 q=6
 8500 ck=1 res=0 q=7
 9000 ck=0 res=0 q=7
 9500 ck=1 res=0 q=8
10000 ck=0 res=0 q=8
10500 ck=1 res=0 q=9
11000 ck=0 res=0 q=9
11500 ck=1 res=0 q=a
12000 ck=0 res=0 q=a
12500 ck=1 res=0 q=b
13000 ck=0 res=0 q=b
13500 ck=1 res=0 q=c
14000 ck=0 res=0 q=c
14500 ck=1 res=0 q=d
15000 ck=0 res=0 q=d
15500 ck=1 res=0 q=e
16000 ck=0 res=0 q=e
16500 ck=1 res=0 q=f
17000 ck=0 res=0 q=f
17500 ck=1 res=0 q=0
18000 ck=0 res=0 q=0
18500 ck=1 res=0 q=1
19000 ck=0 res=0 q=1
19500 ck=1 res=0 q=2
20000 ck=0 res=0 q=2
20500 ck=1 res=0 q=3
21000 ck=0 res=0 q=3
21500 ck=1 res=0 q=4
もふねこ

お疲れさま!システムタスクの使い方、ばっちりかな?🐾
$displayや$monitorなどのタスクを使いこなせば、デバッグがずっと楽になるよ!