テストベンチの基本構造を知る
まず実験:カウンタを10項目で確かめる
テストベンチは入力を作るだけでなく、出力が期待値に合うか確認する役割を持ちます。ここでは、4ビットカウンタのリセット・ロード・カウント・保持・桁あふれを、動かせるサンプルで確認します。初めての方はAND回路の最初の実験から始められます。
回路の仕様
- clkの立ち上がりで値を更新します。
- rst_n=0で非同期に0へ戻します。
- load=1ならdataを読み込みます。loadはenableより優先します。
- load=0かつenable=1なら1増やし、15の次は0です。それ以外は保持します。
回路本体 counter4.v を見る
`timescale 1ns/1ps
`default_nettype none
module counter4 (
input wire clk,
input wire rst_n,
input wire load,
input wire enable,
input wire [3:0] data,
output reg [3:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= 4'd0;
else if (load) q <= data;
else if (enable) q <= q + 4'd1;
end
endmodule
`default_nettype wire
このクロック付きalwaysに最後のelseがないのは、フリップフロップに前の値を保持させるためです。組合せ回路の代入漏れとは区別します。
テストベンチ tb_counter4.v を見る
`timescale 1ns/1ps
`default_nettype none
module tb_counter4;
reg clk = 0;
reg rst_n, load, enable;
reg [3:0] data;
wire [3:0] q;
integer checks = 0;
counter4 dut(.clk(clk), .rst_n(rst_n), .load(load),
.enable(enable), .data(data), .q(q));
always #5 clk = ~clk;
task check;
input [3:0] expected;
begin
checks = checks + 1;
if (q !== expected)
$fatal(1, "FAIL: check=%0d q=%h expected=%h", checks, q, expected);
end
endtask
// Drive on the falling edge, then sample after the rising-edge NBA update.
// #1 is specific to this testbench's 10 ns clock and 1 ns time unit.
task cycle;
input ld, en;
input [3:0] value, expected;
begin
@(negedge clk);
load = ld; enable = en; data = value;
@(posedge clk);
#1;
check(expected);
end
endtask
initial begin
$dumpfile("counter4.vcd");
$dumpvars(0, tb_counter4);
rst_n = 1; load = 0; enable = 0; data = 0;
#1 rst_n = 0;
#1 check(0);
@(negedge clk); rst_n = 1;
cycle(0, 0, 0, 0); // hold
cycle(0, 1, 0, 1); // increment
cycle(1, 1, 14, 14); // load has priority over enable
cycle(0, 1, 0, 15);
cycle(0, 1, 0, 0); // wrap
cycle(1, 0, 7, 7); // synchronous load
cycle(0, 0, 0, 7); // hold a nonzero value
@(negedge clk); load = 1; enable = 1; data = 9;
#2 rst_n = 0; // assert reset between clock edges
#1 check(0);
@(posedge clk); #1 check(0); // reset has priority over load
$display("PASS: counter4 (%0d checks)", checks);
$finish;
end
initial begin
#500;
$fatal(1, "FAIL: testbench timeout");
end
endmodule
`default_nettype wire
実行方法
ZIP内のcounter4.vとtb_counter4.vがあるフォルダで実行します。自分の回路名に置換する必要のない完成例です。
iverilog -g2012 -Wall -s tb_counter4 -o sim.vvp counter4.v tb_counter4.v
vvp sim.vvp
Windows / Icarus Verilog 13.0で2026年10月26日に再実行し、PASS: counter4 (10 checks) と波形ファイル counter4.vcd の生成を確認しました。合成・配置配線・実機の確認は含みません。
10件の判定で見ていること
- 初期リセットで0になる
- enable=0で0を保持する
- enable=1で1増える
- loadとenableが両方1なら14をロードする
- 14から15へ増える
- 15から0へ戻る
- enable=0でもload=1なら7をロードする
- loadとenableが0なら7を保持する
- クロックの途中でリセットを入れて0になる
- リセット中はロードよりリセットが優先される
入力はクロックの立ち下がりで変更し、出力は立ち上がりの1ns後に確認しています。同じタイミングでの入力変更と回路の読み取りが競合しないようにするためです。この1nsは、この例の10ns周期・時間単位に合わせたシミュレーション上の待ち時間で、実回路の遅延を測った値ではありません。
比較には !== を使い、不定値Xも検出します。期待値と違えば $fatal で終了し、終了しない場合には500nsでタイムアウトします。PASSはこの10件に一致したことを示し、未試験の条件まで正しいことを保証するものではありません。
確認問題
カウンタ本体でenableの判定をloadより前へ移すと、何番目の判定で違いが見つかるでしょうか。
答え
4番目です。loadとenableが同時に1のとき、仕様では14をロードします。カウントを優先すると2になり、テストが失敗します。
この完成例の後に、下の章でテスト入力の選び方と階層構造を学びます。テストベンチの応用にも進めます。
(1) ここで学ぶ内容
同じ .v ファイルなのに、回路とテストベンチは何が違うのかな? まずは役割を分けて考えよう。
| 項目 | 回路記述(RTL) | テストベンチ |
|---|---|---|
| 目的 | 入力に対する回路の動作を定める | 入力を与え、出力を仕様と照合する |
| この実習のファイル | counter4.v | tb_counter4.v |
| 使う場面 | シミュレーションと論理合成 | シミュレーション |
| 時間の扱い | クロックやリセットに反応する回路を記述する | #5 などでシミュレーション時間を進める |
RTLが回路の設計図なら、テストベンチは仮想の実験装置だよ。DUT(Design Under Test)は、その実験装置につなぐ検証対象の回路を指すんだ。
- 入力を作る
クロック・リセット・データ - DUTを動かす
設計した回路 - 出力を確かめる
期待値比較・ログ・波形
この実習では回路とテストベンチを別ファイルにして、合成する対象を区別するよ。ファイルを分けること自体はVerilogの文法上の必須条件ではないんだ。#10 を書くだけで、合成後の回路に10ns待つ機能ができるわけでもないよ。たとえばVivado 2022.1の合成資料では遅延制御は無視されると説明されている。実回路での待ち時間は、クロックを数えるなど仕様に合った回路で作ろう。
目標は、入力を選び、期待値を決め、結果を自分で確かめられること。次はテスト入力の選び方を見ていこう。
(2) 動作確認のためのテスト入力1
組合せ回路には、どんな入力を与えるとよいのかな? 小さな回路なら、0と1の全組合せを確かめられるよ。
Q = (A & B) | C
| C | B | A | Q |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 0 |
| 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 0 | 1 | 1 |
| 1 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 |
この回路にはクロックやリセットはないよ。A・B・Cを変え、組合せ回路の評価後にQを真理値表と比較するんだ。
入力が増えたら、境界値と機能で選ぶ
4ビット入力2本の加算器なら2⁸ = 256通り、16ビット入力2本なら2³² = 4,294,967,296通りになるよ。全組合せが多すぎる場合は、0、最大値、桁上がり、桁あふれなど、仕様から重要な条件を選ぼう。たとえば4ビット出力の符号なし加算器なら、15 + 1の結果は下位4ビットの0になる。桁上がり出力を持つ仕様なら、その出力も別に確認する必要があるね。
少数の値が合っただけで全入力を保証したことにはならないよ。全組合せを試しても、そのRTLモデルの機能確認であり、実機のタイミングまで確認したわけではないんだ。次は、過去の値を保持する順序回路を見てみよう。
(3) 動作確認のためのテスト入力2
順序回路では、入力を与える順番も大切だよ。前のクロックで何を保持したか、追いかけられるかな?
表が画面に収まらないときは、表の中を横へスクロールして読んでね。
| 立ち上がり | 直前のDIN | 更新後FF1 | 更新後FF2 | Q |
|---|---|---|---|---|
| 1回目 | 0 | 0 | X | 0 |
| 2回目 | 0 | 0 | 0 | 0 |
| 3回目 | 1 | 1 | 0 | 0 |
| 4回目 | 1 | 1 | 1 | 1 |
| 5回目 | 0 | 0 | 1 | 0 |
| 6回目 | 0 | 0 | 0 | 0 |
この図にはリセットがないので、開始時のFF1・FF2をXとして考えるよ。DIN=0を2回取り込めば両方0になり、その後に1を2回、0を2回取り込むと、FF1・FF2は00→10→11→01→00と変わる。DINは立ち下がりなど、取り込みの立ち上がりと重ならない時刻に変更するんだ。
カウンタでは値と制御の組合せを見る
冒頭のカウンタなら、enable=1で16回進めると一周するよ。それに加えて、ロードする値、保持、リセットの極性、制御の優先順位を確認しよう。単に0〜15が現れたことと、すべての制御条件を検証したことは別なんだ。
次の表は、冒頭の10チェックを実行して得たVCDから、判定時刻の値を取り出したものだよ。シミュレーションの観測値で、FPGA実機の測定結果ではないよ。時刻の単位はすべてns、dataとqは10進数だよ。
| 判定 | 時刻 (ns) | clk | rst_n | load | enable | data | q | 確認内容 |
|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 初期リセット |
| 2 | 26 | 1 | 1 | 0 | 0 | 0 | 0 | 0の保持 |
| 3 | 36 | 1 | 1 | 0 | 1 | 0 | 1 | カウント |
| 4 | 46 | 1 | 1 | 1 | 1 | 14 | 14 | ロード優先 |
| 5 | 56 | 1 | 1 | 0 | 1 | 0 | 15 | 14→15 |
| 6 | 66 | 1 | 1 | 0 | 1 | 0 | 0 | 15→0 |
| 7 | 76 | 1 | 1 | 1 | 0 | 7 | 7 | 7をロード |
| 8 | 86 | 1 | 1 | 0 | 0 | 0 | 7 | 7の保持 |
| 9 | 93 | 0 | 0 | 1 | 1 | 9 | 0 | 非同期リセット |
| 10 | 96 | 1 | 0 | 1 | 1 | 9 | 0 | リセット優先 |
92nsでrst_nを0にすると、次の立ち上がり95nsを待たずにqが0になる。表の93nsの行でその結果を確認できるよ。立ち上がり直後の更新と、確認のための1ns待ちは分けて読もう。
値の変化を追えたら、次はどの大きさの回路を検証するか考えてみよう。
(4) 回路規模に応じたテストベンチ
小さい回路も、まとめた回路も、それぞれ確かめる意味があるよ。どの単位なら不具合の原因を見つけやすいかな?
- 小さな回路
カウンタやセレクタの仕様、境界値 - 機能ブロック
回路同士の接続、処理の順序 - 全体
複数ブロックの連携、外部との入出力
小さな回路の検証は、再利用する部品や変更した機能の確認に役立つよ。ブロックの検証は接続や制御の確認に、全体の検証は連携の確認に使うんだ。回路の行数だけで検証を省くか決めず、仕様の複雑さや変更の影響に合わせて組み合わせよう。
次は、小さな回路にもブロックにも使えるテストベンチの構造だよ。
(5) テストベンチの構造
テスト入力と判定処理は、どこに置けばよいのかな? まずは冒頭のようにDUTをテストベンチの中へ接続する形から始めよう。
| 構造 | 使い方 |
|---|---|
| 上位にテストベンチを置く | tb_counter4からcounter4 dut(...)を呼び出し、入力を操作する。 |
| 入力生成を別モジュールに分ける | 接続用の上位モジュールで入力生成部とDUTをつなぐ。外部装置の動作をモデル化したいときなどに使う。 |
| 期待値の判定を加える | どちらの構造にも追加できる。仕様から用意した期待値と出力を比較する。 |
テストベンチから複数のDUTを呼び出すこともできるよ。その場合はインスタンス名を分ける。最初はdutのように役割が分かる名前を付け、ポート名で接続すると対応を確認しやすいね。
構造が分かったら、実際のファイルの各部分を見ていこう。
(6) テストベンチのHDL記述構造
雛形を使うとき、どこを書き換える必要があるかな? 冒頭のtb_counter4.vを動く基本形として、次の順で読んでみよう。
- 時間単位と信号宣言:
`timescale 1ns/1psは時間単位1ns・精度1ps。#5は5nsになるよ。このVerilogの例では、手続きで値を与える入力側の信号をreg、DUTの出力を受ける信号をwireにしている。 - DUT接続:
.rst_n(rst_n)のように、ポート名とテストベンチ側の信号を対応させる。 - クロック生成:初期値0から5nsごとに反転するので周期10ns。初期値がXのままだと、反転してもXのままだよ。
- 入力と判定:
cycleタスクが立ち下がりで入力を変え、次の立ち上がりの1ns後にcheckを呼ぶ。 - 記録と終了:
$dumpfileと$dumpvarsでVCDを出力し、全判定が通れば$finish、不一致やタイムアウトなら$fatalで終了する。
別の回路へ使い回すときのチェック
- 信号の幅、ポート名、モジュール名を合わせる。
- クロック周期、取り込みエッジ、リセットの極性と同期/非同期を仕様で確かめる。
- 入力を変える時刻と判定時刻を決め直す。この例の1ns待ちを、別の周期へそのまま流用しない。
- 期待値は仕様から用意する。DUTの出力をそのまま期待値へコピーしない。
- ダンプ対象のトップ名と、実行コマンドの
-sを新しいテストベンチ名に合わせる。 - 試験が完了できるタイムアウトを設定する。未接続のDUTで終了メッセージが出ても、検証成功とは呼ばない。
initialとforeverでクロック初期化と反転をまとめる書き方も使えるよ。冒頭のように初期値を与えてからalways #5で反転する形も使える。大切なのは、初期値と時間の進行が明確なことだね。
次は、この基本形を使うときに見落としやすい判定処理を確かめよう。
(7) ワンポイント・アドバイス
「シミュレーションが終わった」と「試験に合格した」を区別できるかな? 表示・比較・終了を分けて考えると分かりやすいよ。
- 入力や出力を調べる
ifなら条件の真偽で分岐するcaseなら一致する値ごとに処理する
自己判定ではif (q !== expected)なら失敗として扱う。
- 繰り返し条件を確認する
- 入力 → 待機 → 判定を行う
- 次の条件へ進み、終われば終了する
クロックを待つ処理などで時間を進めよう。時間が進まない無限ループは、時間指定のタイムアウトでも止められないよ。
taskは入力操作や判定をひとまとまりにする仕組みだよ。$displayは文字を表示するだけで、正誤を判断する機能ではないんだ。期待値との比較を自分で書き、失敗時の処理につなごう。

ボクなら、テストを増やす前に「わざと壊した回路で失敗するかな?」も確かめるよ。波形は変化の理由を調べるのに、自己判定は同じ試験を繰り返すのに役立つんだ。両方を使ってみてね🐾
確認問題:検証そのものの抜けを探す
Q1:条件が成立するのを待ち続けたら?
クロックを動かしながらwait (q == 4'hf);で待つテストを考えよう。回路の不具合でqが15にならない場合、何を用意しておけば終了できるかな?
答え:別のinitialブロックにタイムアウトを置く
冒頭の例なら#500; $fatal(1, "FAIL: testbench timeout");だよ。正常終了と区別できるよう、失敗として終了させる。500nsはこの例の長さに合わせた値で、長い試験では見直そう。
Q2:立ち上がりで入力を変え、その場で判定したら?
@(posedge clk); enable = 1;の直後にqを判定する書き方には、どんな問題があるかな? DUTの出力更新には<=を使っているものとするよ。
答え:入力の競合と出力の更新待ちを分けて直す
DUTがenableを読む処理と、テスト側の代入が同じ時刻に重なり、どちらの値を読むかが実行順に依存するよ。また、そのクロックによる<=の更新前にqを読んでしまう。冒頭のcycleのように入力を立ち下がりで変更し、立ち上がり後に判定しよう。判定を別のalways @(posedge clk)へ移すだけでは、更新後に読める保証にはならないんだ。
Q3:前から入っていた値と一致しただけなら?
q=7の状態で、data=7をロードする試験を1回だけ行い、qが7ならPASSにしたよ。ロードを無視する不具合を見つけられるかな?
答え:見つけられないので、異なる値への更新を試す
ロードしなくてもqが7のままなら合格してしまうね。保持中の値と違う値をロードし、仕様で決まるクロック後に確認しよう。さらにloadとenableを同時に1にする試験なら、優先順位も調べられる。===や!==だけで、試験条件の不足は補えないよ。
3問を説明できたら、カウンタの試験を自分で増やしてみよう。
(8) 修了判定1
冒頭の10件を出発点に、連続したカウントと全ロード値を確かめられるかな? 回路は同じcounter4.vを使い、rst_n=0でリセットする仕様にそろえるよ。
- リセット後、enable=1・load=0で18回連続して数える。1〜15→0→1→2を毎回比較する。
- load=1・enable=1で0〜15を順にロードし、毎回比較する。
- 15を保持した後、クロック途中でリセットして全4ビットが0になることを確かめる。
- リセット中のロードよりリセットが優先すること、解除後の保持とカウント再開を確かめる。
期待値との比較とタイムアウトも入れよう。各試験のタイミングをタスクにまとめると、繰り返しの条件を読みやすくできるよ。
解答例:tb_counter4_extended.v(40チェック)
`timescale 1ns/1ps
`default_nettype none
module tb_counter4_extended;
reg clk = 0;
reg rst_n, load, enable;
reg [3:0] data;
wire [3:0] q;
integer checks = 0;
integer i;
counter4 dut(.clk(clk), .rst_n(rst_n), .load(load),
.enable(enable), .data(data), .q(q));
always #5 clk = ~clk;
task check;
input [3:0] expected;
begin
checks = checks + 1;
if (q !== expected)
$fatal(1, "FAIL: check=%0d q=%h expected=%h", checks, q, expected);
end
endtask
task cycle;
input ld, en;
input [3:0] value, expected;
begin
@(negedge clk);
load = ld; enable = en; data = value;
@(posedge clk); #1;
check(expected);
end
endtask
initial begin
$dumpfile("counter4_extended.vcd");
$dumpvars(0, tb_counter4_extended);
rst_n = 1; load = 0; enable = 0; data = 0;
#1 rst_n = 0;
#1 check(0);
@(negedge clk); rst_n = 1;
// 18 consecutive increments: includes 15 -> 0 -> 1 -> 2.
for (i = 1; i <= 18; i = i + 1)
cycle(0, 1, 0, i % 16);
// Load every 4-bit value; load must win over enable.
for (i = 0; i < 16; i = i + 1)
cycle(1, 1, i[3:0], i[3:0]);
cycle(0, 0, 0, 15);
// Reset all four asserted bits between active clock edges.
@(negedge clk); load = 1; enable = 1; data = 9;
#2 rst_n = 0;
#1 check(0);
@(posedge clk); #1 check(0);
@(negedge clk); load = 0; enable = 0; rst_n = 1;
@(posedge clk); #1 check(0);
cycle(0, 1, 0, 1);
$display("PASS: counter4 extended (%0d checks)", checks);
$finish;
end
initial begin
#2000;
$fatal(1, "FAIL: extended testbench timeout");
end
endmodule
`default_nettype wire
入門10チェック・修了40チェックと回路本体をまとめてダウンロード
40件は、この入力列と観測時刻での判定数だよ。すべての入力履歴を網羅した数ではないんだ。次は自分のPCで実行して、結果を読んでみよう。
(9) 修了判定2
作った試験は本当に不具合を見つけられるかな? まず正常な回路を動かし、その後で意図的に変更したコピーを比べてみよう。
修了判定の実行
ZIPを展開し、counter4.vとtb_counter4_extended.vのあるフォルダで実行してね。冒頭の入門例とはトップモジュール名が違うよ。
iverilog -g2012 -Wall -s tb_counter4_extended -o extended.vvp counter4.v tb_counter4_extended.v
vvp extended.vvp
成功時の判定行は次のとおりだよ。
PASS: counter4 extended (40 checks)
2026年10月26日にWindows / Icarus Verilog 13.0で40チェックの成功とcounter4_extended.vcdの生成を確認したよ。合成・FPGA実機・GTKWave画面の確認は含まないよ。
波形ファイルで見るポイント
GTKWaveを使える環境ならgtkwave counter4_extended.vcdで開き、clk・rst_n・load・enable・data・qを追加しよう。qとdataは10進表示か16進表示にそろえると読みやすいよ。
- qが15→0へ折り返す立ち上がり。
- load=1の立ち上がりで、カウントよりdataの取り込みが優先されること。
- rst_nが0になった時刻で、次の立ち上がりを待たずqが0になること。
わざと優先順位を変えて試す
別フォルダにコピーしたcounter4.vで、loadとenableの分岐順を入れ替えてみよう。冒頭の入門テストをもう一度コンパイルして実行すると、4件目が次のように失敗するよ。正しい回路のファイルは残しておいてね。
FAIL: check=4 q=2 expected=e
eは16進数の14だよ。直前の1を数えて2にしてしまい、仕様の「14をロードする」と違うことが分かるね。この変更で失敗することも、2026年10月26日に確認したよ。
PASSの文字だけで判断せず、どの仕様を何件確かめたかも記録しよう。文法を詳しく学ぶにはテストベンチ専用の文法、試験の組み立てを深めるにはテストベンチの応用へ進んでね。