[Python, FPGA] Прокачиваем скрипты симуляции HDL с помощью Python и PyTest
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Все делают это. Ну ладно, не все, но большинство. Пишут скрипты, чтобы симулировать свои проекты на 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, чтобы не качать его каждый раз сайта, но я, пожалуй, здесь остановлюсь.В целом, мне прям очень зашёл способ организации симуляции и тестирования с помощью Python, это как глоток свежего воздуха после Bash, который я чаще всего применял до этого. И надеюсь, что кому-нибудь описанное пригодится тоже.Все финальные версии скриптов лежат в репозитории pyhdlsim на GitHub.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Машинное обучение, Искусственный интеллект] Распознавание Ворониных на фотографиях: от концепции к делу
- [Python, API, 1С-Битрикс] Как быстро получить много данных от Битрикс24 через REST API
- [Python, Программирование, Проектирование и рефакторинг, Профессиональная литература] Как определять собственные классы исключений в Python (перевод)
- [Python, Программирование, Искусственный интеллект] Constraint Programming или как решить задачу коммивояжёра, просто описав её (перевод)
- [Python, Голосовые интерфейсы] Голосовой ассистент на Python (Виталий alfa 2.0)
- [Python] Поиск известных паттернов на Forex с Python
- [Python, Big Data, Разработка под e-commerce, Управление продуктом] Как мы в СберМаркете боремся с товарами-призраками
- [Python, API, Браузеры, Веб-аналитика] Скрапинг современных веб-сайтов без headless-браузеров (перевод)
- [Open source, Python, Data Mining, Машинное обучение, Звук] Мы опубликовали современный Voice Activity Detector и не только
- Опубликована библиотека urm для Python
Теги для поиска: #_python, #_fpga, #_fpga, #_simulation, #_verilog, #_systemverilog, #_vhdl, #_modelsim, #_icarus, #_vivado, #_python, #_pytest, #_python, #_fpga
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:46
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Все делают это. Ну ладно, не все, но большинство. Пишут скрипты, чтобы симулировать свои проекты на Verilog, SystemVerilog и VHDL. Однако, написание и поддержка таких скриптов часто бывает довольно непроста для типично используемых Bash/Makefile/Tcl. Особенно, если необходимо не только открывать GUI для одного тестбенча и смотреть в диаграммы, но и запускать пачки параметризированных тестов для различных блоков, контролировать результат, параллелизировать их выполнение и т.д. Оказалось, что всё это можно закрыть довольно прозрачным и легко поддерживаемым кодом на Python, что мне даже обидно становится от того, как я страдал ранее и сколько странного bash-кода родил.Конечно, я не первый кто задумывается о подобном. Уже даже существует целый фреймворк VUnit. Однако, как показывает практика и опросы в профильных чатах, такие фреймворки используются нечасто. Вероятно потому, что они предъявляют требования к внутренней структуре самих тестбенчей, с чем наверное можно мириться только на новых проектах, без обширной кодовой базы. Ну и вообще, куда ж без таких вещей как "у нас так не принято" и "not invented here".Собственно, речь пойдет не о написании очередного фреймворка, а просто о замене одних скриптов симуляции на другие, более дружелюбные к расширению и поддержке. Без каких-либо требований к коду, тестбенчам и архитектуре их построения.ЗадачиПопробую перечислить большинство задач, которые так или иначе возникают в процессе разработки и эволюции HDL проекта. От более простых к более сложным:
Репозиторий проекта 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 #!/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() chmod +x test_sqrt.py
./test_sqrt.py #!/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 #@! #!/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
#!/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 ============ # заменим это
@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 $ 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 =================== python3 -m pip install pytest-xdist
# можно также использовать значение 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 $ 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) ============== def pytest_addoption(parser):
parser.addoption("--sim", action="store", default="icarus") @pytest.fixture
def simtool(pytestconfig): return pytestconfig.getoption("sim") pytest --sim modelsim -n auto
- 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 - 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 Даже не смотря на то, что скачивание 1.3GB установочника Modelsim и его распаковка занимают некоторое время (которое тем не менее, очень мало!), он оказывается в итоге ещё и быстрее моментально развертываемого Icarus.Тут конечно можно пойти дальше и подготовить Docker-образ с Modelsim, чтобы не качать его каждый раз сайта, но я, пожалуй, здесь остановлюсь.В целом, мне прям очень зашёл способ организации симуляции и тестирования с помощью Python, это как глоток свежего воздуха после Bash, который я чаще всего применял до этого. И надеюсь, что кому-нибудь описанное пригодится тоже.Все финальные версии скриптов лежат в репозитории pyhdlsim на GitHub. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:46
Часовой пояс: UTC + 5