はじめに
技術部のcpcと申します。FPGA等での回路設計や所謂IoTセキュリティをしています。
さて、FPGAへ実装を行う場合、最終的にはどこかのベンダの製品を使う訳でベンダ依存になるのは当たり前の事ではありますが、ベンダIPが密結合していない部分等で可搬性が高い構成にしたいことは多いです。 そこで今回はオープンソースツールを使ったベンダフリーな検証について書きたいと思います。
有り体に言ってしまえば「 UVM, Verilogシミュレータ, 波形ビューワ…この世にはもっと良いベンダ実装が有ったほうが良い物が沢山あります。それらをOSSのパワーでフォローします。」という事ですね。
なおVerilog系、特に断りがない場合はverilog-2005とSystemVerilog-2012に対応しているツールだけを紹介します。また、本稿でVerilogと書いた場合Verilog-HDLだけではなくSystemVerilogも含んでいると考えてください。
本稿は非常にざっくりとした紹介になってしまう為、こんな方法もあるのか程度に捉えて頂けたらと思います。
単語定義
簡便の為に本稿で使う単語の定義を書きます。
- HDL
- hardware description languageの略で、デジタル回路設計の為に作られた言語の事を指します
- DUT
- Device Under Testの略で、回路設計ではテストする対象の事を指します
- テストベンチ
- 工学分野全般での設計検証に使う仮想環境の事を指します
- テストハーネス
- 一般にはソフトウェアテストで用いられるテスト実行用のソフトウェアですが、回路設計ではシミュレータやHDLをテスト環境で動かせるようにするコードの事を指します
- ゴールデンモデル
- 回路設計作業において正しい設計とするモデルの事を指します
シミュレータの使い分け
当然ながらテスト環境を用意する為にはVerilogの解釈とシミュレートを行ってくれるシミュレータが必須です。 ここではシミュレータがどんな物があるかについて紹介します。
Verilator
Verilatorは業界人だとほぼ確実に知っているVerilogシミュレータです。公式サイトにある"the fastest Verilog/SystemVerilog simulator"という記述はまず確実にそうだろうと言えます。 直接Verilogを解釈するのではなくC++にコンパイルした上でマルチスレッドな実行環境と共にビルドするという形になっており、とにかく速いですが制約も沢山あります。 まずPower, Resetラインをチェックするのには出来ず1、同期リセット前提にする必要がある等の沢山の制約もあります。
制約に関してはFPGA開発日記というブログで有名なmsyksphinzさんの 高速Verilogシミュレータ"Verilator"で「出来ないこと」 という記事が非常に参考になるのですが、書かれた2017年からは数点変わっています。以下に大まかな変更点を記述します:
- "–timing"オプションを使うと遅延記述や wait を含めた全てのタイミング制御が行われます
- Unkown状態はサポートしないが、初期化バグを発見するための"–x-initial"や"–x-assign"オプションが追加されています
このように大きく変更されている事があるため、使うVerilatorのバージョンとそのドキュメント内Input Languagesの項目をじっくり読むと良いでしょう。
Verilatorは前述の"–timing"オプションを正しく使えばVerilog自体でテストベンチを作ることも可能ですが、基本的にはcpp(かSystemC…)を使いテストベンチを書く必要があります。 また、Verilator用のテストハーネスを書く必要があることも多々あります。 公式GitHubレポジトリにあるexamples/make_tracing_c/sim_main.cppを見てみると:
// DESCRIPTION: Verilator: Verilog example module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2017 by Wilson Snyder.
// SPDX-License-Identifier: CC0-1.0
//======================================================================
// For std::unique_ptr
#include <memory>
// Include common routines
#include <verilated.h>
// Include model header, generated from Verilating "top.v"
#include "Vtop.h"
// Legacy function required only so linking works on Cygwin and MSVC++
double sc_time_stamp() { return 0; }
int main(int argc, char** argv) {
// This is a more complicated example, please also see the simpler examples/make_hello_c.
// Prevent unused variable warnings
if (false && argc && argv) {}
// Create logs/ directory in case we have traces to put under it
Verilated::mkdir("logs");
// Construct a VerilatedContext to hold simulation time, etc.
// Multiple modules (made later below with Vtop) may share the same
// context to share time, or modules may have different contexts if
// they should be independent from each other.
// Using unique_ptr is similar to
// "VerilatedContext* contextp = new VerilatedContext" then deleting at end.
const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
// Do not instead make Vtop as a file-scope static variable, as the
// "C++ static initialization order fiasco" may cause a crash
// Set debug level, 0 is off, 9 is highest presently used
// May be overridden by commandArgs argument parsing
contextp->debug(0);
// Randomization reset policy
// May be overridden by commandArgs argument parsing
contextp->randReset(2);
// Verilator must compute traced signals
contextp->traceEverOn(true);
// Pass arguments so Verilated code can see them, e.g. $value$plusargs
// This needs to be called before you create any model
contextp->commandArgs(argc, argv);
// Construct the Verilated model, from Vtop.h generated from Verilating "top.v".
// Using unique_ptr is similar to "Vtop* top = new Vtop" then deleting at end.
// "TOP" will be the hierarchical name of the module.
const std::unique_ptr<Vtop> top{new Vtop{contextp.get(), "TOP"}};
// Set Vtop's input signals
top->reset_l = !0;
top->clk = 0;
top->in_small = 1;
top->in_quad = 0x1234;
top->in_wide[0] = 0x11111111;
top->in_wide[1] = 0x22222222;
top->in_wide[2] = 0x3;
// Simulate until $finish
while (!contextp->gotFinish()) {
// Historical note, before Verilator 4.200 Verilated::gotFinish()
// was used above in place of contextp->gotFinish().
// Most of the contextp-> calls can use Verilated:: calls instead;
// the Verilated:: versions just assume there's a single context
// being used (per thread). It's faster and clearer to use the
// newer contextp-> versions.
contextp->timeInc(1); // 1 timeprecision period passes...
// Historical note, before Verilator 4.200 a sc_time_stamp()
// function was required instead of using timeInc. Once timeInc()
// is called (with non-zero), the Verilated libraries assume the
// new API, and sc_time_stamp() will no longer work.
// Toggle a fast (time/2 period) clock
top->clk = !top->clk;
// Toggle control signals on an edge that doesn't correspond
// to where the controls are sampled; in this example we do
// this only on a negedge of clk, because we know
// reset is not sampled there.
if (!top->clk) {
if (contextp->time() > 1 && contextp->time() < 10) {
top->reset_l = !1; // Assert reset
} else {
top->reset_l = !0; // Deassert reset
}
// Assign some other inputs
top->in_quad += 0x12;
}
// Evaluate model
// (If you have multiple models being simulated in the same
// timestep then instead of eval(), call eval_step() on each, then
// eval_end_step() on each. See the manual.)
top->eval();
// Read outputs
VL_PRINTF("[%" PRId64 "] clk=%x rstl=%x iquad=%" PRIx64 " -> oquad=%" PRIx64
" owide=%x_%08x_%08x\n",
contextp->time(), top->clk, top->reset_l, top->in_quad, top->out_quad,
top->out_wide[2], top->out_wide[1], top->out_wide[0]);
}
// Final model cleanup
top->final();
// Coverage analysis (calling write only after the test is known to pass)
#if VM_COVERAGE
Verilated::mkdir("logs");
contextp->coveragep()->write("logs/coverage.dat");
#endif
// Return good completion status
// Don't use exit() or destructor won't get called
return 0;
}
といった形で、かなり使われているシミュレータだけあり情報や公開されているテストベンチ資産は充実しているほうなのですが、サンプルを見ればわかるようにかなり書き方に癖が出ます。 少しプロジェクトが大きくなれば独自ライブラリを整備する必要が出るのは想像に固くないでしょう(もしくは解読不能テストベンチ.cppが出来あがるか…)
Icarus Verilog
Icarus Verilogは恐らく近年Verilogを勉強しようとしたら使った事があるお馴染みVerilogシミュレータです。 Verilatorと同じくコンパイルする形でシミュレーション実行環境を用意しますが、こちらは強い制約も無く仕様に忠実なシミュレーションをしてくれると言えるでしょう。
iverilogは、CPIやVPIといった(本稿では意図的に書いていない)手法でもテストベンチが書けますが、基本的にはVerilog自体でテストベンチを書く事になります。
波形ビューワ
回路設計では波形は良く使うため、波形ビューワも必須と言えるでしょう。ここではオープンソースな波形ビューワを紹介します。とはいってもオープンソースな物は事実上一択ですが…
GTKWave
GTKWaveはOSSであればこれ一択としか言いようが無いデファクトの波形ビューワです。VCDフォーマットだけならば部分的に対応しているツールは多いのですが、FSTフォーマットとなるとGTKWave位しかありません。
テストベンチ生成
オープンソースツールを使ったテストベンチ生成やその手法について話していきます。オープンソースツールの話をするために少しばかし他の話もします。
Verilogベタ書き
ポータビリティや確実に動くという点でド安定です。DUTの規模が小さければ十分有力な選択肢になりますが比較的直ぐに破綻します。
Perl等の言語でVerilog生成
ある意味では後述のツールのような事を自前で行う方法です。 世の中にはテストケース全部をVerilogベタ書きすると大変だからといった理由で導入され、秘伝のタレperlスクリプトが生息しているという所があったり…
適切にメンテナンス出来れば問題がありませんが、コストがかかるので筆者としてはオススメしません。
cocotb
cocotbはPythonで書けるテストベンチ作成フレームワークです。恐らくはベンダ非依存のテストベンチツールでは最も知名度があるでしょう。Pythonで書ける為、かなりテストベンチの幅が広い事や(比較的)歴史がある為オープンなテストベンチ資産が多い方という点が魅力です。 残念ながら、CocotbはVerilatorのバージョン4.106しか対応していません2。比較的新しい方ではあるのですが、ちょっと辛い点です。
公式GitHubレポジトリからサンプルのhttps://github.com/cocotb/cocotbを抜粋すると:
# This file is public domain, it can be freely copied without restrictions.
# SPDX-License-Identifier: CC0-1.0
import os
import random
from pathlib import Path
import pytest
import cocotb
from cocotb.clock import Clock
from cocotb.runner import get_runner
from cocotb.triggers import FallingEdge
pytestmark = pytest.mark.simulator_required
@cocotb.test()
async def dff_simple_test(dut):
"""Test that d propagates to q"""
clock = Clock(dut.clk, 10, units="us") # Create a 10us period clock on port clk
cocotb.start_soon(clock.start()) # Start the clock
await FallingEdge(dut.clk) # Synchronize with the clock
for i in range(10):
val = random.randint(0, 1)
dut.d.value = val # Assign the random value val to the input port d
await FallingEdge(dut.clk)
assert dut.q.value == val, f"output q was incorrect on the {i}th cycle"
def test_simple_dff_runner():
hdl_toplevel_lang = os.getenv("HDL_TOPLEVEL_LANG", "verilog")
sim = os.getenv("SIM", "icarus")
proj_path = Path(__file__).resolve().parent
verilog_sources = []
vhdl_sources = []
if hdl_toplevel_lang == "verilog":
verilog_sources = [proj_path / "dff.sv"]
else:
vhdl_sources = [proj_path / "dff.vhdl"]
runner = get_runner(sim)()
runner.build(
verilog_sources=verilog_sources, vhdl_sources=vhdl_sources, hdl_toplevel="dff"
)
runner.test(hdl_toplevel="dff", test_module="test_dff")
if __name__ == "__main__":
test_simple_dff_runner()
といった形で比較的シンプルにテストケース等が書けます。
Chisel(FIRRTL)
ChiselはScala上のDSLとして作られたハードウェア構築言語です。FIRRTLという中間言語を活用しています。近年はRISC-Ⅴ公式実装やRISC-Ⅴチップ設計で使われているという事でに有名になっています。
何故ここにきて言語?と思われるでしょうが、ChiselはBlackBoxという機能を使いVerilogを内包出来るため、Verilogのテストハーネスやテストベンチを書くためだけのツールとしても機能します。
という事で筆者一押しのChiselを使ったVerilogテストベンチの話に入ります。 Chisel自体はiotester, ChiselTest(iotester2)という二つの公式テストツールが存在しますが、今回は開発途中ですが新しいChiselTestの方を紹介します。 ChiselTestではtreadleというFIRRTL用のシミュレータやVerilator、開発段階で制約がありますがIcarus VerilogやSynopsys VCSが使えます。
※追記[2023-04-05] 書き忘れていましたが、BlackBoxを使う場合はtreadleは使えません。使おうとするとwarningが出るだけでテストが失敗するだけという不親切な挙動なので引っかかりやすいです、注意。
まずVerilogをChisel上で使えるモジュールにするのは以下のような、BlackBoxを継承してHasBlackboxResourceトレイトが付いたIO定義付きのChiselモジュールを定義するだけで済みます。Veilogコードをインラインで書く場合はHasBlackBoxInlineトレイトとsetInline関数を使います。
class VeriModule [Conf <: RVConfig](conf: Conf) extends BlackBox with HasBlackBoxResource {
val io = IO(new Bundle {
val clock = Input(Clock())
})
setResource ("/veri_module.v")
}
この定義さえ済んでしまえば殆どChiselネイティブのモジュール同等に扱えます。つまり、ChiselTestがほぼそのまま適用できます(注意として外部に露出していない内部信号は基本的にテスト側からは見れませんが)。
では次にChiselTest自体がどのようにテストベンチを書けるかを紹介します。 公式GitHubレポジトリのtests/BasicTest.scalaを抜粋して掲載すると、テストベンチ全体はこのように書けます。
// SPDX-License-Identifier: Apache-2.0
package chiseltest.tests
import org.scalatest._
import chisel3._
import chisel3.experimental.BundleLiterals._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class BasicTest extends AnyFlatSpec with ChiselScalatestTester with Matchers {
behavior of "Testers2"
it should "test reset" in {
test(new Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
io.out := RegNext(io.in, 0.U)
}) { c =>
c.io.out.expect(0.U)
c.io.in.poke(42.U)
c.clock.step()
c.io.out.expect(42.U)
c.reset.poke(true.B)
c.io.out.expect(42.U) // sync reset not effective until next clk
c.clock.step()
c.io.out.expect(0.U)
c.clock.step()
c.io.out.expect(0.U)
c.reset.poke(false.B)
c.io.in.poke(43.U)
c.clock.step()
c.io.out.expect(43.U)
}
}
}
いかがでしょうか?cocotbに匹敵する程にシンプルかつ表現力の高いテストだと思います。
余談
波形出力について
正直な所、本稿では波形ビューワを紹介はしましたが、そもそも私はCPU畑出身で回路設計をやっている人間なので、波形ビューワを使う事はまずありません。何故かと言うと(論理合成フェイズまで行っていない)RTLで時間ユニットでの波形を出すことはコストがかかるからです。 時間ユニット単位の計算をする必要があるのは論理合成やその後の段階を詰める時であり、その段階ですらもはやベンダツールとにらめっこするべきであって波形が寄与する事は少ないです。CPU畑の人がパイプラインテストベンチ等でよくやる手法ですが、テストベンチ側やVerilog側(formatディレクティブ等)でフォーマットしたテキストを出力させるのが圧倒的に波形よりも効率的かつわかりやすいです。
Linter
記事内で紹介しているIcarus Verilogは残念ながら全く意味の無いエラーメッセージしか返さない為、活用しようとした場合には特にLintツールは重要です。
SystemVerilogであれば、chipsallianceが開発しているVeribleというSystemVerilogパーサに非常に便利なLinterやFormatter機能があります。機能豊富かつ開発も活発で非常にオススメです。 Verilog-2005の場合だと、様々なLinterが存在しますが、Verilatorに"–lint-only"オプションを渡すと事実上Linterとして機能するので、これを使うのをオススメします。
Universal Verification Methodology (UVM)
個人的には気が重いですが、検証と名打ったからには書かざるを得ません。UVMは回路設計の検証において既存検証技術の再利用性を高める事を目的として作られたSystemVerilogのクラスライブラリです。 非常に大雑把な説明をしてしまえば、業界標準となったオブジェクト指向なユニットテストツール及び紳士協定と言えるでしょう。FPGAというよりもASIC領域でよく使われる為、FPGA畑の方だと存在は知っていても現場では使わないことも多いです。
UVMは未だ著名なベンダですらサブセット対応という事が多い状況なので、オープンソースツールでも検証出来る物は知る限りcocotb+uvm-python(UVM1.2実装。今も完全対応ではなく開発中です)の組み合わせだけです。 筆者の知る限りではUVMを使った検証をする場合には、悲しいことに使うツール用のUVM記述が必要な状況と言わざるを得ません。諦めてModelsim等のベンダツール固定でやると良いでしょう。UVMの来歴を考えれば本末転倒という気もするのですが……
終わりに
知っている人からすれば常識ではありますが、こういった機会ですので少し書いてみました。 世間一般ではFPGAはニッチな分野と言えるかもしれませんが、最先端を走るハードウェア周辺セキュリティ屋には出来て当然と言える程にFPGA回路設計スキルが必要な時代になっています。 ですがソフト畑の方からすると恐らくは焼け野原と言っても良い位、IDE等の開発環境が未成熟です。せめてこういった弄りやすく情報のあるオープンソースツールを使い、効率的に開発出来ると良いなと思います。