Zynq. AXI GPIO. Мигаем светодиодом по-новому

После написания последнего обзора на новую отладку Я не смог удержаться от того, чтобы не сделать простую проверку работоспособности платы, т.к. очень не хотелось бы напороться на какие-либо проблемы во время решения сложной задачи. Поэтому решил сделать простую мигалку светодиодами и задействовать, плюсом к этому, кнопки на плате. Немного поразмыслив, Я решил, что обычный “ногодрыг” на 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 (строки 109110) закомментируем строки с объявлением регистров, которые мы не будем использовать и изменим 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://habr.com/ru/company/timeweb/blog/725022/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *