Я давно носил идею проверки HDMI на платах Zynq, и вот наконец-то дошли руки до этого интересного топика. В этой статье я покажу, что вывод изображения через HDMI достаточно прост, но ограничусь только рассмотрением вывода изображения из baremetal-приложений, а вопросы про Linux оставлю для следующей статьи. В первую очередь изучим возможность простого вывода изображения в HDMI из генератора тестовых изображений с использованием Test Pattern Generator в PL-логике, а затем коснёмся применения AXI Video DMA.
Всем интересующимся добро пожаловать под кат!
Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте, с чего можно начать при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…
❯ Концепция проекта и постановка задачи
Основная задача, которая стоит перед нами — запустить вывод HDMI-изображения на отладочной плате Zynq Mini.
Если разделить задачу на подпункты, то нам необходимо:
-
Подготовить проект в Xilinx Vivado и проверить работоспособность HDMI c Test Pattern Generator;
-
Создать baremetal-проект для запуска Test Pattern Generator в Vitis;
-
Создать проект для вывода изображения с использованием AXI Video DMA в Vivado;
-
Создать baremetal-проект для вывода изображения с использованием AXI Video DMA в Vitis.
Задача, в целом, не сложная и ниже я опишу как её решал. Приступим.
❯ Вывод HDMI на плате ZynqMini
Для начала разберёмся что такое HDMI. Как пишут в Wiki — HDMI aka High Definition Multimedia Interface — это стандартный интерфейс передачи видео в высоком разрешении с возможностью передачи аудио.
Стандартный разъём HMDI имеет 19 контактов, 8 из которых используются для передачи информации по 4 дифференциальным парам по протоколу TMDS (Transition-Minimized Differential Signaling) видеоинформации:
-
TMDS Clock+/Clock-;
-
TMDS Data0+/Data0-;
-
TMDS Data1+/Data1-;
-
TMDS Data2+/Data2-.
Для вывода видеоданных из ПЛИС мы будем использовать 8 пинов, которые сконфигурируем как 4 дифференциальных TDMS выхода. Но об этом позже.
Общий принцип передачи данных может быть изображён так:
Протокол TMDS кодирует 8 битные каналы в 10 бит. Подробнее рассказано в этом видео. После формирования исходные данные проходят сериализацию и отправляются получателю, который выполняет обратное преобразование для декодирования данных.
Общая схема подключения разъёма HDMI на плате ZynqMini выглядит так:
Со стороны Zynq-7000 распиновка следующая:
Таким образом получаем следующую схему подключения сигналов HDMI к Zynq-7000:
-
HDMI_CLK_P - к H16;
-
HDMI_CLK_N - к H17;
-
HDMI_DATA0_P - к D19;
-
HDMI_DATA0_N - к D20;
-
HDMI_DATA1_P - к C20;
-
HDMI_DATA1_N - к B20;
-
HDMI_DATA2_P - к B19;
-
HDMI_DATA2_N - к A20;
-
HDMI_OUT_EN - к H18.
Поскольку все эти сигналы по полярности соответствуют тому, что предлагает Pin Planner в Vivado — инвертировать ничего не потребуется, идём дальше. Сверимся позже, когда будем формировать constraints-файл:
Чтобы подготовить и отправить данные в HDMI разъём, мы будем использовать IP-ядро, которое любезно предоставляет компания Digilent RGB2DVI. Общая блок-схема данного IP-ядра выглядит следующим образом:
Данный блок принимает на вход сигнал тактирования, 24-битные данные RGB, HSYNC, VSYNC и сигнал валидности полученных данных VDE.
Входная тактовая частота должна быть в 5 раз выше тактовой частоты PixelClk, потому что для каждого пикселя мы кодируем 10 бит данных и данные тактируются по нарастающему и нисходящему фронту тактового сигнала.
Сигнал VDE — это сигнал валидности данных, который выставляется в высокий уровень когда отправлена очередная порция данных.
Для рассмотрения дальнейших шагов создадим новый проект в Vivado.
❯ Подготовка проекта Vivado для запуска Test Pattern Generator
Начинаем подготовку со стандартного проекта Vivado. В этот раз я взял за основу версию 2024.1.1. Запускаем Vivado и создаём новый проект:
Называем наш проект 1.HDMI_TPG и указываем директорию, в которой сохраним проект:
Выбираем пункт RTL Project и ставим галочку Do not specify sources at this time:
В следующем меню выбираем xc7z020clg400-2. Да, на плате Zynq Mini c 7020 используется чип 2 speed grade:
Идём дальше и нажимаем Finish:
Итак, исходный проект создан.
❯ Кастомные IP-ядра
Теперь необходимо добавить в проект IP-ядро RGB2DVI. Так как в составе Xilinx Vivado он отсутствует, его необходимо добавить вручную. Для этого клонируем репозиторий и добавляем IP-ядро в проект:
git clone https://github.com/Digilent/vivado-library
После добавляем новый репозиторий IP-ядер через настройки:
И указываем путь к директории, в которую мы склонировали репозиторий:
❯ Hardware PL Design
Теперь создадим наш проект с Test Pattern Generator. Добавим новый Block Design в проект:
Назовём его top_design:
Теперь добавим в Block Design модуль процессорной системы:
В списке находим ZYNQ7 Processing System и добавляем его:
На Block Design будет добавлен модуль, через который можно сконфигурировать PS-систему. После будет предложено запустить автоматизацию:
Запускаем мастер и нажимаем Ok, оставив значения по умолчанию:
Далее необходимо настроить модуль PS-системы. Делаем двойной клик по нему — откроется меню конфигурации:
Переходим в меню MIO Configuration. Выбираем для Bank 1 I/O Voltage в значение LVCMOS 1.8V:
Мотаем ещё ниже и выбираем UART1 с пинами MIO 48 .. 49:
Далее переходим в меню Clock Configuration и выставляем так как это указано на скриншоте, изменив частоты тактирования FCLK_CLK0:
Переходим в меню DDR Configuration. Выбираем чип MT41J256M16 RE-125 и 16-битный режим:
Остальные блоки с настройками оставляем как есть и нажимаем Ok. Получится следующее:
Следующий элемент, который необходимо добавить в дизайн — Test Pattern Generator:
Далее нам предлагают автоматизацию, давайте сделаем её сразу и оставим выбранные опции по умолчанию:
Получится следующая схема:
Далее, необходимо преобразовать сигнал AXI Stream в Native Video. Для этого добавим IP-блок AXI Stream to Video Out:
К нему в пару необходим Video Timing Controller, который определяет формат выводимого изображения:
Предлагаемую автоматизацию не выполняем. Вместо этого откроем настройки и отключим интерфейс AXI. Детектирование таймингов также отключаем, нам их нужно только сгенерировать:
Далее, переходим на вкладку Default/Constant и выставляем режим вывода изображения в 1080p:
После, начинаем соединять сигналы. Подключаем vtiming_out к vtiming_in:
Сигнал vtg_ce соединяем с gen_clken:
Подключаем тактовые сигналы clk, aclk к основному тактовому сигналу M_AXI_GP0_ACLK:
И сигналы сброса к resetn, aresetn к peripheral_aresetn:
Далее сигнал с генератора тестовых паттернов m_axis_video подключаем к video_in:
Параллельный видеосигнал и набор синхросигналов передаём на преобразователь интерфейса. Для этого необходимо добавить в дизайн RGB to DVI Video Encoder:
Заходим в настройки данного блока и снимаем галочку Reset active high потому что везде используется Reset Active Low, все остальное не трогаем:
После соединяем сигналы RGB и vid_io_out:
Подключаем сигнал сброса aRst_n к общему сигналу сброса:
И подключаем сигнал тактирования PixelClk к общему сигналу тактирования:
Выходной интерфейс необходимо вытащить наружу через клик правой кнопкой Make External:
Добавляем Constant-блок для формирования сигнала HDMI_EN:
Нажимаем F6 для проверки полученного дизайна и получаем сообщение, что всё получилось:
Получаем вот такой дизайн:
Следующим шагом создаем HDL Wrapper с управлением Vivado. Для этого перейдём в меню навигатора Sources и по top_design кликаем правой кнопкой. Затем выбираем пункт Create HDL Wrapper:
Нажимаем Ok выбрав пункт Let Vivado manage wrapper and auto-update:
После этого запускаем синтез и дожидаемся его окончания. После этого выбираем в меню Open Synthesized Design:
Чтобы ускорить процесс синтеза, устанавливайте Number of jobs в максимальное значение:
Будет запущен процесс синтеза. Дожидаемся успешного его окончания и выбираем Open Synthesized Design:
Жмем в главном меню Window - I/O Ports. Откроется меню в котором нужно назначить пины в соответствии со схематиком:
Остальные сигналы дифференциальных пар будут назначены автоматически. Сохраняем проект, именуем файл constraints и перезапускаем синтез.
После этого необходимо перезапустить синтез и запустить генерацию битстрима через Generate Bitstream и дождаться окончания:
Экспортируем BSP-сырцы для работы с ними в Vivado и не забываем включить bitstream-файл. Переходим в меню File - Export - Export Hardware, нажимаем Next. Выбираем Include bitstream:
Оставляем по умолчанию меню Files:
Нажимаем Finish:
Всё. На этом подготовка проекта в Vivado закончена.
❯ Сборка baremetal-приложения для запуска Test Pattern Generator
Теперь можем переходить в Vitis для создания baremetal-приложения, которое запустит генератор тестовых паттернов. Нажимаем Tools - Launch Vitis IDE — запустится Vitis.
Первым шагом в директории проекта создадим каталог для Workspace:
Создаём каталог и нажимаем Ok:
После необходимо создать проект платформы:
Назовем платформу zynqmini_tpg:
Указываем путь к xsa-архиву в корне проекта:
Оставляем опции по умолчанию:
И нажимаем Finish:
После будет создан проект с boot-артефактами. Запускам его сборку через кнопку Build и дожидаемся окончания компиляции:
Далее из примеров создадим проект Hello World:
Назовем проект tpg и нажимаем Next:
Выбираем аппаратную платформу, которую добавили до этого:
Выбираем процессорное ядро и нажимаем Next:
В следующем меню нажимаем Finish:
В структуре созданного проекта открываем файл helloworld.c:
Вставляем в него следующее содержимое:
#include "stdint.h"
#include "stdbool.h"
#include "xv_tpg.h"
#include "sleep.h"
#include "xparameters.h"
XV_tpg tpg;
int main()
{
XV_tpg_Initialize(&tpg, 0);
XV_tpg_Set_width(&tpg, 1920);
XV_tpg_Set_height(&tpg, 1080);
XV_tpg_Set_ZplateHorContDelta(&tpg, 2);
XV_tpg_Set_ZplateHorContStart(&tpg, 2);
XV_tpg_Set_ZplateVerContDelta(&tpg, 2);
XV_tpg_Set_ZplateVerContStart(&tpg, 2);
XV_tpg_Set_motionSpeed(&tpg, 2);
XV_tpg_Set_motionEn(&tpg, 1);
XV_tpg_EnableAutoRestart(&tpg);
XV_tpg_Start(&tpg);
int pattern = 1;
print("Successfully ran TPG application");
while(true)
{
XV_tpg_Set_bckgndId(&tpg, pattern);
if(++pattern > 19)
{
pattern = 1;
}
usleep(5000000);
print("Change pattern");
}
return 0;
}
И запускаем компиляцию через кнопку Build:
После этого, если плата подключена к ПК, можно запустить проект через кнопку Run. Подключаем HDMI монитор, и видим как меняются тестовые паттерны. Первая часть задачи выполнена.
❯ HDMI и Video DMA
Следующим этапом попробуем вывести изображение через Video DMA с использованием baremetal-приложения. Общий принцип вывода HDMI-картинки c использованием VDMA я изобразил следующим образом:
Для реализации этой задумки создадим новый проект, так же как делали в первом проекте. Этот этап я описывать не буду. Сделайте до этапа создания Block Design.
Теперь добавляем на дизайн Zynq Processing System и делаем предложенную автоматизацию, чтобы получить следующую картину:
Открываем его настройки. В первую очередь настроим DDR память так же как мы делали это в прошлом проекте:
Для отладочного вывода сконфигурируем UART. Выбираем UART1 с пинами MIO 48 .. 49:
Далее необходимо добавить несколько AXI-интерфейсов в дизайн. В настройках PS-PL Configuration включаем Master AXI GP0 interface и Slave AXI HP0 interface:
Первый будет использован для подключения AXI Interconnect, через который будет идти управление Timing контроллером и Dynamic Clock Generator, а второй для подключения AXI SmartConnect и AXI Video Direct Memory Access, чтобы отправлять данные из видеобуфера:
Далее необходимо включить сигналы прерывания в PS часть, которые будут генерироваться от тайминг контроллера и AXI VDMA:
Теперь выведем тактовые сигналы с PS части:
На этом нажимаем Ок и получаем следующую картину:
Следующим шагом добавим два блока сброса и сразу их переименуем. Первый будет использоваться для сброса периферии которая работает на частоте 148.5 МГц, а второй для всего остального:
Подключаем сигнал сброса к каждому из них от FCLK_RESET0_N к ext_reset_n каждого из этих блоков:
Подключаем тактирование соответственно 100МГц и 148МГц к сигналу slowest_sync_clk:
После накидаем все необходимое для работы AXI Video DMA. Добавим блок AXI SmartConnect, подключаем все необходимые сигналы и включаем один Slave-интерфейс в настройках:
Добавим в дизайн AXI Video Direct Memory Access и сразу настроим его:
И на следующей вкладке:
Теперь соединяем сигналы данных, тактирования и сброса:
Затем добавляем в дизайн AXI Interconnect c 3-мя Master интерфейсами и соединяем сигналы данных, тактирования и сброса:
Выполним предложенную автоматизацию:
Теперь необходимо добавить блоки, которые управляют таймингами и динамический генератор тактового сигнала. Добавляем Video Timing Controller и Dynamic Clock Generator.
Для начала сконфигурируем Video Timing Controller:
Выставляем режим видео в 1080p:
Подключаем к нему сигналы данных, сброса и тактирования:
После этого можно добавить сигналы прерываний в PS-часть:
Подключаем сигналы Dynamic Clock Generator:
Теперь необходимо добавить блоки, которые будут отвечать за подготовку и вывод видео данных. Это преобразователь сигналов AXI в Native Video AXI4-Stream to Video Out и энкодер RGB to DVI. Перейдем к настройкам AXI4-Stream to Video Out:
Подключаем его сигналы следующим образом:
Настроим так же энкодер:
Подключим его сигналы тоже:
И добавим сигнал HDMI_EN в дизайн:
Получится следующая картина:
Проверяем, что адреса в памяти назначены:
Запускаем дизайн, назначаем пины и генерируем битстрим, выгрузив XSA-архив. На этом подготовка дизайна в Vivado заканчивается и мы переходим к созданию проекта в Vitis.
❯ Создание проекта в Vitis
По примеру из первой части статьи создаем воркспейс, аппаратную платформу и проект Hello World. После этого в проект необходимо добавить несколько важных компонентов. Каждый их них в содержании оснащен избыточным количеством комментариев и в объяснении не нуждается. Не буду приводить здесь длинных листингов программ, а снабжу текст ссылками на мой репозиторий.
Итак. Опишу основные компоненты проекта.
-
Основной текст программы, который нужно вставить в файл helloworld.c;
-
Файл изображения pic_800_600.h, которое будет выведено на экран;
-
Файл объявляющий основные функции dispay_demo.h;
-
Компонент, который производит инициализацию Dynamic Clock Generator: dynclk;
-
Компонент для управления дисплеем: display_ctrl.
Добавьте все эти файлы в структуру проекта:
После этого необходимо добавить директории в проект, чтобы при компиляции все файлы были доступны:
Проверьте настройки запуска и нажмите кнопку Run:
Подключите плату по HDMI и вы увидите очень интересную картинку. А какую — узнаете когда соберете проект =)
❯ Заключение
Вот таким нехитрым образом можно вывести изображение в HDMI. Для более подробного объяснения рекомендую обратиться к документации на каждое из IP ядер и собрать всё воедино. Если расписывать весь pipeline формирования изображения, то статья вышла бы длиной раз в 10 больше этой. Поэтому всем интересующимся предлагаю изучить вопрос самостоятельно, я лишь привел пример как быстро реализовать работоспособный проект. А вот в следующей статье я хотел бы раскрыть вопрос более подробно, но уже в разрезе организации вывода фреймбуфера из Linux.
До встречи в следующих статьях =)
Автор: megalloid