И снова про VGA

Хочу поделиться собственным опытом вывода изображения на монитор через VGA интерфейс. Я понимаю, что подобная задача решалась много раз, разными людьми, на разном оборудовании. Поэтому призываю всех причастных к этой теме отписаться в комментариях о своих вариантах реализации и особенностях разработки.

У меня на полке пылится вот такая отладочная плата, на ней кроме ПЛИС и SDRAM ничего больше нет (естественно, не считая кнопок и светодиодов и т.д.).

4deec4a6ced28753317c24f73ee2217c.jpg

Была у меня идея по реализации одного проекта — в нем необходимо было вывести изображение на монитор; я бы сказал, неотъемлемая часть проекта — это вывод изображения на монитор. Так как на плате нет ни одно интерфейса, значит надо самому выбрать между HDMI, DVI, VGA.  Забегая в перед, скажу, что несмотря на то, что интерфейсы разные, по сути своей они дают одно и тоже: три линии цвета (красный, синий, зеленый) и два сигнала синхронизации, построчная синхронизация и покадровая.

Обзор вариантов.

HDMI. Почитав про данный интерфейс, выяснилось, что он последовательный, а это значит, что если взять разрешения экрана 640×480 при частоте кадров 60Гц, то частота вывода пикселей составляет порядка 25 МГц, а после сериалайзера — все 250 МГц. К тому же этот интерфейс требует модуль TMDS, а с ним связываться совсем не хотелось, поэтому на этой стадии рассмотрение данного интерфейса прекратилось.

Картинка взята с сайта https://marsohod.org/plata-marsokhod3/proekty-dlya-platy-marsokhod3/307-max10-hdmi

DVI. У данного интерфейса есть несколько режимов. Два основных: цифровой (DVI-D) и аналоговый (DVI-A). Аналоговый вариант, в принципе, это тот же самый вариант что и VGA, поэтому его рассмотрим позже. Внутри данного интерфейса, как и у HDMI, есть LVDS линии, что так же принуждают использовать сериалайзер и енкодер. Поэтому, по тем же причинам (лень) пропускаю этот интерфейс.

Картинка взята из документа   Video Connectivity Using TMDS I/O in Spartan-3A FPGAs(XAPP460)

VGA. Возможно, самый простой и самый древний интерфейс (также он является частью DVI-A). Для вывода изображения необходимо сформировать три уровня цвета (красный, синий, зеленый) и два сигнала синхронизации (построчной и покадровой). И в таком базовом варианте можно уже получить 8 различных цветов. На фото ниже показана распиновка VGA интерфейса во время подключения к ПЛИС, и предлагаемый китайский модуль с VGA. На входе модуля три 8-битной шины цвета и два сигнала синхронизации (на модуле стоит две микросхемы повторителя сигнала и массив резисторов для формирования уровня). Естественно, китайский модуль красив и замечателен, но я пошел другим путем — решил собрать все сам.

6c0a4e56bba5c76ce38ce2d25eee7d9a.png

Реализация.

Прочитав несколько статей в интернете (3–4 статьи) на тему вывода изображения на монитор, я отметил для себя ряд особенностей:

  1. У VGA интерфейса есть стандарт вывода (очевидно, но все же). Для каждого режима есть свои длительности невидимых частей поля, частота вывода пикселя, а так же полярность синхронизирующих сигналов. Например, для разрешения в 640×480 полярность синхросигналов для вывода картинки с частотой 60Гц и 100Гц разная (http://tinyvga.com/vga-timing — тут можно получить и посмотреть полный список стандартов разрешений и их тайминги).

  2. Многие статьи предлагают четко разделить невидимые области справа и слева от рабочей области.

Вариант с разделенными областями:

deaf1957113968d2bda1e9367c23f90f.png3240d5a9b13380ab3dce3886d37250bf.png

Рисунки взяты с http://we.easyelectronics.ru/plis/generator-cvetnyh-polos-na-fpga.html, https://eax.me/fpga-vga/

Данный метод немного усложняет понимание для реализации, и я решил использовать другое представление:

Картинка взята с сайта http://4a4ik.blogspot.com/2015/07/vga.html

Активный регион сдвигается к левому верхнему углу, а поля выводятся справа и снизу.

  1. Уровень сигналов 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 полос:

693064daf70576a9379d60f9468c7741.jpg

В процессе игриЩь, я выяснил, что несмотря на то что у стандарта VGA имеются сигналы синхронизации, без которых картинка не выводится (я проверил потом…), заканчивать картинку черной полосой крайне не хорошо. Сразу портится синхронизация и монитор считает, что это невидимая область и скрывает ее, оставляя только 7 полос. Почитав на эту тему и задав неудобные вопросы людям, выяснилось, что некоторые мониторы синхронизируются еще и по одному из цветов.

Жизненный опыт №2. Восемь цветом мне было мало, да и вообще мне было мало полос на мониторе. Но так как на первой стадии я подключил интерфейс на прямую, то получил в итоге однобитный цвет. Поразмышляв, я решил расширить диапазон за счет ШИМ. Поднял частоту модуля до 100.74 МГц , попытался сформировать сигналы RGB с различной шириной импульсов.

98ee47a70d9ba75242b68ca6aebdfbfd.png

Монитор оказался умнее меня). В первый раз он сумел адаптироваться к ШИМ и выдал такую же гладкую картинку, как при первом запуске без использования ШИМ. Но после насильственных мер монитор таки выдал реакцию, и, к сожалению, градиента цвета я не получил. Получились рваные полосы. На рисунке я пытался сделать 4 полосы с различной шириной импульсов. Первая полоса без ШИМ, вторая 25%, третья 50%, четвертая 75%. Попинав этот вариант, я понял, что придется делать нормальный ЦАП.

Жизненный опыт №3. Еще с института я знал, что ЦАП на резисторах есть, и это самый быстрый ЦАП; с тем недостатком — что необходимо подбирать резисторы максимально точно. и от этой точности зависит результат. В моем же случае точность не важна, главное — чтобы было похоже на правду. Схема ЦАП типа R-2R в интернете известна и выглядит следующим образом:

Рисунок взят со страницы http://easyelectronics.ru/parallelnyj-cifro-analogovyj-preobrazovatel-po-sxeme-r-2r.html

Взяв в руки паяльник и набор резисторов на 1кОм и 2кОм приступил к делу. ЦАП сделал 5-битный. Получилось следующее:

5a90f83b41ee832b44a0525134531bec.png

Подключив кучу проводов, запустил:

куча проводов

куча относительная :)

куча относительная :)

d3963aad23e01c859ea83f628b06a930.png

Фото сделано в полной темноте. Свет выключить я догадался не сразу и долго не понимал, что я сделал не так. Проверив пайку мультиметром, увидел, что напряжение меняется на ЦАП, если подавать различное значение на входе. Потом только заметил, что монитор не равномерно черный, а словно на сектора разбит, вот тогда-то я и вырубил свет. Начался опять ряд вопросов, от которых мне было максимально стыдно, ведь эту тему я прекрасно понимаю и почему сам не подумал об этом не знаю. А проблема была в том, что сопротивление на приемнике (в мониторе на каждую линию RGB) составляло 75 Ом, а я подключал ЦАП с сопротивлением больше 2кОм. В итоге формируемое напряжение на приемнике не превышало 123 мВ (и то в лучшем раскладе), а надо 700 мВ.

Вновь открыв браузер и группу в телеграм, пошел изучать вопрос — какой же мне надо делать ЦАП, чтобы яркость картинки соответствовала ожиданию. Один из вариантов был тот же R2R ЦАП, но на резисторах куда меньшего сопротивления — порядка 150–70 Ом (точные значения не помню). Второй вариант был основан на параллельных резисторах. Вот схема:

Рисунок взят с https://www.researchgate.net/figure/VGA-Connections-from-FPGA_fig10_312984160

Данный метод используется во многих отладочных платах, например на плате с плис фирмы Альтера. Естественно, недолго думая, сварганил подобный ЦАП по 5 бит на цвет:

59e3e2c8abbf708721cb83297ac7b632.png

И как результат получился первый адекватный вывод:

a57b69e503e3edda96213facc77da49a.png

Получается на рисунке 32786 оттенков (каждый цвет -синий, зеленый, красный — имеет 32 уровня яркости).

Вот еще пара вариантов, если хочется оценить равномерность уровней яркости:

09dc1a2519b539705f904e1886f88541.png8968dc122bf7b3a60604203b383d020d.png

Жизненный опыт №4. Теперь осталось за малым, наладить механизм вывода изображения из буфера памяти. Я подцепил микросхему памяти SDRAM, описал автомат, который заливает в нее кадр с градиентом подобный тому, что были выше. Так же описал автомат, который вычитывает из памяти кадр и отправляет в модуль с VGA выходом. В целом, все заработало с первого раза (ну, вы понимаете о чем я — с первого раза ничего не работает). Но тут надо понимать, что без особых танцев с бубнами и т.п., получил следующее:

Волнистые линии на стыках градиента. В начале экрана такого нет.

Волнистые линии на стыках градиента. В начале экрана такого нет.

В целом картинка есть, она была статичной, а это показатель того, что она в памяти обновляется правильно, не плывет, значит и вычитывание адекватное. Но если приглядеться там будут видны волнистые линии на границе градиентов.

Вот если плохо видно:

2a399accb83371d5138207ecf27cb5df.jpg

Тут надо понимать, проект у меня уже разросся — добавился контроллер 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 на ПЛИС, и смог загрузить туда своего первого миньона!) Вот миньон:

d4ed991354ab2856d37f4a9f72f58efd.png

Немного уплыли цвета, но как потом выяснилось, я потерял один бит в цвете (на стороне ПК). В результате один канал уплыл, вроде это был зеленый. А вот видео загрузки если интересно, грузил на скорости 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

© Habrahabr.ru