Я уже писал про симуляцию Verilog HDL проектов в ModelSim и с помощью Icarus Verilog. Однако, конечно, существуют и другие средства. Один из самых быстрых симуляторов, и к тому же свободный и бесплатный, - это Verilator. У него есть свои особенности:
- Verilator позволяет преобразовать Verilog модули в C++ классы, которые потом компилируются в обычную исполняемую программу. Запускаем получившуюся программу - запускаем симуляцию. Это позволяет достичь очень высокой производительности.
- Verilator, может обрабатывать только синтезируемый Verilog, то есть именно тот код, из которого потом получается "прошивка" для FPGA. Поведенческие модели, всякие присвоения с задержками вроде A = #5 ~A; работать не будут.
- Из пункта 2 следует, что тестбенч для симуляции нужно будет писать не на самом верилоге, как обычно, а на C++. Впрочем, в некотором смысле это даже плюс.
Ниже я приведу несколько очень простых примеров использования симулятора Verilator. Я умышленно буду все упрощать, возможно даже слишком упрощать, чтобы было лучше понятно, что из себя представляет Verilator.
1. Установка.
Установить Verilator в ОС Linux очень просто. Если у вас Debian или Ubuntu, то воспользуйтесь командой:
>sudo apt install verilator
Все, что нужно будет само собой установлено.
Если хотите знать подробнее, какие файлы и куда были установлены, то используйте команду
>dpkg-query -L verilator
Важно знать, где установлены заголовочные файлы, например, verilated.h и другие важные файлы. У меня в Ubuntu заголовочные файлы размещены в /usr/share/verilator/include/
Кстати, dpkg-query так же покажет и папку с примерами использования /usr/share/doc/verilator/examples.
В Windows использование верилатора возможно, но не так просто. Возможно придется ставить всякие вспомогательные компиляторы вроде Cygwin.
2. Компиляция Verilog модуля с помощью Verilator.
Для примера возьмем вот такой простой файл счетчика с асинхронным сбросом, написанный на Verilog HDL (файл counter.v):
module counter(
input wire rst,
input wire clk,
output reg [7:0]q
);
always @(posedge clk or posedge rst)
if(rst)
q <= 0;
else
q <= q+1;
endmodule
Запускаем верилатор простой командой:
>verilator -Wall -cc counter.v
В текущей папке появляется еще одна папка obj_dir, в которой размещены сгенерированные верилатором новые файлы. Там будут файлы Vcounter.h и Vcounter.cpp, которые описывают наш модуль счетчика, но уже в виде класса на языке C++. Кроме этих двух, там будет еще много других файлов. Есть там и Vcounter.mk - это Makefile с помощью которого можно собрать библиотеку, представляющую класс счетчика. Вот так:
>cd obj_dir
>make -f Vcounter.mk
После этого появляется библиотека для статической линковки Vcounter__ALL.a. Эту библиотеку можно использовать совместно с программой тестбенча.
3. Тестбенч счетчика для верилатора.
Как я писал выше, тестбенч нужно писать на C++. Самый простой будет выглядеть вот так (файл main.cpp):
#include <stdlib.h> #include "obj_dir/Vcounter.h" int main(int argc, char **argv) { // Initialize Verilators variables Verilated::commandArgs(argc, argv); // Create an instance of our module under test Vcounter *top_module = new Vcounter; // switch the clock int clock = 0; int num_cycles = 0; top_module->rst = 0; while( !Verilated::gotFinish() ) { top_module->clk = clock; top_module->eval(); printf("%d%02X\n", clock, top_module->q ); clock ^= 1; num_cycles++; if( num_cycles==20 ) break; } delete top_module; exit(EXIT_SUCCESS); }
Экземпляр модуля counter создается с помощью C++ оператора new() и позже можно устанавливать входные сигналы модуля простым присвоением top_module->clk = clock; После каждого присвоения новых входных сигналов нужно пересчитать модель вызовом функции eval(). Печатать в консоль состояния сигналов можно просто с помощью того же printf или cout. Цикл программы while( !Verilated::gotFinish() ) в этом тестбенче будет работать до тех пор, пока модель не вызовет задачу $finish(). Но поскольку мой счетчик не содержит такого вызова, то проще просто выполнить несколько циклов и выйти по break.
4. Компилирую и запускаю тестбенч.
Команда в консоли:
>g++ -o test -I/usr/share/verilator/include main.cpp /usr/share/verilator/include/verilated.cpp obj_dir/Vcounter__ALL.a
В результате компиляции получается выходной исполняемый файл ./test (я же указал опцию g++ компилятора "-o test").
Получившийся исполняемый файл запускаю из командной строки и получаю:
./test
0 00
1 01
0 01
1 02
0 02
1 03
0 03
1 04
0 04
1 05
0 05
1 06
0 06
1 07
0 07
1 08
0 08
1 09
0 09
1 0A
Как и ожидалось, в первой колонке printf выводит сигнал clock, а во второй колонке выходной сигнал счетчика q[7:0] в шестнадцатиричном виде.
5. Генерация VCD waveform.
Если при симуляции Verilog HDL проекта нужно получить временные диаграммы, то тестбенч нужно немного изменить и компилировать нужно немного по другому.
Прежде всего, генерировать файлы для библиотеки будем с дополнительной опцией --trace:
>verilator -Wall --trace -cc counter.v
Теперь точно так же, как и раньше будет создана папка obj_dir, но там теперь будет больше новых файлов. Да и сам C++ класс Vcounter будет содержать дополнительные функции, например, функцию trace(..) и другие.
В файл тестбенча теперь допишем немного кода, который влияет на трассировку и сохранение дампа сигналов.
#include <stdlib.h> #include "obj_dir/Vcounter.h" #include <verilated_vcd_c.h> int main(int argc, char **argv) { // Initialize Verilators variables Verilated::commandArgs(argc, argv); // Create an instance of our module under test Vcounter *top_module = new Vcounter; VerilatedVcdC* vcd = nullptr; Verilated::traceEverOn(true); // Verilator must compute traced signals vcd = new VerilatedVcdC; top_module->trace(vcd, 99); // Trace 99 levels of hierarchy vcd->open("out.vcd"); // Open the dump file // switch the clock vluint64_t vtime = 0; int clock = 0; top_module->rst = 0; while( !Verilated::gotFinish() ) { vtime+=1; if( vtime%8==0) clock ^= 1; if( vtime>45 && vtime<=49 ) top_module->rst = 1; else top_module->rst = 0; top_module->clk = clock; top_module->eval(); vcd->dump( vtime ); printf("%d%02X\n", clock, top_module->q ); if( vtime>500 ) break; } top_module->final(); if( vcd ) vcd->close(); delete top_module; exit(EXIT_SUCCESS); }
Для записи дампа сигналов в файл нужно оператором new() создать экземпляр класса VerilatedVcdC:
vcd = new VerilatedVcdC;
Затем у модуля, который требуется трассировать вызвать функцию trace:
top_module->trace(vcd, 99);
Ну и еще нужно открыть файл, куда сигналы будут записываться:
vcd->open("out.vcd");
Нужно ввести дополнительную переменную времени
vluint64_t vtime = 0;
и в рабочем цикле ее нужно наращивать:
vtime+=1;
Сигналы тестбенча привязываются к этому времени. Например, я ввел управление сигналом сброса rst:
if( vtime>45 && vtime<=49 ) top_module->rst = 1; else top_module->rst = 0;
После каждого перевычисления сигналов топ модуля eval() нужно делать запись сигналов в файл:
top_module->eval(); vcd->dump( vtime );
Теперь, программа тестбенча несомненно усложнилась, но ничего особо критично сложного тут нет.
Правда, чтобы скомпилировать проект придется компилятору еще подсунуть дополнительный файл verilated_vcd_c.cpp:
>g++ -o test -I/usr/share/verilator/include main.cpp /usr/share/verilator/include/verilated.cpp /usr/share/verilator/include/verilated_vcd_c.cpp obj_dir/Vcounter__ALL.a
После компиляции опять появляется исполняемый файл ./test, который и запускаю. По завершению симуляции вижу новый файл out.vcd. Его можно посмотреть с помощью GtkWave:
>gtkwave out.vcd
На временных диаграммах сигналов видно все как и положено:
Стоит еще заметить, что правильней всего было бы сделать управляемую/отключаемую запись сигналов в VCD файл. И это можно сделать с помощью условной компиляции C/C++ и проверкой дополнительных флагов командной строки запуска симуляции.
Переделаю файл тестбенча вот так:
#include <stdlib.h> #include "obj_dir/Vcounter.h" #ifdef VM_TRACE #include <verilated_vcd_c.h> #endif vluint64_t vtime = 0; // Called by $time in Verilog double sc_time_stamp() { return (double)vtime; } int main(int argc, char **argv) { // Initialize Verilators variables Verilated::commandArgs(argc, argv); // Create an instance of our module under test Vcounter *top_module = new Vcounter; #ifdef VM_TRACE VerilatedVcdC* vcd = nullptr; const char* flag = Verilated::commandArgsPlusMatch("trace"); if (flag && 0==strcmp(flag, "+trace")) { printf("VCD waveforms will be saved!\n"); Verilated::traceEverOn(true); // Verilator must compute traced signals vcd = new VerilatedVcdC; top_module->trace(vcd, 99); // Trace 99 levels of hierarchy vcd->open("out.vcd"); // Open the dump file } #endif // switch the clock int clock = 0; top_module->rst = 0; while( !Verilated::gotFinish() ) { vtime+=1; if( vtime%8==0) clock ^= 1; if( vtime>45 && vtime<=49 ) top_module->rst = 1; else top_module->rst = 0; top_module->clk = clock; top_module->eval(); #ifdef VM_TRACE if( vcd ) vcd->dump( vtime ); #endif //printf("%d %02X\n", clock, top_module->q ); if( vtime>500 ) break; } top_module->final(); #ifdef VM_TRACE if( vcd ) vcd->close(); #endif delete top_module; exit(EXIT_SUCCESS); }
Здесь все, что связанно с записью сигналов в VCD файл обрамлено в обычную сишную условную компиляцию
#ifdef VM_TRACE
....
#endif
Ну и дополнительно проверяется командная строка исполняемого файла:
const char* flag = Verilated::commandArgsPlusMatch("trace"); if (flag && 0==strcmp(flag, "+trace")) { printf("VCD waveforms will be saved!\n");
Теперь и компилировать нужно с флагом -DVM_TRACE, вот так:
>g++ -o test -DVM_TRACE -I/usr/share/verilator/include main.cpp /usr/share/verilator/include/verilated.cpp /usr/share/verilator/include/verilated_vcd_c.cpp obj_dir/Vcounter__ALL.a
После компиляции программы-симулятора, если хотите, просто симулятор, то запускаете просто
>./test
Если же хотите, чтоб симулятор записал временные диаграммы сигналов в файл VCD, то запускаете с опцией +trace:
>./test +trace
В завершении статьи хочу заметить, что Verilator имеет еще много неописанных мною возможностей, так что тут есть что поизучать. Например, я совершенно не коснулся темы coverage - покрытия тестами и темы интерфеса PLI. Ну и конечно, процесс сборки проектов можно упростить, если использовать не ручной запуск verilator/g++, а сборку через Makefile. В самом пакете verilator все примеры (из папки /usr/share/doc/verilator/examples) собираются через Makefile, то есть с использованием просто одной команды make. Но мне хотелось именно рассказать про внутреннюю кухню верилатора. Именно поэтому я предпочел в этой статье вручную запускать и verilator и g++.
Подробнее...