§ Установка ПО

Всем привет! Пока еще не запретили Интернет и всякие китайские товары (вещание происходит из чревовещателя за 14 апреля 2022 ужасного года), я бродкастирую (вещаю) из своего уютного бункера. Сегодня я решил начать серию рассказов о том, каким образом можно создать простую схемку, а потом чуть более сложную схему и запустить ее выполняться на верилоге.
Первое и самое важное, что я хочу сказать — работать я буду только в linux ubuntu, потому что там есть необходимое мне для работы программное обеспечение. Для винды не знаю, не пробовал, потому без понятия, как там настроить нормально. Например, если еще в винде есть iverilog и прочие ништяки, то verilator что-то на горизонте не наблюдался, либо я искал плохо.
Итак, что требуется для установки (sudo apt install):
  • iverilog gtkwave — для синтеза
  • verilator — для тестирования
  • libsdl2-2.0-0 libsdl2-dev — для запуска теста
После того, как все установлено, можно начинать кодить.

§ Самый простой проект

Пользоваться будем компилятором icarus verilog и потому я создам самый простой проект в истории человечества, а именно hello world. Файл назову tb.v:
1`timescale 10ns / 1ns
2module tb;
3
4  reg           clock;
5  always  #0.5  clock = ~clock;
6  initial begin clock = 0; #2000 $finish; end
7
8endmodule
Разберу построчно.
  • `timescale 10ns / 1ns — это директива компилятору и симулятору, что минимальной единицей измерения является 1 наносекунда, а единицей отсчета - 10 наносекунд. Это примерно как 1 миллиметр и 10 миллиметров (1 сантиметр). Отсчеты ведутся именно на 10ns.
  • module tb; ... endmodule — начало и завершение модуля (обертка модуля)
  • reg clock; — объявление регистра clock. Регистр - это 1 бит памяти, который может хранить либо 0, либо 1
  • initial begin clock = 0; #2000 $finish; end — инициализация при старте кода, вначале регистру clock присваивается значение 0, и спустя 2000*10ns (указано в timescale) = 20 000 нс, или 20 микросекунд, симуляция будет завершена
  • always #0.5 clock = ~clock; — эта конструкция означает, что каждые 0.5*10 нс или каждые 5 нс будет перебрасываться из 0 в 1, и из 1 в 0, тем самым симулируется тактовый генератор.

§ Создание makefile

Обычно я люблю создавать makefile для того, чтобы выполнять разные рутинные действия:
1all:
2	iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v
3	vvp tb.qqq >> /dev/null
Компиляция
  • iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v — это компилятор, который компилирует файл tb.v в tb.qqq
  • -g2005-sv — опция означает, что используется system verilog 2005
  • -DICARUS=1 — передача параметр ICARUS=1, то есть, делается #define ICARUS 1, параметров может быть много
  • -o tb.qqq — куда выгрузить откомпилированный файл
Симулятор
vvp tb.qqq >> /dev/null
Эта программа приблизительно симулирует поведение верилог-файла, который был передан. Результаты выгружаются отдельно, команда, которая выгружает результаты, задается в tb.v файле:
1module tb;
2...
3initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
4...
5endmodule;
Что означает:
  • $dumpfile("tb.vcd"); — команда говорит, что результаты симуляции будут выгружены в tb.vcd файл
  • $dumpvars(0, tb); — то, из какого модуля будут выгружены результаты
Помимо значений регистров и проводов в модуле, будут выгружены также результаты из всех субмодулей, если они есть.

§ Отладка

После компиляции появится 2 новых файла:
  • tb.qqq
  • tb.vcd
Чтобы посмотреть, что получилось в симуляции, я пользуюсь GtkWave, для этого надо запустить команду gtkwave tb.vcd.

Перед тем, как сигнал посмотреть, надо выбрать модуль, потом выбрать один из проводов или регистров и добавить через Append. Обновлять сигналы можно через сочетание клавиш ctrl+shift+r, чтобы не перезагружать снова.
После того, как сигналы были добавлены, можно сохранить их через ctrl+s, выбрав имя, например, tb.gtkw. Теперь же, после каждой загрузки можно не добавлять сигналы снова, а просто вызвать команду gtkwave tb.gtkw.

§ Verilator

Признаюсь, верилятор я освоил совсем недавно и был крайне удивлен тому, как он работает и вообще, считаю его самым интересным инструментом для работы. К нему я еще могу вернуться в будущем, когда буду делать видеоадаптер. А сейчас пока что расскажу как первично его настроить.
Для чего нужен верилятор вообще? Собственно, только для того, чтобы преобразовать модуль из верилога в c++ код, представить его полный эквивалент, чтобы потом использовать в тестировании кода вне ПЛИС-а.
Как обычно, сначала перейдем к рассмотрению makefile
1VINC=/usr/share/verilator/include
2
3all:
4	verilator -Wall -Wno-unused -cc tb.v
5	cd obj_dir && make -f Vtb.mk
6	g++ -o tb -I$(VINC) tb.cc $(VINC)/verilated.cpp obj_dir/Vtb__ALL.a
Теперь внимательно разберусь с каждым пунктом:
  • VINC=/usr/share/verilator/include - это путь к библиотекам верилятора, очень важно
  • verilator -Wall -Wno-unused -cc tb.v - собственно, компиляция tb.v
  • cd obj_dir && make -f Vtb.mk - переход в рабочий каталог и сборка
  • g++ -o tb -I$(VINC) tb.cc $(VINC)/verilated.cpp obj_dir/Vtb__ALL.a - строка компиляции С++ файла
Есть момент такой, что когда синтезируется модуль tb, то имя его класса в С++ становится Vtb - то есть, приписывается V вначале (от слова Verilated, я так понимаю). Здесь obj_dir/Vtb__ALL.a - это архивированные коды для дальнейшего их использования в линковщике g++.
Итак, теперь рассмотрим то, что находится в tb.cc:
1#include "obj_dir/Vtb.h"
2
3int main(int argc, char **argv) {
4
5    Verilated::commandArgs(argc, argv);
6    Vtb* top = new Vtb;
7
8    top->a = 0;
9
10    // Обработка 10 тактов
11    for (int i = 0; i < 10; i++) {
12
13        top->eval();
14        printf("i=%d | a=%d, b=%d\n", i, top->a, top->b);
15        top->a = !top->a;
16
17        if (Verilated::gotFinish()) break;
18    }
19
20    delete top;
21    return 0;
22}
Этот код можно считать неким таким шаблонов для тестбенчей на вериляторе.В самом начале подключается необходимый h-файл:
#include "obj_dir/Vtb.h"
А потом создается объект класса:
Vtb* top = new Vtb;
При каждом запуске обязательно надо инициализировать код:
Verilated::commandArgs(argc, argv);
Инициализируется top->a = 0; стартовое состояние входа a. Выполняется 10 тактов. На каждом такте происходит исполнение модуля, симуляция:
top->eval();
И потом значение входа меняется на противоположное:
top->a = !top->a;
И так 10 раз.
В результате выполнения программы получается следующее:
i=0 | a=0, b=1
i=1 | a=1, b=0
i=2 | a=0, b=1
i=3 | a=1, b=0
i=4 | a=0, b=1
i=5 | a=1, b=0
i=6 | a=0, b=1
i=7 | a=1, b=0
i=8 | a=0, b=1
i=9 | a=1, b=0
Что, собственно, и правильно.