Я давно носил идею проверки 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.
До встречи в следующих статьях =)
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

? Читайте также:
Комментарии (15)
checkpoint
13.10.2024 08:27Совершенно не спортивно! Сигнал на разъёме HDMI можно формировать без всяких кастомных ядер, и даже без сериалайзеров, базовыми средствами ПЛИС. Я сейчас пишу очередной лонгрид на эту тему, надеюсь к НГ опубликовать. :-)
megalloid Автор
13.10.2024 08:27Можно и так, главное целесообразность ;) для разбора ваша статья будет более интересная, для быстрой реализации функциональности - моя)))
Ждём вашу статью тоже :)
zbot
13.10.2024 08:27а можно даже без плиз и кастомных ядер. Микроконтроллер rp2040 (Raspbery PI Pico) с этим справляется вполне себе программными средствами.
checkpoint
13.10.2024 08:27Если тактовой частоты процессора хватает чтобы манипулировать GPIO сигналами с частотой 250 МГц - то да. Не знаю как с этим у RP2040.
Goron_Dekar
13.10.2024 08:27А можно и без микроконтроллеров. Вон на компьютере нарисовал картинку в пейнте, вывел на весь экран и... вуаля!
Однако, если вам нужно реализовать HDMI на плис вам реализация из rp2040 или ручной ногодрыг врядли поможет, тут принято реюзать чужой код, а не писать велосипед.
ElPrg
13.10.2024 08:27А в шапке где то было про спорт ? Из вашего сообщение видно, что вы хотите предложить другое подход решения задачи, и не более, а автор, вроде даже четко описал ТЗ
Основная задача, которая стоит перед нами — запустить вывод HDMI-изображения на отладочной плате Zynq Mini.
Так что лучше убрать "!" ( шутка ).
Имхо я бы начал с позитива! А так наверное на данной борде из самых оптимальных способов к реализации передачи данных через HDMI !checkpoint
13.10.2024 08:27Вывести сигнал на HDMI проприетарными средствами изначально заложенными в ПЛИС - не бог весть какая проблема, достаточно внимательно прочитать мануал. Но что Вы будете делать если потребуется уйти от этого вендора и перейти на другого ? Курить еще один мануал ? А что если потребуется вендоронезависимое решение ?
Это так, мысли в слух, можете не отвечать.
yamifa_1234
13.10.2024 08:27Не хватает фотографии отладки. Чтобы знать что искать
megalloid Автор
13.10.2024 08:27Посмотрите у меня в публикациях, я делал как-то обзор на эту отладку
ElPrg
13.10.2024 08:27Спасибо за статью! Как всегда красиво!
Жаль в этот раз мало про физику, протокола. Как было про I2C ^)Goron_Dekar
13.10.2024 08:27Там физика протокола немного сложнее чем в I2C, и с трудом поместится в отдельную статью, даже если не углублятся в согласования дифпар.
AlTres
13.10.2024 08:27Насколько приятнее проделать по шагам предложенную автором реализацию, когда есть ссылки на готовые файлы. В статье с работой по ДМА такой роскоши не было. Следующий шаг для ленивого читателя (например меня) выложить весь проект на github. Спасибо за статью.
dlinyj
Ну вот, на самом интересном месте :))). Спасибо большое за статью. Больше подобных статей на хабре!