Разработка железа на 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
Это создает наш исполняемый файл моделирования из тестового стенда и преобразованных источников. Мы говорим 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 из наших предыдущих результатов моделирования:
Кроме того, если у вас есть код, который присваивает 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
Теперь, если мы сделаем чистые и создадим волны, мы увидим, что теперь сигналы инициализируются случайными значениями в начале симуляции:
Теперь, когда сигналы рандомизированы, мы можем взглянуть на применение нашего сигнала сброса.
Сброс тестируемого устройства
Чтобы сбросить наше тестируемое устройство и его входные сигналы, мы обновляем основной цикл нашего тестового стенда, чтобы он выглядел следующим образом:
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, наш сигнал сброса успешно сгенерирован в тестовом стенде. Чтобы сделать основной цикл немного чище, давайте переместим элементы сброса в отдельную функцию вне 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:
Игнорируя входы 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, вот результаты нашей новой случайной симуляции с самопроверкой:
Готовый тестовый стенд
Вот текущая законченная версия нашего тестового стенда 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++.
И хотя текущая версия нашего тестового стенда еще довольно проста, она уже начинает напоминать более продвинутую среду проверки. Тестовый стенд теперь инициализирует все сигналы случайными значениями и содержит как случайные стимулы, так и непрерывный мониторинг, подобный утверждению, по крайней мере, для одного из выходов.
Лента материалов
Соблюдение Правил конференции строго обязательно!
Флуд, флейм и оффтоп преследуются по всей строгости закона!
Комментарии, содержащие оскорбления, нецензурные выражения (в т.ч. замаскированный мат), экстремистские высказывания, рекламу и спам, удаляются независимо от содержимого, а к их авторам могут применяться меры вплоть до запрета написания комментариев и, в случае написания комментария через социальные сети, жалобы в администрацию данной сети.
Комментарии Правила