После написания последнего обзора на новую отладку Я не смог удержаться от того, чтобы не сделать простую проверку работоспособности платы, т.к. очень не хотелось бы напороться на какие-либо проблемы во время решения сложной задачи. Поэтому решил сделать простую мигалку светодиодами и задействовать, плюсом к этому, кнопки на плате. Немного поразмыслив, Я решил, что обычный “ногодрыг” на Verilog - это уже не так интересно и мне показалось, что лучше сделать это с помощью AXI GPIO и своего IP-ядра, инициировав экшн из baremetal-приложения. В общем, кому интересно, заглядывайте в статью, там Я описал, как добавить свое кастомное AXI Peripheral IP-ядро, как правильно организовать проект и обратиться к GPIO для чтения и записи логического уровня. Поехали…
![](https://habrastorage.org/getpro/habr/upload_files/192/913/aba/192913aba04f1aebbc9d44df3bbba0b9.png)
Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
Как обычно, сначала создаем проект…
Открываем Vivado и из главного окна создаем новый проект. Пишем его название и указываем директорию куда будем сохранять проект:
![](https://habrastorage.org/getpro/habr/upload_files/ab0/7b8/1fe/ab07b81fe0b8f4a29243094db998114f.png)
Далее выбираем RTL Project, оставляем включенной галочку Do not specify sources at this time и идем дальше. Находим интересующую нас модель SoC Zynq:
![](https://habrastorage.org/getpro/habr/upload_files/67c/a27/199/67ca27199c31541cb8465e8e38270b50.png)
Тут стоит отдельно отметить, что у меня проект корректно работал на ZynqMini со 2-м Speed Grade. Невозможно было однозначно определить Speed Grade чипа, т.к. QR-код на чипе зашлифован. Надо будет проверить в более крупных проектах.
Идём далее и перед нами предстает окно Vivado, готовое к работе.
Постановка задачи
Итак. Прежде чем приступать к действиям, надо придумать интересную задачу и понять, что мы должны получить в результате. Предлагаю сделать следующее. Задействуем каждую из кнопок для включения разных сценариев моргания четырьмя светодиодами: первый сценарий - светодиоды будут загораться поочередно, второй сценарий - сменим направление у анимации, третий сценарий, когда нажаты обе кнопки - анимация сходится в центре, и по умолчанию просто мигаем светодиодами. Сделаем всё с использованием AXI GPIO, через свое собственное IP-ядро и запрограммируем логику работы кнопок через C-приложение которое мы запустим в baremetal на одном из ARM ядер. В целом, звучит не сложно. Идём дальше.
Создаем своё новое IP-ядро
В первую очередь создадим и опишем логику нашего собственного IP-ядра, который будет взаимодействовать с AXI-интерконнектом. Нажимаем в главном меню опцию Tools - Create and Package New IP:
![](https://habrastorage.org/getpro/habr/upload_files/cc7/488/2b5/cc74882b5664f4404d6b956ddd744e23.png)
Откроется мастер создания IP-ядра, нажимаем Next и выбираем Create a new AXI4 peripheral:
![](https://habrastorage.org/getpro/habr/upload_files/dd1/e85/e01/dd1e85e0195ea4ee26556f604b5f8fd9.png)
Заполняем имя, версию и прочую информацию. Сразу советую использовать короткие имена и без дефисов в названии. У меня получилось вот так:
![](https://habrastorage.org/getpro/habr/upload_files/a43/cf3/26d/a43cf326de5343d61db087c0324553ba.png)
Описываем интерфейс AXI4 и его параметры:
![](https://habrastorage.org/getpro/habr/upload_files/fe9/358/c42/fe9358c426a655206e6e3a3544c9e15f.png)
После переключаемся в режим редактирования установив опцию Edit IP и нажимаем Finish:
![](https://habrastorage.org/getpro/habr/upload_files/c63/79d/2a6/c6379d2a6aeefcd4232c157badb4deee.png)
Откроется отдельное окно в котором можно настроить дополнительные параметры IP-ядра:
![](https://habrastorage.org/getpro/habr/upload_files/3eb/d5c/1ff/3ebd5c1ff3b0ffbebe043a714d2c386b.png)
Теперь создадим Verilog-файл, в котором мы опишем основную логику маршрутизации сигналов. Для этого в меню Sources нажимаем на синий крестик и вызовем мастер добавления Sources. Выберем пункт меню Add or create design sources.
![](https://habrastorage.org/getpro/habr/upload_files/821/062/414/82106241435e18b4b5377ff621f23093.png)
В следующем меню нажимаем кнопку Create File и назовем его gpio_logic.v и выберем место хранения и нажимаем Finish:
![](https://habrastorage.org/getpro/habr/upload_files/a32/788/d44/a32788d44e9905ed1157e879b8137c09.png)
В следующем окне, предлагающем нам определить порты 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:
![](https://habrastorage.org/getpro/habr/upload_files/cd5/523/3ed/cd55233ed6e13ca98b18e99f7770d9db.png)
Открыв файл на редактирование находим блок в котором в комментариях написано Users to add ports here. Между комментариями мы запишем определение сигналов и сохраним изменения:
![](https://habrastorage.org/getpro/habr/upload_files/6a2/91f/6e5/6a291f6e5dd977348654ca402c526af2.png)
Запишем следующее (не забывая про запятые, т.к. это перечисление портов 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. В него тоже запишем перечисление портов, как в предыдущем файле:
![](https://habrastorage.org/getpro/habr/upload_files/2f9/032/2b4/2f90322b41f88904f26929b02f585046.png)
Напишем следующее:
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. Там дополняем создаваемый экземпляр шины и дополним его нашими сигналами:
![](https://habrastorage.org/getpro/habr/upload_files/e21/3e1/9bd/e213e19bd7e6527103e0d980bfc063f6.png)
Пишем следующее:
.gpio_input(gpio_input),
.gpio_output(gpio_output),
После этого в файле axi_gpio_button_and_led_v1_0_S00_AXI.v (строки 109-110) закомментируем строки с объявлением регистров, которые мы не будем использовать и изменим register на wire в объявлении slv_reg1:
![](https://habrastorage.org/getpro/habr/upload_files/37b/401/e69/37b401e694ba34e78b74541d1b4e6853.png)
Теперь нужно закомментировать лишнее в строках 224, 225 и 226:
![](https://habrastorage.org/getpro/habr/upload_files/2e9/b84/072/2e9b840723eda94269d39597dc486353.png)
После закомментируем большой блок кода отвечающий за работу с неиспользуемыми регистрами:
![](https://habrastorage.org/getpro/habr/upload_files/341/6be/9b8/3416be9b856785bfa752793fa2115866.png)
Идем еще ниже и комментируем ещё три строки (263, 264, 265):
![](https://habrastorage.org/getpro/habr/upload_files/c26/4b6/c77/c264b6c77e51985231ce732839649691.png)
Спускаемся еще ниже и комментируем:
![](https://habrastorage.org/getpro/habr/upload_files/c66/adb/90d/c66adb90db9d3ccfdbc2085ef646d705.png)
Итак, теперь можно считать, что наш сигнал регистров обработан и теперь можно связать экземпляр модуля gpio_logic.v с логикой AXI который мы создавали ранее.
![](https://habrastorage.org/getpro/habr/upload_files/331/60a/491/33160a491d05bd5184d06df121ce3cc5.png)
Теперь мы заканчиваем с редактированием шаблона AXI-модуля и можно перейти к его упаковке. Открываем главное меню Package IP и нажимаем Merge changes from File Group Wizard:
![](https://habrastorage.org/getpro/habr/upload_files/21b/243/455/21b24345550282a1ebb4fa107d3db59f.png)
После переходим в меню Customization Parameters и так же мерджим параметры:
![](https://habrastorage.org/getpro/habr/upload_files/7c5/3ad/cdf/7c53adcdf77d7b506db056b68386cc52.png)
Переходим в последний пункт и нажимаем Package IP:
![](https://habrastorage.org/getpro/habr/upload_files/0b5/773/d2b/0b5773d2be851ee260bb9164c287f3cc.png)
После нас спросят, хотим ли мы закрыть проект - нажимаем Yes и идём дальше. В главном меню открываем пункт IP Catalog и смотрим, что там появился созданный ранее нами модуль, проверяем, что все необходимые порты присутствуют:
![](https://habrastorage.org/getpro/habr/upload_files/fce/23c/dbd/fce23cdbd1d24602ad6ccd6c9ce01ca3.png)
Теперь можно конфигурировать Block Design и PS-часть Zynq.
Конфигурируем Zynq PS
Теперь когда наше IP-ядро готово, можем создать Block Design и слинковать всю логику. Нажимаем пункт меню Create Block Design и сразу можем добавить ZYNQ7 Processing System:
![](https://habrastorage.org/getpro/habr/upload_files/626/f63/7ca/626f637ca99fc5bc76a42add6eb55345.png)
Переходим в настройки периферии. Глубоко конфигурировать тут ничего не придётся, нужно лишь включить UART1 на пинах 48 и 49:
![](https://habrastorage.org/getpro/habr/upload_files/b4b/0ba/826/b4b0ba826652a36dbb5c3fe8cfa7c612.png)
И выбрать оперативную память MT41J256M16 RE-125 в 16-битном режиме:
![](https://habrastorage.org/getpro/habr/upload_files/ec7/e5c/ace/ec7e5cacee08e3cd294da898753f931d.png)
Все остальное можно оставить по умолчанию. Добавляем на наш Block Design недавно созданное IP-ядро:
![](https://habrastorage.org/getpro/habr/upload_files/077/d3d/003/077d3d003aaef71af37526e2eea03a57.png)
После можем запустить мастер Block Automation подсвеченный зеленым и выполнить все предложенные по умолчанию автоматизации. Теперь нужно сделать порты gpio_input и gpio_output в нашем IP-ядре внешними, нажав на них правой кнопкой и выполнив команду Make External. Получится следующая картина:
![](https://habrastorage.org/getpro/habr/upload_files/a60/772/dad/a60772dadc0ff793b018da0c2f7069ac.png)
Проверим, что адресное пространство AXI-блока с которым будем взаимодействовать начинается с адреса 0x43C00000:
![](https://habrastorage.org/getpro/habr/upload_files/0b7/73b/b34/0b773bb343b097d1f5b451171a40553c.png)
После можно создавать HDL Wrapper, нажав правой клавишей мыши по созданному нами Block Design:
![](https://habrastorage.org/getpro/habr/upload_files/cb1/110/ae9/cb1110ae90825783e7667853ad9bf331.png)
Оставляем опцию по умолчанию:
![](https://habrastorage.org/getpro/habr/upload_files/d6b/35a/6b1/d6b35a6b16305820b53ecac1c92444d6.png)
После так же развернем Block Design иерархию и сделаем Generate Output Products и нажимаем Generate:
![](https://habrastorage.org/getpro/habr/upload_files/b35/331/3c2/b353313c27b7de431ddaa944ae19851e.png)
Потребуется некоторое время на генерацию. И после запустится синтез полученных исходников. После окончания синтеза нажимаем Open Synthesized Design:
![](https://habrastorage.org/getpro/habr/upload_files/2cb/ebe/575/2cbebe575455e538aa8659267b113a17.png)
После того как закончится эта операция - нужно перейти к разметке пинов GPIO. Но перед этим необходимо сохранить сделанные изменения и сгенерированный Constraints File. Нажимаем в меню Open Elaborated Design и нажимаем Ctrl + S:
![](https://habrastorage.org/getpro/habr/upload_files/59f/e91/bae/59fe91bae1896ae4f970c884af100d61.png)
Записываем имя файла, сохраняем и можем запустить синтез, чтобы потом сделать назначение I\O интересующих нас пинов. После окончания открываем синтезированный дизайн и у нас откроется меню Package и I/O Ports:
![](https://habrastorage.org/getpro/habr/upload_files/e3f/6d0/a55/e3f6d0a5511850e02faeede8c28662e7.png)
Все наши GPIO пины используются в логике LVCMOS33, поэтому выставим им это значение и заполним используемые пины в соответствии со схематиком присланным производителем. После внесения информации о пинах - сохраняем и запускаем генерацию Bitstream.
Дожидаемся окончания генерации:
![](https://habrastorage.org/getpro/habr/upload_files/c34/8fe/bf6/c348febf6ab9a1a32cbd43d6a626ad47.png)
Экспортируем Hardware-файлы, для последующего их использования в SDK:
![](https://habrastorage.org/getpro/habr/upload_files/ff5/af7/ff8/ff5af7ff8f00ad9152ab0bc155cb00e7.png)
Ставим галочку в пункте Include bitstream и нажимаем ОК:
![](https://habrastorage.org/getpro/habr/upload_files/005/476/a6f/005476a6f687db0be37d6b11d67c59a5.png)
После запускаем меню File - Launch SDK и переходим к созданию baremetal-приложения.
Создаем приложение и моргнем светодиодами
Создаем новое приложение в SDK через меню File - New - Application Project:
![](https://habrastorage.org/getpro/habr/upload_files/fbb/814/952/fbb8149524c0f18b4f1b9a9f69960f9c.png)
Пишем имя проекта и нажимаем Next и выбираем Hello World:
![](https://habrastorage.org/getpro/habr/upload_files/453/4a0/aa0/4534a0aa001cb912f713734a1060a541.png)
В дереве проекта находим файл helloworld.c и в нем опишем логику соответствующую нашему замыслу:
![](https://habrastorage.org/getpro/habr/upload_files/04b/de6/743/04bde67430bc7fb8e7ba21595bc36c32.png)
В этот файл вносим изменения и описываем логику:
#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).
![](https://habrastorage.org/getpro/habr/upload_files/fba/adc/5c8/fbaadc5c828b5dbf984a894ec021a68a.png)
После запуска - светодиоды будут мигать и от нажатия клавиш будут изменяться анимации светодиодов. Плюсом к этому если подключить USB-кабель в порт UART - можно увидеть текущее значение регистра в который записывается состояние входных сигналов. Если ни одна из кнопок не нажата - будет возвращено значение 0x3, если одна кнопка нажата - будет возвращаться знание 0x2 или 0x1, в зависимости от кнопки и 0x0 если зажаты две кнопки одновременно.
Будем считать, что цель достигнута. То есть мы через взаимодействие с AXI прямой записью\чтением по адресу памяти поработали с GPIO. А теперь в следующей главе разберем все грабли которые Я собрал, пока решал эту задачу.
Танцы на граблях
Коротко перечислю те проблемы, с которыми Я столкнулся т.к. все прошло не сильно легко и гладко.
Одна из проблем, связана с длинной имени IP-ядра. Длинные имена не очень подходят для IP-ядер, плюсом использование знаков “дефис” - тоже видимо противоречит правилам именования в Vivado. Кажется, пробежки по таким граблям неизбежны.
Вторая проблема которая возникла состоит в непонятной невозможности перенести изменения в кастомном IP-блоке в проект. Идея была в следующем. Сначала Я сделал входной сигнал с одной кнопки, чтобы проверить, что все работает, прежде чем подключать вторую. Проверил - работает. После внес изменения в IP-ядро, везде все обновил - и ни в какую не получилось получить шину вместо wire в результатах синтеза. Открыл в меню RTL ANALYSIS - Open Elaborated Design и начал просматривать схематик сигналов, чтобы понять, где у меня проблема. И обнаружил, что даже после внесения изменений не изменяется количество сигнальных проводников:
![](https://habrastorage.org/getpro/habr/upload_files/b5b/4ab/813/b5b4ab813932d8fbe0c5b22d51810dad.png)
И что бы я ни делал, как бы не изменял IP-ядро - не получилось добиться того, чтобы был шинный интерфейс [1:0] вместо одного проводника gpio_input. Полное пересоздание проекта и IP-ядра помогло решить эту проблему:
![](https://habrastorage.org/getpro/habr/upload_files/00b/4a8/8a6/00b4a88a619fcefdd37d6dab428b0584.png)
То есть и сигналы были организованы шиной, и в нужном количестве, и все синтезировано верно. Я пока не нашел способов решения этой проблемы (внесение изменений и перенос их в проект) и думаю, что если найдется способ решения или найдется причина - опишу в следующих статьях.
Исправив эту проблему, я столкнулся со следующей. Я не понял, почему при запуске проекта в SDK - не загружается автоматом сгенерированный bitstream-файл в FPGA. Поковырявшись в настройках Debug-конфигурации - нашел, где включается этот параметр: в структуре проектов кликаем правой кнопкой, открываем меню Properties и переходим в самый нижний пункт Run/Debug Settings и нажимаем Edit на первом варианте конфигурации:
![](https://habrastorage.org/getpro/habr/upload_files/cc0/073/b0e/cc0073b0e73f2566aa5a561af0fb604c.png)
Устанавливаем галочки у пунктов обозначенных стрелками:
![](https://habrastorage.org/getpro/habr/upload_files/18b/574/60c/18b57460cdc5070e2b693aa666601266.png)
После запуска через меню Run - у нас сначала прошьется FPGA, а потом будет запущено приложение.
Заключение
По итогу этого урока, мы поморгали светодиодами, обработали сигналы с кнопок с использованием кастомного AXI IP-блока. По ходу подготовки материала для статьи пришлось немного подебажить проект. Я постарался максимально полно описать грабли, по которым пришлось пробежаться т.к. на сегодняшний день Я считаю, что в этом состоит основная ценность материалов, подобных этой статье и имеет гораздо больший вес, нежели тупое описание step-by-step.
P.S. В следующей статье, попробуем сделать более интересную задачу и выведем картинку на OLED-дисплей, который подключен к PL-части.
Ссылки
Исходные коды: https://github.com/megalloid/zynq_mini_lessons/tree/main/first_lesson
![](https://habrastorage.org/webt/mx/ua/nb/mxuanbovcusqgmqdgugvpnql8vq.jpeg)
Daniloniks
Спасибо за Вашу статью!
Для чего необходима эта проверка?
Вы обновляли IP-ядро через IP-Manager -> Upgrade Selected -> Upgrade IP?
megalloid Автор
Рад стараться! :)
Насчёт адреса. Это скорее для проверки того, что Кривада подставила туда хотя бы что-то. Подстраховка, я бы сказал. Зная, что много всяких глюков - стараюсь перепроверять все.
Насчёт Upgrade IP. Да, Вы предложили абсолютно верный и подходящий способ. Его я использовал в первую очередь, но не знаю, почему не апгрейдилось. Так же после создания IP-блока порты "прорастают" не сразу, только AXI-порты и все, потом удаляешь, добавляешь снова и порты появляются))))
В общем, не расслабиться с этой Вивадой)))