Тестбенчи и симуляция: проверка до железа

Осваиваем тестбенч — отдельный модуль, который «дёргает» вашу схему и проверяет её до прошивки в железо.

Тестбенч (testbench) — специальный Verilog-модуль без портов, который создаёт экземпляр проверяемой схемы, подаёт на неё входные сигналы (стимулы) и наблюдает за выходами в симуляторе.

Найти ошибку в железе — это часы с осциллографом и перепрошивками. Найти ту же ошибку в симуляции — секунды. Поэтому золотое правило FPGA-разработки: «сначала симулируй, потом прошивай». Инструмент проверки — тестбенч: модуль, который играет роль «испытательного стенда» для вашей схемы. Важно: тестбенч не синтезируется в железо — он существует только для симуляции, поэтому в нём разрешены конструкции, которых нет в синтезируемом коде (задержки, печать).

Анатомия тестбенча

Тестбенч устроен иначе, чем обычный модуль: у него нет портов (он самодостаточен), он сам создаёт такт и стимулы. Вот тестбенч для счётчика:

module counter_tb;
    reg clk = 0;
    reg rst = 1;
    wire [3:0] count;

    // создаём экземпляр проверяемой схемы (DUT — Device Under Test)
    counter4 dut(.clk(clk), .rst(rst), .count(count));

    // генерация такта: каждые 5 единиц времени инвертируем clk -> период 10
    always #5 clk = ~clk;

    // сценарий проверки
    initial begin
        #12 rst = 0;        // снимаем сброс через 12 ед. времени
        #100 $finish;       // через 100 ед. завершаем симуляцию
    end
endmodule

Разберём по частям: clk и rst объявлены reg, потому что тестбенч ими управляет. Конструкция always #5 clk = ~clk; формирует тактовый сигнал. Блок initial выполняется один раз и задаёт сценарий: символ # — это задержка в единицах модельного времени. $finish — системная задача, останавливающая симуляцию.

$display и $monitor: наблюдаем за схемой

Чтобы видеть, что происходит, используют системные задачи (начинаются с $). Они работают только в симуляции:

  • $display(...) — печатает строку один раз (как print), с форматами %b (двоичное), %d (десятичное), %h (шестнадцатеричное).
  • $monitor(...) — печатает автоматически каждый раз, когда меняется любой из перечисленных сигналов; задаётся один раз.
  • $time — текущее модельное время.
initial begin
    $monitor("t=%0d  rst=%b  count=%d", $time, rst, count);
end

Это выведет строку при каждом изменении count — удобно следить за поведением счётчика во времени.

Как работает под капотом

Симулятор моделирует ход времени и срабатывание триггеров. Промоделируем, что напечатал бы $monitor для нашего счётчика после снятия сброса, — на Python:

# Имитация вывода $monitor для 4-битного счётчика
count = 0
rst = 1
time = 0
print("имитация вывода $monitor:")
for step in range(8):
    # на времени 12 снимаем сброс (как #12 rst=0 в тестбенче)
    if time >= 12:
        rst = 0
    print(f"t={time:3d}  rst={rst}  count={count:2d}")
    # фронт такта каждые 10 ед. времени
    if rst:
        count = 0
    else:
        count = (count + 1) % 16
    time += 10

Вывод:

имитация вывода $monitor:
t=  0  rst=1  count= 0
t= 10  rst=1  count= 0
t= 20  rst=0  count= 0
t= 30  rst=0  count= 1
t= 40  rst=0  count= 2
t= 50  rst=0  count= 3
t= 60  rst=0  count= 4
t= 70  rst=0  count= 5

Пока rst=1, счётчик держит 0; после снятия сброса начинает считать 1,2,3... Ровно это и показал бы реальный симулятор — и так, не трогая железо, вы убеждаетесь, что счётчик работает.

Самопроверяющиеся тестбенчи

Зрелый тестбенч не просто печатает значения, а сам проверяет их через if и $display с сообщением об ошибке (или системную задачу $error). Тогда регрессию можно гонять автоматически: «прошёл/не прошёл» без чтения логов глазами. Это основа верификации — отдельной инженерной дисциплины.

Частые ошибки

  • Писать в тестбенче синтезируемый стиль. Тестбенч — не железо; задержки #, initial, $display в синтезируемом коде недопустимы, но в тестбенче — норма.
  • Забыть сгенерировать такт. Без always #5 clk = ~clk; тактируемая схема «замрёт» — ничего не произойдёт.
  • Прошивать без симуляции. Пропуск симуляции превращает отладку в многочасовое мучение с железом.

Итог

  • Тестбенч — несинтезируемый модуль, подающий стимулы на проверяемую схему (DUT).
  • Такт формируют always #5 clk = ~clk;, сценарий — в initial с задержками #.
  • $display печатает раз, $monitor — при каждом изменении сигналов.
  • Самопроверяющиеся тестбенчи позволяют гонять регрессию автоматически.
Проверьте себя
1. Что такое тестбенч (testbench) в Verilog?
AСинтезируемый модуль, который попадает в железо
BНесинтезируемый модуль без портов, подающий стимулы на проверяемую схему в симуляторе
CФайл конфигурации FPGA
DТактовый генератор в железе
2. Чем $monitor отличается от $display?
AОни идентичны
B$monitor печатает автоматически при каждом изменении перечисленных сигналов, а $display — один раз при вызове
C$display работает только в железе
D$monitor генерирует такт
3. Почему конструкции #5, initial и $display допустимы в тестбенче, но не в синтезируемом коде?
AОни слишком медленные
BТестбенч не превращается в железо — он только для симуляции, поэтому может использовать несинтезируемые конструкции
CОни устарели
DЭто ошибка в стандарте Verilog