После написания последнего обзора на новую отладку Я не смог удержаться от того, чтобы не сделать простую проверку работоспособности платы, т.к. очень не хотелось бы напороться на какие-либо проблемы во время решения сложной задачи. Поэтому решил сделать простую мигалку светодиодами и задействовать, плюсом к этому, кнопки на плате. Немного поразмыслив, Я решил, что обычный “ногодрыг” на Verilog - это уже не так интересно и мне показалось, что лучше сделать это с помощью AXI GPIO и своего IP-ядра, инициировав экшн из baremetal-приложения. В общем, кому интересно, заглядывайте в статью, там Я описал, как добавить свое кастомное AXI Peripheral IP-ядро, как правильно организовать проект и обратиться к GPIO для чтения и записи логического уровня. Поехали…
Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
Как обычно, сначала создаем проект…
Открываем Vivado и из главного окна создаем новый проект. Пишем его название и указываем директорию куда будем сохранять проект:
Далее выбираем RTL Project, оставляем включенной галочку Do not specify sources at this time и идем дальше. Находим интересующую нас модель SoC Zynq:
Тут стоит отдельно отметить, что у меня проект корректно работал на ZynqMini со 2-м Speed Grade. Невозможно было однозначно определить Speed Grade чипа, т.к. QR-код на чипе зашлифован. Надо будет проверить в более крупных проектах.
Идём далее и перед нами предстает окно Vivado, готовое к работе.
Постановка задачи
Итак. Прежде чем приступать к действиям, надо придумать интересную задачу и понять, что мы должны получить в результате. Предлагаю сделать следующее. Задействуем каждую из кнопок для включения разных сценариев моргания четырьмя светодиодами: первый сценарий - светодиоды будут загораться поочередно, второй сценарий - сменим направление у анимации, третий сценарий, когда нажаты обе кнопки - анимация сходится в центре, и по умолчанию просто мигаем светодиодами. Сделаем всё с использованием AXI GPIO, через свое собственное IP-ядро и запрограммируем логику работы кнопок через C-приложение которое мы запустим в baremetal на одном из ARM ядер. В целом, звучит не сложно. Идём дальше.
Создаем своё новое IP-ядро
В первую очередь создадим и опишем логику нашего собственного IP-ядра, который будет взаимодействовать с AXI-интерконнектом. Нажимаем в главном меню опцию Tools - Create and Package New IP:
Откроется мастер создания IP-ядра, нажимаем Next и выбираем Create a new AXI4 peripheral:
Заполняем имя, версию и прочую информацию. Сразу советую использовать короткие имена и без дефисов в названии. У меня получилось вот так:
Описываем интерфейс AXI4 и его параметры:
После переключаемся в режим редактирования установив опцию Edit IP и нажимаем Finish:
Откроется отдельное окно в котором можно настроить дополнительные параметры IP-ядра:
Теперь создадим Verilog-файл, в котором мы опишем основную логику маршрутизации сигналов. Для этого в меню Sources нажимаем на синий крестик и вызовем мастер добавления Sources. Выберем пункт меню Add or create design sources.
В следующем меню нажимаем кнопку Create File и назовем его gpio_logic.v и выберем место хранения и нажимаем Finish:
В следующем окне, предлагающем нам определить порты I\O модуля - нажимаем OK, мы это сделаем вручную. Откроем в списке Sources только что созданный файл и запишем в него следующее:
module gpio_logic(
// from buttons
input wire [1:0]gpio_input,
// to led pins
output wire [3:0]gpio_output,
// to zynq read
output wire [1:0]zynq_gpio_input,
// from zynq write
input wire [3:0]zynq_gpio_output
);
assign zynq_gpio_input[1:0] = gpio_input[1:0];
assign gpio_output[3:0] = zynq_gpio_output[3:0];
endmodule
По сути, он связывает сигналы Zynq PS и PL. После этого открываем меню File Groups и открываем файл axi_gpio_button_and_led_v1_0.v, в него мы внесем некоторые изменения для корректной маршрутизации сигналов из AXI:
Открыв файл на редактирование находим блок в котором в комментариях написано Users to add ports here. Между комментариями мы запишем определение сигналов и сохраним изменения:
Запишем следующее (не забывая про запятые, т.к. это перечисление портов I\O):
input wire [1:0] gpio_input, // from FPGA pins
output wire [3:0] gpio_output, // to FPGA pins
Затем изменим обработку сигналов на следующем уровне. Откроем файл axi_gpio_button_and_led_v1_0_S00_AXI.v. В него тоже запишем перечисление портов, как в предыдущем файле:
Напишем следующее:
input wire [1:0] gpio_input, // from FPGA pins
output wire [3:0] gpio_output, // to FPGA pins
Сохраняем и переходим обратно к файлу axi_gpio_button_and_led_v1_0.v. Листаем ниже до пункта Instantiation of Axi Bus Interface S00_AXI. Там дополняем создаваемый экземпляр шины и дополним его нашими сигналами:
Пишем следующее:
.gpio_input(gpio_input),
.gpio_output(gpio_output),
После этого в файле axi_gpio_button_and_led_v1_0_S00_AXI.v (строки 109-110) закомментируем строки с объявлением регистров, которые мы не будем использовать и изменим register на wire в объявлении slv_reg1:
Теперь нужно закомментировать лишнее в строках 224, 225 и 226:
После закомментируем большой блок кода отвечающий за работу с неиспользуемыми регистрами:
Идем еще ниже и комментируем ещё три строки (263, 264, 265):
Спускаемся еще ниже и комментируем:
Итак, теперь можно считать, что наш сигнал регистров обработан и теперь можно связать экземпляр модуля gpio_logic.v с логикой AXI который мы создавали ранее.
Теперь мы заканчиваем с редактированием шаблона AXI-модуля и можно перейти к его упаковке. Открываем главное меню Package IP и нажимаем Merge changes from File Group Wizard:
После переходим в меню Customization Parameters и так же мерджим параметры:
Переходим в последний пункт и нажимаем Package IP:
После нас спросят, хотим ли мы закрыть проект - нажимаем Yes и идём дальше. В главном меню открываем пункт IP Catalog и смотрим, что там появился созданный ранее нами модуль, проверяем, что все необходимые порты присутствуют:
Теперь можно конфигурировать Block Design и PS-часть Zynq.
Конфигурируем Zynq PS
Теперь когда наше IP-ядро готово, можем создать Block Design и слинковать всю логику. Нажимаем пункт меню Create Block Design и сразу можем добавить ZYNQ7 Processing System:
Переходим в настройки периферии. Глубоко конфигурировать тут ничего не придётся, нужно лишь включить UART1 на пинах 48 и 49:
И выбрать оперативную память MT41J256M16 RE-125 в 16-битном режиме:
Все остальное можно оставить по умолчанию. Добавляем на наш Block Design недавно созданное IP-ядро:
После можем запустить мастер Block Automation подсвеченный зеленым и выполнить все предложенные по умолчанию автоматизации. Теперь нужно сделать порты gpio_input и gpio_output в нашем IP-ядре внешними, нажав на них правой кнопкой и выполнив команду Make External. Получится следующая картина:
Проверим, что адресное пространство AXI-блока с которым будем взаимодействовать начинается с адреса 0x43C00000:
После можно создавать HDL Wrapper, нажав правой клавишей мыши по созданному нами Block Design:
Оставляем опцию по умолчанию:
После так же развернем Block Design иерархию и сделаем Generate Output Products и нажимаем Generate:
Потребуется некоторое время на генерацию. И после запустится синтез полученных исходников. После окончания синтеза нажимаем Open Synthesized Design:
После того как закончится эта операция - нужно перейти к разметке пинов GPIO. Но перед этим необходимо сохранить сделанные изменения и сгенерированный Constraints File. Нажимаем в меню Open Elaborated Design и нажимаем Ctrl + S:
Записываем имя файла, сохраняем и можем запустить синтез, чтобы потом сделать назначение I\O интересующих нас пинов. После окончания открываем синтезированный дизайн и у нас откроется меню Package и I/O Ports:
Все наши GPIO пины используются в логике LVCMOS33, поэтому выставим им это значение и заполним используемые пины в соответствии со схематиком присланным производителем. После внесения информации о пинах - сохраняем и запускаем генерацию Bitstream.
Дожидаемся окончания генерации:
Экспортируем Hardware-файлы, для последующего их использования в SDK:
Ставим галочку в пункте Include bitstream и нажимаем ОК:
После запускаем меню File - Launch SDK и переходим к созданию baremetal-приложения.
Создаем приложение и моргнем светодиодами
Создаем новое приложение в SDK через меню File - New - Application Project:
Пишем имя проекта и нажимаем Next и выбираем Hello World:
В дереве проекта находим файл helloworld.c и в нем опишем логику соответствующую нашему замыслу:
В этот файл вносим изменения и описываем логику:
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "sleep.h"
#include "string.h"
#define IIC_BASEADDRESS 0x43C00000
#define REG0_OFFSET 0
#define REG1_OFFSET 4
u32 gpio_input_value = 0;
char buf_print[64] = {0};
int main()
{
int i = 0;
init_platform();
while(1)
{
for(i=0; i<64; i++) buf_print[i] = 0;
gpio_input_value = Xil_In32(IIC_BASEADDRESS + REG1_OFFSET);
sprintf(buf_print, "input gpio_value = %d\r\n", gpio_input_value);
print(buf_print);
gpio_input_value = Xil_In32(IIC_BASEADDRESS + REG1_OFFSET);
if (gpio_input_value == 2)
{
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x1);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x2);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x4);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x8);
usleep(100000);
}
else if (gpio_input_value == 1)
{
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x8);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x4);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x2);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x1);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);
usleep(100000);
}
else if (gpio_input_value == 0)
{
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x9);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x6);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x6);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x9);
usleep(100000);
}
else if(gpio_input_value == 3)
{
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0xF);
usleep(100000);
Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);
usleep(100000);
}
}
cleanup_platform();
return 0;
}
После этого можно перейти в меню Xilinx - Program FPGA и запускаем bitstream-файл. При успешном запуске будет включен светодиод PL DONE. После этого кликаем правой кнопкой в дереве проектов на имени проекта и в контекстном меню выбираем Run As - Launch On Hardware (System Hardware).
После запуска - светодиоды будут мигать и от нажатия клавиш будут изменяться анимации светодиодов. Плюсом к этому если подключить USB-кабель в порт UART - можно увидеть текущее значение регистра в который записывается состояние входных сигналов. Если ни одна из кнопок не нажата - будет возвращено значение 0x3, если одна кнопка нажата - будет возвращаться знание 0x2 или 0x1, в зависимости от кнопки и 0x0 если зажаты две кнопки одновременно.
Будем считать, что цель достигнута. То есть мы через взаимодействие с AXI прямой записью\чтением по адресу памяти поработали с GPIO. А теперь в следующей главе разберем все грабли которые Я собрал, пока решал эту задачу.
Танцы на граблях
Коротко перечислю те проблемы, с которыми Я столкнулся т.к. все прошло не сильно легко и гладко.
Одна из проблем, связана с длинной имени IP-ядра. Длинные имена не очень подходят для IP-ядер, плюсом использование знаков “дефис” - тоже видимо противоречит правилам именования в Vivado. Кажется, пробежки по таким граблям неизбежны.
Вторая проблема которая возникла состоит в непонятной невозможности перенести изменения в кастомном IP-блоке в проект. Идея была в следующем. Сначала Я сделал входной сигнал с одной кнопки, чтобы проверить, что все работает, прежде чем подключать вторую. Проверил - работает. После внес изменения в IP-ядро, везде все обновил - и ни в какую не получилось получить шину вместо wire в результатах синтеза. Открыл в меню RTL ANALYSIS - Open Elaborated Design и начал просматривать схематик сигналов, чтобы понять, где у меня проблема. И обнаружил, что даже после внесения изменений не изменяется количество сигнальных проводников:
И что бы я ни делал, как бы не изменял IP-ядро - не получилось добиться того, чтобы был шинный интерфейс [1:0] вместо одного проводника gpio_input. Полное пересоздание проекта и IP-ядра помогло решить эту проблему:
То есть и сигналы были организованы шиной, и в нужном количестве, и все синтезировано верно. Я пока не нашел способов решения этой проблемы (внесение изменений и перенос их в проект) и думаю, что если найдется способ решения или найдется причина - опишу в следующих статьях.
Исправив эту проблему, я столкнулся со следующей. Я не понял, почему при запуске проекта в SDK - не загружается автоматом сгенерированный bitstream-файл в FPGA. Поковырявшись в настройках Debug-конфигурации - нашел, где включается этот параметр: в структуре проектов кликаем правой кнопкой, открываем меню Properties и переходим в самый нижний пункт Run/Debug Settings и нажимаем Edit на первом варианте конфигурации:
Устанавливаем галочки у пунктов обозначенных стрелками:
После запуска через меню Run - у нас сначала прошьется FPGA, а потом будет запущено приложение.
Заключение
По итогу этого урока, мы поморгали светодиодами, обработали сигналы с кнопок с использованием кастомного AXI IP-блока. По ходу подготовки материала для статьи пришлось немного подебажить проект. Я постарался максимально полно описать грабли, по которым пришлось пробежаться т.к. на сегодняшний день Я считаю, что в этом состоит основная ценность материалов, подобных этой статье и имеет гораздо больший вес, нежели тупое описание step-by-step.
P.S. В следующей статье, попробуем сделать более интересную задачу и выведем картинку на OLED-дисплей, который подключен к PL-части.
Ссылки
Исходные коды: https://github.com/megalloid/zynq_mini_lessons/tree/main/first_lesson
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале ↩