Pumping HDL Simulation Scripts with Python and 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, , , , .





In general, I really liked the way of organizing simulation and testing using Python, it's like a breath of fresh air after Bash, which I most often used before. And I hope that someone described will be useful too.





All final versions of the scripts are in the pyhdlsim repository on GitHub .








All Articles