Текстовый VGA-модуль на VHDL

 После нескольких экспериментов с платой Марсоход2 у меня возникла необходимость выводить на экран монитора различные данные. Плата оборудована разъёмом VGA и цифро-аналоговым преобразователем, но, разумеется, реализация работы видеоинтерфейса ложится на плечи разработчика. Для машины Brainfuck был использован текстовый адаптер, созданный Николаем для своих нужд, мне же это решение не подходит по нескольким причинам.

Во-первых, модуль адаптера написан на языке Verilog, с которым я практически не знаком. Таким образом, при необходимости внесения каких-либо правок в модуль и подстройке его под особенности моего проекта мне бы пришлось обращаться за помощью к знающим людям. А это и трата времени, и причинение определённых неудобств тем самым "знающим людям". Во-вторых, академический интерес – знание принципов работы аналогового видеоинтерфейса (пускай и на самом начальном уровне) не будет лишним в копилке знаний будущего инженера. В-третьих, известное правило "10 тысяч часов" – чтобы добиться успеха в какой-либо области, нужно потратить очень, очень много времени на практику.

Итак, вернёмся к видеоадаптеру. VGA – это стандартный интерфейс для управления аналоговыми мониторами. Источник сигнала – в нашем случае это будет ПЛИС платы Марсоход2 – снабжает монитор сигналами цветовых величин красного, зелёного и синего компонента, а также сигналами горизонтальной (строчной) и вертикальной (кадровой) синхронизации. Последние два являются цифровыми, в то время как данные о цвете предварительно превращаются в уровень напряжения в цифро-аналоговом преобразователе. Модуль требует входного сигнала тактовой частоты (pixel clock).

Male VGA connector


Стандартная вилка VGA-разъёма

Также в интерфейсе есть четыре вывода, которые используются для загрузки расширенных данных идентификации дисплея из ПЗУ монитора. В простейшем случае их можно не подключать. Кроме того, некоторые из мониторов поддерживают синхронизацию по зелёному – в этом случае сигналы вертикальной и горизонтальной синхронизации передаются по каналу зелёного цветового компонента, а необходимость в отдельных синхронизирующих линиях, соответственно, исчезает. Заострять внимание на этом сейчас тоже нет смысла, но....

Изображение ниже показывает, как располагаются сигналы синхронизации относительно видимой области. Синхронизирующие импульсы в начале и в конце выделяются "порожками" (англ. porch – "крыльцо"), причём длительность импульса и порожков определяется режимом работы контроллера. Также надо заметить, что для строк длительность измеряется в тактах опорной частоты, а для кадров – в строках.

VGA signal timing diagram

Расположение видимой области и сигналов синхронизации

В таблице показаны параметры для наиболее распространённых разрешений:

Разрешение

Частота обновления (Гц)

Частота пикселей (МГц)

Строки

Кадры

Полярность 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 МГц, которая использована для памяти.

Altera mega-wizard


Пользоваться MegaWizard Plug-In Manager легко

Рассмотрим интерфейсную часть основного модуля. Входами здесь являются порты pixel_clk для частоты пикселей, 16-разрядный вектор q_ram для запрошенных из ОЗУ данных, и 8-разрядный вектор q_rom – для данных их ПЗУ. В числе выходов присутствуют сигналы величин цветовых компонентов (r, g, b), сигналы вертикальной (vsync) и горизонтальной (hsync) синхронизации, 14-разрядный адрес запрашиваемого из ОЗУ слова rd и 12-разрядный адрес запрашиваемого из ПЗУ слова adr_rom.

vga entity


Графический символ текстового модуля

Если по интерфейсной части модуля вопросов не возникает, то можно перейти непосредственно к описанию его архитектуры. Прежде всего, необходимо задать два счётчика – горизонтальный (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.

VGA symbol font 001

Пример знака из алфавита, записанного в ПЗУ

Сюрпризом стало то, модуль рисовал на экране зеркально отображённые символы. Поэтому потребовался дополнительный сигнал 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');

В качестве теста и источника данных я использовал простой счётчик. В результате мы видим на экране бегущие строки разноцветных символов в строгом соответствии с их порядком в алфавите шрифта.

Разумеется, вместо счётчика в качестве источника данных может использоваться более сложная логика, но это уже совсем другая история. Вопросы по поводу тех или иных аспектов проекта, не получивших достаточного объяснения в статье, можно задать в комментариях.

Исходные данные проекта: 

 


Добавить комментарий