Verilator — это инструмент, который компилирует исходные коды Verilog и SystemVerilog в высокооптимизированный (и, возможно, многопоточный) циклически точный код C++ или SystemC. Преобразованные модули можно создавать и использовать в тестовом стенде C++ или SystemC для проверки и/или моделирования.
Дополнительную информацию можно найти на официальном сайте Verilator и в официальном руководстве.
Зачем использовать Verilator?
Verilator — это, по сути, симулятор Verilog/SystemVerilog. Это коммерческий продукт, сверхбыстрый, бесплатный и с открытым исходным кодом, но он не является прямой заменой Modelsim, Questa Sim, Synopsys VCS, Vivado Xsim и других симуляторов, основанных на событиях. Verilator — это симулятор, основанный на циклах, что означает, что он не оценивает время в пределах одного тактового цикла и не имитирует точную синхронизацию схемы. Вместо этого состояние схемы обычно оценивается один раз за такт, поэтому любые внутрипериодные сбои не наблюдаются, а временные задержки сигнала не поддерживаются. Это имеет как преимущества, так и недостатки при сравнении Verilator с другими симуляторами.
Скорость
Поскольку Verilator основан на циклах, его нельзя использовать для временной симуляции, списков соединений с обратными аннотациями, асинхронной (бестактовой) логики или вообще любых изменений сигнала, которые включают понятие времени - все выходы переключаются мгновенно всякий раз, когда оценивается схема.
Однако, поскольку все, что находится между фронтами тактовых импульсов, игнорируется, симуляции Verilator выполняются очень быстро и отлично подходят для имитации функциональности синхронных цифровых логических схем с одним или несколькими тактовыми генераторами или для создания программных моделей из вашего кода Verilog/SystemVerilog для использования в разработке программного обеспечения.
Качество кода
Поскольку верилятор основан на циклах, он не может полностью поддерживать стандарты IEEE Verilog и SystemVerilog. Но не волнуйтесь, инструменты синтеза, как правило, также не полностью совместимы.
Следовательно, Verilator очень строго относится к коду Verilog/SystemVerilog, который вы ему предоставляете. Помимо того, что он не поддерживает никаких временных задержек, он также не будет принимать большую часть несинтезируемого кода*, поэтому (как правило) вы не можете просто взять свой тестовый стенд SystemVerilog и построить его с помощью Verilator без каких-либо существенных модификаций. Вы быстро узнаете, не поддерживает ли Verilator ваш код, так как он будет бомбардировать вас кучей предупреждений и сообщений об ошибках.
Поскольку Verilator не поддерживает несинтезируемый код, неожиданное преимущество заключается в том, что он ближе к поведению инструментов синтеза по сравнению с другими симуляторами. Это заставит вас писать более качественный код для синтеза, потенциально уменьшив количество проблем, с которыми вы столкнетесь позже в процессе разработки. * за исключением таких вещей, как $display(), $finish(), $fatal(), некоторых версий $assert и других.
Цена
Verilator имеет открытый исходный код и бесплатен как бесплатное пиво, так и свобода слова. Для имитации проекта с помощью Verilator из проверенного HDL-кода и тестового стенда C++ с помощью GCC и Make создается собственный двоичный исполняемый файл. Поскольку вся цепочка инструментов бесплатна, нет ограничений на количество экземпляров, которые вы можете запускать, или количество пользователей, которые могут ее использовать. Благодаря скорости Verilator моделирование еще более сложных проектов выполняется быстро на старых машинах и ноутбуках, поэтому вам не нужно покупать дорогостоящее вычислительное оборудование. Отлично подходит для студентов!
Однако не позволяйте цене обмануть вас. Verilator также имеет широкий круг коммерческих пользователей:
Приступая к работе
Это руководство написано для тех, кто в первую очередь знает Verilog/Systemverilog и немного C/C++, но никогда не использовал Verilator. Я предполагаю, что вы используете какую-то форму Linux в своей системе и в указанной системе установлены Verilator, GTKWave, Make и GCC. Вы можете получить готовые исходные коды для примера проекта Verilator, используемого в этом руководстве, на Github и изучить его по своему усмотрению:
git clone https://github.com/n-kremeris/verilator_basics
git checkout verilator_pt1
Или следуйте инструкциям ниже, пока мы настраиваем все с нуля.
Наше тестируемое устройство
Чтобы проверить, как работает Verilator, нам понадобится какое-то DUT (тестируемое устройство). Я написал базовое ALU в SystemVerilog, которое мы проверим в этой серии руководств, состоящей из нескольких частей, источник которой приведен ниже:
/****** alu.sv ******/
typedef enum logic [1:0] {
add = 2'h1,
sub = 2'h2,
nop = 2'h0
} operation_t /*verilator public*/;
module alu #(
parameter WIDTH = 6
) (
input clk,
input rst,
input operation_t op_in,
input [WIDTH-1:0] a_in,
input [WIDTH-1:0] b_in,
input in_valid,
output logic [WIDTH-1:0] out,
output logic out_valid
);
operation_t op_in_r;
logic [WIDTH-1:0] a_in_r;
logic [WIDTH-1:0] b_in_r;
logic in_valid_r;
logic [WIDTH-1:0] result;
// Register all inputs
always_ff @ (posedge clk, posedge rst) begin
if (rst) begin
op_in_r <= '0;
a_in_r <= '0;
b_in_r <= '0;
in_valid_r <= '0;
end else begin
op_in_r <= op_in;
a_in_r <= a_in;
b_in_r <= b_in;
in_valid_r <= in_valid;
end
end
// Compute the result
always_comb begin
result = '0;
if (in_valid_r) begin
case (op_in_r)
add: result = a_in_r + b_in_r;
sub: result = a_in_r + (~b_in_r+1'b1);
default: result = '0;
endcase
end
end
// Register outputs
always_ff @ (posedge clk, posedge rst) begin
if (rst) begin
out <= '0;
out_valid <= '0;
end else begin
out <= result;
out_valid <= in_valid_r;
end
end
endmodule;
Чтобы следовать этому руководству, создайте новый рабочий каталог и сохраните исходный код ALU как alu.sv.
Как видите, это АЛУ очень простое. Он имеет две ступени регистра, без задержек и поддерживает только две операции: сложение и вычитание. Вот пример сигнала того, как мы ожидаем, что ALU будет вести себя:
Из рисунка 2 видно, что на первом этапе регистрируются входные сигналы, а на втором этапе комбинаторный результат регистрируется на выходах. Имея это в виду, давайте теперь рассмотрим шаги, необходимые для создания базового тестового стенда.
Преобразование SystemVerilog в C++
Проверка
Как упоминалось во введении, Verilator требует, чтобы тестовая среда C++ была скомпилирована в собственный двоичный файл системы. Однако мы не можем включить наш SystemVerilog ALU в тестовую среду C++ как есть: сначала нам нужно использовать Verilator для преобразования кода SystemVerilog в C++ или «проверить» его, что в самой простой форме делается следующим образом:
verilator --cc alu.sv
Здесь параметр --cc указывает Verilator преобразовать код в C++. Verilator также поддерживает преобразование в SystemC, которое можно выполнить с помощью --sc, но пока мы не будем использовать эту функцию.
Результаты конвертации
Выполнение вышеуказанной команды создает новую папку с именем obj_dir в нашем рабочем каталоге. Вот куда пошли все наши конвертированные исходники:
$ ls -l obj_dir/
Valu___024unit.cpp Valu___024unit__Slow.cpp Valu.cpp Valu.mk
Valu__Syms.cpp Valu__ver.d Valu___024unit.h Valu_classes.mk
Valu.h Valu__Slow.cpp Valu__Syms.h Valu__verFiles.dat
Сгенерированные файлы .mk будут использоваться с Make для создания нашего исполняемого файла моделирования, а файлы .h и .cpp содержат наши заголовки C++ и исходные коды реализации, полученные в результате преобразования SystemVerilog. Я предлагаю вам немного исследовать и посмотреть, сможете ли вы понять, что где.
Сейчас нас больше всего интересуют два файла: Valu.h и Valu___024unit.h:
1. Valu.h — это основной заголовок проекта, который содержит преобразованное определение класса «ALU» — это то, что мы «создадим» в нашем тестовом стенде C++ в качестве DUT.
2.Valu___024unit.h — это внутренний заголовок для класса «ALU», и он содержит определение нашего типа operation_t.
Разработка базового тестового стенда Verilator
Пример тестового стенда C++
Теперь, когда мы преобразовали наше тестируемое устройство в C++, мы можем приступить к написанию нашего тестового стенда. Наш тестовый стенд будет находиться в новом файле с именем tb_alu.cpp. Давайте начнем с изучения минимального примера кода тестового стенда C++:
#include <stdlib.h>
#include <iostream>
#include <verilated.h>
#include <verilated_vcd_c.h>
#include "Valu.h"
#include "Valu___024unit.h"
#define MAX_SIM_TIME 20
vluint64_t sim_time = 0;
int main(int argc, char** argv, char** env) {
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->clk ^= 1;
dut->eval();
m_trace->dump(sim_time);
sim_time++;
}
m_trace->close();
delete dut;
exit(EXIT_SUCCESS);
}
Первые несколько строк с операторами include говорят сами за себя. Нам нужно включить <verilated.h> и <verilated_vcd_c.h>, которые поставляются с установкой Verilator, чтобы получить доступ к общим процедурам Verilator и записать сигналы в файл VCD (дамп изменения значений).
Как описано ранее, «Valu.h» содержит верхний класс нашего модуля Verilated ALU, а «Valu___024unit.h» содержит проверенную версию нашего перечисления typedef.
Далее у нас есть следующие две строки:
#define MAX_SIM_TIME 20
vluint64_t sim_time = 0;
Мы будем использовать переменную sim_time, чтобы отслеживать, когда закончить симуляцию. Есть и другие способы завершить симуляцию, но сейчас мы просто выйдем, как только смоделируем 20 фронтов тактовых импульсов.
Затем у нас получилась следующая неразбериха с основной функцией, которая будет выглядеть странно, если вы до этого не писали тестбенч на C++:
int main(int argc, char** argv, char** env) {
Valu *dut = new Valu;
Verilated::traceEverOn(true);
VerilatedVcdC *m_trace = new VerilatedVcdC;
dut->trace(m_trace, 5);
m_trace->open("waveform.vcd");
/* <...> */
m_trace->close();
delete dut;
exit(EXIT_SUCCESS);
}
Строка Valu *dut = new Valu; создает экземпляр нашего преобразованного модуля ALU. В SystemVerilog это примерно соответствует alu dut (.*);.
Следующие четыре строки настраивают сброс сигнала. Примечательно, что мы создаем объект m_trace и передаем его нашему dut в строке dut->trace(m_trace, 5);. Параметр 5 просто ограничивает глубину трассировки до 5 уровней ниже тестируемого устройства.
Наконец, у нас есть то, что делает симуляцию реальностью:
while (sim_time < MAX_SIM_TIME) {
dut->clk ^= 1;
dut->eval();
m_trace->dump(sim_time);
sim_time++;
}
Во время каждой итерации цикла while строка dut->clk ^= 1; инвертирует часы ALU (это создаст нарастающие/спадающие фронты частоты). Это будут единственные частоты, которые мы будем использовать в нашем тестовом стенде.
Вызов dut->eval(); оценивает все сигналы в нашем модуле ALU, и m_trace->dump(sim_time); записывает все значения трассируемых сигналов в наш файл дампа сигнала. Как видите, в настоящее время модель оценивается только по частоте.
Затем время моделирования увеличивается, и цикл продолжается до тех пор, пока не достигнет MAX_SIM_TIME. Обратите внимание, что на самом деле это не значение времени — переменная просто отслеживает, сколько раз мы инвертировали частоту.
Как видите, этот тестовый стенд не делает ничего, кроме синхронизации дизайна, но это хорошая отправная точка для дальнейшего развития.
Создание исполняемого файла моделирования
Теперь, когда наш тестовый стенд написан, нам нужно создать исполняемый файл для запуска моделирования.
В отличие от некоторых других симуляторов, например Modelsim, в которых вы либо открываете исходники в графическом интерфейсе, либо передаете их в исполняемый файл Modelsim, приложение Verilator не используется для имитации тестового стенда.
Вместо этого приложение Verilator используется только для преобразования Verilog в C++ и создания инструкций по сборке для Make. Симулятором в данном случае является сам тестовый стенд C++.
Тестовый стенд и преобразованные исходные коды HDL, по сути, представляют собой приложение C++, которое создается и запускается на вашем компьютере. Запуск скомпилированного исполняемого файла — это то, что имитирует ваш проект, а коллекция компиляторов GNU (GCC) — это основной выбор для создания исполняемых файлов Verilator.
Чтобы создать исполняемый файл моделирования, нам нужно снова запустить Verilator, чтобы повторно сгенерировать файлы .mk для включения тестового стенда C++ — это делается с помощью --exe tb_alu.cpp:
$ verilator -Wall --trace -cc alu.sv --exe tb_alu.cpp
Кроме того, теперь мы используем следующие дополнительные параметры:
-Wall - включить все предупреждения C++. Не обязательно, но полезно, когда вы только начинаете
--trace — включить трассировку сигнала
Чтобы собрать наш исполняемый файл, мы делаем следующее:
$ make -C obj_dir -f Valu.mk Valu
-C obj_dir указывает make работать в каталоге obj_dir. Затем мы передаем необходимый make-файл, используя -f Valu.mk. Наконец, мы говорим make собрать целевое значение Valu, которое является именем скомпилированного исполняемого файла testbench.
Если исполняемый файл testbench был собран успешно, вы найдете двоичный файл Valu в obj_dir.
Запуск тестового стенда
После сборки просто запустите двоичный файл Valu, чтобы запустить симуляцию:
$ ./obj_dir/Valu
$
На первый взгляд, не так уж и много. В конце концов, в нашем коде тестового стенда
нет операторов печати, так что это ожидаемо. Однако запуск моделирования привел к тому, что в нашем рабочем каталоге был сгенерирован файл сигнала с именем waveform.vcd.
Просмотр сигналов Verilator
Вы можете открыть вышеупомянутый файл дампа сигнала Verilator с помощью GTKwave:
gtkwave waveform.vcd
Затем вам будет представлено окно GTKWave, которое выглядит следующим образом:
Выполните шаги, показанные здесь, чтобы загрузить сигналы из тестируемого устройства и просмотреть их формы:
Поздравляем! Вы только что завершили свою первую симуляцию с помощью Verilator!
Наблюдения
Во-первых, вы, возможно, заметили, что в нашей симуляции нет никаких «x» или неизвестных значений. Это связано с тем, что Verilator является симулятором с двумя состояниями, и по умолчанию все сигналы инициализируются 0. Отлично подходит для скорости, так как 2 состояния меньше, чем 4, но не очень хорошо, если мы хотим проверить, насколько хорошо работает наша логика сброса. Мы рассмотрим это позже.
Во-вторых, половина тактового цикла занимает 1 пс. Это шкала времени по умолчанию для Verilator, и в данном конкретном случае она не означает какого-либо конкретного значения времени.
Что дальше?
После прочтения этого руководства вы, надеюсь, получили некоторое базовое представление о том, как работает Verilator. Однако, как вы можете видеть, мы едва коснулись поверхности, поэтому, пожалуйста, переходите к части 2, где мы реализуем некоторые базовые функции проверки в нашем тестовом стенде C++.