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

В этой статье я представлю текстовый VGA модуль, написанный на VHDL. Этот модуль может быть полезен при отладке платы, и занимает относительно немного места.

Общие характеристики


Модуль работает на тактовой частоте 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 = '1then
        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 <= '0when VCounter <  2 else '1';
hsync <= '0when HCounter < 96 else '1';
vga_blank <= '1when ((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 <= '1when (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 = '1then
      Screen(scrAddrB) <= scrDinB;
    end if;
    scrDataA <= Screen(scrAddrA);
    scrDataB <= Screen(scrAddrB);
  end if;
end process;
 
scrDinB  <= scrDinBLowLevel  when isLowLevel = '1else scrDinBNormal;
scrWeb   <= scrWeBLowLevel   when isLowLevel = '1else scrWebNormal;
scrAddrB <= scrAddrBLowLevel when isLowLevel = '1else scrAddrBNormal;
 
scrDinBLowLevel <= data(17 downto 0);
scrWeBLowLevel  <= '1when (conv_integer(addr) = 300 or conv_integer(addr) = 309) and iowr = '1else '0';
 
scrAddrBNormal <= GotoY * 80 + GotoX;
scrDiNBNormal  <= BgColor & Color & data(7 downto 0);
scrWebNormal   <= '1when conv_integer(addr) = 306 and iowr = '1else '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, определяющие текущее положение на экране, а на выходе – цвет пикселя, который необходимо вывести.

Этапы конвейера:
  1. Вычисление сигналов scrX, scrY, определяющих положение в видимой части экрана.
  2. Вычисление сигналов, определяющих ячейку в памяти экрана с необходимым символом
    textX = scrX / 8
    textY = scrY / 16
    Вычисление сигналов, определяющих необходимый пиксель в символе
    SymbolX = scrX mod 8 — 1
    SymbolY = scrY mod 16
    где mod — остаток от деления.
  3. Вычисление адресов по сигналам X, Y.
  4. Получение текущего символа.
  5. Получение текущего пикселя и формирование цвета.

process(clk)
begin
  if rising_edge(clk) then
   if div = '1then
    -- 1
    scrX <= conv_std_logic_vector(HCounter - 13910);
    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) = '1then
        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;
 
<= curColor(23 downto 16) when (vga_blank = '1') else "00000000";
<= curColor(15 downto  8) when (vga_blank = '1') else "00000000";
<= 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 = '1then 
      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 = '1then 
      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)

Спасибо за внимание.
  • +25
  • 9.8k
  • 9
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    0
    Здорово, всегда хотел сделать что-то такое, только повода не было.
    Есть два момента:
    1) На мой взгляд стоит сделать более прозрачное описание интерфейса модуля — чтобы было понятно что по какому адресу грузить — чтоб можно было взять ваш модуль и пустить в работу с минимальными усилиями.
    2) В коде по ссылке я вижу, что выходы на VGA одноразрядные, что вызывает недоумение:
    r: out STD_LOGIC;
    g: out STD_LOGIC;
    b: out STD_LOGIC;
      0
      1) Чтобы было прозрачно, можно в generic задать базовый адресс.
      2) Неувязочка вышла. Дело в том, что код я брал из текущей версии модуля (там как раз сигналы напрямую к VGA-разъему идут). Впрочем, изменения для 8-битного выхода и 1-битного минимальные.
      0
      Я имел в виду документацию — вот у вас 32 разрядный адрес (о боже, куда столько?), а по какому адресу писать, чтобы вывести 15 символ в 3 строке непонятно…
      И еще вопрос — знакогенератор перепрограммируемый или фиксированный?
        0
        Добавил в топик небольшое описание.
        Знакогенератор перепрограммируемый.
        +4
        я как-то пытался сделать такой же, но с хранением таблицы символов и видеопамяти в SRAM, куда таблица подгружалась из flash. В принципе, можно сделать довольно эффективный по ячейкам алгоритм, если хранить только один байт — текущую строку текущего символа. Поскольку символ шириной в 8 точек, то можно успеть сделать 8 обращений к SRAM, пока он рисуется, но достаточно двух. Естественно, такие обращения должны быть приоритетными и прерывать другую активность с памятью. Только тогда до меня дошло, почему при размещении программы в одном банке с видеопамятью в ZX-spectrum летели к черту все расчеты по количеству тактов на операцию :)
          +4
          Если попытаться создать свой суперпроцессор, почему-то всегда получается Zilog :)
          +2
          Мы (студенты Латвийского университета) тоже делали такой модуль как часть курсового проекта на курсе по цифровому проектированию ;-) Проектировался он для Xilinx Spartan-3E, шрифт и видеопамять были в RAM-блоках, размерность была 40×30 символов (потому что хорошо умещается в 640×480).

          Потом уже на основе этого модуля мы сделали традиционную змейку и «Matrix digital rain»:
          www.youtube.com/watch?v=tfB1G7xCYi0
          www.youtube.com/watch?v=wDJkkOI-s9w
            0
            Кстати, если уж вы Spartan упомянули. Вам случайно не приходилось прошивать плату Gigilent Nexys2 под линуксом? Ну или подобную. Вроде как слышал что это возможно, но у меня не получалось.
              0
              У нас был Spartan-3E Starter Kit. Он работал с линуксом (32-битный Ubuntu), но, насколько я помню, там надо было специальные драйверы от Xilinx ставить.

          Only users with full accounts can post comments. Log in, please.