Хочу поделиться собственным опытом вывода изображения на монитор через VGA интерфейс. Я понимаю, что подобная задача решалась много раз, разными людьми, на разном оборудовании. Поэтому призываю всех причастных к этой теме отписаться в комментариях о своих вариантах реализации и особенностях разработки.
У меня на полке пылится вот такая отладочная плата, на ней кроме ПЛИС и SDRAM ничего больше нет (естественно, не считая кнопок и светодиодов и т.д.).
Была у меня идея по реализации одного проекта — в нем необходимо было вывести изображение на монитор; я бы сказал, неотъемлемая часть проекта — это вывод изображения на монитор. Так как на плате нет ни одного интерфейса, значит надо самому выбрать между HDMI, DVI, VGA. Забегая в перед, скажу, что несмотря на то, что интерфейсы разные, по сути своей они дают одно и тоже: три линии цвета(красный, синий, зеленый) и два сигнала синхронизации, построчная синхронизация и покадровая.
Обзор вариантов
HDMI. Почитав про данный интерфейс, выяснилось, что он последовательный, а это значит, что если взять разрешения экрана 640x480 при частоте кадров 60Гц, то частота вывода пикселей составляет порядка 25 МГц, а после сериалайзера — все 250 МГц. К тому же этот интерфейс требует модуль TMDS, а с ним связываться совсем не хотелось, поэтому на этой стадии рассмотрение данного интерфейса прекратилось.
DVI. У данного интерфейса есть несколько режимов. Два основных: цифровой (DVI-D) и аналоговый (DVI-A). Аналоговый вариант, в принципе, это тот же самый вариант что и VGA, поэтому его рассмотрим позже. Внутри данного интерфейса, как и у HDMI, есть LVDS линии, что так же принуждают использовать сериалайзер и енкодер. Поэтому, по тем же причинам(лень) пропускаю этот интерфейс.
VGA. Возможно, самый простой и самый древний интерфейс(также он является частью DVI-A). Для вывода изображения необходимо сформировать три уровня цвета (красный, синий, зеленый) и два сигнала синхронизации (построчной и покадровой). И в таком базовом варианте можно уже получить 8 различных цветов. На фото ниже показана распиновка VGA интерфейса во время подключения к ПЛИС, и предлагаемый китайский модуль с VGA. На входе модуля три 8-битной шины цвета и два сигнала синхронизации (на модуле стоит две микросхемы повторителя сигнала и массив резисторов для формирования уровня). Естественно, китайский модуль красив и замечателен, но я пошел другим путем — решил собрать все сам.
Реализация
Прочитав несколько статей в интернете (3-4 статьи) на тему вывода изображения на монитор, я отметил для себя ряд особенностей:
У VGA интерфейса есть стандарт вывода (очевидно, но все же). Для каждого режима есть свои длительности невидимых частей поля, частота вывода пикселя, а так же полярность синхронизирующих сигналов. Например, для разрешения в 640х480 полярность синхросигналов для вывода картинки с частотой 60Гц и 100Гц разная (http://tinyvga.com/vga-timing - тут можно получить и посмотреть полный список стандартов разрешений и их тайминги).
Многие статьи предлагают четко разделить невидимые области справа и слева от рабочей области.
Вариант с разделенными областями:
Данный метод немного усложняет понимание для реализации, и я решил использовать другое представление:
Активный регион сдвигается к левому верхнему углу, а поля выводятся справа и снизу.
Уровень сигналов RGB порядка 0.7 Вольт, сопротивление линии 75 Ом (но этот пункт я осознал позже).
Жизненный опыт №1. Исходя из распиновки (представлена выше), в лоб можно реализовать 8 различных цветов. Я решил использовать разрешение 640х480 с частотой кадров 60Гц, по информации с сайта http://tinyvga.com/vga-timing/640x480@60Hz ясно, что общая область 800х525 точек. Алгоритм вывода изображения крайне прост: два счетчика по вертикали и горизонтали. Линии отрисовываются по горизонтали строчка за строчкой (это общеизвестный факт), и пока счетчики находятся в пределах рабочей области(640х480) — выводится изображение.
Если с алгоритмом все понятно, то в стандарте так же сказано про пиксельную частоту. Она равна 25.175Мгц. На этой стадии наступила боль. Если во многих примерах в проектах с ПЛИС фирмы Альтера PLL позволяли сгенерировать большой диапазон частот, то в моем случае PLL была ограничена и никак не хотела выдавать то что мне надо. Через боль и страдания я поставил две PLL последовательно и получил 25.185 МГц. В целом погрешность не велика - порядка 0.04%(на этой стадии я считал, что частота должна быть точно такой как сказано в стандарте, иначе картинка будет плыть или вообще не синхронизируется ничего. Но как оказалось, я ошибался…).
В итоге я получил свои заветные 8 полос:
В процессе игриЩь, я выяснил, что несмотря на то что у стандарта VGA имеются сигналы синхронизации, без которых картинка не выводится (я проверил потом…), заканчивать картинку черной полосой крайне не хорошо. Сразу портится синхронизация и монитор считает, что это невидимая область и скрывает ее, оставляя только 7 полос. Почитав на эту тему и задав неудобные вопросы людям, выяснилось, что некоторые мониторы синхронизируются еще и по одному из цветов.
Жизненный опыт №2. Восемь цветом мне было мало, да и вообще мне было мало полос на мониторе. Но так как на первой стадии я подключил интерфейс на прямую, то получил в итоге однобитный цвет. Поразмышляв, я решил расширить диапазон за счет ШИМ. Поднял частоту модуля до 100.74 МГц , попытался сформировать сигналы RGB с различной шириной импульсов.
Монитор оказался умнее меня). В первый раз он сумел адаптироваться к ШИМ и выдал такую же гладкую картинку, как при первом запуске без использования ШИМ. Но после насильственных мер монитор таки выдал реакцию, и, к сожалению, градиента цвета я не получил. Получились рваные полосы. На рисунке я пытался сделать 4 полосы с различной шириной импульсов. Первая полоса без ШИМ, вторая 25%, третья 50%, четвертая 75%. Попинав этот вариант, я понял, что придется делать нормальный ЦАП.
Жизненный опыт №3. Еще с института я знал, что ЦАП на резисторах есть, и это самый быстрый ЦАП; с тем недостатком — что необходимо подбирать резисторы максимально точно. и от этой точности зависит результат. В моем же случае точность не важна, главное — чтобы было похоже на правду. Схема ЦАП типа R-2R в интернете известна и выглядит следующим образом:
Взяв в руки паяльник и набор резисторов на 1кОм и 2кОм приступил к делу. ЦАП сделал 5-битный. Получилось следующее:
Подключив кучу проводов, запустил:
куча проводов
Фото сделано в полной темноте. Свет выключить я догадался не сразу и долго не понимал, что я сделал не так. Проверив пайку мультиметром, увидел, что напряжение меняется на ЦАП, если подавать различное значение на входе. Потом только заметил, что монитор не равномерно черный, а словно на сектора разбит, вот тогда-то я и вырубил свет. Начался опять ряд вопросов, от которых мне было максимально стыдно, ведь эту тему я прекрасно понимаю и почему сам не подумал об этом не знаю. А проблема была в том, что сопротивление на приемнике (в мониторе на каждую линию RGB) составляло 75 Ом, а я подключал ЦАП с сопротивлением больше 2кОм. В итоге формируемое напряжение на приемнике не превышало 123 мВ(и то в лучшем раскладе), а надо 700мВ.
Вновь открыв браузер и группу в телеграм, пошел изучать вопрос — какой же мне надо делать ЦАП, чтобы яркость картинки соответствовала ожиданию. Один из вариантов был тот же R2R ЦАП, но на резисторах куда меньшего сопротивления - порядка 150-70 Ом (точные значения не помню). Второй вариант был основан на параллельных резисторах. Вот схема:
Данный метод используется во многих отладочных платах, например на плате с плис фирмы Альтера. Естественно, недолго думая, сварганил подобный ЦАП по 5 бит на цвет:
И как результат получился первый адекватный вывод:
Получается на рисунке 32786 оттенков (каждый цвет -синий, зеленый, красный - имеет 32 уровня яркости).
Вот еще пара вариантов, если хочется оценить равномерность уровней яркости:
Жизненный опыт №4. Теперь осталось за малым, наладить механизм вывода изображения из буфера памяти. Я подцепил микросхему памяти SDRAM, описал автомат, который заливает в нее кадр с градиентом подобный тому, что были выше. Так же описал автомат, который вычитывает из памяти кадр и отправляет в модуль с VGA выходом. В целом, все заработало с первого раза (ну, вы понимаете о чем я — с первого раза ничего не работает). Но тут надо понимать, что без особых танцев с бубнами и т.п., получил следующее:
В целом картинка есть, она была статичной, а это показатель того, что она в памяти обновляется правильно, не плывет, значит и вычитывание адекватное. Но если приглядеться там будут видны волнистые линии на границе градиентов.
Вот если плохо видно:
Тут надо понимать, проект у меня уже разросся — добавился контроллер SDRAM, пара автоматов, FIFO, обработка кнопок и моргание лампочек (в рамках отладки), так же добавился ЧипСкоп. То есть, когда я описывал отдельно модуль VGA, ничего лишнего не было, и границы были ровные, а тут появилось — и у меня возникла новая куча вопросов.
После долго обсуждения этой темы была озвучена мысль, что нужно избавляться от двух последовательных PLL. Во-первых, плата китайская и вряд ли генератор на 50МГц выдает идеальные значения. Так же, сама по себе PLL вносит дополнительный джиттер, а с учетом того что там два модуля PLL , эффект джиттера становится намного заметнее.
На этом моменте я попал в ступор, где мне взять 25.175МГц (которые требует стандарт), если PLL не может выдать их. В принципе можно изменить выбранное разрешение. Например, есть такие разрешения как: SXGA (Mode 101) 640x480@85 Hz (pixel clock 36.0 MHz), SVGA 800x600@60 Hz (pixel clock 40.0 MHz), VESA 800x600@72 Hz (pixel clock 50.0 MHz), VESA 1024x768@70 Hz (pixel clock 75.0 MHz). У данных стандартов значение частоты целое. Но, выбрав один из подобных стандартов, увеличится скорость данных, которую нужно будет вычитывать из памяти, и записывать. Структуру проекта я строил таким образом, чтобы в память за 1 такт записывался один пиксель (все 15 разрядов). Частота работы памяти в моем случае 100 МГц (хотя микросхема позволяет поднять до 133 МГц). Таким образом, для дальнейшей свободы (и гибкости) в разработке, пиксельную частоту надо выбирать меньше 50 МГц, желательно значительно.
По итогу, ничего не решив, я предпринял отчаянный шаг (мне так казалось), выставил частоту вывода пикселя 25 МГц (используя один PLL) при разрешении 640x480@60Hz. В итоге все заработало, красиво и ровно. Я в очередной раз понял, что монитор оказался гораздо умнее чем я думал!)
Следующей стадией я описал UART интерфейс, написал программу в QT для загрузки изображения по UART на ПЛИС, и смог загрузить туда своего первого миньона!) Вот миньон:
Немного уплыли цвета, но как потом выяснилось, я потерял один бит в цвете (на стороне ПК). В результате один канал уплыл, вроде это был зеленый. А вот видео загрузки если интересно, грузил на скорости 115200 бод/с:
Заключение
Статья получилась длинная, местами даже громоздкая. В любом случае, я хотел поделиться своим опытом разработки VGA интерфейса. Считаю данный интерфейс максимально дружелюбным и простым для начинающего (и не только…) плисовода. У верен многие занимались реализацией подобного интерфейса, прошу поделиться опытом.
Куда же без кода:
Модуль вычитывает из FIFO, настроенного в режиме AXI-Stream данные по 16 разрядов. Из которых младшие 15 разрядов — это RGB, а старший 16-й бит используется для синхронизации кадра между модулями. На тот случай если будет потеря данных или какой-то сбой, модуль VGA интерфейса выполняет синхронизацию по старшему биту. Он поднимается в конце кадра, реализуя своеобразный last. Модуль реализует формат VGA 640x480@60 Hz Industry standard (pixel clock 25.175 MHz). Для адаптации к остальным режимам необходимо уточнять полярность синхроимпульсов и размер выводимой области.
module top_vga(
input clk,
input [15:0] data_rgb, //5bit R,G,B
input valid_rgb,
output ready_rgb,
output end_frame, //last pixel in frame
output reg [4:0] r_vga,
output reg [4:0] g_vga,
output reg [4:0] b_vga,
output reg v_sync_vga,
output reg h_sync_vga
);
reg [10:0] cnt_h = 'd0;
reg [10:0] cnt_v = 'd0;
wire flg_imag = (cnt_h < 'd640 && cnt_v < 'd480) ? 'd1 : 'd0;
reg error_frame = 'd0;
always@(posedge clk)
begin
if(cnt_h >= 'd799) begin //640 + 16 + 96(sync) + 48
cnt_h <= 'd0;
if(cnt_v >= 'd524) begin //480 + 10 + 2(sync) + 33
cnt_v <= 'd0;
end else begin
cnt_v <= cnt_v + 1;
end
end else begin
cnt_h <= cnt_h + 1;
end
if(data_rgb[15]=='d1 && flg_imag=='d1 && valid_rgb && error_frame==0) begin
if(cnt_h !='d639 && cnt_v != 'd479) begin
error_frame <= 'd1;
end
end else begin
if(cnt_h >= 'd799 && cnt_v >= 'd524) begin
error_frame <= 'd0;
end
end
end
assign ready_rgb = flg_imag && (!error_frame);
assign end_frame = (cnt_h == 'd799 && cnt_v == 'd524);
always@(posedge clk)
begin
r_vga <= (flg_imag && valid_rgb && ready_rgb) ? data_rgb[14:10] :
(flg_imag) ? 5'b01111 :
'd0;
g_vga <= (flg_imag && valid_rgb && ready_rgb) ? data_rgb[9:5] :
(flg_imag) ? 5'b00111 :
'd0;
b_vga <= (flg_imag && valid_rgb && ready_rgb) ? data_rgb[4:0] :
(flg_imag) ? 5'b01111 :
'd0;
h_sync_vga <= (cnt_h>(640+16-1) && cnt_h<(640+16+96-1)) ? 'd0 : 'd1; // for 640*480 active level - low
v_sync_vga <= (cnt_v>(480+10-1) && cnt_v<(480+10 +2-1)) ? 'd0 : 'd1; // for 640*480 active level - low
end
endmodule
Комментарии (35)
EBolov
22.05.2023 16:53По поводу "волнистых линий": чипскопом пробовали смотреть выход VGA? Там он наблюдается? Констрейнты на выходные порты написаны? 25МГц не такая уж и большая частота для 6 спартана
yamifa_1234 Автор
22.05.2023 16:53Чипскопом не смотрел, и констрейны не описывал. если только на сам клок. Мне кажется в чипскопе такие линии не видны будут, ведь если фаза клока плывет так она плывет для всего проекта, а относительно клока в мониторе они выражаются волнистыми линиями.
EBolov
22.05.2023 16:53Для чипскопа идеально подходит серый градиент: счётчик от 0 до максимума. А раз "волнушка" с амплитудой в пиксель, то и нарушения в счётчике будут отлично видно. Я на циклоне 3 рисовал на VGA 1280х1024@60(108МГц), единственное что картинка генерировалось, а не считалась из памяти
yamifa_1234 Автор
22.05.2023 16:53все равно не понимаю как волны в чипскопе будут видны. ведь чипскоп будет на том же клоке что и модуль VGA.
EBolov
22.05.2023 16:53Вот так выглядит нормально работающий счётчик:
А вот так со всякими "приколами":
Т.е. если мы видим нервную линию, то пиксели будут "гулять" и попадать на следующий такт.
MiraclePtr
22.05.2023 16:53+1Я так понял, вы использовали ЖК-монитор и он довольно умный (он жо сути дела обратно из аналога оцифровывает картинку), а не пробовали потестировать с "тупым" ЭЛТ-дисплеем?
yamifa_1234 Автор
22.05.2023 16:53+1хорошая идея, но у меня нет ЭЛТ монитора) да и покупать на авито уже не хочетсЯ) цель то была вывести картинку. думаете она была бы равномерно кривой?)
Stanislavvv
22.05.2023 16:53+2В полностью аналоговых мониторах допустимые отклонения частот будут в процентах, так что точно будет работать.
firegurafiku
22.05.2023 16:53+11Думаю, вы зря так быстро отбросили DVI: он простой, как палка и верёвка, и работает на любой разумной пискельной частоте.
Выяснилось, что [HDMI —] последовательный, а это значит, частота вывода пикселей составляет порядка 25 МГц, а после сериалайзера — все 250 МГц.
Это совершенно не проблема. Если верить даташиту на это семейство ПЛИС, OSERDES2 (сериализующий выходной дифференциальный буфер) на чипах спидгрейда -2 (такой чип на картинке) выдаёт 500-1080 мегабит/с в зависимости от источника клока. Если не все OSERDES2 заняты под DDR-память, на них можно было бы организовать видеовыход.
Цифры я смотрел тут: https://docs.xilinx.com/v/u/en-US/ds162, стр. 18.
К тому же этот интерфейс требует модуль TMDS [...]
У этого семейства заявлена поддержка TMDS, но есть ограничения по банкам и питающим напряжениям. Для TMDS-источника даже внешних резисторов не надо, они ставятся на стороне приёмника.
Про схему согласования есть в другом даташите: https://docs.xilinx.com/v/u/en-US/ug381, стр. 36.
Внутри данного интерфейса, как и у HDMI, есть LVDS-линии [...]
У вас тут фактическая ошибка. LVDS — это не общий термин для вообще любого дифференциального интерфейса, а отдельный стандарт. У LVDS и TMDS отличаются уровни напряжений и по-разному делается согласование приёмника с источником. Поэтому ни в DVI, ни в HDMI нет LVDS-линий.
yamifa_1234 Автор
22.05.2023 16:53+1А вот и знания пошли) спасибо) я об этом не знал, изучу вопрос на досуге
SGordon123
22.05.2023 16:53а вариант как на марсоходе LVDS не рассматривали?https://marsohod.org/projects/plata1/173-phframe2
yamifa_1234 Автор
22.05.2023 16:53нет, хотелось именно монитор)
SGordon123
22.05.2023 16:53Нечего разобрать или принципиально не разбираете что ли?
yamifa_1234 Автор
22.05.2023 16:53хотелось принципиально иметь возможность подключить любой монитор который найду. это значит надо реализовывать один из видео интерфейсов.
Mad__Max
22.05.2023 16:53+4А вот видео загрузки если интересно, грузил на скорости 115200 бод/с:
Прямо очень живо напомнило загрузку заставки (статической картинки прямо) с кассеты напрямую в видеобуфер на ZX Spectrum, которые там обычно грузились перед игрой или другой большой программой.
vesper-bot
22.05.2023 16:53+2Скорее уж загрузку BMP на диалапе. На ZX это совсем не тянет, и линий одна а не 8, и данные о цвете доступны сразу, а не "потом" :)
Mad__Max
22.05.2023 16:53+1На крупный BMP без сжатия на диап-ап в принципе тоже.
А по ZX там же очень разные варианты были реализован в зависимости от конкретных разработчиков ПО. Т.к. это не аппаратное ограничение, а реализация загрузчика конкретной софтины, с загрузкой обычно в 3 стадии (иногда и больше): 1 — микро-программа загрузчик, 2 — картинка-заставка, 3 — собственно основная часть ПО.
И по 1 строчке и по 8 строк и с цветом "потом" и цветом сразу варианты были.Правда с цветом сразу это все-таки не сразу "прямо с кассеты в видеобуфер" с промежуточной буферизацией загружаемых данных в обычной ОЗУ. По схеме кассета ==> рабочее ОЗУ ==> копируем из ОЗУ в видеобуфер ==> продолжаем грузить программу с кассеты в осбоводвшиеся ОЗУ.
Пример серия игр "Саботер": https://www.youtube.com/watch?v=MKZy_d5BTZc
Или робокоп: https://youtu.be/nVyMfMe4YNU?t=49
Сразу с цветом.И тут построчная (черезстрочная) загрузка прямо в видеопамять (но да, тогда цвет подтягивается "потом") игра Коммандо (видео тоже самое, но перемотать на конец 8й минуты если временная метка не сработает): https://youtu.be/nVyMfMe4YNU?t=529
Но где-то вроде варианты с цветом сразу были, т.к. записано было так, что сначала грузился с кассеты блок с данными о цвете, а потом уже битовая карта самих пикселей против стандартного варианта, когда сначала графика, а потом цвет.
Вот тут большая подборка нестандартных загрузчиков существовавших для ZX:
https://www.youtube.com/watch?v=8e_IkqfMeD4Основные моменты общие — примерно та же скорость загрузки (вообще у ZX битовая скорость загрузки конечно намного ниже чем 115 кбод в статье, но и разрешение экрана намного ниже), и немного "вырвиглазные" цвета за счет урезанной палитры в отличии от BMP на диал-апе. Скорее тогда уж скорее очень жирный GIF на диал-апе.
CC rkfg
rkfg
22.05.2023 16:53Это да, я имел в виду дефолтную линейную загрузку. Ещё была загрузка по столбцам и квадратами 8х8 в произвольном порядке, причём, сразу с цветовыми атрибутами, так что картинка осмысленно рисовалась в соответствии с задумкой создателя. В какой-то части Dizzy даже сжатие применялось для этой заставки, ну там понятно, что грузилось сначала в буфер, потом оттуда распаковывалось на экран. Алгоритм был то ли RLE, то ли даже Хаффман, не помню.
rkfg
22.05.2023 16:53+1В робокопе и саботёре слышно же, что картинка грузится как обычно, просто перед загрузкой выставляется цвет тона и фона одинаковый (синий у саботёра и чёрный у робокопа), поэтому пикселей не видно, пока не загрузятся атрибуты. Никакой буфер в другой части памяти тут не участвует.
А видео с загрузчиками очень крутое, даже вон мини-игры встраивали, пока грузится основная игра! Я и десятой части не видел, максимум загрузка по квадратикам, как выше писал, и счётчик в углу с кастомным шрифтом.
yamifa_1234 Автор
22.05.2023 16:53интересно а есть какието более менее современные устройства(с поддержкой usb или СОМ порта) которые позволят данные на кассету записать?)
vesper-bot
22.05.2023 16:53Большинство игр, в которые я играл, загрузку заставки реализовывали через
LOAD xxx SCREEN$
что загружает 6912 байт в видеобуфер ZX'a. Всё остальное — вероятно, перезапись функции LOAD как таковая, так как обычно ZX ничего не делает во время загрузки, кроме приема данных и поиска сигнала конца блока данных. Ну и давно это было, но я помню пару игр, которые заставку загружали другим образом, а иногда и эффекты по экрану в процессе загрузки запускали (мини-игр не видел, может, мало 48 кбайт, чтобы ещё и мини-игру всобачить?)
rezedent12
22.05.2023 16:53Если поставить 2 чипа памяти, то можно удвоить пропускную способность.
yamifa_1234 Автор
22.05.2023 16:53+1Куда поставить?) На плате только одно место и уже с чипом)
alex-open-plc
22.05.2023 16:53+1значит надо самому выбрать между HDMI, DVI, VGA. Забегая в перед, скажу, что несмотря на то, что интерфейсы разные,
О как!
DVI-D и HDMI отличаются только разъёмом. DVI-A - да, уникальный.
D - цифра
A - аналоговый сигнал
I - интегрированный (оба)
ynoxinul
22.05.2023 16:53+1Спасибо за статью, интересные эксперименты! Когда я делал свой VGA-контроллер, я не мог найти информацию, насколько мониторы требовательны к таймингам и как именно должны идти сигналы синхронизации (сдвинут ли vsync относительно hsync, например). Оказывается, мониторы прощают много ошибок.
zatim
У меня как раз на столе появилась макетная плата с maxII с али. Светодиодиком я уже помигал, а вы подкинули замечательную идею что можно еще с ней сделать.
Кстати, все аналоговые стандарты допускают весьма большие допуски с рабочими частотами, не нужно было об этом излишне волноваться. Некоторые жк мониторы даже вполне стабильно работают с частотами разверток ТВ стандарта 15625 и 50 Гц. Такие модели пользуются популярностью у любителей спектрумов.