Содержание статьи
INFO
О том, что такое FPGA, как они устроены и почему во многих случаях они предпочтительнее, чем CPU, GPU и ASIC, читай в предыдущей статье: «FPGA. Разбираемся, как устроены программируемые логические схемы и чем они хороши».
Для примера я возьму простую и доступную Zybo board, но все будет работать на любой современной плате Xilinx, которая поддерживает Vivado. Если понадобится поменять файл ограничений, напиши мне, я смогу помочь.
Стоит сказать о требованиях к операционной системе. Я работаю в Ubuntu 16.04, но подойдут и Windows 7 или 10, CentOS 6 и 7 или SUSE последних версий.
Устанавливаем Vivado
Первым делом скачивай Vivado Design Suite — HLx Editions — 2018.2 Full Product Installation для своей ОС отсюда (на выбор варианты для Linux и для Windows).
Перед установкой следует зарегистрироваться на сайте.
Для ознакомительных целей я рекомендую установить бесплатную версию Vivado — WEB Pack, она по набору функций ничем не отличается от платной версии, но имеет ограничение на размер дизайна. Это значит, что счетчик в ней можно спроектировать, а что-то посложнее, что можно было бы продать, — вряд ли.
В конце установки откроется Vivado License Manager, также его можно открыть и из Vivado — через вкладку Help в главном меню. Сюда нам нужно подсунуть файл лицензии. Давай создадим ее.
- Заходим на xilinx.com, попадаем на следующую страницу.
- Выбираем свой аккаунт.
- Ставим галочку напротив Vivado Design Suite HL: WebPACK 2015 and Earlier License и жмем на Generate Node-Locked License.
- Далее используем MAC своей сетевой карты для идентификации хоста и генерируем файл лицензии, который ты позже получишь на почту.
- Получив файл лицензии в Vivado License Manager, жмем на Load Licence → Copy License.
Теперь Vivado установлена, и нам нужно убедиться, что она корректно запускается.
Открываем терминал в Linux и пишем:
$ source $XILINX_INSTALL_PATH/Vivado/2018.2/settings64.sh
где XILINX_INSTALL_PATH
— место установки Vivado. Теперь для запуска достаточно написать vivado
.
Также надо установить драйверы кабеля USB для загрузки прошивки. В Windows для этого просто ставим галочку Install Cable Drivers во время установки. В Linux следует вернуться в терминал и набрать следующее:
$ cd $XILINX_INSTALL_PATH/Vivado/2018.2/data/xicom/cable_drivers/lin64/install_script/install_drivers
Теперь ставим драйверы для платы Zybo:
$ sudo ./install_digilent.sh
Запускаем пример и моделируем схему
Мы спроектируем четырехбитный бинарный счетчик с задаваемым направлением счета и выводом значения на светодиоды. Счетчик будет синхронным: работать он будет на одной тактовой частоте. При работе в железе счетчик станет изменять свое значение не каждый период тактовой частоты, а один раз в секунду, иначе мы не увидим моргание (при частоте 125 МГц оно для глаза сольется в ровный свет).
После загрузки проекта в FPGA он будет работать так: каждую секунду четыре светодиода переключаются в соответствии с бинарным представлением значения счетчика. Помимо светодиодов, на плате есть еще кнопки и тумблеры. При нажатии на одну кнопку счетчик сбрасывается в ноль. Один из тумблеров разрешает счет, второй тумблер — задает направление счета.
Чтобы получить исходный код и запустить проект в Linux, нам нужно ввести такие команды:
$ git clone https://github.com/urock/rtl_examples.git
$ cd rtl_examples/counter_sv/vivado
$ source $XILINX_INSTALL_PATH/Vivado/2018.2/settings64.sh
$ ./create_project.sh
В Windows в месте выполнения скрипта create_project.sh
надо открыть специальное терминальное окно из меню «Пуск» и из папки, созданной во время установки Vivado, перейти в директорию rtl_examples/counter_sv/vivado
, где выполнить команду из файла create_project.sh
.
В терминале открывается Vivado, создает проект, добавляет в него исходные файлы. Далее открывается Vivado TCL shell, здесь вводим команду start_gui
, чтобы перейти в режим GUI. В окне Flow Navigator жмем Simulation → Run Simulation → Run Behavioral Simulation и видим окно вейвформ сигналов.
В этом окне отображаются зависимости от времени логических сигналов внутри нашей схемы счетчика. Мы видим тактовый сигнал clk
, сигнал сброса reset
, сигнал разрешения работы enable
, сигнал выбора направления счета dir
, сигнал cnt_en
, который определяет частоту переключения битов счетчика, и, наконец, значение счетчика, выведенное на четыре светодиода.
Самое время посмотреть на исходные файлы! Закрываем симуляцию и смотрим в окно Sources.
В разделе Design Sources лежат исходные файлы RTL на языке System Verilog: counter.sv
и counter_top.sv
. В каждом определено по одноименному модулю, причем, как видно, модуль counter
находится внутри модуля counter_top
, что определяет иерархию модулей проекта.
В разделе Constraints находятся файлы ограничений XDC (Xilinx Design Constraints). Как уже упоминалось, в них определяются ножки микросхемы, к которым должны подключаться порты ввода-вывода верхнего уровня RTL (top level) и период тактового сигнала.
В разделе Simulation Sources, помимо наших файлов RTL, мы видим еще один уровень иерархии — самый верхний. Это так называемый test bench
(tb) — виртуальный стенд, куда мы поместили модуль counter_top
. Надо обратить внимание, что модуль counter_tb
не имеет портов ввода-вывода: входные сигналы для нашей схемы назначаются непосредственно средствами языка System Verilog.
Посмотрим на код counter.sv
.
timescale 1ns / 10ps
module counter
#(parameter
WIDTH = 4
)(
input logic clk,
input logic reset,
input logic cnt_en,
input logic dir,
output logic [WIDTH-1:0] cnt_val
);
always_ff @(posedge clk) begin
if (reset) begin
cnt_val <= 0;
end else begin
if (cnt_en) begin
if (dir) begin
cnt_val <= cnt_val + 1;
end else begin
cnt_val <= cnt_val - 1;
end
end
end
end
endmodule // counter
Описание модуля counter
начинается с определения его параметров и портов ввода-вывода. Параметр WIDTH
определяет ширину слова регистра счетчика. По умолчанию оно равно 4, следовательно, счетчик может принимать значения без знака от 0 до 15.
Далее идет блок always_ff
, в котором задается регистр счетчика cnt_val
. Он может переключаться только по переднему фронту сигнала синхронизации clk
. Сигнал сброса reset
имеет наибольший приоритет при назначении сигнала cnt_val
, если он равен 1, то cnt_val
обнуляется. В противном случае если сигнал разрешения счета cnt_en
равен единице, то cnt_val
увеличивается или уменьшается на 1.
Между передними фронтами сигнала синхронизации clk
счетчик сохраняет свое значение независимо от других входных сигналов. Если reset = 0
, то счетчик будет менять свое значение на каждом такте, на котором cnt_en = 1
.
Получается, что, управляя сигналом cnt_en
, можно менять частоту переключения счетчика. Как раз это и происходит в модуле counter_top
.
timescale 1ns / 10ps
module counter_top
#(parameter
WIDTH = 4,
DIV = 125000000
)(
input logic clk,
input logic reset,
input logic enable,
input logic dir,
output logic [WIDTH-1:0] leds
);
// Input clk frequency = 125 MHz
// That is 125 M ticks per second
// Minimum bit length for 125M is 27 bits
logic [26:0] div_cnt;
logic div_clr;
always_ff @(posedge clk) begin
if (reset) begin
div_cnt <= 0;
end else begin
if (enable) begin
if (div_clr) begin
div_cnt <= 0;
end else begin
div_cnt <= div_cnt + 1;
end
end
end
end
assign div_clr = (div_cnt == DIV) ? 1'b1 : 1'b0;
counter #(
.WIDTH (WIDTH)
) counter_rtl (
.clk (clk),
.reset (reset),
.cnt_en (div_clr & enable),
.dir (dir),
.cnt_val (leds)
);
endmodule // counter_top
Тут в начале файла точно так же идет определение параметров и портов ввода-вывода. Параметр WIDTH
мы уже знаем, параметр DIV
рассмотрим чуть позже. Интерфейсные сигналы модуля counter_top
подсоединяются к внешним компонентам FPGA. На вход clk
подается сигнал 125 МГц от внешнего кварцевого генератора, вход reset
подключен к кнопке сброса, входы enable
и dir
— к тумблерам. Выход leds[3:0]
подключен к четырем светодиодам.
Теперь посмотрим, как модуль counter
вставляется внутри модуля counter_top
(конец файла, строки с 35 по 43). Начинается вставка с имени модуля, в нашем случае counter
. Далее идет блок указания параметров модуля. Затем название конкретного экземпляра — в нашем случае counter_rtl
.
INFO
Один и тот же модуль может быть вставлен в дизайне несколько раз. Логика и аппаратные ресурсы при этом дублируются, поэтому название экземпляра каждый раз должно быть уникальным.
Дальше идет блок подключения локальных сигналов модуля, где происходит вставка с указанием портов ввода-вывода того модуля, который вставляется. Синтаксис тут такой: после точки идет имя порта, затем в скобках указывается локальный сигнал для соединения с этим портом.
Мы видим, что порты clk
, reset
, dir
и cnt_val
экземпляра counter_rtl
подключены напрямую к портам ввода модуля counter_top
. Внутри модуля его интерфейсные порты ввода-вывода можно рассматривать как локальные сигналы. А к порту cnt_en
подключен сигнал div_clr & enable
— логическое «И» сигналов div_clr
и enable
.
Получается, что cnt_en
будет равен единице тогда и только тогда, когда оба сигнала div_clr
и enable
равны единице. Enable
— это сигнал с тумблера, он либо есть, либо отсутствует, использовать его для управления частотой счета не получится.
А вот div_clr
мы сформировали сами чуть выше, он равен единице, когда счетчик div_cnt
досчитал до значения параметра DIV
, равного единице. В остальное время div_clr
равен нулю.
DIV
по умолчанию имеет значение 125 000 000, что равно частоте тактового сигнала 125 МГц. Счетчик div_cnt
сбрасывается сигналом div_clr
, в противном случае он инкрементируется каждый такт системной частоты.
Получается следующее: div_cnt
считает по кругу от 0 до 124 999 999, div_clr
на один такт системной частоты становится равен 1, и происходит это ровно один раз в секунду. Тогда и для модуля counter
сигнал cnt_en
будет выставляться ровно на один такт каждую секунду, и счетчик будет изменять свое значение на единицу, что мы и увидим в железе.
В учебных целях симуляцию схемы удобно проверять, смотря глазами на вейвформы сигналов. 125 миллионов тактов сложно посчитать человеку, поэтому при симуляции я задаю DIV
равным 5 и cnt_en
у меня равен единице каждые пять тактов. Логику схемы это не меняет. Предлагаю тебе самому в этом убедиться, равно как и разобраться в коде counter_tb.sv
, там все довольно просто, а я пока начну собирать прошивку для FPGA дальше.
Синтезируем код и анализируем результат
Как ты помнишь, следующим шагом после моделирования кода RTL и выявления логических ошибок идет синтез схемы. На этом шаге абстрактные конструкции RTL реализуются в виде связанного набора компонентов аппаратных ресурсов, доступных для конкретно микросхемы FPGA.
Но прежде чем нажимать Synthesis → Run Synthesis в окне Flow Navigator, давай посмотрим на другой подход к проектированию цифровых устройств, а именно на схемотехническое описание. Раньше оно было довольно распространено в качестве основного инструмента для ввода информации о схеме, но с развитием языков VHDL и Verilog отошло на второй план. Тем не менее очень полезно взглянуть на графическое представление того, что ты написал на Verilog. Для этого во Flow Navigator жмем RTL Analysis → Open Elaborated Design → Schematic.
Тут мы видим верхний уровень нашей схемы, а именно порты ввода-вывода, счетчик div_cnt
, представленный в виде регистров из 27 D-триггеров, а также комбинационную схему формирования сигналов div_clr
и cnt_en
.
Интересно посмотреть на div_clr
, по коду он у нас равен 1, когда div_cnt
равен DIV
, то есть единице. По сути, это операция сравнения числа с константой, и она реализовалась в виде булевой функции с одним входом, представленной на рисунке компонентом RTL_ROM
. Для этой функции есть таблица истинности, состоящая из огромного числа нулей и только одной единицы, которая соответствует входному значению DIV
.
Теперь раскроем модуль counter_rtl
.
Тут мы видим красивую и понятную схему четырехбитного счетчика, представленного регистром и комбинационной схемой, которая меняет его значение. В зависимости от значения dir
через мультиплексор на вход регистра поступает либо выход схемы сумматора (инкремент текущего значения), либо выход схемы разности (декремент текущего значения).
Давай сравним эти красивые схемы с тем, что получится после синтеза. Закрываем Elaborated Design и жмем Synthesis → Run Synthesis, после окончания процесса жмем Synthesis → Open Synthesised Design → Schematic. Если ты работаешь в Vivado, то увидишь огромную схему из десятков компонентов. Найди на схеме блок counter_rtl
и открой его.
Видно, что абстрактные комбинационные схемы типа сумматора и мультиплексора исчезли, вместо них появилось много LUT. Это уже LUT, который относится к твоему кристаллу FPGA. Разобраться в этой схеме сложнее, чем прочитать бинарный машинный код. Но это и не требуется, программы-синтезаторы, как и компиляторы кода для ЦП, сейчас достаточно развиты и надежны.
Перейдем к следующему шагу — размещению компонентов на кристалле (Place) и конфигурации связей между ними (Route). Для этого в окне Flow Navigator жмем Implementation → Run Implementation. Процесс займет пару минут.
После его завершения жмем на Implementation → Open Implemented Design, что откроет окно Device. В нем отображаются аппаратные ресурсы FPGA. Занятые ресурсы подсвечены сине-зеленым цветом. Выдели мышкой прямоугольник вокруг таких ресурсов, чтобы увеличить масштаб в этой области и увидеть отдельные CLB и занятые в них ресурсы.
На рисунке видно, что в выбранном CLB заняты все четыре LUT и четыре из восьми триггеров, а также используется специальная цепь переноса, нужная для операции сложения. Каждый ресурс можно выделить мышкой, узнать его номер, статус и какой логической цепи он соответствует. Оставляю тебя исследовать это окно самостоятельно.
Получаем прошивку и загружаем ее в FPGA
Осталось сгенерировать файл прошивки и загрузить его в FPGA.
Жмем на Program and Debug → Generate Bitstream. После окончания процесса подключаем к плате кабель microUSB в порт PROG/UART и включаем питание тумблером на плате. Далее в Vivado жмем на Program and Debug → Open Hardware Target → Open Target → Auto Connect.
В открывшемся окне Hardware Manager правой кнопкой мыши кликаем по названию кристалла xc7z010_1
и выбираем Program Device.
Далее на плате находим тумблеры SW1 (P15) — сигнал enable
и SW0(G15) — dir
. Устанавливаем SW1 вверх и наблюдаем, как раз в секунду переключаются светодиоды. Кнопка BNT0 (R18) reset
сбрасывает счетчик в 0.
Выводы
В этой и предыдущей статьях я хотел познакомить тебя с технологией FPGA: дать первое представление об использовании, архитектуре и методах проектирования.
Хотя я говорил, что существуют высокоуровневые средства проектирования на языке C/C++, все равно для успешного использования FPGA необходимо иметь твердое понимание, каким образом код C++ будет переведен в «железо» и какая цифровая схема будет синтезирована. Для этого нужно уметь проектировать на уровне RTL на языках VHDL или Verilog.
Должен сказать, что пока высокоуровневые средства не могут полностью заменить RTL и большинство команд, которые отвечают за FPGA в серьезных компаниях, продолжают использовать именно этот способ.
Для изучения RTL я хочу порекомендовать книгу «Цифровая схемотехника и архитектура компьютера» за авторством Дэвида и Сары Харрис. Все, что там написано, я считаю обязательным для понимания инженером-программистом FPGA. Еще одна книга для изучения истории и архитектуры FPGA — «Проектирование на ПЛИС. Архитектура, средства и методы. Курс молодого бойца» Клайва Максфилда. Отличное неутомительное чтение!
На рынке специалисты FPGA традиционно ценятся и у нас, и на Западе. Дело в том, что хотя задач для таких специалистов меньше, чем для других программистов, и обычно на компанию требуется не больше одной команды до пяти человек, но мест, где готовят FPGA-шников, еще меньше, а знания RTL специфичны.
Поэтому хороших спецов мало и все они нарасхват. При этом, конечно, очень важно иметь сильные навыки в других языках программирования, а также математическую подготовку для понимания вычислительных алгоритмов, которые ты реализуешь.
Главное — критично относиться к необходимости использования FPGA для решения той или иной задачи: при всех своих плюсах разработка на FPGA занимает намного больше времени, чем на CPU или GPU. Используй свои знания с умом и продолжай совершенствоваться!