Я давно носил идею проверки HDMI на платах Zynq, и вот наконец-то дошли руки до этого интересного топика. В этой статье я покажу, что вывод изображения через HDMI достаточно прост, но ограничусь только рассмотрением вывода изображения из baremetal-приложений, а вопросы про Linux оставлю для следующей статьи. В первую очередь изучим возможность простого вывода изображения в HDMI из генератора тестовых изображений с использованием Test Pattern Generator в PL-логике, а затем коснёмся применения AXI Video DMA.

Всем интересующимся добро пожаловать под кат!

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте, с чего можно начать при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…

❯ Концепция проекта и постановка задачи

Основная задача, которая стоит перед нами — запустить вывод HDMI-изображения на отладочной плате Zynq Mini.

Если разделить задачу на подпункты, то нам необходимо:

  1. Подготовить проект в Xilinx Vivado и проверить работоспособность HDMI c Test Pattern Generator;

  2. Создать baremetal-проект для запуска Test Pattern Generator в Vitis;

  3. Создать проект для вывода изображения с использованием AXI Video DMA в Vivado;

  4. Создать 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. После этого в проект необходимо добавить несколько важных компонентов. Каждый их них в содержании оснащен избыточным количеством комментариев и в объяснении не нуждается. Не буду приводить здесь длинных листингов программ, а снабжу текст ссылками на мой репозиторий.

Итак. Опишу основные компоненты проекта.

  1. Основной текст программы, который нужно вставить в файл helloworld.c;

  2. Файл изображения pic_800_600.h, которое будет выведено на экран;

  3. Файл объявляющий основные функции dispay_demo.h;

  4. Компонент, который производит инициализацию Dynamic Clock Generator: dynclk;

  5. Компонент для управления дисплеем: display_ctrl.

 Добавьте все эти файлы в структуру проекта:

После этого необходимо добавить директории в проект, чтобы при компиляции все файлы были доступны:

Проверьте настройки запуска и нажмите кнопку Run:
Проверьте настройки запуска и нажмите кнопку Run:

Проверьте настройки запуска и нажмите кнопку Run:

Подключите плату по HDMI и вы увидите очень интересную картинку. А какую — узнаете когда соберете проект =)

❯ Заключение

Вот таким нехитрым образом можно вывести изображение в HDMI. Для более подробного объяснения рекомендую обратиться к документации на каждое из IP ядер и собрать всё воедино. Если расписывать весь pipeline формирования изображения, то статья вышла бы длиной раз в 10 больше этой. Поэтому всем интересующимся предлагаю изучить вопрос самостоятельно, я лишь привел пример как быстро реализовать работоспособный проект. А вот в следующей статье я хотел бы раскрыть вопрос более подробно, но уже в разрезе организации вывода фреймбуфера из Linux.

До встречи в следующих статьях =)


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud  в нашем Telegram-канале 

Перейти ↩

? Читайте также:

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


  1. dlinyj
    13.10.2024 08:27

    Для более подробного объяснения рекомендую обратиться к документации на каждое из IP ядер и собрать всё воедино. Если расписывать весь pipeline формирования изображения, то статья вышла бы длиной раз в 10 больше этой. 

    Ну вот, на самом интересном месте :))). Спасибо большое за статью. Больше подобных статей на хабре!


  1. checkpoint
    13.10.2024 08:27

    Совершенно не спортивно! Сигнал на разъёме HDMI можно формировать без всяких кастомных ядер, и даже без сериалайзеров, базовыми средствами ПЛИС. Я сейчас пишу очередной лонгрид на эту тему, надеюсь к НГ опубликовать. :-)


    1. megalloid Автор
      13.10.2024 08:27

      Можно и так, главное целесообразность ;) для разбора ваша статья будет более интересная, для быстрой реализации функциональности - моя)))

      Ждём вашу статью тоже :)


    1. zbot
      13.10.2024 08:27

      а можно даже без плиз и кастомных ядер. Микроконтроллер rp2040 (Raspbery PI Pico) с этим справляется вполне себе программными средствами.


      1. checkpoint
        13.10.2024 08:27

        Если тактовой частоты процессора хватает чтобы манипулировать GPIO сигналами с частотой 250 МГц - то да. Не знаю как с этим у RP2040.


        1. 15432
          13.10.2024 08:27

          И до 466 МГц разгоняли. Проект по DVI/HDMI вот тут


  1. yamifa_1234
    13.10.2024 08:27

    Не хватает фотографии отладки. Чтобы знать что искать