И снова про VGA
Хочу поделиться собственным опытом вывода изображения на монитор через VGA интерфейс. Я понимаю, что подобная задача решалась много раз, разными людьми, на разном оборудовании. Поэтому призываю всех причастных к этой теме отписаться в комментариях о своих вариантах реализации и особенностях разработки.
У меня на полке пылится вот такая отладочная плата, на ней кроме ПЛИС и SDRAM ничего больше нет (естественно, не считая кнопок и светодиодов и т.д.).
Была у меня идея по реализации одного проекта — в нем необходимо было вывести изображение на монитор; я бы сказал, неотъемлемая часть проекта — это вывод изображения на монитор. Так как на плате нет ни одно интерфейса, значит надо самому выбрать между HDMI, DVI, VGA. Забегая в перед, скажу, что несмотря на то, что интерфейсы разные, по сути своей они дают одно и тоже: три линии цвета (красный, синий, зеленый) и два сигнала синхронизации, построчная синхронизация и покадровая.
Обзор вариантов.
HDMI. Почитав про данный интерфейс, выяснилось, что он последовательный, а это значит, что если взять разрешения экрана 640×480 при частоте кадров 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 — тут можно получить и посмотреть полный список стандартов разрешений и их тайминги).
Многие статьи предлагают четко разделить невидимые области справа и слева от рабочей области.
Вариант с разделенными областями:
Рисунки взяты с http://we.easyelectronics.ru/plis/generator-cvetnyh-polos-na-fpga.html, https://eax.me/fpga-vga/
Данный метод немного усложняет понимание для реализации, и я решил использовать другое представление:
Активный регион сдвигается к левому верхнему углу, а поля выводятся справа и снизу.
Уровень сигналов RGB порядка 0.7 Вольт, сопротивление линии 75 Ом (но этот пункт я осознал позже).
Жизненный опыт №1. Исходя из распиновки (представлена выше), в лоб можно реализовать 8 различных цветов. Я решил использовать разрешение 640×480 с частотой кадров 60Гц, по информации с сайта http://tinyvga.com/vga-timing/640×480@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) 640×480@85 Hz (pixel clock 36.0 MHz), SVGA 800×600@60 Hz (pixel clock 40.0 MHz), VESA 800×600@72 Hz (pixel clock 50.0 MHz), VESA 1024×768@70 Hz (pixel clock 75.0 MHz). У данных стандартов значение частоты целое. Но, выбрав один из подобных стандартов, увеличится скорость данных, которую нужно будет вычитывать из памяти, и записывать. Структуру проекта я строил таким образом, чтобы в память за 1 такт записывался один пиксель (все 15 разрядов). Частота работы памяти в моем случае 100 МГц (хотя микросхема позволяет поднять до 133 МГц). Таким образом, для дальнейшей свободы (и гибкости) в разработке, пиксельную частоту надо выбирать меньше 50 МГц, желательно значительно.
По итогу, ничего не решив, я предпринял отчаянный шаг (мне так казалось), выставил частоту вывода пикселя 25 МГц (используя один PLL) при разрешении 640×480@60Hz. В итоге все заработало, красиво и ровно. Я в очередной раз понял, что монитор оказался гораздо умнее чем я думал!)
Следующей стадией я описал UART интерфейс, написал программу в QT для загрузки изображения по UART на ПЛИС, и смог загрузить туда своего первого миньона!) Вот миньон:
Немного уплыли цвета, но как потом выяснилось, я потерял один бит в цвете (на стороне ПК). В результате один канал уплыл, вроде это был зеленый. А вот видео загрузки если интересно, грузил на скорости 115200 бод/с:
Заключение:
Статья получилась длинная, местами даже громоздкая. В любом случае, я хотел поделиться своим опытом разработки VGA интерфейса. Считаю данный интерфейс максимально дружелюбным и простым для начинающего (и не только…) плисовода. У верен многие занимались реализацией подобного интерфейса, прошу поделиться опытом.
Куда же без кода:
Модуль вычитывает из FIFO, настроенного в режиме AXI-Stream данные по 16 разрядов. Из которых младшие 15 разрядов — это RGB, а старший 16-й бит используется для синхронизации кадра между модулями. На тот случай если будет потеря данных или какой-то сбой, модуль VGA интерфейса выполняет синхронизацию по старшему биту. Он поднимается в конце кадра, реализуя своеобразный last. Модуль реализует формат VGA 640×480@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