verilator logo

Я уже писал про симуляцию Verilog HDL проектов в ModelSim и с помощью Icarus Verilog. Однако, конечно, существуют и другие средства. Один из самых быстрых симуляторов, и к тому же свободный и бесплатный, - это Verilator. У него есть свои особенности:

  1. Verilator позволяет преобразовать Verilog модули в C++ классы, которые потом компилируются в обычную исполняемую программу. Запускаем получившуюся программу - запускаем симуляцию. Это позволяет достичь очень высокой производительности.
  2. Verilator, может обрабатывать только синтезируемый Verilog, то есть именно тот код, из которого потом получается "прошивка" для FPGA. Поведенческие модели, всякие присвоения с задержками вроде A = #5 ~A; работать не будут.
  3. Из пункта 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

На временных диаграммах сигналов видно все как и положено:

gtkwave signals

Стоит еще заметить, что правильней всего было бы сделать управляемую/отключаемую запись сигналов в 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++.

 


Добавить комментарий