Claude CodeでVerilogを書く — Verilatorで合否を機械判定する
波形を見られないClaude Codeに、Verilatorのテストベンチで合否を返させる検証ループの作り方です。--binaryでの実行、PASS行の判定、DUTをわざと壊す確認、CLAUDE.mdと権限の設定例を載せています。
Claude CodeにVerilogを書かせるときの勘所は、コードの生成より先に「合否を文字で返す検証」を用意することです。Claude Codeは波形ビューアを見られません。テキストで返ってくる$displayの行と、コマンドの成否だけが判断材料になります。Verilatorは--binaryでシミュレーション実行ファイルまで一気に作れるので、このループを組むのに向いています。
この記事では、小さなカウンタを題材に、テストベンチ・実行スクリプト・CLAUDE.md・権限設定を順に作ります。
前提: Verilatorの導入とバージョン確認
使うのはVerilatorとC++コンパイラ、GNU Makeです。--buildはVerilatorがツールチェーンを呼んでモデルを組み立てるオプションで、GNU Makeが使えることを必要とします。--binaryは--buildを含みます。
Ubuntuならパッケージマネージャーで入ります。
apt-get install verilator
verilator --versionディストリビューションのパッケージには最新のVerilatorがほとんど入らないため、Installationページは最新版が要るならGitからのビルドを勧めています。--binaryが使えないときは、まずverilator --versionで版を確かめます。--binaryがどの版から使えるかは、Executable and Argument Referenceに記載がありません。古い版のときは、Git版を入れ直すか、パッケージを更新します。以降の例は、最新のガイドに載っているオプションで書いています。
なぜ「合否が文字で出る」ことが要るのか
Claude Codeのベストプラクティスは、Claudeに自分の作業を検証する手段を渡すことを最初に挙げています。テストスイート、ビルドの終了コード、リンター、出力を期待値と突き合わせるスクリプトなどです。ハードウェア記述でも同じで、Claudeが読める信号がなければ「それらしいRTL」を出して終わります。
RTLの検証には、ソフトウェアのテストと違う事情があります。
- 結果の本体は波形で、人間は波形ビューアで目視する
- 波形を前提にした検証手順は、Claudeには回せない
- 合否を
$displayの1行に畳めば、Claudeは他のテスト結果と同じ扱いで読める
つまり、波形で見ていた判断をテストベンチの中の比較に移し、結果を1行にします。シミュレーションの終わりには$finishを呼びます。Verilatorの実行ファイルは、これで終了します。
Claudeに合否を返させるループ
- 1
テストベンチが自己判定する
期待値との比較をテストベンチの中に書き、最後に
TB_PASSかTB_FAILを1行で出します。 - 2
スクリプトが1コマンドで回す
Verilation、実行、PASS行の確認までを
run_tb.shにまとめます。 - 3
Claudeが出力を読んで直す
失敗行に時刻・信号名・期待値が入っているので、直す場所を絞れます。
Verilatorで1コマンド実行する
Verilatorはシミュレータというより、SystemVerilogをC++モデルに変換するコンパイラです。通常は、モデルを呼ぶC++のmainを自分で書く必要があります。--binaryを使うとこの手間が要りません。--binaryは--main --exe --build --timingの別名です。
最小の例は、VerilatorのExampleにあります。
verilator --binary -j 0 -Wall our.v
obj_dir/Vour-j 0は使えるCPUスレッドをすべて使う指定、-Wallは警告を厳しくする指定です。実行するとHello Worldに続いてour.v:2: Verilog $finishの行が出ます。出力先はobj_dir/で、実行ファイルはVに最上位モジュール名を付けた名前になります。
--timingが含まれる点は、テストベンチで効きます。#5 clk = ~clk;のような遅延でクロックを作る書き方が、そのまま使えるからです。
題材: 4ビットカウンタと自己判定テストベンチ
動作は「同期リセットで0、enが1のときだけ加算、15の次は0に戻る」です。ファイルは2つに分けます。
// counter.sv
module counter (
input logic clk,
input logic rst_n,
input logic en,
output logic [3:0] count
);
always_ff @(posedge clk) begin
if (!rst_n) count <= 4'd0;
else if (en) count <= count + 4'd1;
end
endmoduleテストベンチでは、比較をタスクにまとめます。失敗したら時刻付きで1行出し、最後にerrorsの数でTB_PASSかTB_FAILを決めます。
// tb_counter.sv
module tb_counter;
logic clk = 0;
logic rst_n = 0;
logic en = 0;
logic [3:0] count;
int errors = 0;
counter dut (.clk, .rst_n, .en, .count);
always #5 clk = ~clk;
task automatic check(input string name,
input logic [3:0] got, input logic [3:0] want);
if (got !== want) begin
$display("FAIL %s: got=%0d want=%0d t=%0t", name, got, want, $time);
errors++;
end
endtask
// 無限待ちの保険
initial begin
#1000;
$display("TB_FAIL watchdog");
$fatal(1, "watchdog timeout");
end
initial begin
repeat (2) @(posedge clk);
#1 check("reset", count, 4'd0);
rst_n = 1; en = 1;
repeat (3) @(posedge clk);
#1 check("count3", count, 4'd3);
en = 0;
repeat (2) @(posedge clk);
#1 check("hold", count, 4'd3);
en = 1;
repeat (13) @(posedge clk);
#1 check("wrap", count, 4'd0); // 3 + 13 = 16 -> 0
if (errors == 0) $display("TB_PASS");
else $display("TB_FAIL errors=%0d", errors);
$finish;
end
endmoduleウォッチドッグは、DUTのバグで待ち条件が満たされないとき、シミュレーションが永久に終わらないのを防ぎます。Claudeのセッションがコマンド待ちで固まる事態を避ける意味もあります。
$fatalや$finishは、Verilatorが対応するシステムタスクです。$finishと$stopの任意の引数は無視され、$exitは$finishの別名として扱われます。$error・$fatal・$info・$warningも「概ね対応」とされています。
合否をスクリプトで確定する
終了コードだけに頼らず、TB_PASSの行が出たかで判定します。テストベンチが自分で出す文字列を合格の条件にすれば、判定の入口が1つで済み、Verilatorの終了コードの扱いに左右されません。
#!/usr/bin/env bash
# run_tb.sh — 合格時のみ exit 0
set -euo pipefail
rm -rf obj_dir
verilator --binary -j 0 -Wall --top-module tb_counter tb_counter.sv counter.sv
obj_dir/Vtb_counter +verilator+quiet | tee sim.log
grep -q '^TB_PASS$' sim.log+verilator+quietは、終了時のシミュレーションレポート(End at simtime ...で始まる統計)を消す実行時オプションです。Claudeが読む出力を短くできます。teeでsim.logにも残すので、失敗時にgrep FAIL sim.logで絞れます。
次のように使います。
chmod +x run_tb.sh
./run_tb.sh期待どおりなら、最後にTB_PASSが1行出ます。実行の打ち切りはテストベンチ内のウォッチドッグに任せ、OS側のタイムアウトコマンドには依存させていません。
テストベンチ自体を疑う — DUTをわざと壊す
合格表示が出ても、テストベンチが何も検査していなければ意味がありません。Claudeが自分で書いたテストベンチとRTLの組を通した結果は、特にこの確認が要ります。
手順は単純で、RTLを1か所壊してFAILが出るかを見ます。
sed -i.bak "s/count + 4'd1/count + 4'd2/" counter.sv
./run_tb.sh || echo "FAILした(想定どおり)"
mv counter.sv.bak counter.svこの改変では、例えば次のような形の行が出ると想定できます(count3は加算が2倍になるため6になり、enが0のhold区間もその値を保つので2行目も落ちます。wrapは6に26を足して32になり、4ビットでは0に戻るため通ります)。実行結果そのものではなく、書式の目安です。
FAIL count3: got=6 want=3 t=46
FAIL hold: got=6 want=3 t=66
TB_FAIL errors=2壊してもTB_PASSが出るなら、テストベンチが検査を省いています。これをClaudeに依頼文として渡すと、検証の穴を自分で探してくれます。
初期値の取りこぼしを乱数で洗う
Verilatorは既定で2値のシミュレーションです。--fourstateは別オプションで、通常はXを区別しません。リセット忘れは、初期値が偶然0になって見逃されがちです。
--x-initialの既定はuniqueで、初期化されていない変数の初期値を関数で決めます。ランダム化には実行時に+verilator+rand+reset+2を指定し、+verilator+seed+<値>で乱数の種を変えます。
for s in 1 2 3 4 5; do
obj_dir/Vtb_counter +verilator+quiet +verilator+rand+reset+2 +verilator+seed+$s \
| grep -q '^TB_PASS$' || echo "seed=$s で失敗"
done失敗した種は再現に使えます。選んだ種を出力に残しておけば、同じ種で再実行できます。
変換前にlintだけ回す、assertを混ぜる
RTLを書き換えた直後は、ビルドまで進まなくても警告だけ先に見られます。--lint-onlyは、ファイルのlint違反だけを調べて、ほかの出力を作らないオプションです。-Wallと組み合わせて使えます。
verilator --lint-only -Wall --top-module counter counter.svRTLだけを渡すので、テストベンチの記述ミスと切り分けられます。Claudeに「まずlintを通してからシミュレーション」と頼むと、直す順序が安定します。
比較をassertで書きたい場合は、対応範囲を見てからにします。Verilatorは、表明と表明駆動の機能カバレッジに「部分的に」しか対応していません。このため、この記事のテストベンチはifと$displayの自前判定にしています。assertを使う場合も、合格の条件は同じTB_PASS行に集約し、assertの失敗はerrorsを増やす形にすると、判定の入口が1つで済みます。
CLAUDE.mdに固定する
ループが決まったら、毎回説明しなくて済むようCLAUDE.mdに書きます。次は一例で、公式のテンプレートではありません。
## RTL 検証
- RTL を変更したら必ず `./run_tb.sh` を実行し、`TB_PASS` が出るまで直す
- テストベンチは自己判定式にする。波形ではなく `$display` の FAIL 行で原因を特定する
- 合格表示を出すために、期待値や比較を緩めない。`-Wno-fatal` で警告を消さない
- 新しいテストを足したら、RTL を 1 か所壊して FAIL が出ることも確かめる
- 乱数初期化の確認: `+verilator+rand+reset+2 +verilator+seed+<n>` を数種類「期待値を緩めない」の一文は、Claudeが合格を急いでテストの側を書き換えるのを防ぎます。テスト自動実行の組み方は、hooksでテストを自動実行する方法に整理しています。同じ型の検証ループをCLAUDE.mdに固定する例は、SolidityとFoundryの記事にあります。
権限を絞って承認の手間を減らす
verilatorと実行ファイルだけを許可しておくと、ループが止まりません。権限ルールでは、Bash(npm run build)のようにコマンド全体を書く完全一致のほか、サブコマンドの後に*を置く書き方ができます。
{
"permissions": {
"allow": [
"Bash(verilator *)",
"Bash(./run_tb.sh)",
"Bash(obj_dir/Vtb_counter *)"
]
}
}設定の保存先は.claude/settings.jsonです。Bash(obj_dir/Vtb_counter *)は、種を変えた再実行のように引数が付く呼び出し用です。
Claudeが「終わった」と宣言しても、TB_PASSが出ていなければ止めたい場合は、Stopフックが使えます。agent hookでテストが通るまで止めない設定に、timeoutとターン数の設計をまとめています。
依頼文の型
最初の依頼には、検証の条件を含めます。
4ビットのアップカウンタを SystemVerilog で作って。
- 同期リセット(rst_n)、en が 1 のときだけ加算、15 の次は 0
- 自己判定テストベンチを書く。合格は TB_PASS を 1 行出す
- ./run_tb.sh で TB_PASS が出るまで直す
- 最後に DUT を壊して FAIL が出ることを確認して報告する「波形を見て確認して」とは頼みません。Verilatorには--traceでVCD波形を作るオプションがありますが、Claudeが判断に使う主役は文字の出力です。失敗の原因が出力だけでは分からない場合に限って、該当時刻の周辺だけ$displayを増やすほうが、トークンの消費を抑えられます。
つまずきやすい点
- 警告で止まる:
-Wallは未使用信号や幅の不一致も指摘します。直すのはRTL側で、-Wno-fatalは最後の手段です --top-moduleを忘れる: テストベンチとRTLの両方を渡すと、最上位が曖昧になります。--top-module tb_counterと明示します- ウォッチドッグの時間不足: 長いテストでは
#1000を延ばさないと、正しいDUTでもwatchdogで落ちます - 古い
obj_dir: スクリプト先頭のrm -rf obj_dirで、古いビルドを引きずらないようにしています
まとめ
Claude CodeでVerilogを扱うときの足場は、「テストベンチが自己判定し、スクリプトが1コマンドで合否を確定する」ことです。--binaryなら、C++のmainを書かずに済みます。加えて、DUTをわざと壊してFAILが出ることと、乱数初期化で複数の種を回すことを手順に入れると、見かけだけの合格を避けられます。