Pompage de scripts de simulation HDL avec Python et PyTest

. , , . , Verilog, SystemVerilog VHDL. , Bash/Makefile/Tcl. , GUI , , , .. , Python, , bash- .





, . VUnit. , , . , , , . , " " "not invented here".





, , , . - , .





, HDL . :





  • . , , , .





  • GUI. , , .





  • . GUI , (), .





  • -/. , . , HDL.





  • . .





  • . , ( , .).





  • . / . .





  • . , .





  • CI. CI ( , " " ..).





, , - , , .





:





  • ( );





  • ( include);





  • ;





  • , ( );





  • ( );





  • , ..





Python, Simulator



, run()



, ? , Icarus Verilog, Modelsim Vivado Simulator, subprocess



. CliArgs



, argparse



, . , . sim.py.





, , - , Python, sim.py



.





, . An FPGA Implementation of a Fixed-Point Square Root Operation.





pyhdlsim GitHub.





:





$ tree -a -I .git
.
├── .github
│   └── workflows # Github Actions
│       ├── icarus-test.yml #     Icarus Verilog     github
│       └── modelsim-test.yml #     Modelsim     github
├── .gitignore
├── LICENSE.txt
├── README.md
├── sim #    
│   ├── conftest.py
│   ├── sim.py
│   └── test_sqrt.py
└── src # 
    ├── beh #    
    │   └── sqrt.py
    ├── rtl #  HDL 
    │   └── sqrt.v
    └── tb # HDL  
        └── tb_sqrt.sv

      
      



tb_sqrt.sv



: , "" $sqrt()



, , , .





, , , , ( HDL sim.py



). sim



. , .





test_sqrt.py



.





#!/usr/bin/env python3

from sim import Simulator

sim = Simulator(name='icarus', gui=True, cwd='work')
sim.incdirs += ["../src/tb", "../src/rtl", sim.cwd]
sim.sources += ["../src/rtl/sqrt.v", "../src/tb/tb_sqrt.sv"]
sim.top = "tb_sqrt"
sim.setup()
sim.run()
      
      



Icarus GTKWave . . , . - sim.setup()



work



( , ) (sim.run()



).





:





chmod +x test_sqrt.py
./test_sqrt.py
      
      



GTKWave.





GUI

, . CliArgs



. , .





#!/usr/bin/env python3

from sim import Simulator, CliArgs

def test(tmpdir, defines, simtool, gui):
    sim = Simulator(name=simtool, gui=gui, cwd=tmpdir)
    sim.incdirs += ["../src/tb", "../src/rtl", sim.cwd]
    sim.sources += ["../src/rtl/sqrt.v", "../src/tb/tb_sqrt.sv"]
    sim.defines += defines
    sim.top = "tb_sqrt"
    sim.setup()
    sim.run()

if __name__ == '__main__':
    # run script with key -h to see help
    args = CliArgs(default_test="test").parse()
    test(tmpdir='work', simtool=args.simtool, gui=args.gui, defines=args.defines)
      
      



:





$ ./test_sqrt.py -h
usage: test_sqrt.py [-h] [-t <name>] [-s <name>] [-b] [-d <def> [<def> ...]]

optional arguments:
  -h, --help            show this help message and exit
  -t <name>             test <name>; default is 'test'
  -s <name>             simulation tool <name>; default is 'icarus'
  -b                    enable batch mode (no GUI)
  -d <def> [<def> ...]  define <name>; option can be used multiple times
      
      



:





$ ./test_sqrt.py -b
Run Icarus (cwd=/space/projects/pyhdlsim/simtmp/work)
TOP_NAME=tb_sqrt SIM
iverilog -I /space/projects/pyhdlsim/src/tb -I /space/projects/pyhdlsim/src/rtl -I /space/projects/pyhdlsim/simtmp/work -D TOP_NAME=tb_sqrt -D SIM -g2005-sv -s tb_sqrt -o worklib.vvp /space/projects/pyhdlsim/src/rtl/sqrt.v /space/projects/pyhdlsim/src/tb/tb_sqrt.sv

vvp worklib.vvp -lxt2
LXT2 info: dumpfile dump.vcd opened for output.
Test started. Will push 8 words to DUT.
!@# TEST PASSED #@!
      
      



:





#   
./test_sqrt.py -s modelsim -b
#    GUI
./test_sqrt.py -s modelsim
      
      



, , , :





$ ./test_sqrt.py -b -d ITER_N=42
Run Icarus (cwd=/space/projects/pyhdlsim/simtmp/work)
TOP_NAME=tb_sqrt SIM
iverilog -I /space/projects/pyhdlsim/src/tb -I /space/projects/pyhdlsim/src/rtl -I /space/projects/pyhdlsim/simtmp/work -D TOP_NAME=tb_sqrt -D SIM -g2005-sv -s tb_sqrt -o worklib.vvp /space/projects/pyhdlsim/src/rtl/sqrt.v /space/projects/pyhdlsim/src/tb/tb_sqrt.sv

vvp worklib.vvp -lxt2
LXT2 info: dumpfile dump.vcd opened for output.
Test started. Will push 42 words to DUT.
!@# TEST PASSED #@!
      
      



-/

, . , Verilog , Python. - Python, . src/beh/sqrt.py



. nrsqrt()



.





, , , test_sv



. test_py



, nrsqrt()



.





#!/usr/bin/env python3

from sim import Simulator, CliArgs, path_join, write_memfile
import random
import sys
sys.path.append('../src/beh')
from sqrt import nrsqrt

def create_sim(cwd, simtool, gui, defines):
    sim = Simulator(name=simtool, gui=gui, cwd=cwd)
    sim.incdirs += ["../src/tb", "../src/rtl", cwd]
    sim.sources += ["../src/rtl/sqrt.v", "../src/tb/tb_sqrt.sv"]
    sim.defines += defines
    sim.top = "tb_sqrt"
    return sim

def test_sv(tmpdir, defines, simtool, gui):
    sim = create_sim(tmpdir, simtool, gui, defines)
    sim.setup()
    sim.run()

def test_py(tmpdir, defines, simtool, gui=False, pytest_run=True):
    # prepare simulator
    sim = create_sim(tmpdir, simtool, gui, defines)
    sim.setup()
    # prepare model data
    try:
        din_width = int(sim.get_define('DIN_W'))
    except TypeError:
        din_width = 32
    iterations = 100
    stimuli = [random.randrange(2 ** din_width) for _ in range(iterations)]
    golden = [nrsqrt(d, din_width) for d in stimuli]
    write_memfile(path_join(tmpdir, 'stimuli.mem'), stimuli)
    write_memfile(path_join(tmpdir, 'golden.mem'), golden)
    sim.defines += ['ITER_N=%d' % iterations]
    sim.defines += ['PYMODEL', 'PYMODEL_STIMULI="stimuli.mem"', 'PYMODEL_GOLDEN="golden.mem"']
    # run simulation
    sim.run()

if __name__ == '__main__':
    args = CliArgs(default_test="test_sv").parse()
    try:
        globals()[args.test](tmpdir='work', simtool=args.simtool, gui=args.gui, defines=args.defines)
    except KeyError:
        print("There is no test with name '%s'!" % args.test)
      
      



, , :





#      
./test_sqrt.py -t test_py
      
      



.





2 , 202, , . pytest.





- pytest.





  • , pytest test* : , , , .





  • , ( assert



    ).





  • (fixtures). , test_a(a)



    .





  • conftest.py



    , .





:





  • pytest



    - , ;





  • pytest -v



    - e ;





  • pytest -rP



    - stdout , ;





  • pytest test_sqrt.py::test_sv



    - .





pytest . pytest. simtool



defines



. , . gui



pytest_run



. , .. pytest , .





, , pytest_run



, pytest, .





tmpdir



- , , . .. - sim



.





- pytest, .. test_.





, is_passed



. , !@# TEST PASSED #@!



stdout. , , , . , . stdout sim.stdout



.





#!/usr/bin/env python3

import pytest
from sim import Simulator, CliArgs, path_join, write_memfile
import random
import sys
sys.path.append('../src/beh')
from sqrt import nrsqrt

@pytest.fixture()
def defines():
    return []

@pytest.fixture
def simtool():
    return 'icarus'

def create_sim(cwd, simtool, gui, defines):
    sim = Simulator(name=simtool, gui=gui, cwd=cwd, passed_marker='!@# TEST PASSED #@!')
    sim.incdirs += ["../src/tb", "../src/rtl", cwd]
    sim.sources += ["../src/rtl/sqrt.v", "../src/tb/tb_sqrt.sv"]
    sim.defines += defines
    sim.top = "tb_sqrt"
    return sim

def test_sv(tmpdir, defines, simtool, gui=False, pytest_run=True):
    sim = create_sim(tmpdir, simtool, gui, defines)
    sim.setup()
    sim.run()
    if pytest_run:
        assert sim.is_passed

def test_py(tmpdir, defines, simtool, gui=False, pytest_run=True):
    # prepare simulator
    sim = create_sim(tmpdir, simtool, gui, defines)
    sim.setup()
    # prepare model data
    try:
        din_width = int(sim.get_define('DIN_W'))
    except TypeError:
        din_width = 32
    iterations = 100
    stimuli = [random.randrange(2 ** din_width) for _ in range(iterations)]
    golden = [nrsqrt(d, din_width) for d in stimuli]
    write_memfile(path_join(tmpdir, 'stimuli.mem'), stimuli)
    write_memfile(path_join(tmpdir, 'golden.mem'), golden)
    sim.defines += ['ITER_N=%d' % iterations]
    sim.defines += ['PYMODEL', 'PYMODEL_STIMULI="stimuli.mem"', 'PYMODEL_GOLDEN="golden.mem"']
    # run simulation
    sim.run()
    if pytest_run:
        assert sim.is_passed

if __name__ == '__main__':
    args = CliArgs(default_test="test_sv").parse()
    try:
        globals()[args.test](tmpdir='work', simtool=args.simtool, gui=args.gui, defines=args.defines, pytest_run=False)
    except KeyError:
        print("There is no test with name '%s'!" % args.test)
      
      



:





$ pytest
========== test session starts ===========
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /space/projects/misc/habr-publications/pyhdlsim/pyhdlsim/simtmp
plugins: xdist-2.2.0, forked-1.3.0
collected 2 items

test_sqrt.py ..                    [100%]

=========== 2 passed in 0.08s ============

$ pytest -v
========== test session starts ===========
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /space/projects/misc/habr-publications/pyhdlsim/pyhdlsim/simtmp
plugins: xdist-2.2.0, forked-1.3.0
collected 2 items

test_sqrt.py::test_sv PASSED       [ 50%]
test_sqrt.py::test_py PASSED       [100%]

=========== 2 passed in 0.08s ============
      
      



. , N , , . pytest.





, defines



:





#  
@pytest.fixture()
def defines():
    return []
#  
@pytest.fixture(params=[[], ['DIN_W=16'], ['DIN_W=18'], ['DIN_W=25'], ['DIN_W=32']])
def defines(request):
    return request.param
      
      



5 . :





$ pytest -v
================== test session starts ==================
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /space/projects/misc/habr-publications/pyhdlsim/pyhdlsim/simtmp
plugins: xdist-2.2.0, forked-1.3.0
collected 10 items

test_sqrt.py::test_sv[defines0] PASSED            [ 10%]
test_sqrt.py::test_sv[defines1] PASSED            [ 20%]
test_sqrt.py::test_sv[defines2] PASSED            [ 30%]
test_sqrt.py::test_sv[defines3] PASSED            [ 40%]
test_sqrt.py::test_sv[defines4] PASSED            [ 50%]
test_sqrt.py::test_py[defines0] PASSED            [ 60%]
test_sqrt.py::test_py[defines1] PASSED            [ 70%]
test_sqrt.py::test_py[defines2] PASSED            [ 80%]
test_sqrt.py::test_py[defines3] PASSED            [ 90%]
test_sqrt.py::test_py[defines4] PASSED            [100%]

================== 10 passed in 0.28s ===================
      
      



, 5 .





. :





python3 -m pip install pytest-xdist
      
      



, , 4:





#     auto, pytest    
pytest -n 4
      
      



, :





def test_slow(tmpdir, defines, simtool, gui=False, pytest_run=True):
    sim = create_sim(tmpdir, simtool, gui, defines)
    sim.defines += ['ITER_N=500000']
    sim.setup()
    sim.run()
    if pytest_run:
        assert sim.is_passed
      
      



( 3*5=15):





$ pytest
=================== test session starts ====================
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /space/projects/misc/habr-publications/pyhdlsim/pyhdlsim/simtmp
plugins: xdist-2.2.0, forked-1.3.0
collected 15 items

test_sqrt.py ...............                         [100%]

============== 15 passed in 242.74s (0:04:02) ==============

$ pytest -n auto
=================== test session starts ====================
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /space/projects/misc/habr-publications/pyhdlsim/pyhdlsim/simtmp
plugins: xdist-2.2.0, forked-1.3.0
gw0 [15] / gw1 [15] / gw2 [15] / gw3 [15]
...............                                      [100%]
============== 15 passed in 145.66s (0:02:25) ==============
      
      



, , .





, pytest -s



. pytest. , - simtool



.





conftest.py



, pytest. sim.py



:





def pytest_addoption(parser):
    parser.addoption("--sim", action="store", default="icarus")
      
      



test_sqrt.py



simtool



:





@pytest.fixture
def simtool(pytestconfig):
    return pytestconfig.getoption("sim")
      
      



:





pytest --sim modelsim -n auto
      
      



CI. Github Actions + (Modelsim | Icarus)

(CI). .github/workflows/icarus-test.yml



.github/workflows/modelsim-test.yml



. Github Actions - , Github. , .





Icarus Verilog:





- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install pytest pytest-xdist
    sudo apt-get install iverilog
- name: Test code
  working-directory: ./sim
  run: |
    pytest -n auto
      
      



Modelsim Intel Starter Pack:





- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
      pip install pytest pytest-xdist
      sudo dpkg --add-architecture i386
      sudo apt update
      sudo apt install -y libc6:i386 libxtst6:i386 libncurses5:i386 libxft2:i386 libstdc++6:i386 libc6-dev-i386 lib32z1 libqt5xml5 liblzma-dev
    wget https://download.altera.com/akdlm/software/acdsinst/20.1std/711/ib_installers/ModelSimSetup-20.1.0.711-linux.run
        chmod +x ModelSimSetup-20.1.0.711-linux.run
    ./ModelSimSetup-20.1.0.711-linux.run --mode unattended --accept_eula 1 --installdir $HOME/ModelSim-20.1.0 --unattendedmodeui none
    echo "$HOME/ModelSim-20.1.0/modelsim_ase/bin" >> $GITHUB_PATH
- name: Test code
  working-directory: ./sim
  run: |
    pytest -n auto --sim modelsim
      
      



Modelsim. - ! Ubuntu/Fedora (, , Quartus+Modelsim 19.1 Fedora 29).





:





, 1.3GB Modelsim ( , !), Icarus.





Docker- Modelsim, , , , .





En général, j'ai beaucoup aimé la façon d'organiser la simulation et les tests avec Python, c'est comme une bouffée d'air frais après Bash, que j'ai le plus souvent utilisé avant. Et j'espère que quelqu'un décrit sera également utile.





Toutes les versions finales des scripts se trouvent dans le référentiel pyhdlsim sur GitHub .








All Articles