6. Инструменты для тестирования

Как мы увидели в прошлый раз, писать тестбенчи на SystemVerilog не очень приятно. Конечно, уже существуют решения для облегчения этой задачи. Одно из них — библиотека cocotb, её мы и рассмотрим подробнее. Для начала, нам нужно её установить. Можно посмотреть, как это сделать здесь.

Для начала посмотрим на простейший пример.

src/06-cocotb/adder_1_bit/adder_1_bit_tb.py
import cocotb
from cocotb.triggers import Timer


@cocotb.test()  (1)
async def adder_test(dut):  (2)
    a = 1  (3)
    b = 0
    c_in = 0

    expected_sum, excpected_c_out = (1, 0)

    dut.a.value = a  (4)
    dut.b.value = b
    dut.c_in.value = c_in

    await Timer(1, units="ns")  (5)

    assert (
        dut.sum.value == expected_sum
    ), f"sum = {dut.sum.value} is not equals 1"  (6)
    assert (
        dut.c_out.value == excpected_c_out
    ), f"c_out = {dut.c_out.value} is not equals 0"
1 Если мы хотим указать, что это тестирующая функция, то надо использовать такой декоратор[1].
2 Заметим, что функция асинхронная. Надо к этому привыкать, весь инструмент сильно завязан на этом. dut — знакомое нам название, тут оно тоже означает объект, который мы тестируем.
3 Задаём входные параметры. Здесь можно писать просто числа, но иногда нужно делать что-то более хитрое[2].
4 Кладём значения в переменные модуля.
5 Задаём задержку в 1 наносекунду.
6 Проверяем какое-то условие, концепция assert нам уже знакома.

Для того, чтобы запустить тест можно написать Makefile (есть альтернатива, которую мы пока рассматривать не будем).

src/06-cocotb/adder_1_bit/Makefile
TOPLEVEL_LANG ?= verilog

PWD=$(shell pwd)


ifeq ($(TOPLEVEL_LANG),verilog)
    VERILOG_SOURCES = $(PWD)/../../02-combination-logic/adder_logic_1_bit.sv
else
    $(error A valid value (verilog) was not provided for TOPLEVEL_LANG=$(TOPLEVEL_LANG))
endif

TOPLEVEL := adder_logic_1_bit
MODULE   := adder_1_bit_tb

include $(shell cocotb-config --makefiles)/Makefile.sim

На самом деле, последняя строчка делает за нас почти всю работу. Нам остаётся лишь указать тестируемые компоненты и модуль с тестом. Подробнее о том, что значит каждый элемент можно посмотреть в документации.

Ради наглядности посмотрим на пример теста, который был написан в прошлый раз.

src/06-cocotb/bus/bus_test.py
import cocotb
from cocotb.clock import Clock
import cocotb.decorators
from cocotb.triggers import ClockCycles, RisingEdge
from cocotb.types import LogicArray
import random


class HelperSerialParallel:  (1)
    OutWidth = 8
    counter = 0
    res = [0] * OutWidth

    def __init__(self, dut):
        self.dut = dut

    async def generate_rnd_input(self):
        self.dut.s_valid.value = random.randint(0, 1)
        self.dut.s_data.value = random.randint(0, 1)
        self.dut.m_ready.value = random.randint(0, 1)
        await RisingEdge(self.dut.clk)  (2)

    async def initialize_rst(self):
        self.dut.aresetn.value = 0
        await ClockCycles(self.dut.clk, 2)  (3)
        self.dut.aresetn.value = 1

    async def setup(self):
        self.dut.s_valid.value = 0
        self.dut.m_ready.value = random.randint(0, 1)

    async def my_serial_to_parallel(self):  (4)
        if not self.dut.aresetn.value:
            self.res = [0] * self.OutWidth
            self.counter = 0
        else:
            if self.dut.s_valid.value and self.dut.s_ready.value:
                self.res = [self.dut.s_data.value.to_unsigned()] + self.res[
                    0 : self.OutWidth - 1
                ]
                if self.counter == self.OutWidth - 1:
                    self.counter = 0
                else:
                    self.counter += 1


@cocotb.test()
async def bus_test(dut):
    NOfIterations = 1000

    clock = Clock(dut.clk, 10, units="ns")  (5)
    helper = HelperSerialParallel(dut)
    cocotb.start_soon(clock.start(start_high=False))  (6)

    await RisingEdge(dut.clk)

    cocotb.start_soon(helper.setup())
    cocotb.start_soon(helper.initialize_rst())

    await RisingEdge(dut.aresetn)  (7)
    for _ in range(NOfIterations):
        cocotb.start_soon(helper.generate_rnd_input())

        await RisingEdge(dut.clk)

        cocotb.start_soon(helper.my_serial_to_parallel())

        if helper.counter == helper.OutWidth:
            assert dut.m_valid, f"Incorrect m_valid = {dut.m_valid.value}"
            assert (
                LogicArray(helper.res) == dut.m_data.value  (8)
            ), f"m_data = {dut.m_data.value}, res = {LogicArray(helper.res)}"
1 Хоть в SystemVerilog есть классы[3], но ООП в python привычнее.
2 Ждём переднего фронта сигнала[4]. Заметим, что это именно задержка, не то же самое, что @(posedge clk) в SystemVerilog.
3 Ждём, пока пройдёт два такта[5].
4 Реализуем ту же функциональность, которую тестируем. В абсолютном большинстве случаев, на python это делать приятнее.
5 Подготавливаем запуск управляющего сигнала[6]. Можно и вручную, но, как несложно догадаться, это типовая задача, для которой сделали обёртку.
6 И запускаем его, start_high=False выставляем, чтобы избежать проблем с лишним засчитыванием переднего фронта в самом начале. Функция start_soon запускает переданную в неё функцию одновременно со всеми остальными[7].
7 Ждём сброса и начинаем.
8 Возвращаемые значения могут быть в специфическом формате. Для корректной работы, иногда нужно явно приводить типы[8].

Как мы видим, код не стал сильно проще, но стал гораздо более привычным, что обычно помогает сократить количество ошибок.

Почему всё асинхронное? Просто потому что иначе было бы совсем неясно, как общаться с симуляцией. Код на cocotb не транслируется в тестбенч на SystemVerilog, вместо этого он использует API для общения с ним. Только помня об этом, можно, например, уловить разницу между start_sun(func()) и await start(fun()).
Обязательно порешайте упражнения.