В этой статье я представлю текстовый VGA модуль, написанный на VHDL. Этот модуль может быть полезен при отладке платы, и занимает относительно немного места.
Модуль работает на тактовой частоте 50 МГц.
Выдает картинку с разрешением 640х480 с частотой 60Гц.
Размер символа 8х16 точек. На экране 80х25 символов.
Палитра на 32 цвета.
Занимаемые ресурсы в ПЛИС:

Интерфейс модуля включает в себя вход тактового генератора (clk), выход на VGA-разъем (r, g, b, vsync, hsync) и выход на процессорную шину (addr, data, iowr, dout). На плате, с которой я работал, стоят 8-битные ЦАП в количестве трех штук (на каждый цвет по одной), поэтому из модуля идут 8-битные линии по цветам. Взяв старший бит цвета, получаем рабочий вариант для плат без ЦАП.

Для работы монитора необходимы 3 сигнала синхронизации и 3 сигнала цвета. Для формирования сигналов синхронизации используются счетчики VCounter и HCounter. Сигналы синхронизации выдаются в моменты, показанные на схеме.

Код счетчиков:
Формирование сигналов синхронизации:
Размер одного символа 8х16 точек. В массиве символов будет 256 ячеек по 128 бит. Каждый бит отвечает за то, есть ли точка в текущей позиции или нет. Расположение точек в ячейке памяти:

В FPGA существует блочная память конфигурации 1024х18 бит (18432 бит), на которую мы и будем ориентироваться. САПР автоматически реализует практически любую выбранную конфигурацию, используя такие блоки памяти. Итак, для памяти символов необходимо 256х128 бит = 32768 бит памяти, что займет 2 блока памяти (BRAM).
Память символов инициализируется при проектировании и может быть изменена в процессе работы.
Еще одна необходимая память — память экрана (видимой области). В этой памяти закодируем номер символа, цвет символа и цвет фона. Для экономии памяти цвета закодируем номером в палитре. Исходя из конфигурации памяти с ячейкой в 18 бит, отведем под номер символа 8 бит, под цвет символа 5 бит, и под цвет фона тоже 5 бит. Под палитру отведено 32 ячейки по 24 бита (8 бит на цвет).

Исходя из разрешения и размера символа, на экране умещается 80х30 символов. Но это занимает в памяти 2400 ячеек (3 блока памяти). Так как, с самого начала ставилась задача минимального размера этого модуля, то были вырезаны 5 нижних строк. В итоге получим 80х25 отображаемых символов, что занимает 2000 ячеек памяти (2 блока памяти).
Генерация памяти символов осуществляется в coregenerator'е. Подключение памяти к модулю осуществляется следующим образом:
Память экрана создается путем написания шаблонного кода (хотя можно поступить так же, как и с памятью символов).
Также необходимо задать таблицу цветов палитры, что осуществляется следующим образом.
Вся необходимая память сформирована, теперь можно заняться реализацией подготовки пикселя для вывода на экран.
Современные тенденции проектирования FPGA подразумевают полностью синхронные проекты. Не будем отступать от этих тенденций, и реализуем для подготовки очередного пикселя конвейер. На входе будут счетчики HCount и VCount, определяющие текущее положение на экране, а на выходе – цвет пикселя, который необходимо вывести.
Этапы конвейера:
Работать с модулем будем через шину с интерфейсом
Шиной управляет форт-процессор, реализованый на этой же FPGA. По команде OUTPORT выставляются линии Addr, Data и на один такт поднимается в «1» линия Iowr. По команде INPORT процессор выставляет линию Addr и забирает данные с линии Dout.
Пример работы с шиной:
Таким же образом реализуется большинство команд для работы с модулем.
Полный листинг модуля доступен по ссылке.
Листинг модуля
Для инициализации памяти символов
UPD:
Базовые функции
300 – запись/чтение из памяти экрана
301 – адрес для записи в память экрана
302 – установить Х координату каретки (GotoX)
303 – установить У координату каретки (GotoY)
304 – установить цвет символа (SetColor)
305 – установить цвет фона (SetBgColor)
306 – вывод символа и перемещение каретки (EMIT)
307 – переключение режима
1 – LowLevel режим. Доступны функции 300, 301 для прямой работы с памятью
0 – Обычный режим. Доступна функция 306 для установки символа с заданным цветом в заданную позицию и перемещения каретки.
308 – CR
309 – VGA_SetData(A++)
Параметры курсора
310 – установить параметры курсора.
0 бит – нижняя полоска в курсоре
1 бит – черный квадрат
2 бит – мерцание
3 бит – вертикальная полоска
311 – установка бита №0 в параметрах курсора
…
314 – установка бита №3 в параметрах курсора
320 – установка цвета курсора
321 – установка времени моргания курсора
322 – установка времени горящего курсора
Работа с палитрой
330 – Установка адреса палитры цветов
331 – Запись цвета по установленному адресу
Работа с таблицей символов
335 – Установка адреса в таблице символов
337 – Запись NewSymbol по заданному адресу
340 – Задание младших 32 битов сигнала NewSymbol (NewSymbol(31:0))
341 – Задание следующих 32 битов NewSymbol(63:32)
342 – NewSymbol(95:64)
343 – NewSymbol(127:96)
Спасибо за внимание.
Общие характеристики
Модуль работает на тактовой частоте 50 МГц.
Выдает картинку с разрешением 640х480 с частотой 60Гц.
Размер символа 8х16 точек. На экране 80х25 символов.
Палитра на 32 цвета.
Занимаемые ресурсы в ПЛИС:

Реализация
Интерфейс модуля включает в себя вход тактового генератора (clk), выход на VGA-разъем (r, g, b, vsync, hsync) и выход на процессорную шину (addr, data, iowr, dout). На плате, с которой я работал, стоят 8-битные ЦАП в количестве трех штук (на каждый цвет по одной), поэтому из модуля идут 8-битные линии по цветам. Взяв старший бит цвета, получаем рабочий вариант для плат без ЦАП.

entity vga_text is
Port ( clk : in STD_LOGIC;
iowr : in STD_LOGIC;
addr : in STD_LOGIC_VECTOR (31 downto 0);
data : in STD_LOGIC_VECTOR (31 downto 0);
dout: out std_logic_vector(31 downto 0);
r : out STD_LOGIC_VECTOR (7 downto 0);
g : out STD_LOGIC_VECTOR (7 downto 0);
b : out STD_LOGIC_VECTOR (7 downto 0);
vga_blank : out STD_LOGIC;
vsync : out STD_LOGIC;
hsync : out STD_LOGIC);
end vga_text;
Для работы монитора необходимы 3 сигнала синхронизации и 3 сигнала цвета. Для формирования сигналов синхронизации используются счетчики VCounter и HCounter. Сигналы синхронизации выдаются в моменты, показанные на схеме.

Код счетчиков:
architecture Behavioral of vga_text is
signal VCounter : integer range 0 to 520 := 0;
signal HCounter : integer range 0 to 800 := 0;
signal div : std_logic := '0';
begin
process(clk)
begin
if rising_edge(clk) then
div <= not(div);
if div = '1' then
if HCounter = 799 then
HCounter <= 0;
if VCounter = 520 then
VCounter <= 0;
else
VCounter <= VCounter + 1;
end if;
else
HCounter <= HCounter + 1;
end if;
end if;
end if;
end process;
end Behavioral;
Формирование сигналов синхронизации:
vsync <= '0' when VCounter < 2 else '1';
hsync <= '0' when HCounter < 96 else '1';
vga_blank <= '1' when ((HCounter > 143) and (HCounter < 784) and
(VCounter > 30) and (VCounter < 511)) else '0';
Размер одного символа 8х16 точек. В массиве символов будет 256 ячеек по 128 бит. Каждый бит отвечает за то, есть ли точка в текущей позиции или нет. Расположение точек в ячейке памяти:

В FPGA существует блочная память конфигурации 1024х18 бит (18432 бит), на которую мы и будем ориентироваться. САПР автоматически реализует практически любую выбранную конфигурацию, используя такие блоки памяти. Итак, для памяти символов необходимо 256х128 бит = 32768 бит памяти, что займет 2 блока памяти (BRAM).
Память символов инициализируется при проектировании и может быть изменена в процессе работы.
Еще одна необходимая память — память экрана (видимой области). В этой памяти закодируем номер символа, цвет символа и цвет фона. Для экономии памяти цвета закодируем номером в палитре. Исходя из конфигурации памяти с ячейкой в 18 бит, отведем под номер символа 8 бит, под цвет символа 5 бит, и под цвет фона тоже 5 бит. Под палитру отведено 32 ячейки по 24 бита (8 бит на цвет).

Исходя из разрешения и размера символа, на экране умещается 80х30 символов. Но это занимает в памяти 2400 ячеек (3 блока памяти). Так как, с самого начала ставилась задача минимального размера этого модуля, то были вырезаны 5 нижних строк. В итоге получим 80х25 отображаемых символов, что занимает 2000 ячеек памяти (2 блока памяти).
Генерация памяти символов осуществляется в coregenerator'е. Подключение памяти к модулю осуществляется следующим образом:
signal SAddrA: std_logic_vector(7 downto 0);
signal SAddrRead, SAddrWrite: integer range 0 to 255;
signal SDataA, SDinA: std_logic_vector(127 downto 0);
signal SWeA: std_logic;
-- ...
component vga640_symbols
port (
addr: IN std_logic_VECTOR(7 downto 0);
clk: IN std_logic;
din: IN std_logic_VECTOR(127 downto 0);
dout: OUT std_logic_VECTOR(127 downto 0);
we: IN std_logic);
end component;
-- ...
begin
-- ...
vga640_symbols_0: vga640_symbols
Port map(
addr => SAddrA,
clk => clk,
din => NewSymbol,
dout => SDataA,
we => SWeA
);
SWeA <= '1' when (isLowLevel = '1') and (conv_integer(addr) = 337 and iowr = '1') else '0';
SAddrA <= conv_std_logic_vector(SAddrWrite, 8) when (isLowLevel = '1') and (conv_integer(addr) = 337 and iowr = '1') else conv_std_logic_vector(SAddrRead, 8);
Память экрана создается путем написания шаблонного кода (хотя можно поступить так же, как и с памятью символов).
type TScreen is array (0 to 1999) of std_logic_vector(17 downto 0);
signal Screen: TScreen;
signal scrAddrA, scrAddrB: integer range 0 to 1999;
signal scrDataA, scrDataB, scrDinB: std_logic_vector(17 downto 0);
signal scrWeB: std_logic;
-- ...
begin
-- ...
process(clk)
begin
if rising_edge(clk) then
if scrWeB = '1' then
Screen(scrAddrB) <= scrDinB;
end if;
scrDataA <= Screen(scrAddrA);
scrDataB <= Screen(scrAddrB);
end if;
end process;
scrDinB <= scrDinBLowLevel when isLowLevel = '1' else scrDinBNormal;
scrWeb <= scrWeBLowLevel when isLowLevel = '1' else scrWebNormal;
scrAddrB <= scrAddrBLowLevel when isLowLevel = '1' else scrAddrBNormal;
scrDinBLowLevel <= data(17 downto 0);
scrWeBLowLevel <= '1' when (conv_integer(addr) = 300 or conv_integer(addr) = 309) and iowr = '1' else '0';
scrAddrBNormal <= GotoY * 80 + GotoX;
scrDiNBNormal <= BgColor & Color & data(7 downto 0);
scrWebNormal <= '1' when conv_integer(addr) = 306 and iowr = '1' else '0';
Также необходимо задать таблицу цветов палитры, что осуществляется следующим образом.
type TColor is array (0 to 31) of std_logic_vector(23 downto 0);
signal PalleteColor: TColor :=
(
0 => x"000000",
1 => x"0000FF",
2 => x"00FF00",
3 => x"00FFFF",
4 => x"FF0000",
5 => x"FF00FF",
6 => x"FFFF00",
7 => x"FFFFFF",
others => x"FFFFFF"
);
signal PalleteAddr: integer range 0 to 31;
Вся необходимая память сформирована, теперь можно заняться реализацией подготовки пикселя для вывода на экран.
Современные тенденции проектирования FPGA подразумевают полностью синхронные проекты. Не будем отступать от этих тенденций, и реализуем для подготовки очередного пикселя конвейер. На входе будут счетчики HCount и VCount, определяющие текущее положение на экране, а на выходе – цвет пикселя, который необходимо вывести.
Этапы конвейера:
- Вычисление сигналов scrX, scrY, определяющих положение в видимой части экрана.
- Вычисление сигналов, определяющих ячейку в памяти экрана с необходимым символом
textX = scrX / 8
textY = scrY / 16
Вычисление сигналов, определяющих необходимый пиксель в символе
SymbolX = scrX mod 8 — 1
SymbolY = scrY mod 16
где mod — остаток от деления. - Вычисление адресов по сигналам X, Y.
- Получение текущего символа.
- Получение текущего пикселя и формирование цвета.
process(clk)
begin
if rising_edge(clk) then
if div = '1' then
-- 1
scrX <= conv_std_logic_vector(HCounter - 139, 10);
scrY <= conv_std_logic_vector(VCounter - 70 , 10);
-- 2
textX <= conv_integer(scrX(9 downto 3));
textY <= conv_integer(scrY(9 downto 4));
SymbolX <= scrX(2 downto 0) - 1;
SymbolY <= scrY(3 downto 0);
-- 3
scrAddrA <= textY * 80 + textX;
SymbolPoint <= conv_integer(SymbolY) * 8 + 7 - conv_integer(SymbolX);
-- 4
SAddrRead <= conv_integer(scrDataA(7 downto 0));
scrData <= scrDataA;
-- 5
if (VCounter > 69) and (VCounter < 470) then
if SDataA(SymbolPoint) = '1' then
curColor <= PalleteColor(conv_integer(scrData(12 downto 8)));
else
curColor <= PalleteColor(conv_integer(scrData(17 downto 13)));
end if;
else
curColor <= x"000000";
end if;
end if;
end if;
end process;
b <= curColor(23 downto 16) when (vga_blank = '1') else "00000000";
g <= curColor(15 downto 8) when (vga_blank = '1') else "00000000";
r <= curColor( 7 downto 0) when (vga_blank = '1') else "00000000";
Работать с модулем будем через шину с интерфейсом
- Addr(32 bit) – адрес
- Data(32 bit) – данные
- Iowr(1 bit) – запись (OUTPORT)
- Dout(32 bit) – ответные данные (INPORT)
Шиной управляет форт-процессор, реализованый на этой же FPGA. По команде OUTPORT выставляются линии Addr, Data и на один такт поднимается в «1» линия Iowr. По команде INPORT процессор выставляет линию Addr и забирает данные с линии Dout.
Пример работы с шиной:
process(clk)
begin
if rising_edge(clk) then
-- установка курсора в позицию X (GOTOX)
if conv_integer(addr) = 302 and iowr = '1' then
if conv_integer(data) > 79 then
GotoX <= 0;
else
GotoX <= conv_integer(data);
end if;
end if;
-- установка курсора в позицию Y (GOTOY)
if conv_integer(addr) = 303 and iowr = '1' then
if conv_integer(data) > 24 then
GotoY <= 0;
else
GotoY <= conv_integer(data);
end if;
end if;
end if;
end process;
Таким же образом реализуется большинство команд для работы с модулем.
Полный листинг модуля доступен по ссылке.
Листинг модуля
Для инициализации памяти символов
UPD:
Базовые функции
300 – запись/чтение из памяти экрана
301 – адрес для записи в память экрана
302 – установить Х координату каретки (GotoX)
303 – установить У координату каретки (GotoY)
304 – установить цвет символа (SetColor)
305 – установить цвет фона (SetBgColor)
306 – вывод символа и перемещение каретки (EMIT)
307 – переключение режима
1 – LowLevel режим. Доступны функции 300, 301 для прямой работы с памятью
0 – Обычный режим. Доступна функция 306 для установки символа с заданным цветом в заданную позицию и перемещения каретки.
308 – CR
309 – VGA_SetData(A++)
Параметры курсора
310 – установить параметры курсора.
0 бит – нижняя полоска в курсоре
1 бит – черный квадрат
2 бит – мерцание
3 бит – вертикальная полоска
311 – установка бита №0 в параметрах курсора
…
314 – установка бита №3 в параметрах курсора
320 – установка цвета курсора
321 – установка времени моргания курсора
322 – установка времени горящего курсора
Работа с палитрой
330 – Установка адреса палитры цветов
331 – Запись цвета по установленному адресу
Работа с таблицей символов
335 – Установка адреса в таблице символов
337 – Запись NewSymbol по заданному адресу
340 – Задание младших 32 битов сигнала NewSymbol (NewSymbol(31:0))
341 – Задание следующих 32 битов NewSymbol(63:32)
342 – NewSymbol(95:64)
343 – NewSymbol(127:96)
Спасибо за внимание.