Найти в Дзене
Данил Волокитин

Разработка железа на System Verilog HDL/VHDL с использованием верилатора. Часть 2

Прежде чем что-либо делать с нашим тестовым стендом, следует сказать, что никому не нравится вводить одни и те же команды снова и снова. И поскольку мы не пещерные люди, мы будем использовать (создавать) [https://www.gnu.org/software/make /] для быстрого создания и запуска нашей симуляции.Большинство команд сборки, используемых в Makefile ниже, должны быть знакомы из части 1, но на всякий случай давайте еще раз кратко рассмотрим их:verilator -Wall --trace -cc alu.sv --exe tb_alu.cpp Это преобразует наш исходный код alu.sv в C++ и создает файлы сборки для создания исполняемого файла моделирования. Мы используем -Wall для включения всех ошибок C++, --trace для включения трассировки сигналов, -cc alu.sv для преобразования нашего модуля alu.sv в C++ и --exe tb_alu.cpp для указания Verilator, какой файл является нашим тестовым стендом C++.make -C obj_dir -f Valu.mk Valu Это создает наш исполняемый файл моделирования из тестового стенда и преобразованных источников. Мы говорим M

Прежде чем что-либо делать с нашим тестовым стендом, следует сказать, что никому не нравится вводить одни и те же команды снова и снова. И поскольку мы не пещерные люди, мы будем использовать (создавать) [https://www.gnu.org/software/make /] для быстрого создания и запуска нашей симуляции.Большинство команд сборки, используемых в Makefile ниже, должны быть знакомы из части 1, но на всякий случай давайте еще раз кратко рассмотрим их:verilator -Wall --trace -cc alu.sv --exe tb_alu.cpp

Это преобразует наш исходный код alu.sv в C++ и создает файлы сборки для создания исполняемого файла моделирования. Мы используем -Wall для включения всех ошибок C++, --trace для включения трассировки сигналов, -cc alu.sv для преобразования нашего модуля alu.sv в C++ и --exe tb_alu.cpp для указания Verilator, какой файл является нашим тестовым стендом C++.make -C obj_dir -f Valu.mk Valu

Это создает наш исполняемый файл моделирования из тестового стенда и преобразованных источников. Мы говорим Make изменить рабочий каталог на obj_dir, использовать файл сборки с именем Valu.mk и построить цель с именем Valu../obj_dir/Valu

Это запускает наш исполняемый файл моделирования, который имитирует испытательный стенд и генерирует наши сигналы.В рабочем каталоге создайте файл с именем Makefile и вставьте следующее содержимое:
MODULE=alu

.PHONY:sim

sim: waveform.vcd

.PHONY:verilate

verilate: .stamp.verilate

.PHONY:build

build: obj_dir/Valu

.PHONY:waves

waves: waveform.vcd

@echo

@echo "### WAVES ###"

gtkwave waveform.vcd

waveform.vcd: ./obj_dir/V$(MODULE)

@echo

@echo "### SIMULATING ###"

@./obj_dir/V$(MODULE)

./obj_dir/V$(MODULE): .stamp.verilate

@echo

@echo "### BUILDING SIM ###"

make -C obj_dir -f V$(MODULE).mk V$(MODULE)

.stamp.verilate: $(MODULE).sv tb_$(MODULE).cpp

@echo

@echo "### VERILATING ###"

verilator -Wall --trace -cc $(MODULE).sv --exe tb_$(MODULE).cpp

@touch .stamp.verilate

.PHONY:lint

lint: $(MODULE).sv

verilator --lint-only $(MODULE).sv

.PHONY: clean

clean:

rm -rf .stamp.*;

rm -rf ./obj_dir

rm -rf waveform.vcd

Makefile должен быть простым для тех, кто знаком с Make.

Сохранив файл, вы сможете быстро перестроить всю симуляцию, запустив make sim в своем терминале, открыв GTKWave с помощью make wave, проверив свой проект с помощью make verilate или собрав проверенные исходники с помощью make build.

Обратите внимание, что существует дополнительная цель make lint, которая вызывает Verilator с параметром --lint-only. Это полезно для быстрого анализа исходных файлов Verilog/SystemVerilog и проверки на наличие проблем. Это можно использовать для проверки ваших источников, даже если вы не используете Verilator для моделирования.

Наконец, есть цель make clean, которая удаляет весь мусор, созданный в процессе сборки.

И со всем этим давайте заставим этот испытательный стенд сиять.

Рандомизированные начальные значения

Одним из наблюдений из части 1 было то, что Verilator является симулятором двух состояний, а это означает, что он поддерживает только логические значения 1 и 0 и не поддерживает X (и только ограниченную поддержку Z). Поэтому Verilator по умолчанию инициализирует все сигналы равными 0, что можно увидеть на рис. 1 из наших предыдущих результатов моделирования:

Рис. 1: По умолчанию все инициализировано до 0
Рис. 1: По умолчанию все инициализировано до 0

Кроме того, если у вас есть код, который присваивает X проводу или регистру, то по умолчанию он также получает значение 0.Однако мы можем изменить это поведение с помощью параметров командной строки — мы можем заставить Verilator инициализировать все сигналы равными 1 или, что еще лучше, случайным значением. Это позволит нам проверить, работает ли наш сигнал сброса, как только мы добавим его в тестовую среду.Чтобы наш тестовый стенд инициализировал сигналы случайными значениями, нам сначала нужно вызвать Verilated::commandArgs(argc, argv); перед созданием объекта DUT:

int main(int argc, char** argv, char** env) {

Verilated::commandArgs(argc, argv);

Valu *dut = new Valu;

<...>

Затем нам нужно обновить нашу команду сборки цели проверки, добавив --x-assign unique и --x-initial unique. Строка 31 нашего Makefile теперь должна выглядеть так:

verilator -Wall --trace --x-assign unique --x-initial unique -cc $(MODULE).sv --exe tb_$(MODULE).cpp

Наконец, нам нужно передать +verilator+rand+reset+2 нашему исполняемому файлу моделирования, чтобы установить случайный метод инициализации сигнала во время выполнения. Это означает изменение строки 21 в нашем Makefile на:

@./obj_dir/V$(MODULE) +verilator+rand+reset+2

Теперь, если мы сделаем чистые и создадим волны, мы увидим, что теперь сигналы инициализируются случайными значениями в начале симуляции:

Рис. 2: Случайная инициализая
Рис. 2: Случайная инициализая

Теперь, когда сигналы рандомизированы, мы можем взглянуть на применение нашего сигнала сброса.

Сброс тестируемого устройства

Чтобы сбросить наше тестируемое устройство и его входные сигналы, мы обновляем основной цикл нашего тестового стенда, чтобы он выглядел следующим образом:

while (sim_time < MAX_SIM_TIME) {

dut->rst = 0;

if(sim_time > 1 && sim_time < 5){

dut->rst = 1;

dut->a_in = 0;

dut->b_in = 0;

dut->op_in = 0;

dut->in_valid = 0;

}

dut->clk ^= 1;

dut->eval();

m_trace->dump(sim_time);

sim_time++;

}

В строке 3 я произвольно выбрал, что хочу, чтобы мой сброс происходил между тактовыми фронтами 3 и 5. Вы, конечно, можете настроить это, если требуется.В строке 4 устанавливается высокий уровень сброса, а в последующих линиях все входы ИУ сбрасываются на 0.Строки 11-14 не изменяются. Мы ставим галочку на часах и увеличиваем счетчик времени.Строка 2 добавляется для сброса счетчика обратно в 0 при последующих итерациях цикла. Вместе строки 2-3-4 будут эквивалентны следующему коду SystemVerilog:always_comb begin

dut.rst = 1'b0;

if (sim_time >= 3 && sim_time < 6) begin

dut.rst = 1'b1;

end

end

Повторный запуск симуляции теперь дает нам это:

Рис.3 Сигнал сброса в дейтсвии
Рис.3 Сигнал сброса в дейтсвии

Как видно из рисунка 3, наш сигнал сброса успешно сгенерирован в тестовом стенде. Чтобы сделать основной цикл немного чище, давайте переместим элементы сброса в отдельную функцию вне main():

void dut_reset (Valu *dut, vluint64_t &sim_time){

dut->rst = 0;

if(sim_time >= 3 && sim_time < 6){

dut->rst = 1;

dut->a_in = 0;

dut->b_in = 0;

dut->op_in = 0;

dut->in_valid = 0;

}

}

Затем добавляем вызов dut_reset в основной цикл:

while (sim_time < MAX_SIM_TIME) {

dut_reset(dut, sim_time);

dut->clk ^= 1;

dut->eval();

m_trace->dump(sim_time);

sim_time++;

}

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

Базовая проверка
На данный момент у нас есть следующее в нашем основном цикле моделирования:

while (sim_time < MAX_SIM_TIME) {

dut_reset(dut, sim_time);

dut->clk ^= 1;

dut->eval();

m_trace->dump(sim_time);

sim_time++;

}

Теперь, если бы мы моделировали тестовый стенд Verilog/SystemVerilog в качестве dut вместо нашего модуля alu, мы могли бы добавить проверку для Verilated::gotFinish() и остановить симуляцию, если для нее установлено значение true. Это происходит, когда $finish() вызывается из Verilog/SystemVerilog. Тогда нашего тестового стенда C++ будет достаточно для имитации тестового стенда Verilog/SystemVerilog.Однако этого нам будет недостаточно, так как нам нужно вставить стимул и проверочный код где-то в основной цикл тестового стенда C++, чтобы запустить и проверить наше тестируемое устройство.

Счётчик тактов

Есть много способов снять шкуру с мертвой лошади одним камнем, но вот что мы собираемся сделать сейчас:Во-первых, мы создадим новую переменную для подсчета положительных фронтов тактового сигнала. Эта переменная будет того же типа, что и sim_time:    

vluint64_t sim_time = 0;
vluint64_t posedge_cnt = 0;

Затем мы модифицируем наш код генерации ребер, добавив счетчик положительных ребер:
dut->clk ^= 1;            // Invert clock

dut->eval();              // Evaluate dut on the current edge

if(dut->clk == 1){

posedge_cnt++;        // Increment posedge counter if clk is 1

}

m_trace->dump(sim_time);  // Dump to waveform.vcd

sim_time++;               // Advance simulation time

Добавление этого счетчика между eval и dump дает нам что-то похожее на следующее в Verilog:

initial posedge_cnt <= '0;

always_ff @ (posedge clk, posedge rst) begin

posedge_cnt <= posedge_cnt + 1'b1;

end

И на этом этапе мы, наконец, можем приступить к проверке нашего ALU.

Примитивные тестируемые стимулы и проверки

Давайте еще раз взглянем на ожидаемые формы сигналов для нашего ALU:

Рис. 4: Ожидаемое поведение ALU
Рис. 4: Ожидаемое поведение ALU

Игнорируя входы a, b и операции, а также выходные данные, давайте сначала проверим, что наш входной допустимый сигнал распространяется на выход.Мы знаем, что у нас есть 2 этапа регистрации, которые в упрощенном виде будут выглядеть так:

always_ff @ (posedge clk) begin

in_valid_r <= in_valid;

out_valid <= out_valid_r;

end

Таким образом, если мы применили 1 к in_valid на 5-м положительном фронте тактов, мы должны увидеть 1 на out_valid после двух тактов, или, другими словами, на 7-м положительном фронте тактов. Вот как мы это проверяем:

while (sim_time < MAX_SIM_TIME) {

dut_reset(dut, sim_time);

dut->clk ^= 1;

dut->eval();

dut->in_valid = 0;

if (dut->clk == 1){

posedge_cnt++;

if (posedge_cnt == 5){

dut->in_valid = 1;       // assert in_valid on 5th cc

}

if (posedge_cnt == 7){

if (dut->out_valid != 1) // check in_valid on 7th cc

std::cout << "ERROR!" << std::endl;

}

}

m_trace->dump(sim_time);

sim_time++;

}

То, что выполняет выделенный код, будет похоже на это:

always_comb begin

in_valid = 0;

if (posedge_cnt == 5)

in_valid = 1;

if (posedge_cnt == 7)

assert (out_valid == 1) else $error("ERROR!")

end

Главное здесь — убедиться, что код стимулов/проверки, который вы пишете, следует следующему порядку операций:

1.Перейдите на 1 такт, оцените, чтобы создать положительный фронт, а затем установите входы/проверьте выходы перед сбросом и увеличением времени моделирования.

2.При следующем положительном фронте тактового сигнала внутри цикла while() входные данные, установленные ранее, будут распространяться на дизайн во время eval, а затем сразу после eval входные данные должны быть сброшены к их значениям по умолчанию.

Мониторинг сигналов, подобный утверждениюУстановка in_valid на 5-м фронте и проверка того, что out_valid равно 1, безусловно, работает, но если мы хотим проверять валидность на большем количестве тактов, нам нужно добавить гораздо больше проверок. Кроме того, мы не проверяем, что значение out_valid равно 0 там, где оно должно быть, а это означает, что значение out_valid может застрять на значении 1, и тестовый стенд не выйдет из строя. Таким образом, наш проверочный код можно было бы значительно улучшить, написав код на C++ для непрерывного мониторинга in_valid и out_valid, аналогично тому, как работают утверждения SystemVerilog.Мы можем написать функцию для этого следующим образом:

#define VERIF_START_TIME 7

void check_out_valid(Valu *dut, vluint64_t &sim_time){

static unsigned char in_valid = 0; //in valid from current cycle

static unsigned char in_valid_d = 0; //delayed in_valid

static unsigned char out_valid_exp = 0; //expected out_valid value

if (sim_time >= VERIF_START_TIME) {

// note the order!

out_valid_exp = in_valid_d;

in_valid_d = in_valid;

in_valid = dut->in_valid;

if (out_valid_exp != dut->out_valid) {

std::cout << "ERROR: out_valid mismatch, "

<< "exp: " << (int)(out_valid_exp)

<< " recv: " << (int)(dut->out_valid)

<< " simtime: " << sim_time << std::endl;

}

}

}

VERIF_START_TIME необходимо, чтобы убедиться, что мы не запускаем этот код проверки до или во время сброса, чтобы предотвратить обнаружение ложных ошибок. Если вы обратитесь к рис. 5, вы увидите, что rst возвращается к 0 через 6 пс (равно sim_time, равному 6), поэтому sim_time, равное 7, — это то место, где мы должны начать проверку нашей достоверности.Код проверки довольно прост — он просто моделирует конвейер регистрации между in_valid и out_valid. Мы можем заменить исходный код вышеприведенной функцией следующим образом:

while (sim_time < MAX_SIM_TIME) {

dut_reset(dut, sim_time);

dut->clk ^= 1;

dut->eval();

if (dut->clk == 1){

dut->in_valid = 0;

posedge_cnt++;

if (posedge_cnt == 5){

dut->in_valid = 1;

}

check_out_valid(dut, sim_time);

}

m_trace->dump(sim_time);

sim_time++;

}

Если вы запустите моделирование сейчас, вы не должны получить никаких ошибок, потому что мы уже проверили и знаем, что действительный сигнал распространяется правильно. Однако, чтобы полностью убедиться, что новый код работает, мы можем зайти в наш alu.sv и изменить выходной каскад, чтобы всегда устанавливать значение out_valid равным 1:

always_ff @ (posedge clk, posedge rst) begin

if (rst) begin

out       <= '0;

out_valid <= '0;

end else begin

out       <= result;

out_valid <= 1'b1;  //**** this should be in_valid_r ****//

end

end

Снова запустив симуляции, мы получим следующий результат:### SIMULATING ###

./obj_dir/Valu +verilator+rand+reset+2

ERROR: out_valid mismatch, exp: 0 recv: 1 simtime: 8

ERROR: out_valid mismatch, exp: 0 recv: 1 simtime: 10

ERROR: out_valid mismatch, exp: 0 recv: 1 simtime: 14

ERROR: out_valid mismatch, exp: 0 recv: 1 simtime: 16

ERROR: out_valid mismatch, exp: 0 recv: 1 simtime: 18

Круто, теперь мы действительно куда-то движемся.

Случайная допустимая генерация

Прежде чем завершить эту часть серии руководств Verilator, давайте также быстро заменим это единственное присвоение in_valid чем-то, что случайным образом устанавливает его в 1 или 0.

Для этого мы можем включить заголовок C++ cstdlib:

#include <cstdlib>

и используйте функцию генерации псевдослучайных чисел rand() для генерации случайных 1 и 0 в пользовательской функции set_rnd_out_valid:

void set_rnd_out_valid(Valu *dut, vluint64_t &sim_time){

if (sim_time >= VERIF_START_TIME) {

dut->in_valid = rand() % 2; // generate values 0 and 1

}

}

Нам также нужно запустить генератор случайных чисел, вызвав srand, который можно поставить прямо в начале основной функции:

int main(int argc, char** argv, char** env) {

srand (time(NULL));

Verilated::commandArgs(argc, argv);

Valu *dut = new Valu;

<...>

Мы также должны увеличить MAX_SIM_TIME до чего-то более существенного, например, до 300:

#define MAX_SIM_TIME 300

И, после запуска make sim и make wave, вот результаты нашей новой случайной симуляции с самопроверкой:

Рис. 6: Обновленное моделирование со случайными значениями
Рис. 6: Обновленное моделирование со случайными значениями

Готовый тестовый стенд
Вот текущая законченная версия нашего тестового стенда C++:

#include <stdlib.h>

#include <iostream>

#include <cstdlib>

#include <verilated.h>

#include <verilated_vcd_c.h>

#include "Valu.h"

#include "Valu___024unit.h"

#include "Valu.h"

#include "Valu___024unit.h"

#define MAX_SIM_TIME 300

#define VERIF_START_TIME 7

vluint64_t sim_time = 0;

vluint64_t posedge_cnt = 0;

void dut_reset (Valu *dut, vluint64_t &sim_time){

dut->rst = 0;

if(sim_time >= 3 && sim_time < 6){

dut->rst = 1;

dut->a_in = 0;

dut->b_in = 0;

dut->op_in = 0;

dut->in_valid = 0;

}

}

void check_out_valid(Valu *dut, vluint64_t &sim_time){

static unsigned char in_valid = 0; //in valid from current cycle

static unsigned char in_valid_d = 0; //delayed in_valid

static unsigned char out_valid_exp = 0; //expected out_valid value

if (sim_time >= VERIF_START_TIME) {

out_valid_exp = in_valid_d;

in_valid_d = in_valid;

in_valid = dut->in_valid;

if (out_valid_exp != dut->out_valid) {

std::cout << "ERROR: out_valid mismatch, "

<< "exp: " << (int)(out_valid_exp)

<< " recv: " << (int)(dut->out_valid)

<< " simtime: " << sim_time << std::endl;

}

}

}

void set_rnd_out_valid(Valu *dut, vluint64_t &sim_time){

if (sim_time >= VERIF_START_TIME) {

dut->in_valid = rand() % 2;

}

}

int main(int argc, char** argv, char** env) {

srand (time(NULL));

Verilated::commandArgs(argc, argv);

Valu *dut = new Valu;

Verilated::traceEverOn(true);

VerilatedVcdC *m_trace = new VerilatedVcdC;

dut->trace(m_trace, 5);

m_trace->open("waveform.vcd");

while (sim_time < MAX_SIM_TIME) {

dut_reset(dut, sim_time);

dut->clk ^= 1;

dut->eval();

if (dut->clk == 1){

dut->in_valid = 0;

posedge_cnt++;

set_rnd_out_valid(dut, sim_time);

check_out_valid(dut, sim_time);

}

m_trace->dump(sim_time);

sim_time++;

}

m_trace->close();

delete dut;

exit(EXIT_SUCCESS);

}

Заключение
Способ написания тестовых стендов на C++, безусловно, отличается от того, как можно спроектировать тестовый стенд на Verilog/SystemVerilog, но из примеров, приведенных в этом руководстве, вы можете увидеть, как отдельные функциональные части, написанные на Verilog, похожи на C++. Таким образом, глубокое понимание правильного порядка вызовов C++ для создания перепадов тактовых импульсов, стимуляции/проверки сигналов и вывода значений осциллограмм имеет решающее значение, если вы хотите применить свои навыки написания тестового стенда Verilog к C++.
И хотя текущая версия нашего тестового стенда еще довольно проста, она уже начинает напоминать более продвинутую среду проверки. Тестовый стенд теперь инициализирует все сигналы случайными значениями и содержит как случайные стимулы, так и непрерывный мониторинг, подобный утверждению, по крайней мере, для одного из выходов.