[FPGA]FPGA × cocotb(python)(1)

## はじめに
**え、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を使うメリットですが、おそらく以下になるかと思います。
<dl>
  <dt>・ライブラリが豊富なので色々な処理が簡単にできる</dt>
  <dd>例えば、結果をcsvに保存したい場合などは保存用の便利なライブラリがあるので簡単にできる</dd>
  <dd></dd>
  <dt>・少ない記述量で色々なこと、複雑なこと(pythonらしい)ができる</dt>
  <dd>例えば、640×480の画像データを用意したい場合、以下の一行で済みます。</dd>

```
[[(j - 255) if j > 255 else j for j in range(640)] for i in range(480)]
```
  <dd>という感じに簡単に大量のデータも用意できます。</dd>
  <dt>・↑つまり検証工数を減らすことができる</dt>
  <dd>上記2項目に挙げた通り、「便利に簡単に色々なことができる🟰工数削減」</dd>
  <dd>必然的にこうなりますね。</dd>
</dl>

メリットはありますが、ある程度難易度があるのかなと思ったりもします。
まぁなるべくわかりやすいように伝えていければと。

## インストール方法
■macの場合
まずpythonをインストールしていない方はpythonをインストールしてください。
と言ってもmacの場合は標準でpythonが入っているはずなのでターミナルを開いて
以下のコマンドで確認してみてください。ver情報が返ってきたら入ってます。

```
python --version
```
※最新のverが欲しいなどあれば以下の方法で入れてください。
　ただし、homebrewやpyenvを使用しているのでちょっとだけややこしいかも...

https://prog-8.com/docs/python-env

##### ①ターミナルで以下のコマンド入力
```
pip install cocotb
```
##### ②ターミナルで以下のコマンド入力しcocotbが入っていることを確認
```
pip freeze
```
以下のようにcocotbが表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/16ba9ae9-d670-8bb2-3058-fabdc6d33621.png)

■windowsの場合
色々やり方があるのですが今回はminiconda上で構築していきます。
(他にもMSYS2やCygwinで構築する方法もあります)
※minicondaとはpythonの開発環境の一つと思っていただければよいです
　上位互換としてanacondaもあります

##### ➀minicondaをインストール
　インストール手順は以下を参照してください。使い方もちょこっと載っています。

https://progzakki.sanachan.com/tools/miniconda-install-and-basic-command/

##### ➁make関連を入れるため以下のコマンドを入力
```
conda install -c msys2 m2-base m2-make
```
##### ➂cocotbを入れるため以下のコマンドを入力
```
pip install cocotb
```

## pythonの書き方
軽く(最低限)pythonの文法を説明します。

<dl>
  <dt>・インデント(空白)</dt>
  <dd>pythonはインデントによって処理を切り分けています。</dd>
  <dd>なのでインデントを合わせないとエラーが出ます。</dd>
  <dd>)例</dd>

```
処理1
処理2
　 処理3
処理4
```
  <dd>例えば上の例だと処理3だけインデントが2個入っているのでエラーが出ます。</dd>
  <dd>同じ流れの処理にしたければ処理3のインデントを削除してください。</dd>
  <dt>・関数の書き方</dt>
  <dd>そもそも関数とは処理をひとまとめにしたものを関数と言います。</dd>
  <dd>verilogで言うところのタスクやファンクションがそれに当たります。</dd>

```
def xxx(引数):
    処理...
```
  <dd>注意点としては処理の前には必ずインデントを設けてください。</dd>
  <dd>設けないとエラーが出ます。</dd>
  <dt>・変数宣言の書き方</dt>
  <dd>pythonの場合変数の型は動的に決まります。</dd>
  <dd>verilogやSystemVerilogではreg、integer、realなどを宣言時に記載しますが、</dd>
  <dd>pythonの場合それらは不要です。</dd>
  <dd>※ただし、静的に記述も可能です</dd>

```
data = in_data
en = in_en
```
  <dd>上記みたいに使用する際に直接記述する感じになります。</dd>
  <dd>また、関数の引数を宣言する際も型は不要です。</dd>
</dl>

以上がpythonの文法になります。
他にも色々ありますが、今回はそこまでごちゃごちゃしないので上記を知っていれば
とりあえずは問題ないと思います。

## 使い方
まず検証対象モジュールを用意してください。
今回は前回の記事に載せたカメラ処理の検証環境を例で使います。

https://github.com/kiiisy/camera

sim/camera
の中のscenario.svを使用します。
まずはこのシナリオがどのような動作をしているかというと以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/1f08b963-9017-1415-f69b-df26d02d458a.png)
- インプット
　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という感じに宣言できません。
　普通に変数を宣言し、固定値を入れる感じになります。
　今回は以下の３つを使用するので記載します。
　※一応定数だとわかるように全て大文字にするという暗黙ルールがあります
```
# define
HMAX = 10　          # ←Hsyncの期間です
VMAX = 5　           # ←Vsyncの期間です
RESET_TIME_NS = 20 # ←リセット時間設定用です
```

##### ④テストシナリオのメインを記載する
　cocotbの場合、以下の宣言をした関数がメイン関数になります。
　関数の引数のdutはテスト対象モジュールのことです。
　※実際のモジュール名じゃなくていいの？と思われると思いますが、
　　これはcocotbが内部的に変換しているためです
　※asyncはコルーチンを定義しています(コルーチンが何かは以下を参照してください)

https://fukkalog.com/coroutines/

```
@cocotb.test()
async def xxx(dut)
```
ということで以下がメイン処理になります。
※今回は期待値比較をしていません(これは別記事で書ければと)
<details><summary>メイン処理</summary>

```
@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
```
</details>

んん？わからんぞ？と思った方、大丈夫です。
以下のポイントがあるので合わせて読んでみてください。

<dl>
  <dt>・dutのアクセス方法</dt>
  <dd>dut内の信号へのアクセスは「.」を使用してアクセスします。</dd>
  <dd>例えばdut内にclkがあった場合、「dut.clk.value」とすればアクセスできます。</dd>
  <dd>※注意点として.valueを最後につけないと信号の値にはアクセスできません</dd>
  <dt>・await</dt>
  <dd>すごい簡単に言うと待ちを意味します。(上のリンク先に説明があります)</dd>
  <dd>「await xxx」を書けばxxxが終わるまで次の処理には進みません。</dd>
  <dd>なぜこのようなことをするかと言うと、待ちを入れないとpythonの処理</dd>
  <dd>の方が先に終わってしまうためです。</dd>
  <dt>・Clock関数</dt>
  <dd>クロックを生成する関数になります。</dd>
  <dd>第一引数はメインクロックを指定。第二引数と第三引数で時間を指定。</dd>
  <dd>戻り値は後で使うので必要です。</dd>
  <dt>・cocotb.start_soon(clk.start(start_high=False))</dt>
  <dd>Clock関数のオブジェクトを入れて、「.start()」を使用することにより開始します。</dd>
  <dd>引数のstart_highは開始クロックを1からスタートするかどうかです。</dd>
  <dt>・Timer関数</dt>
  <dd>引数で指定した時間待ちを生成します。</dd>
  <dt>・RisingEdge関数</dt>
  <dd>引数で指定しクロックの立ち上がりを検知します。</dd>
  <dd>※FallingEdgeもあります。その場合importにFallingEdgeを追記してください。</dd>
  <dt>・for文の読み方</dt>
  <dd>pythonの場合「for x in range(x)」と記載します。</dd>
  <dd>10回ループを書く場合</dd>
  <dd>■SystemVerilogの場合</dd>

```
for (int j=0; j<10; ++j)　
```
  <dd>■pythonの場合</dd>

```
for i in range(10): # iが自動的にカウントアップしてくれる
```
  <dd>という感じになります。それと配列の場合は以下の書き方になります。</dd>

```
array_data = [10,20,30,40,50]
for data in array_data:
    data #1回目は10が入っている。2回目は20、3回目は30、4回目は40、5回目は50
```

  <dd>で、後説明していない箇所が二つ。init関数とgenerate_pixdata関数です。</dd>
  <dd>init関数の中身は以下になります。</dd>

```
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
```
 <dd>これは初期化しかしていないので特に説明はなしでいいかと。</dd>

 <dd>次にgenerate_pixdata関数は以下です。</dd>

```
async def generate_pixdata(hmax):
    # Generate 0 to 9 array data
    pixdata = [data for data in range(hmax)]
    return pixdata
```
  <dd>なんだこの書き方は？？と思った方、これはpythonならではの書き方になります。</dd>
  <dd>わかりやすいように書き直すと以下になります。</dd>
  <dd>※内包表記という書き方です。カッコつけたい方はこのような書き方をするのもあり。</dd>

```
async def generate_pixdata(hmax):
    # Generate 0 to 9 array data
    for data in range(hmax):
        pixdata = data
    return pixdata
```
</dl>

これで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と同じですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/64b34511-2ba2-7c6a-a0c6-2352261f3949.png)

## 最後に
今回はここまでになります。
やはり触ってみた感じすごく便利だなと思いました。
本当にpythonをプログラミングしている感覚で検証シナリオが作成できるので、
色々な可能性を感じれました。

あくまでcocotbの触りの部分だけを記載しましたのでまだまだ機能があります。
(拡張機能でAXIバスやEtherなどの機能もあります)
なのでその辺はおいおい記事にできたらなと。