Во-первых, модуль адаптера написан на языке Verilog, с которым я практически не знаком. Таким образом, при необходимости внесения каких-либо правок в модуль и подстройке его под особенности моего проекта мне бы пришлось обращаться за помощью к знающим людям. А это и трата времени, и причинение определённых неудобств тем самым "знающим людям". Во-вторых, академический интерес – знание принципов работы аналогового видеоинтерфейса (пускай и на самом начальном уровне) не будет лишним в копилке знаний будущего инженера. В-третьих, известное правило "10 тысяч часов" – чтобы добиться успеха в какой-либо области, нужно потратить очень, очень много времени на практику.
Итак, вернёмся к видеоадаптеру. VGA – это стандартный интерфейс для управления аналоговыми мониторами. Источник сигнала – в нашем случае это будет ПЛИС платы Марсоход2 – снабжает монитор сигналами цветовых величин красного, зелёного и синего компонента, а также сигналами горизонтальной (строчной) и вертикальной (кадровой) синхронизации. Последние два являются цифровыми, в то время как данные о цвете предварительно превращаются в уровень напряжения в цифро-аналоговом преобразователе. Модуль требует входного сигнала тактовой частоты (pixel clock).
Стандартная вилка VGA-разъёма
Также в интерфейсе есть четыре вывода, которые используются для загрузки расширенных данных идентификации дисплея из ПЗУ монитора. В простейшем случае их можно не подключать. Кроме того, некоторые из мониторов поддерживают синхронизацию по зелёному – в этом случае сигналы вертикальной и горизонтальной синхронизации передаются по каналу зелёного цветового компонента, а необходимость в отдельных синхронизирующих линиях, соответственно, исчезает. Заострять внимание на этом сейчас тоже нет смысла, но....
Изображение ниже показывает, как располагаются сигналы синхронизации относительно видимой области. Синхронизирующие импульсы в начале и в конце выделяются "порожками" (англ. porch – "крыльцо"), причём длительность импульса и порожков определяется режимом работы контроллера. Также надо заметить, что для строк длительность измеряется в тактах опорной частоты, а для кадров – в строках.
Расположение видимой области и сигналов синхронизации
В таблице показаны параметры для наиболее распространённых разрешений:
Разрешение |
Частота обновления (Гц) |
Частота пикселей (МГц) |
Строки |
Кадры |
Полярность h_sync* |
Полярность v_sync* |
||||||
Пиксели |
Передний порожек |
Синхронизация |
Задний порожек |
Строки |
Передний порожек |
Синхронизация |
Задний порожек |
|||||
640x350 |
70 |
25.175 |
640 |
16 |
96 |
48 |
350 |
37 |
2 |
60 |
p |
n |
640x350 |
85 |
31.5 |
640 |
32 |
64 |
96 |
350 |
32 |
3 |
60 |
p |
n |
640x400 |
70 |
25.175 |
640 |
16 |
96 |
48 |
400 |
12 |
2 |
35 |
n |
p |
640x400 |
85 |
31.5 |
640 |
32 |
64 |
96 |
400 |
1 |
3 |
41 |
n |
p |
640x480 |
60 |
25.175 |
640 |
16 |
96 |
48 |
480 |
10 |
2 |
33 |
n |
n |
640x480 |
73 |
31.5 |
640 |
24 |
40 |
128 |
480 |
9 |
2 |
29 |
n |
n |
640x480 |
75 |
31.5 |
640 |
16 |
64 |
120 |
480 |
1 |
3 |
16 |
n |
n |
640x480 |
85 |
36 |
640 |
56 |
56 |
80 |
480 |
1 |
3 |
25 |
n |
n |
640x480 |
100 |
43.16 |
640 |
40 |
64 |
104 |
480 |
1 |
3 |
25 |
n |
p |
720x400 |
85 |
35.5 |
720 |
36 |
72 |
108 |
400 |
1 |
3 |
42 |
n |
p |
768x576 |
60 |
34.96 |
768 |
24 |
80 |
104 |
576 |
1 |
3 |
17 |
n |
p |
768x576 |
72 |
42.93 |
768 |
32 |
80 |
112 |
576 |
1 |
3 |
21 |
n |
p |
768x576 |
75 |
45.51 |
768 |
40 |
80 |
120 |
576 |
1 |
3 |
22 |
n |
p |
768x576 |
85 |
51.84 |
768 |
40 |
80 |
120 |
576 |
1 |
3 |
25 |
n |
p |
768x576 |
100 |
62.57 |
768 |
48 |
80 |
128 |
576 |
1 |
3 |
31 |
n |
p |
800x600 |
56 |
36 |
800 |
24 |
72 |
128 |
600 |
1 |
2 |
22 |
p |
p |
800x600 |
60 |
40 |
800 |
40 |
128 |
88 |
600 |
1 |
4 |
23 |
p |
p |
800x600 |
75 |
49.5 |
800 |
16 |
80 |
160 |
600 |
1 |
3 |
21 |
p |
p |
800x600 |
72 |
50 |
800 |
56 |
120 |
64 |
600 |
37 |
6 |
23 |
p |
p |
800x600 |
85 |
56.25 |
800 |
32 |
64 |
152 |
600 |
1 |
3 |
27 |
p |
p |
800x600 |
100 |
68.18 |
800 |
48 |
88 |
136 |
600 |
1 |
3 |
32 |
n |
p |
1024x768 |
43 |
44.9 |
1024 |
8 |
176 |
56 |
768 |
0 |
8 |
41 |
p |
p |
1024x768 |
60 |
65 |
1024 |
24 |
136 |
160 |
768 |
3 |
6 |
29 |
n |
n |
1024x768 |
70 |
75 |
1024 |
24 |
136 |
144 |
768 |
3 |
6 |
29 |
n |
n |
1024x768 |
75 |
78.8 |
1024 |
16 |
96 |
176 |
768 |
1 |
3 |
28 |
p |
p |
1024x768 |
85 |
94.5 |
1024 |
48 |
96 |
208 |
768 |
1 |
3 |
36 |
p |
p |
1024x768 |
100 |
113.31 |
1024 |
72 |
112 |
184 |
768 |
1 |
3 |
42 |
n |
p |
1152x864 |
75 |
108 |
1152 |
64 |
128 |
256 |
864 |
1 |
3 |
32 |
p |
p |
1152x864 |
85 |
119.65 |
1152 |
72 |
128 |
200 |
864 |
1 |
3 |
39 |
n |
p |
1152x864 |
100 |
143.47 |
1152 |
80 |
128 |
208 |
864 |
1 |
3 |
47 |
n |
p |
1152x864 |
60 |
81.62 |
1152 |
64 |
120 |
184 |
864 |
1 |
3 |
27 |
n |
p |
1280x1024 |
60 |
108 |
1280 |
48 |
112 |
248 |
1024 |
1 |
3 |
38 |
p |
p |
1280x1024 |
75 |
135 |
1280 |
16 |
144 |
248 |
1024 |
1 |
3 |
38 |
p |
p |
1280x1024 |
85 |
157.5 |
1280 |
64 |
160 |
224 |
1024 |
1 |
3 |
44 |
p |
p |
1280x1024 |
100 |
190.96 |
1280 |
96 |
144 |
240 |
1024 |
1 |
3 |
57 |
n |
p |
1280x800 |
60 |
83.46 |
1280 |
64 |
136 |
200 |
800 |
1 |
3 |
24 |
n |
p |
1280x960 |
60 |
102.1 |
1280 |
80 |
136 |
216 |
960 |
1 |
3 |
30 |
n |
p |
1280x960 |
72 |
124.54 |
1280 |
88 |
136 |
224 |
960 |
1 |
3 |
37 |
n |
p |
1280x960 |
75 |
129.86 |
1280 |
88 |
136 |
224 |
960 |
1 |
3 |
38 |
n |
p |
1280x960 |
85 |
148.5 |
1280 |
64 |
160 |
224 |
960 |
1 |
3 |
47 |
p |
p |
1280x960 |
100 |
178.99 |
1280 |
96 |
144 |
240 |
960 |
1 |
3 |
53 |
n |
p |
1368x768 |
60 |
85.86 |
1368 |
72 |
144 |
216 |
768 |
1 |
3 |
23 |
n |
p |
1400x1050 |
60 |
122.61 |
1400 |
88 |
152 |
240 |
1050 |
1 |
3 |
33 |
n |
p |
1400x1050 |
72 |
149.34 |
1400 |
96 |
152 |
248 |
1050 |
1 |
3 |
40 |
n |
p |
1400x1050 |
75 |
155.85 |
1400 |
96 |
152 |
248 |
1050 |
1 |
3 |
42 |
n |
p |
1400x1050 |
85 |
179.26 |
1400 |
104 |
152 |
256 |
1050 |
1 |
3 |
49 |
n |
p |
1400x1050 |
100 |
214.39 |
1400 |
112 |
152 |
264 |
1050 |
1 |
3 |
58 |
n |
p |
1440x900 |
60 |
106.47 |
1440 |
80 |
152 |
232 |
900 |
1 |
3 |
28 |
n |
p |
1600x1200 |
60 |
162 |
1600 |
64 |
192 |
304 |
1200 |
1 |
3 |
46 |
p |
p |
1600x1200 |
65 |
175.5 |
1600 |
64 |
192 |
304 |
1200 |
1 |
3 |
46 |
p |
p |
1600x1200 |
70 |
189 |
1600 |
64 |
192 |
304 |
1200 |
1 |
3 |
46 |
p |
p |
1600x1200 |
75 |
202.5 |
1600 |
64 |
192 |
304 |
1200 |
1 |
3 |
46 |
p |
p |
1600x1200 |
85 |
229.5 |
1600 |
64 |
192 |
304 |
1200 |
1 |
3 |
46 |
p |
p |
1600x1200 |
100 |
280.64 |
1600 |
128 |
176 |
304 |
1200 |
1 |
3 |
67 |
n |
p |
1680x1050 |
60 |
147.14 |
1680 |
104 |
184 |
288 |
1050 |
1 |
3 |
33 |
n |
p |
1792x1344 |
60 |
204.8 |
1792 |
128 |
200 |
328 |
1344 |
1 |
3 |
46 |
n |
p |
1792x1344 |
75 |
261 |
1792 |
96 |
216 |
352 |
1344 |
1 |
3 |
69 |
n |
p |
1856x1392 |
60 |
218.3 |
1856 |
96 |
224 |
352 |
1392 |
1 |
3 |
43 |
n |
p |
1856x1392 |
75 |
288 |
1856 |
128 |
224 |
352 |
1392 |
1 |
3 |
104 |
n |
p |
1920x1200 |
60 |
193.16 |
1920 |
128 |
208 |
336 |
1200 |
1 |
3 |
38 |
n |
p |
1920x1440 |
60 |
234 |
1920 |
128 |
208 |
344 |
1440 |
1 |
3 |
56 |
n |
p |
1920x1440 |
75 |
297 |
1920 |
144 |
224 |
352 |
1440 |
1 |
3 |
56 |
n |
p |
* p – положительный ("1"), n – отрицательный ("0"). |
Я буду ориентироваться на режим 1280 на 1024 точки при частоте кадров 60 Гц, но модуль можно подстроить под любой из режимов, изменив соответствующие параметры.
Одних сигналов синхронизации для модуля мало. Поскольку основное его предназначение – отображение текстовой информации, требуется ПЗУ, в котором будет храниться алфавит используемого шрифта. Я выбрал шрифт, ранее использованный Николаем и включающий 256 различных символов с размерами 8 на 16 точек. Таким образом, ПЗУ должно иметь 4096 восьмиразрядных слов. Отдельно требуется блок ОЗУ, где хранится текущее состояние экрана и куда сохраняются текстовые данные, поступающие от какого-либо источника. Каждое слово ОЗУ – несложно сосчитать, что их будет 10240, по максимальному числу отображаемых символов – соответствует одному знакоместу на экране и состоит из 16 бит – старший байт определяет атрибуты цвета фона и текста, младший же кодирует символ.
Оба блока памяти создаются через MegaWizard Plug-In Manager. Замечу, что я выбрал однопортовое ПЗУ, для его конфигурации использован файл vgafont.mif, в то время как ОЗУ имеет раздельные порты для чтения и записи данных. Сделано это для того, чтобы текстовый модуль мог свободно читать необходимую для отображения информацию из памяти и записывать новые данные. На входы ОЗУ data, wraddress и wren подаются записываемые данные (текст с атрибутами цвета), адрес знакоместа и сигнал разрешения записи от любого внешнего источника или пользовательской логики. Через мастер добавлен и модуль PLL, превращающий базовую частоту 100 МГц в частоту пикселей 108 МГц и частоту 54 МГц, которая использована для памяти.
Пользоваться MegaWizard Plug-In Manager легко
Рассмотрим интерфейсную часть основного модуля. Входами здесь являются порты pixel_clk для частоты пикселей, 16-разрядный вектор q_ram для запрошенных из ОЗУ данных, и 8-разрядный вектор q_rom – для данных их ПЗУ. В числе выходов присутствуют сигналы величин цветовых компонентов (r, g, b), сигналы вертикальной (vsync) и горизонтальной (hsync) синхронизации, 14-разрядный адрес запрашиваемого из ОЗУ слова rd и 12-разрядный адрес запрашиваемого из ПЗУ слова adr_rom.
Графический символ текстового модуля
Если по интерфейсной части модуля вопросов не возникает, то можно перейти непосредственно к описанию его архитектуры. Прежде всего, необходимо задать два счётчика – горизонтальный (hcnt), считающий такты частоты пикселей, и вертикальный (vcnt), считающий строки. Первый считает от 0 до 1687 (столько пикселей влезает в строку в выбранном мною режиме), второй – от 0 до 1065, причём его увеличение на единицу происходит только в момент обнуления горизонтального счётчика.
Следом нужно описать алгоритм формирования сигналов синхронизации. Значение сигнала выбирается в соответствии с его полярностью (при отрицательной полярности при "отсутствии" сигнала линия должна находится в высоком логическом состоянии), о том, как определить длительность и расположение синхронизирующего импульса я писал выше. Кстати говоря, синхронизирующий сигнал может проходить как после видимой зоны, так и до, чем я и воспользовался для выравнивания задержки видеопамяти, поставив горизонтальную синхронизацию до отображаемой строки пикселей. Кадровая синхронизация же происходит после отрисовки видимой зоны.
На основе значения счётчиков формируется сигнал display_en, принимающий единичные значения тогда, когда состояние адаптера соответствует одной из видимых на экране точек. В этом случае цветовым компонентам будут назначаться некоторые значения, получаемые из памяти, иначе же графическая информация не выводится.
process(pixel_clk)
begin
if rising_edge(pixel_clk) then
--counters
if (hcnt = 1687) then
hcnt <= 0;
if (vcnt = 1065) then
vcnt <= 0;
else
vcnt <= vcnt+1;
end if;
else
hcnt <= hcnt+1;
end if;
--sync pulses
if (hcnt > 47 and hcnt < 160) then
hsync <= '1';
else
hsync <= '0';
end if;
if(vcnt > 1025 and vcnt < 1028) then
vsync <= '1';
else
vsync <= '0';
end if;
--display enable
if (hcnt > 407 and vcnt < 1024) then
display_en <= '1';
else
display_en <= '0';
end if;
end if;
end process;
Самой важной частью текстового модуля будет получение адреса отображаемого в конкретный момент времени знакоместа и считывания из ПЗУ нужного слова, характеризующего расположение закрашенных точек символа. Во-первых, для этого из ОЗУ нужно запросить данные знакоместа – цвет фона, цвет текста и код самого символа. Числовой адрес знакоместа совпадает с таковым у слова в ОЗУ, а определяется он по такой формуле:
160*(vcnt/16)+ (hcnt-401)/8
Напомню, что высота знака 16 точек, а ширина – 8. Таким образом, в одну строку влезает 160 символов, номер символа в строке – это результат деления текущего значения hcnt (с поправкой на то, что видимая область начинается не от нуля) без остатка на восемь. Дополнительно к этому результату следует прибавить результат деления текущего значения счётчика vcnt на 16 (показывает номер отображаемой строки символов), умноженный на 160 – именно такое количество знаков влезает в одну строку. Очевидно, сумма укажет на искомое знакоместо. Из-за особенностей работы алгоритма первый символ в строке искажается, поэтому в действительности знакомест в строке 161, но первое не попадает в видимую зону и отсекается.
Далее нужно запросить из ПЗУ адрес слова, описывающего текущий фрагмент расположенного в знакоместе символа. Как уже неоднократно подмечалось, символ состоит из 16 строк по 8 точек в каждой. Одно слово в ПЗУ – это одна такая строка, состоящая из нулей (обозначают "фон") и единиц (соответственно, "знак"). Итак, адрес вычисляется следующим образом:
16*(to_integer(q_ram(7 downto 0))) + (vcnt rem 16)
Первод в целое число младшего байта вектора считанной из ОЗУ характеристики знакоместа даст результат от 0 до 255, который покажет, какой именно знак будет читаться из ПЗУ. Поскольку на один знак выделено 16 слов памяти, полученное число нужно умножить на 16, а к произведению прибавить остаток от деления текущего значения счётчика vcnt на 16.
Пример знака из алфавита, записанного в ПЗУ
Сюрпризом стало то, модуль рисовал на экране зеркально отображённые символы. Поэтому потребовался дополнительный сигнал col, за счёт которого слово ПЗУ "просматривается" в обратном порядке.
В последнюю очередь назначаются величины цветовых компонентов. Здесь всё просто: если адаптер находится в зоне отображения, а точка в текущем знакоместе – фон, цветам назначаются значения по старшим четырём разрядам старшего байта слова, считанного из ОЗУ; в противном случае (точка – часть символа) цвета назначаются из младшей тетрады старшего байта; а если адаптер вне видимой зоны – цветам назначаются нули (чёрный цвет).
red <= (others => q_ram(14)) when (display_en='1' and q_rom(to_integer(col(2 downto 0)))='0')
else (others => q_ram(10)) when display_en='1' else (others => '0');
green <= (others => q_ram(11)) when (display_en='1' and q_rom(to_integer(col(2 downto 0)))='0')
else (others => q_ram(9)) when display_en='1' else (others => '0');
blue <= (others => q_ram(12)) when (display_en='1' and q_rom(to_integer(col(2 downto 0)))='0')
else (others => q_ram(8)) when display_en='1' else (others => '0');
В качестве теста и источника данных я использовал простой счётчик. В результате мы видим на экране бегущие строки разноцветных символов в строгом соответствии с их порядком в алфавите шрифта.
Разумеется, вместо счётчика в качестве источника данных может использоваться более сложная логика, но это уже совсем другая история. Вопросы по поводу тех или иных аспектов проекта, не получивших достаточного объяснения в статье, можно задать в комментариях.
Исходные данные проекта:
Подробнее...