Если ты хочешь превратить код в микросхему, используя FPGA, то эта статья поможет тебе освоиться со всеми инструментами. Мы создадим простейший бинарный счетчик, способный считать вниз и вверх. Исходный код на языке Verilog мы промоделируем и синтезируем в среде разработки Xilinx Vivado. Заодно познакомимся с процессом разработки прошивки, а результат можно будет проверить на отладочной плате.

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 2018.2
Программа установки Vivado 2018.2

В конце установки откроется Vivado License Manager, также его можно открыть и из Vivado — через вкладку Help в главном меню. Сюда нам нужно подсунуть файл лицензии. Давай создадим ее.

Скрин страницы http://www.xilinx.com/getlicense
Скрин страницы http://www.xilinx.com/getlicense
  1. Заходим на xilinx.com, попадаем на следующую страницу.
  2. Выбираем свой аккаунт.
  3. Ставим галочку напротив Vivado Design Suite HL: WebPACK 2015 and Earlier License и жмем на Generate Node-Locked License.
  4. Далее используем MAC своей сетевой карты для идентификации хоста и генерируем файл лицензии, который ты позже получишь на почту.
  5. Получив файл лицензии в Vivado License Manager, жмем на Load Licence → Copy License.
Скрин Vivado License Manager на данном этапе
Скрин Vivado License Manager на данном этапе

Теперь 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.

RTL Analysis TOP
RTL Analysis TOP

Тут мы видим верхний уровень нашей схемы, а именно порты ввода-вывода, счетчик div_cnt, представленный в виде регистров из 27 D-триггеров, а также комбинационную схему формирования сигналов div_clr и cnt_en.

Интересно посмотреть на div_clr, по коду он у нас равен 1, когда div_cnt равен DIV, то есть единице. По сути, это операция сравнения числа с константой, и она реализовалась в виде булевой функции с одним входом, представленной на рисунке компонентом RTL_ROM. Для этой функции есть таблица истинности, состоящая из огромного числа нулей и только одной единицы, которая соответствует входному значению DIV.

Теперь раскроем модуль counter_rtl.

RTL Analysis Counter
RTL Analysis Counter

Тут мы видим красивую и понятную схему четырехбитного счетчика, представленного регистром и комбинационной схемой, которая меняет его значение. В зависимости от значения dir через мультиплексор на вход регистра поступает либо выход схемы сумматора (инкремент текущего значения), либо выход схемы разности (декремент текущего значения).

Давай сравним эти красивые схемы с тем, что получится после синтеза. Закрываем Elaborated Design и жмем Synthesis → Run Synthesis, после окончания процесса жмем Synthesis → Open Synthesised Design → Schematic. Если ты работаешь в Vivado, то увидишь огромную схему из десятков компонентов. Найди на схеме блок counter_rtl и открой его.

Counter after Synthesis
Counter after Synthesis

Видно, что абстрактные комбинационные схемы типа сумматора и мультиплексора исчезли, вместо них появилось много LUT. Это уже LUT, который относится к твоему кристаллу FPGA. Разобраться в этой схеме сложнее, чем прочитать бинарный машинный код. Но это и не требуется, программы-синтезаторы, как и компиляторы кода для ЦП, сейчас достаточно развиты и надежны.

Перейдем к следующему шагу — размещению компонентов на кристалле (Place) и конфигурации связей между ними (Route). Для этого в окне Flow Navigator жмем Implementation → Run Implementation. Процесс займет пару минут.

После его завершения жмем на Implementation → Open Implemented Design, что откроет окно Device. В нем отображаются аппаратные ресурсы FPGA. Занятые ресурсы подсвечены сине-зеленым цветом. Выдели мышкой прямоугольник вокруг таких ресурсов, чтобы увеличить масштаб в этой области и увидеть отдельные CLB и занятые в них ресурсы.

Placed hardware resources
Placed hardware resources

На рисунке видно, что в выбранном 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
Hardware Manager

В открывшемся окне 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. Используй свои знания с умом и продолжай совершенствоваться!

Оставить мнение