После написания последнего обзора на новую отладку Я не смог удержаться от того, чтобы не сделать простую проверку работоспособности платы, т.к. очень не хотелось бы напороться на какие-либо проблемы во время решения сложной задачи. Поэтому решил сделать простую мигалку светодиодами и задействовать, плюсом к этому, кнопки на плате. Немного поразмыслив, Я решил, что обычный “ногодрыг” на 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

Комментарии (2)


  1. Daniloniks
    00.00.0000 00:00

    Спасибо за Вашу статью!

    Проверим, что адресное пространство AXI-блока с которым будем взаимодействовать начинается с адреса 0x43C00000

    Для чего необходима эта проверка?

    После внес изменения в IP-ядро, везде все обновил - и ни в какую не получилось получить шину вместо wire в результатах синтеза

    Вы обновляли IP-ядро через IP-Manager -> Upgrade Selected -> Upgrade IP?


    1. megalloid Автор
      00.00.0000 00:00
      +1

      Рад стараться! :)

      Насчёт адреса. Это скорее для проверки того, что Кривада подставила туда хотя бы что-то. Подстраховка, я бы сказал. Зная, что много всяких глюков - стараюсь перепроверять все.

      Насчёт Upgrade IP. Да, Вы предложили абсолютно верный и подходящий способ. Его я использовал в первую очередь, но не знаю, почему не апгрейдилось. Так же после создания IP-блока порты "прорастают" не сразу, только AXI-порты и все, потом удаляешь, добавляешь снова и порты появляются))))

      В общем, не расслабиться с этой Вивадой)))