読み込み中...

SystemVerilogにおけるprint文の使い方と応用15選

print文 徹底解説 Verilog
この記事は約23分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラム(回路記述)の基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●SystemVerilogのprint文とは?

デジタル回路設計の世界で、SystemVerilogは非常に重要な役割を果たしています。

その中でも、print文は開発者にとって欠かせないツールとなっています。

皆さんは、コードの動作を確認する際に苦労した経験はありませんか?

print文を活用すれば、そんな悩みも解消できるかもしれません。

○print文の基本と使用方法

SystemVerilogのprint文は、プログラムの実行中に情報を表示するための機能です。

デバッグやログ出力に使われ、開発者が回路の動作を理解するのに役立ちます。

基本的な使い方は非常にシンプルで、$display$writeなどの関数を使用します。

例えば、変数の値を表示したい場合は次のように記述します。

integer x = 10;
$display("xの値は %d です", x);

実行結果

xの値は 10 です

○SystemVerilogにおける出力の種類と特徴

SystemVerilogには、様々な出力関数が用意されています。

主なものを紹介しましょう。

  1. $display -> 改行付きで出力します。
  2. $write -> 改行なしで出力します。
  3. $strobe -> シミュレーション時間が進んだ後に出力します。
  4. $monitor -> 値が変化したときに自動的に出力します。

各関数の特徴を理解することで、状況に応じた適切な出力方法を選択できます。

例えば、連続したデータを1行で表示したい場合は$writeが便利です。

一方、変数の変化を監視したい場合は$monitorが適しています。

○サンプルコード1:基本的なprint文の使用例

それでは、実際にprint文を使用したコード例を見てみましょう。

module print_example;
  integer counter;

  initial begin
    for(counter = 0; counter < 5; counter++) begin
      $display("カウンター値: %d", counter);
    end
    $write("カウント終了");
  end
endmodule

実行結果

カウンター値: 0
カウンター値: 1
カウンター値: 2
カウンター値: 3
カウンター値: 4
カウント終了

このコードでは、$displayを使ってカウンター値を1行ずつ表示し、最後に$writeで改行なしのメッセージを出力しています。

print文の基本を押さえたところで、次はより高度なテクニックに移りましょう。

データのフォーマットや桁数指定など、出力をカスタマイズする方法を学びます。

●データフォーマットと桁数指定のテクニック

プログラムの出力を見やすく、理解しやすくするためには、データのフォーマットを適切に設定することが重要です。

SystemVerilogのprint文では、C言語のprintfに似た書式指定子を使用してデータをフォーマットできます。

○サンプルコード2:printf関数でのフォーマット指定

module format_example;
  real pi = 3.14159;
  integer hex_value = 255;

  initial begin
    $display("整数: %d, 浮動小数点: %f", 42, pi);
    $display("16進数: %h, 2進数: %b", hex_value, hex_value);
  end
endmodule

実行結果

整数: 42, 浮動小数点: 3.141590
16進数: ff, 2進数: 11111111

このコードでは、異なる型のデータに対して適切なフォーマット指定子を使用しています。

%dは整数、%fは浮動小数点数、%hは16進数、%bは2進数を表します。

○サンプルコード3:桁数と左右詰めの制御

データの桁数を指定したり、左詰めや右詰めを制御したりすることで、出力をさらに見やすくできます。

module alignment_example;
  integer small_num = 42;
  integer large_num = 1234567;

  initial begin
    $display("右詰め5桁: |%5d|", small_num);
    $display("左詰め10桁: |%-10d|", large_num);
    $display("0埋め8桁: |%08d|", small_num);
  end
endmodule

実行結果

右詰め5桁: |   42|
左詰め10桁: |1234567   |
0埋め8桁: |00000042|

このコードでは、%5dで5桁の右詰め、%-10dで10桁の左詰め、%08dで8桁の0埋めを指定しています。

○サンプルコード4:string型を用いた文字列出力

SystemVerilogではstring型を使用して文字列を扱うことができます。

これを活用することで、より柔軟な文字列操作が可能になります。

module string_example;
  string message = "Hello, SystemVerilog!";
  string formatted_message;

  initial begin
    $display("元のメッセージ: %s", message);
    formatted_message = $sformatf("フォーマット済み: %s", message);
    $display(formatted_message);
  end
endmodule

実行結果

元のメッセージ: Hello, SystemVerilog!
フォーマット済み: Hello, SystemVerilog!

このコードでは、string型変数を直接出力する方法と、$sformatf関数を使用してフォーマットされた文字列を生成する方法を表しています。

○サンプルコード5:複合的なフォーマット指定

より複雑なデータ構造や多様な型が混在する場合、複合的なフォーマット指定が必要になることがあります。

次の例では、構造体とベクトルを組み合わせた出力を行います。

module complex_format_example;
  typedef struct {
    string name;
    int age;
    bit [7:0] score;
  } Student;

  Student students[2];

  initial begin
    students[0] = '{"Alice", 20, 8'hA5};
    students[1] = '{"Bob", 22, 8'h7F};

    foreach(students[i]) begin
      $display("学生%0d: 名前=%-10s 年齢=%3d 点数=0x%02h", 
               i+1, students[i].name, students[i].age, students[i].score);
    end
  end
endmodule

実行結果

学生1: 名前=Alice      年齢= 20 点数=0xA5
学生2: 名前=Bob        年齢= 22 点数=0x7F

このコードでは、構造体の各フィールドに対して適切なフォーマット指定を行っています。

名前は左詰めで10文字分のスペースを確保し、年齢は3桁で右詰め、点数は16進数で2桁に固定しています。

●変数出力とモニタリングの極意

SystemVerilogにおいて、変数の状態を把握することは設計の要です。

変数の値が予想通りに変化しているか、適切なタイミングで更新されているかを確認できれば、バグの早期発見につながります。

ここでは、変数出力とモニタリングの極意を伝授します。

○サンプルコード6:モジュール内変数の出力

モジュール内の変数を出力する方法から始めましょう。

単純そうで奥が深い技です。

module variable_output_example;
  int counter;
  logic [3:0] state;

  initial begin
    counter = 0;
    state = 4'b0000;

    repeat(5) begin
      #10 counter++;
      state = state + 1;
      $display("時刻 %0t: カウンター = %0d, 状態 = %b", $time, counter, state);
    end
  end
endmodule

実行結果

時刻 10: カウンター = 1, 状態 = 0001
時刻 20: カウンター = 2, 状態 = 0010
時刻 30: カウンター = 3, 状態 = 0011
時刻 40: カウンター = 4, 状態 = 0100
時刻 50: カウンター = 5, 状態 = 0101

このコードでは、$timeシステム関数を使用してシミュレーション時間を表示しています。

時間の経過とともに変数の変化を追跡できるため、タイミングの問題を特定しやすくなります。

○サンプルコード7:タスクを用いた変数表示

大規模なデザインでは、変数出力のコードが散在すると可読性が低下します。

タスクを使用して出力処理をまとめましょう。

module task_variable_display;
  int data_value;
  logic [7:0] control_register;

  task automatic display_variables;
    input string caller;
    $display("呼び出し元: %s", caller);
    $display("  データ値: %0d", data_value);
    $display("  制御レジスタ: %b", control_register);
  endtask

  initial begin
    data_value = 42;
    control_register = 8'hA5;
    display_variables("初期化後");

    #10;
    data_value *= 2;
    control_register = control_register << 1;
    display_variables("更新後");
  end
endmodule

実行結果

呼び出し元: 初期化後
  データ値: 42
  制御レジスタ: 10100101
呼び出し元: 更新後
  データ値: 84
  制御レジスタ: 01001010

タスクを使用することで、出力フォーマットの一貫性が保たれ、コードの保守性が向上します。

呼び出し元の情報を含めることで、デバッグ時のコンテキスト把握も容易になります。

○サンプルコード8:シミュレーション中の変数モニタリング

シミュレーション中、特定の変数の変化を常に監視したい場合があります。

$monitorシステムタスクがうってつけです。

module variable_monitoring;
  logic clk;
  logic [3:0] counter;

  always #5 clk = ~clk;

  always @(posedge clk)
    counter <= counter + 1;

  initial begin
    clk = 0;
    counter = 0;
    $monitor("時刻 %0t: クロック = %b, カウンター = %h", $time, clk, counter);
    #100 $finish;
  end
endmodule

実行結果

時刻 0: クロック = 0, カウンター = 0
時刻 5: クロック = 1, カウンター = 0
時刻 10: クロック = 0, カウンター = 1
時刻 15: クロック = 1, カウンター = 1
時刻 20: クロック = 0, カウンター = 2
時刻 25: クロック = 1, カウンター = 2
時刻 30: クロック = 0, カウンター = 3
時刻 35: クロック = 1, カウンター = 3
時刻 40: クロック = 0, カウンター = 4
...

$monitorは指定した変数の値が変化するたびに自動的に出力を行います。

クロックや状態変数の監視に非常に便利で、タイミング問題の発見に役立ちます。

変数出力とモニタリングの技を磨けば、デバッグ作業の効率は飛躍的に向上するでしょう。

次は、ファイル入出力の活用法に進みます。大量のデータを扱う際に欠かせない技術です。

●ファイル入出力の活用法

シミュレーション結果を永続化したり、大量のテストデータを読み込んだりする場合、ファイル入出力は欠かせません。

SystemVerilogには、ファイル操作のための豊富な機能が用意されています。

○サンプルコード9:fopenによるファイル操作とエラーチェック

ファイル操作の基本は、ファイルを開いて閉じることです。

エラーチェックを忘れずに。

module file_operation_example;
  int file_handle;

  initial begin
    file_handle = $fopen("output.txt", "w");
    if (file_handle == 0) begin
      $display("エラー: ファイルを開けませんでした。");
      $finish;
    end

    $fdisplay(file_handle, "シミュレーション開始時刻: %0t", $time);

    // シミュレーション処理
    #100;

    $fdisplay(file_handle, "シミュレーション終了時刻: %0t", $time);
    $fclose(file_handle);
  end
endmodule

このコードでは、$fopen関数を使用してファイルを開き、戻り値をチェックしてエラー処理を行っています。

$fdisplay関数でファイルに書き込み、最後に$fcloseでファイルを閉じています。

○サンプルコード10:readmemb/readmemhによるデータ入力

テストベンチで大量のデータを扱う場合、ファイルからデータを読み込むと便利です。

module memory_read_example;
  logic [7:0] mem [0:15];  // 16バイトのメモリ

  initial begin
    $readmemh("memory_data.hex", mem);

    for (int i = 0; i < 16; i++) begin
      $display("メモリアドレス %0d: %h", i, mem[i]);
    end
  end
endmodule

memory_data.hexファイルの内容(例)

A5 3C F0 1B 27 8D 69 E4
51 9A 0F C2 76 D8 3E B7

実行結果

メモリアドレス 0: a5
メモリアドレス 1: 3c
メモリアドレス 2: f0
メモリアドレス 3: 1b
メモリアドレス 4: 27
メモリアドレス 5: 8d
メモリアドレス 6: 69
メモリアドレス 7: e4
メモリアドレス 8: 51
メモリアドレス 9: 9a
メモリアドレス 10: 0f
メモリアドレス 11: c2
メモリアドレス 12: 76
メモリアドレス 13: d8
メモリアドレス 14: 3e
メモリアドレス 15: b7

$readmemh関数は16進数形式のファイルを読み込みます。

2進数形式の場合は$readmembを使用します。

○サンプルコード11:fdisplayを使用したファイル出力

大量のデータをファイルに出力する場合、$fdisplay関数が便利です。

module file_output_example;
  int file_handle;
  logic [31:0] data_array [0:9];

  initial begin
    file_handle = $fopen("data_output.txt", "w");
    if (file_handle == 0) begin
      $display("エラー: ファイルを開けませんでした。");
      $finish;
    end

    for (int i = 0; i < 10; i++) begin
      data_array[i] = $random;  // ランダムデータ生成
      $fdisplay(file_handle, "データ %0d: %h", i, data_array[i]);
    end

    $fclose(file_handle);
    $display("データがdata_output.txtに出力されました。");
  end
endmodule

実行結果(コンソール出力)

データがdata_output.txtに出力されました。

data_output.txtファイルの内容(例)

データ 0: 3a7f90c1
データ 1: 2e5d8b46
データ 2: 1f3c6a95
データ 3: 80d27e59
データ 4: 6b4a1c3f
データ 5: 9e7d5b2a
データ 6: 4c8f3d61
データ 7: 7a1e9d0b
データ 8: 5f2c8e74
データ 9: 0b3a6d9c

$fdisplay関数は、$displayと同様のフォーマット指定が可能で、ファイルに出力する際に便利です。

●よくあるエラーと対処法

SystemVerilogのprint文を使いこなす過程で、様々なエラーに遭遇することがあります。初心者エンジニアの方々が頭を抱えるような場面も少なくありません。ですが、ご安心ください。エラーは学びの機会です。よくあるエラーとその対処法を知っておけば、デバッグ作業がぐっと楽になります。

○フォーマット指定ミスによる出力エラー

フォーマット指定ミスは、print文使用時に最もよく見られるエラーの一つです。データ型と指定子の不一致が主な原因となります。例えば、整数型の変数に対して浮動小数点数の指定子を使用してしまうケースがあります。

誤った例:

int x = 42;
$display("x = %f", x);  // 誤ったフォーマット指定

正しい例:

int x = 42;
$display("x = %d", x);  // 正しいフォーマット指定

フォーマット指定ミスを防ぐには、変数の型を常に意識することが大切です。整数には%d、浮動小数点数には%f、文字列には%sを使用するよう心がけましょう。また、ビット幅を指定する場合は%h%bと組み合わせて使用します。

○ファイルオープン失敗時の対応

ファイル入出力操作時、ファイルのオープンに失敗することがあります。原因としては、ファイルパスの誤り、権限の問題、ディスク容量の不足などが考えられます。

エラー処理を含んだ適切なコード例:

int file_handle;
file_handle = $fopen("output.txt", "w");
if (file_handle == 0) begin
  $display("エラー: ファイルのオープンに失敗しました。");
  $finish;
end else begin
  $fdisplay(file_handle, "ファイルオープン成功");
  $fclose(file_handle);
end

ファイルオープンの失敗に対処するには、戻り値のチェックが不可欠です。$fopen関数が0を返した場合、オープンに失敗したことを意味します。エラーメッセージを表示し、適切な処理を行うことで、プログラムの安定性が向上します。

○シミュレーション時の出力タイミング問題

シミュレーション中の出力タイミングも、しばしば頭を悩ませる問題です。特に非ブロッキング代入を使用している場合、変数の更新と出力のタイミングがずれることがあります。

問題のあるコード例:

always @(posedge clk) begin
  counter <= counter + 1;
  $display("カウンター値: %d", counter);
end

このコードでは、counterの値が更新される前に$displayが実行されてしまいます。結果として、古い値が出力されることになります。

改善されたコード例:

always @(posedge clk) begin
  counter <= counter + 1;
end

always @(posedge clk) begin
  #1 $display("カウンター値: %d", counter);
end

改善後のコードでは、出力を別のalwaysブロックに移動し、わずかな遅延(#1)を加えています。こうすることで、counterの更新後の値が確実に出力されます。

タイミング問題に対処する別の方法として、$strobeシステムタスクの使用も効果的です。$strobeは現在のシミュレーション時間の最後に実行されるため、非ブロッキング代入の結果を正確に反映できます。

always @(posedge clk) begin
  counter <= counter + 1;
  $strobe("カウンター値: %d", counter);
end

●print文の高度な応用例

print文の基本を押さえたら、次は高度な応用例に挑戦してみましょう。

ここでは、デバッグ効率を劇的に向上させる技を紹介します。

○サンプルコード12:条件付き出力とデバッグレベル設定

大規模なプロジェクトでは、全ての出力を常に表示していては情報が多すぎて混乱してしまいます。

条件付き出力とデバッグレベルの設定が、そんな悩みを解決してくれます。

module debug_level_example;
  int debug_level = 1;  // デバッグレベルの設定

  task automatic debug_print(int level, string message);
    if (level <= debug_level) begin
      $display("DEBUG[%0d]: %s", level, message);
    end
  endtask

  initial begin
    debug_print(1, "重要なメッセージ");
    debug_print(2, "詳細な情報");
    debug_print(3, "超詳細な情報");

    // デバッグレベルを上げる
    debug_level = 3;
    debug_print(1, "重要なメッセージ");
    debug_print(2, "詳細な情報");
    debug_print(3, "超詳細な情報");
  end
endmodule

実行結果

DEBUG[1]: 重要なメッセージ
DEBUG[1]: 重要なメッセージ
DEBUG[2]: 詳細な情報
DEBUG[3]: 超詳細な情報

このコードでは、デバッグレベルを設定し、そのレベルに応じて出力を制御しています。

デバッグレベルを変更することで、必要な情報だけを表示できるようになります。

○サンプルコード13:出力結果を用いたパラメータ自動調整

print文は単なる出力だけでなく、システムの動的な調整にも活用できます。

出力結果を分析し、パラメータを自動調整する例を見てみましょう。

module auto_adjust_example;
  real target = 10.0;
  real current = 0.0;
  real step = 1.0;

  task automatic adjust_parameter();
    real diff = target - current;
    if (abs(diff) < 0.1) begin
      $display("目標達成: 現在値 = %f", current);
    end else begin
      current += step * sign(diff);
      $display("調整中: 現在値 = %f, 差分 = %f", current, diff);
      #10 adjust_parameter();  // 再帰的に呼び出し
    end
  endtask

  function real sign(real x);
    return (x > 0) ? 1.0 : ((x < 0) ? -1.0 : 0.0);
  endfunction

  initial begin
    adjust_parameter();
  end
endmodule

実行結果

調整中: 現在値 = 1.000000, 差分 = 10.000000
調整中: 現在値 = 2.000000, 差分 = 9.000000
調整中: 現在値 = 3.000000, 差分 = 8.000000
調整中: 現在値 = 4.000000, 差分 = 7.000000
調整中: 現在値 = 5.000000, 差分 = 6.000000
調整中: 現在値 = 6.000000, 差分 = 5.000000
調整中: 現在値 = 7.000000, 差分 = 4.000000
調整中: 現在値 = 8.000000, 差分 = 3.000000
調整中: 現在値 = 9.000000, 差分 = 2.000000
調整中: 現在値 = 10.000000, 差分 = 1.000000
目標達成: 現在値 = 10.000000

このコードでは、目標値に向けてパラメータを自動調整しています。

各ステップでの現在値と差分を出力することで、調整プロセスを可視化しています。

○サンプルコード14:大量データの効率的な出力と解析

大規模シミュレーションでは、大量のデータを効率的に出力し、後で解析する必要があります。

CSVフォーマットでの出力例を見てみましょう。

module csv_output_example;
  int file_handle;
  int data[10];

  initial begin
    file_handle = $fopen("simulation_data.csv", "w");
    if (file_handle == 0) begin
      $display("エラー: ファイルを開けませんでした。");
      $finish;
    end

    $fdisplay(file_handle, "Index,Value,Squared");

    for (int i = 0; i < 10; i++) begin
      data[i] = $urandom_range(1, 100);
      $fdisplay(file_handle, "%0d,%0d,%0d", i, data[i], data[i] * data[i]);
    end

    $fclose(file_handle);
    $display("データがsimulation_data.csvに出力されました。");
  end
endmodule

このコードは、インデックス、ランダム値、その二乗値をCSV形式で出力します。

生成されたCSVファイルは、Excelなどの表計算ソフトで簡単に解析できます。

○サンプルコード15:FPGAデザインでのprint文活用

FPGAデザインでは、ハードウェア記述言語のprint文がシミュレーション時にのみ動作し、実機では無視されることを利用して、デバッグ情報を埋め込むことができます。

module fpga_debug_example(
  input wire clk,
  input wire reset,
  input wire [7:0] data_in,
  output reg [7:0] data_out
);

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      data_out <= 8'h00;
      $display("リセット発生");  // シミュレーション時のみ出力
    end else begin
      data_out <= data_in + 1;
      $display("データ処理: 入力 = %h, 出力 = %h", data_in, data_out);  // シミュレーション時のみ出力
    end
  end

  // シンセシス時に無視される assert 文
  always @(posedge clk) begin
    assert(data_out == data_in + 1) else $error("データ処理エラー");
  end

endmodule

このコードでは、FPGAの動作をシミュレートしながら、各ステップでの入出力値を表示しています。

また、アサーション文を使用してデータ処理の正確性を確認しています。

シミュレーション時には詳細なデバッグ情報が得られますが、実機にシンセシスする際にはprint文やassert文は無視されるため、パフォーマンスに影響を与えません。

まとめ

SystemVerilogのprint文は、単なる出力機能以上の可能性を秘めています。

基本的な使い方から高度な応用例まで、様々なテクニックを紹介しました。

SystemVerilogのprint文をマスターすることは、効率的なデバッグと高品質な設計につながります。

本記事で紹介したテクニックを活用し、より洗練されたコード開発を目指しましょう。