シミュレーションとテストベンチ
合成の前にシミュレーションを行う理由、VerilogやVHDLのテストベンチが設計を駆動する仕組み、そして出力される波形の読み方を解説します。
このレッスンで学ぶこと
- シミュレーションがボード上のテストより速く安価にバグを見つけられる理由を説明できる
- クロック生成、刺激、$display、$finishを備えたVerilogテストベンチを書ける
- 刺激プロセスとアサーションを使った同等のVHDLテストベンチを書ける
- Icarus Verilogを実行し、生成されたVCDファイルをGTKWaveまたはSurferで開ける
- 波形を読み、見えているものをコード中のクロックエッジと対応付けられる
前回のレッスンでは、モジュールを書いて合成まで通しました。今回のレッスンは、本来なら毎回合成の前に行うべき工程、シミュレーションについてです。小さなカウンタのテストベンチをVerilogとVHDLの両方で書き、無料のツールで実行し、出てくる波形の読み方を学びます。入門トラックから習慣をひとつだけ持ち帰るなら、これにしてください。
そもそもなぜシミュレーションするのか
小さな設計の合成には1〜2分かかります。配置配線はもっとかかります。ボードに書き込んで指でつついて確かめるのはさらに時間がかかり、LEDがおかしな動きをしたときに、その理由を知る手がかりはほとんどありません。見えるのは出力だけで、それ以外は何も見えません。
シミュレータは、HDLをPC上のプログラムとして実行します。この規模の設計なら1秒もかからずに起動し、設計中のあらゆる信号をあらゆる時点で観察できます。バグが現れたら、誤った出力から、レジスタが誤った値を取り込んだまさにそのクロックエッジまで遡って追跡できます。LEDを見つめるのとはまったく種類の異なるデバッグです。
理由はもうひとつあります。テストベンチは、設計に期待する動作を書き留めた記録です。半年後にモジュールを変更したとき、同じテストベンチが壊していないかどうかを教えてくれます。ボードにはそれができません。
シミュレーションが教えてくれない唯一のことは、実際のチップで設計がタイミングを満たすかどうかです。それはタイミングレポートの仕事で、レッスン10で扱います。シミュレーションが確認するのは機能であって、速度ではありません。
テストベンチの構造
テストベンチ自体もHDLモジュールですが、ポートを持ちません。被試験設計(DUT)の上に位置し、DUTのポートに自身を接続して、それらを駆動します。決して合成されないため、ハードウェアとしての意味を持たない構文を使えます。遅延、時間の経過を待つループ、ファイル出力、コンソールへの表示などです。
ほぼすべてのテストベンチに4つの部分が現れます。クロック生成器は自走するクロックを作ります。刺激(スティミュラス)ブロックは、ステップの間にクロックエッジを待ちながら、決めた順序で入力を駆動します。モニタは出力を監視し、表示または検査します。そしてどこかで波形ダンプが有効にされ、実行後に結果を調べられるようにします。
被試験設計
DUTは、同期リセットとイネーブルを備えた4ビットカウンタで、レッスン04で紹介した類のものです。
// A 4-bit counter with synchronous reset and enable.
module counter4 (
input wire clk,
input wire rst,
input wire en,
output reg [3:0] count
);
always @(posedge clk) begin
if (rst)
count <= 4'd0;
else if (en)
count <= count + 4'd1;
end
endmodule
Verilogのテストベンチ
これがその完全なテストベンチです。上から下まで読んでから、下の注釈を読んでください。
`timescale 1ns / 1ps
// Testbench for counter4. Not synthesisable.
module tb_counter4;
reg clk = 1'b0;
reg rst = 1'b1;
reg en = 1'b0;
wire [3:0] count;
// Device under test.
counter4 dut (.clk(clk), .rst(rst), .en(en), .count(count));
// 100 MHz clock: toggle every 5 ns.
always #5 clk = ~clk;
// Stimulus.
initial begin
$timeformat(-9, 0, " ns", 6);
$dumpfile("counter4.vcd");
$dumpvars(0, tb_counter4);
// Hold reset for two clock edges, then release.
repeat (2) @(posedge clk);
rst <= 1'b0;
// Count for five cycles.
en <= 1'b1;
repeat (5) @(posedge clk);
// Pause, then count three more.
en <= 1'b0;
repeat (2) @(posedge clk);
en <= 1'b1;
repeat (3) @(posedge clk);
#1; // let the last edge settle before checking
if (count !== 4'd8)
$display("FAIL: expected 8, got %0d", count);
else
$display("PASS: count = %0d", count);
$finish;
end
// Print every change on the output.
always @(count)
$display("t=%0t count=%0d", $time, count);
endmodule
timescaleディレクティブは、#遅延の単位(1 ns)と、シミュレータが扱う分解能(1 ps)を設定します。これがないと、遅延はツールのデフォルトの単位になり、それはツールごとに異なります。
入力は、テストベンチが手続きブロックから代入するためregとして宣言します。出力はDUTが駆動するためwireです。
クロックは1行、always #5 clk = ~clk;だけです。このブロックには感度リストがないので永遠に動き続け、5 nsごとにclkを反転して周期10 nsを作ります。これが動くのは、clkに初期値0を与えたからにほかなりません。初期化されていないregはxで、~xはやはりxなので、クロックは決して始まりません。
刺激はinitialブロックに置きます。これは時刻0から一度だけ実行されます。鍵となるイディオムは@(posedge clk)で、次の立ち上がりエッジまでブロックします。刺激をナノ秒単位の遅延ではなくクロックエッジで記述するのは、早いうちに身につける価値のある習慣です。後でクロック周期を変えてもテストはそのまま動きます。
入力は、エッジの直後にノンブロッキング演算子<=で代入します。これは実際の上流レジスタの振る舞いを模しています。値はエッジの少し後に変わるので、DUTはすでに過ぎたエッジではなく次のエッジでそれをサンプリングします。これにより、ほぼ全員が一度は引っかかる、テストベンチとDUTの間のレースを避けられます。
$displayはコンソールに表示します。%0dはパディングなしの10進数、%0tは$timeformatに従って整形された現在時刻です。最後の検査では!=ではなく!==を使い、出力のxが「不明だが等しい」として黙って扱われるのではなく、失敗として数えられるようにしています。
$finishはシミュレーションを終了します。これがないとクロック生成器が永遠に動き続け、シミュレータは戻ってきません。
Icarus Verilogで実行する
Icarus Verilogは、Verilog-2005とSystemVerilogの有用なサブセットに対応した、無料のオープンソースシミュレータです。DUTとテストベンチを一緒にコンパイルし、結果を実行します。
iverilog -g2012 -o counter4.vvp counter4.v tb_counter4.v
vvp counter4.vvp
-g2012フラグはSystemVerilog-2012の構文を有効にします。素のVerilogには害がなく、後でlogicやalways_ffを使えるようになります。次のような出力が見えるはずです。
t=5 ns count=0
t=25 ns count=1
t=35 ns count=2
t=45 ns count=3
t=55 ns count=4
t=65 ns count=5
t=95 ns count=6
t=105 ns count=7
t=115 ns count=8
PASS: count = 8
時刻をコードと照らし合わせて読んでください。最初のエッジは5 nsで、リセットがHighなのでカウントは0になります。リセットは15 nsのエッジの後に解除され、同時にenが上がるので、最初のインクリメントは25 nsのエッジで起こります。65 nsから95 nsの間の空白は、enがLowだった2サイクルです。これらの時刻のどれかに驚いたなら、それこそシミュレータが役に立っている証拠です。
波形を見る
実行によりcounter4.vcd、つまりValue Change Dumpも書き出されました。これを読める無料のビューアが2つあります。
GTKWaveは長年使われてきた定番です。gtkwave counter4.vcdを実行し、左のツリーでtb_counter4を展開して信号を選び、Appendを押します。Surferはより新しいビューアで、インターフェースがすっきりしており、ブラウザでも動きます。どちらも同じものを表示します。x軸に時間、信号ごとに1行、バスは値のラベルが付いた帯として描かれます。
波形を見るときは、3つの点を確認してください。第一に、レジスタ出力はすべてクロックエッジでのみ変化し、その間では決して変化しないこと。第二に、変化はそれを引き起こした入力の1エッジ後に起こること。入力はエッジでサンプリングされ、出力はその後に更新されるからです。第三に、赤いものがないこと。ビューアはx(不定)を赤で、z(非駆動)を別の色で描きます。信号の先頭にある赤い帯がいつまでも消えない場合、たいていはリセットも初期値もないレジスタが原因です。
VHDLで同じテストベンチを書く
VHDLにはinitialブロックも#遅延演算子もありませんが、考え方はそのまま対応します。まずDUTです。
-- A 4-bit counter with synchronous reset and enable.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter4 is
port (
clk : in std_logic;
rst : in std_logic;
en : in std_logic;
count : out unsigned(3 downto 0)
);
end entity;
architecture rtl of counter4 is
signal count_r : unsigned(3 downto 0) := (others => '0');
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
count_r <= (others => '0');
elsif en = '1' then
count_r <= count_r + 1;
end if;
end if;
end process;
count <= count_r;
end architecture;
次にテストベンチです。
-- Testbench for counter4. Not synthesisable.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_counter4 is
end entity;
architecture sim of tb_counter4 is
constant clk_period : time := 10 ns; -- 100 MHz
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal en : std_logic := '0';
signal count : unsigned(3 downto 0);
signal done : boolean := false;
begin
-- Device under test.
dut : entity work.counter4
port map (clk => clk, rst => rst, en => en, count => count);
-- Clock runs until the stimulus process sets done.
clk <= not clk after clk_period / 2 when not done else '0';
stim : process
begin
wait for 2 * clk_period;
rst <= '0';
en <= '1';
wait for 5 * clk_period;
en <= '0';
wait for 2 * clk_period;
en <= '1';
wait for 3 * clk_period;
assert count = 8
report "FAIL: expected 8, got " & integer'image(to_integer(count))
severity error;
report "count = " & integer'image(to_integer(count));
done <= true;
wait;
end process;
monitor : process (count)
begin
report "count=" & integer'image(to_integer(count));
end process;
end architecture;
クロックは並行条件付き代入です。clkが変化するたびに再評価され、半周期後に反転値をスケジュールします。これをdoneが真になるまで続けます。この最後の部分が重要です。VHDLには$finishがなく、シミュレータはどれかの信号に変化がスケジュールされている限り動き続けます。クロックを止めることが、シミュレーションを終える方法なのです。
刺激は感度リストを持たないプロセスで、時刻0から実行されます。wait forが#の役割を果たし、最後の裸のwait;はプロセスを永遠に停止させます。severity error付きのassertはメッセージを表示して続行します。テストでは通常それが望ましい動作です。severity failureなら実行が停止します。
VHDL向けの無料シミュレータはGHDLとnvcです。nvcならフロー全体が1コマンドです。
nvc --std=2008 -a counter4.vhd tb_counter4.vhd -e tb_counter4 -r --wave=counter4.fst
-aステップがファイルを解析し、-eがトップレベルをエラボレートし、-rが実行します。--waveオプションはFSTファイルを書き出し、GTKWaveとSurferのどちらでも開けます。
テストベンチにできないこと
テストベンチは、検査しようと思いついたことしか検査しません。上のカウンタのテストは、rstとenを同時に試すことも、15から0への折り返しを確認することも、enがLowの間countが保持されることをアサートすることもありません。本物のテストが埋めるべきなのは、こうした隙間です。検証のトピックでは、自己検査型テストベンチ、ランダム化した刺激、形式検証ツールへと進みます。今のところのルールは、設計が間違っていれば失敗する検査を少なくとも1つ書くこと、そして設計をわざと壊したときに実際に失敗することを確認することです。
演習
tb_counter4.vを拡張して、15まで数え上げ、さらに2サイクル進めて、countが1に折り返したことを検査してください。次に、enがHighのままの状態でrstをアサートし、リセットが優先されることを検査してください。Icarus Verilogで実行し、VCDをGTKWaveまたはSurferで開いて、波形上で折り返しを見つけてください。最後に、DUTのリセットを非同期(always @(posedge clk or posedge rst))に変更し、波形のどの部分が動くかを見てください。
VHDLで取り組んでいる場合は、tb_counter4.vhdとnvcまたはGHDLで同じことをしてください。
まとめ
- シミュレーションはHDLをPC上で実行し、内部信号をすべて表示します。ボード上のテストより速く、はるかに多くの情報が得られますが、確認するのは機能であってタイミングではありません。
- テストベンチにはポートがありません。クロックを生成し、クロックエッジに合わせて刺激を駆動し、出力を監視し、波形をダンプします。
- Verilogでは、
always #5 clk = ~clk;がクロックを作り、@(posedge clk)がエッジを待ち、$displayが表示し、$finishが停止します。 - VHDLでは、
doneフラグ付きの条件付きクロック代入、wait forを使う刺激プロセス、assertが同じ役割を果たします。 - Icarus Verilogとnvcは無料で、入門トラック全体に十分です。GTKWaveとSurferは、それらが生成する波形ファイルを読めます。
さらに読む
- Icarus Verilogユーザーガイド
- nvc、VHDLシミュレータ
- 波形ビューアのGTKWaveとSurfer
- IEEE 1800-2017、SystemVerilog言語リファレンス
- WaveDromチュートリアル。シミュレーションの前に、期待するタイミング図を描くために