はじめに
え、FPGAの検証環境まだSystemVerilogで書いてんの?もっとモダンな検証環境にしないと
(ひよっこがすみません、調子乗りました)
はい、気を取り直して今回は開発じゃないネタになります。
タイトルにもある通り、cocotb(python)を使って検証をしよう!です。
といってもcocotbってなに?python知らんし、、、という方もいると思いますので一緒に勉強していきましょう。
※今回はpythonを使いますがハードウェアの方でもなるべくわかるように説明するつもりですので安心してください
開発環境
cocotb : 1.8.0
言語 : SystemVerilog,python(3.11.1)
シミュレーションツール : Icarus Verilog(iverilog)
波形表示ツール : gtkwave v3.3.104
ホストマシン : macOS(Ventura 13.3.1)
仮想マシン : VirtualBox7.0(Ubuntu 22.04.2)
cocotbとは
cocotbとはpythonのフレームワークを使用した検証環境になります。
https://docs.cocotb.org/en/stable/index.html
要は今まで検証をする場合はSystemVerilogで検証環境を構築していたけど、
代わりにpythonを使って便利に簡単に検証をしよう!というのが狙いです。
ただ、私は便利に?簡単に?と疑問に思います。というのもですね、
FPGAって今はまだハードウェアの方がやるイメージ(というか実際そう)なので、
ハードウェアの方からしたらpython?なにそれ?おいしいの?状態だと思います。
pythonはプログラミング言語の中では比較的簡単な言語なので覚えやすいですが、
それはあくまでソフトウェアの方の目線に過ぎないのかなと思います。
(まぁどこまで覚えるのかにもよりますが)
そもそも検証でpythonを使うメリットですが、おそらく以下になるかと思います。
- ・ライブラリが豊富なので色々な処理が簡単にできる
- 例えば、結果をcsvに保存したい場合などは保存用の便利なライブラリがあるので簡単にできる
- ・少ない記述量で色々なこと、複雑なこと(pythonらしい)ができる
- 例えば、640×480の画像データを用意したい場合、以下の一行で済みます。
- という感じに簡単に大量のデータも用意できます。
- ・↑つまり検証工数を減らすことができる
- 上記2項目に挙げた通り、「便利に簡単に色々なことができる🟰工数削減」
- 必然的にこうなりますね。
[[(j - 255) if j > 255 else j for j in range(640)] for i in range(480)]
メリットはありますが、ある程度難易度があるのかなと思ったりもします。
まぁなるべくわかりやすいように伝えていければと。
インストール方法
■macの場合
まずpythonをインストールしていない方はpythonをインストールしてください。
と言ってもmacの場合は標準でpythonが入っているはずなのでターミナルを開いて
以下のコマンドで確認してみてください。ver情報が返ってきたら入ってます。
python --version
※最新のverが欲しいなどあれば以下の方法で入れてください。
ただし、homebrewやpyenvを使用しているのでちょっとだけややこしいかも...
①ターミナルで以下のコマンド入力
pip install cocotb
②ターミナルで以下のコマンド入力しcocotbが入っていることを確認
pip freeze
■windowsの場合
色々やり方があるのですが今回はminiconda上で構築していきます。
(他にもMSYS2やCygwinで構築する方法もあります)
※minicondaとはpythonの開発環境の一つと思っていただければよいです
上位互換としてanacondaもあります
➀minicondaをインストール
インストール手順は以下を参照してください。使い方もちょこっと載っています。
➁make関連を入れるため以下のコマンドを入力
conda install -c msys2 m2-base m2-make
➂cocotbを入れるため以下のコマンドを入力
pip install cocotb
pythonの書き方
軽く(最低限)pythonの文法を説明します。
- ・インデント(空白)
- pythonはインデントによって処理を切り分けています。
- なのでインデントを合わせないとエラーが出ます。
- )例
- 例えば上の例だと処理3だけインデントが2個入っているのでエラーが出ます。
- 同じ流れの処理にしたければ処理3のインデントを削除してください。
- ・関数の書き方
- そもそも関数とは処理をひとまとめにしたものを関数と言います。
- verilogで言うところのタスクやファンクションがそれに当たります。
- 注意点としては処理の前には必ずインデントを設けてください。
- 設けないとエラーが出ます。
- ・変数宣言の書き方
- pythonの場合変数の型は動的に決まります。
- verilogやSystemVerilogではreg、integer、realなどを宣言時に記載しますが、
- pythonの場合それらは不要です。
- ※ただし、静的に記述も可能です
- 上記みたいに使用する際に直接記述する感じになります。
- また、関数の引数を宣言する際も型は不要です。
処理1
処理2
処理3
処理4
def xxx(引数):
処理...
data = in_data
en = in_en
以上がpythonの文法になります。
他にも色々ありますが、今回はそこまでごちゃごちゃしないので上記を知っていれば
とりあえずは問題ないと思います。
使い方
まず検証対象モジュールを用意してください。
今回は前回の記事に載せたカメラ処理の検証環境を例で使います。
sim/camera
の中のscenario.svを使用します。
まずはこのシナリオがどのような動作をしているかというと以下です。
- インプット
vsyncをアサートした後にhrefをアサートする。
その後pixdataを入力する。 - アウトプット
pclkからvfd_clkを生成し、出力する。
そのクロックに合わせて、vfd_dataを出力する。
というカメラ入力のシナリオとなります。
ではこのシナリオをcocotbを使用して構築していきましょう!
Pythonファイル作成
①まずはpythonファイルを作成する
拡張子を「.py」にするとpythonのファイルになります。
今回名前は「scenario.py」にします。
②①で作った「scenario.py」を開きcocotbをインクルードする
インクルードしないとcocotbが使用できないので先頭にインクルード宣言を記載します。
外部ライブラリを使用する場合verilogなどはinculdeになりますが、
pythonではimportになります。
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import Timer, RisingEdge
”from xxx import xxx”の書き方は特定の関数のみを引っ張ってきたい場合にこのような
書き方をします。
③定数の定義をする
pythonの場合、定数という概念がないです。
なのでverilgoみたいにlocalparamやparamaterという感じに宣言できません。
普通に変数を宣言し、固定値を入れる感じになります。
今回は以下の3つを使用するので記載します。
※一応定数だとわかるように全て大文字にするという暗黙ルールがあります
# define
HMAX = 10 # ←Hsyncの期間です
VMAX = 5 # ←Vsyncの期間です
RESET_TIME_NS = 20 # ←リセット時間設定用です
④テストシナリオのメインを記載する
cocotbの場合、以下の宣言をした関数がメイン関数になります。
関数の引数のdutはテスト対象モジュールのことです。
※実際のモジュール名じゃなくていいの?と思われると思いますが、
これはcocotbが内部的に変換しているためです
※asyncはコルーチンを定義しています(コルーチンが何かは以下を参照してください)
@cocotb.test()
async def xxx(dut)
ということで以下がメイン処理になります。
※今回は期待値比較をしていません(これは別記事で書ければと)
メイン処理
@cocotb.test()
async def tb_scenario(dut):
_dut = dut # dut変数を_dutにコピーしています
# Wait 20ns
await init(_dut, RESET_TIME_NS) # init関数が完了するまで待ちます
# Generate clock 25MHz
clk = Clock(dut.clk, 40, units="ns") # 25MHzを生成します
# Start scenario
cocotb.start_soon(clk.start(start_high=False)) # 始まるよの合図
# Wait 10us
await Timer(10, units="us") # 10usの待ち時間
pixdata = await generate_pixdata(HMAX) # インプットデータ(配列)を作成
for _ in range(VMAX): # for文でVMAX回分回します
await RisingEdge(dut.clk) # dut.clkの立ち上がりを検知
# Enable Vsync
dut.vsync.value = 1 # Vsyncに1を代入
await RisingEdge(dut.clk)
# Insert TestBench-data into dut module
for data in pixdata: # for文でpixdataを回します
# Enable Href
dut.href.value = 1 # Vsyncに1を代入
dut.data_i.value = data # pixdata要素をdutに代入
await RisingEdge(dut.clk)
dut._log.info("vfb_data: 0x%x", dut.vfb_data.value) # ログ表示
# Disable Href and data
dut.href.value = 0
await RisingEdge(dut.clk)
# Disable Vsync
dut.vsync.value = 0
んん?わからんぞ?と思った方、大丈夫です。
以下のポイントがあるので合わせて読んでみてください。
- ・dutのアクセス方法
- dut内の信号へのアクセスは「.」を使用してアクセスします。
- 例えばdut内にclkがあった場合、「dut.clk.value」とすればアクセスできます。
- ※注意点として.valueを最後につけないと信号の値にはアクセスできません
- ・await
- すごい簡単に言うと待ちを意味します。(上のリンク先に説明があります)
- 「await xxx」を書けばxxxが終わるまで次の処理には進みません。
- なぜこのようなことをするかと言うと、待ちを入れないとpythonの処理
- の方が先に終わってしまうためです。
- ・Clock関数
- クロックを生成する関数になります。
- 第一引数はメインクロックを指定。第二引数と第三引数で時間を指定。
- 戻り値は後で使うので必要です。
- ・cocotb.start_soon(clk.start(start_high=False))
- Clock関数のオブジェクトを入れて、「.start()」を使用することにより開始します。
- 引数のstart_highは開始クロックを1からスタートするかどうかです。
- ・Timer関数
- 引数で指定した時間待ちを生成します。
- ・RisingEdge関数
- 引数で指定しクロックの立ち上がりを検知します。
- ※FallingEdgeもあります。その場合importにFallingEdgeを追記してください。
- ・for文の読み方
- pythonの場合「for x in range(x)」と記載します。
- 10回ループを書く場合
- ■SystemVerilogの場合
- ■pythonの場合
- という感じになります。それと配列の場合は以下の書き方になります。
- で、後説明していない箇所が二つ。init関数とgenerate_pixdata関数です。
- init関数の中身は以下になります。
- これは初期化しかしていないので特に説明はなしでいいかと。
- 次にgenerate_pixdata関数は以下です。
- なんだこの書き方は??と思った方、これはpythonならではの書き方になります。
- わかりやすいように書き直すと以下になります。
- ※内包表記という書き方です。カッコつけたい方はこのような書き方をするのもあり。
for (int j=0; j<10; ++j)
for i in range(10): # iが自動的にカウントアップしてくれる
array_data = [10,20,30,40,50]
for data in array_data:
data #1回目は10が入っている。2回目は20、3回目は30、4回目は40、5回目は50
async def init(_dut, duration_time):
_dut.rst_n.value = 0
_dut.vsync.value = 0
_dut.href.value = 0
_dut.data_i.value = 0
await Timer(duration_time, units="ns")
_dut.rst_n.value = 1
async def generate_pixdata(hmax):
# Generate 0 to 9 array data
pixdata = [data for data in range(hmax)]
return pixdata
async def generate_pixdata(hmax):
# Generate 0 to 9 array data
for data in range(hmax):
pixdata = data
return pixdata
これでpythonの実装は終わりです。
やったー!これで実施できると思わせといてまだです...
最後に大事なMakefileを作る必要があります。
えー、まだあんの...って思う気持ちもわかりますが、これはすぐ終わります。
Makefile作成
早速ですが以下が中身です。
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
VERILOG_SOURCES += ../../src/camera/camera_top.sv
VERILOG_SOURCES += ../../src/camera/camera_if.sv
VERILOG_SOURCES += ../../src/camera/camera_pg.sv
TOPLEVEL = camera_top
MODULE = scenario
include $(shell cocotb-config --makefiles)/Makefile.sim
説明します。
- SIM
→シミュレーションツールを指定する
ModelsimDEやVerilatorも使えます - TOPLEVEL_LANG
→使用言語を指定する - VERILOG_SOURCES
→対象のRTLを記載する - TOPLEVEL
→対象RTLのtopモジュールを指定する - MODULE
→pythonファイルを記載する(関数名とかではなくファイル名です)
最後のinclude文は必須です。これを契機に処理(make)が走ります。
これでシミュレーション実施までの準備が整った。
では実施していこう。以下のコマンドを入力してください。
make #オプションでWAVES=1にすると波形保存、GUI=1にするとGUI表示できます
シミュレーション結果
ということで以下の波形が出力されました。SyetemVerilogと同じですね。
最後に
今回はここまでになります。
やはり触ってみた感じすごく便利だなと思いました。
本当にpythonをプログラミングしている感覚で検証シナリオが作成できるので、
色々な可能性を感じれました。
あくまでcocotbの触りの部分だけを記載しましたのでまだまだ機能があります。
(拡張機能でAXIバスやEtherなどの機能もあります)
なのでその辺はおいおい記事にできたらなと。