Site icon Embarcados – Sua fonte de informações sobre Sistemas Embarcados

Exibindo caracteres ASCII com controlador VGA – Parte 3

VGA controlador VGA caracteres ASCII com controlador VGA

Olá, caro leitor. Recentemente escrevi sobre o controlador VGA, descrevendo os sinais de sincronismo e o processo de varredura de um frame. Como procedimento de teste, foram utilizados os sinais de controle das componentes RGB para demonstrar as possíveis combinações de cores. Neste artigo, apresentarei um procedimento para exibir caracteres ASCII com controlador VGA, criado anteriormente.  

Caracteres ASCII

ASCII é acrônimo de American Standard Code for Information Interchange (em português significa Código Padrão Americano para o Intercâmbio de Informação). Esses códigos são valores binários utilizados para representar caracteres, sendo 7 bits utilizados para esse fim. Os caracteres são mostrados na Figura 1.

Figura 1: Tabela ASCII.

Representação dos caracteres

O procedimento mostrado aqui é propriedade do Dr. Pong P. Chu. Todo material dele pode ser encontrado neste link. A estrutura proposta por Chu é a seguinte. Todos os 128 caracteres são armazenados em uma memória, em que um determinado byte corresponde à parte da representação de um caractere.

O arquivo font_rom.vhd, que representa essa memória, pode ser verificado neste link (code listing – capítulo 13). Basicamente, esse módulo define um array de valores do tipo std_logic_vector e possui um conjunto de sinais para determinar o endereço os dados de leitura. Essas operações são sincronizadas pelo sinal de clock.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity font_rom is
   port(
      clk: in std_logic;
      addr: in std_logic_vector(10 downto 0);
      data: out std_logic_vector(7 downto 0)
   );
end font_rom;

De modo geral, um caractere completo é composto por 16 linhas. Por conseguinte, um caractere ocupa 16 bytes de memória, ou 16×8 bits. Assim, a largura de um caractere é de 8 pixels. A Figura 2 mostra como o caractere ‘A’ é armazenado na memória.

Figura 2: Representação dos caracteres armazenados na memória.

Cabe ressaltar que cada pixel é representado por um bit. O código abaixo mostra o procedimento executado sempre que ocorre uma borda de subida no sinal de clock. Uma pequena alteração no módulo foi realizada para que sua saída seja alterada conforme endereço determinado no momento do evento de clock.

 process (clk)
 begin
    if (clk'event and clk = '1') then
      data <= ROM(to_integer(unsigned(addr)));
    end if;
 end process;

Para acessar toda a informação de um caractere, basta saber o seu endereço base. Dessa maneira, o código ASCII do caractere determina os bits mais significativos do endereço. Já os bits menos significativos determinam a linha, ou região, atual que está sendo exibida. 

Exibindo caracteres

Para exibir os caracteres, somente o módulo PixelGen será alterado. É importante lembrar que tal módulo já recebia como entrada as coordenadas X e Y, relativas aos processo de varredura definido no controlador VGA. Assim, esse valores serão utilizados como estrutura condicional para apresentar no monitor a frase “portal embarcados”. Antes de descrever o procedimento para escrita, vamos definir os sinais que serão utilizados no módulo PixelGen.

ARCHITECTURE arch OF PixelGen IS

    COMPONENT font_rom IS
	port(
		clk: in std_logic;
		addr: in std_logic_vector(10 downto 0);
		data: out std_logic_vector(7 downto 0)
	);
    END COMPONENT font_rom;
	
   --Coordenadas X e Y do pixel atual
   SIGNAL pix_x, pix_y: UNSIGNED(9 DOWNTO 0);
	
   --Endereço que será acessado na memória de caracteres
   SIGNAL rom_addr: STD_LOGIC_VECTOR(10 DOWNTO 0);
	
   --Código ASCII do caractere atual (parte do endereço)
   SIGNAL char_addr: STD_LOGIC_VECTOR(6 DOWNTO 0);
	
   --Parte do caractere (0~15) que está sendo exibida na linha atual Y
   SIGNAL row_addr: STD_LOGIC_VECTOR(3 DOWNTO 0);
	
   --Pixel relativo a coordenada X atual
   SIGNAL bit_addr: STD_LOGIC_VECTOR(2 DOWNTO 0);
	
   --Conteúdo armazenado no endereço indicado por 'rom_addr'
   SIGNAL font_word: STD_LOGIC_VECTOR(7 DOWNTO 0);
	
   --Valor do bit 'bit_addr' na palavra 'font_word'
   SIGNAL font_bit: STD_LOGIC;
	
   --Valor das componentes rgb
   SIGNAL font_rgb: STD_LOGIC_VECTOR(2 DOWNTO 0);
	
   --Flag que indica se a frase deve ser exibida
   SIGNAL txt_on: STD_LOGIC;

BEGIN

O endereço que será acessado na memória de caracteres é composto pelo código do caractere e da respectiva linha. É importante lembrar que os caracteres possuem 16 linhas. Logo, os 4 bits menos significativos da posição vertical definem qual byte da representação do caractere será acessado. Esse procedimento é mostrado abaixo, em que o endereço completo é a concatenação dos sinais char_addr com row_addr.

-- Coordenadas XY atuais
pix_x <= UNSIGNED(F_COLUMN(9 DOWNTO 0));
pix_y <= UNSIGNED(F_ROW);
	
-- Memória dos caracteres
font_unit: font_rom PORT MAP(clk=>not F_CLOCK, addr=>rom_addr, data=>font_word);
	
-- Determinação do endereço que será acessado
row_addr <= STD_LOGIC_VECTOR(pix_y(3 DOWNTO 0));
rom_addr <= char_addr & row_addr;

Além disso, o conteúdo dessa posição deve ser lido corretamente. Considerando o sinal X da varredura horizontal e o tamanho de 8 bits da largura de um caractere, tem-se que os 3 bits menos significativos representam o pixel que deve ser acessado. Porém, a ordem dos bits é inversa, pois quando X é “000”, o bit acessado deve ser o da posição 7. O procedimento que faz essa operação é mostrado abaixo. 

-- De acordo com os 3 bits menos significativo de X, a posição do bit é determinada
bit_addr <= NOT STD_LOGIC_VECTOR(pix_x(2 DOWNTO 0));

--Valor do bit na posição bit_addr
font_bit <= font_word(to_integer(UNSIGNED( bit_addr))); 

Tendo essas definições, basta definir quais caracteres serão apresentados e sua respectiva posição. Para isso, um sinal booleano indica quando as coordenadas X e Y estão dentro da região que a frase será exibida. Já em outro trecho de código, com base na posição X, um caractere será selecionado. É importante lembrar que esse código do caractere compõe o endereço acessado na memória ROM.

WITH pix_x(7 DOWNTO 3) SELECT
  char_addr <="1110000" WHEN "01000", -- p
	      "1101111" WHEN "01001", -- o
	      "1110010" WHEN "01010", -- r
	      "1110100" WHEN "01011", -- t
	      "1100001" WHEN "01100", -- a
	      "1101100" WHEN "01101", -- l
	      "0000000" WHEN "01110", --
	      "1100101" WHEN "01111", -- e
	      "1101101" WHEN "10000", -- m
	      "1100010" WHEN "10001", -- b
	      "1100001" WHEN "10010", -- a
	      "1110010" WHEN "10011", -- r
	      "1100011" WHEN "10100", -- c
	      "1100001" WHEN "10101", -- a
	      "1100100" WHEN "10110", -- d
              "1101111" WHEN "10111", -- o
	      "1110011" WHEN "11000", -- s
	      "0000000" WHEN OTHERS;


 txt_on <= '1' WHEN (pix_x >= 320 AND pix_x <= 455) AND (pix_y >= 292 AND pix_y <= 305) ELSE
 '0';

Neste exemplo, a frase será exibida a partir da posição X=320 e Y=292. No sinal pix_x, são verificados somente os bits da posição 7 até 3. Isso ocorre pois os 3 bits menos significativos indicam a coluna do caractere. Portanto, a cada 8 posições, um novo caractere é exibido. Dessa maneira, esses bits representam qual caractere deve ser exibido.

Além disso, o trecho de código abaixo realiza o acionamento das componentes RGB. É importante destacar que isso depende do sinal que indica a região ativa do frame VGA ‘F_ON’, da regra ‘txt_on’ que define se a mensagem deve ser exibida e do próprio valor do pixel ‘font_bit’.

font_rgb <="111" WHEN font_bit='1' ELSE "000";

PROCESS(F_ON,font_rgb,txt_on)
BEGIN	
	IF F_ON ='0' or txt_on='0' THEN
		 R_OUT <= '0';
		 G_OUT <= '0';
		 B_OUT <= '0';
	ELSE
		 R_OUT <= font_rgb(0);
		 G_OUT <= font_rgb(1);
		 B_OUT <= font_rgb(2);		 
	END IF;
END PROCESS; 

Antes de testar, cabe ressaltar que a leitura dos caracteres implica em um atraso de um ciclo de clock. Logo, o sinal de clock da font_rom é oposto ao clock principal, de forma que os sinais de controle estejam em sincronismo com os valores do pixel. Isso foi exibido no trecho de código acima.

-- Memória dos caracteres
font_unit: font_rom PORT MAP(clk=>not F_CLOCK, addr=>rom_addr, data=>font_word);

Além disso, o módulo principal foi alterado para acionar os pinos de controle VGA conforme o sinal de clock. Esse procedimento é importante para que os sinais de controle sejam estabelecidos nos pinos no mesmo instante.

	PROCESS(CLOCK_50)
	BEGIN
		
		IF CLOCK_50'EVENT AND CLOCK_50='1' THEN
		
			V_SYNC <= VSYNC;
			H_SYNC <= HSYNC;
			R <= Rp;	
			G <= Gp;
			B <= Bp;
		
		END IF;
	END PROCESS;

O controlador VGA em operação é mostrado na Figura 3.

Figura 3: Teste do controlador VGA.

O código completo de Pixelgen.vhd é mostrado abaixo.

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

ENTITY PixelGen IS
	PORT(
		RESET : IN STD_LOGIC; -- Entrada para reiniciar o estado do controlador
		F_CLOCK : IN STD_LOGIC; -- Entrada de clock (50 MHz)
		F_ON : IN STD_LOGIC; --Indica a região ativa do frame
		F_ROW : IN STD_LOGIC_VECTOR(9 DOWNTO 0); -- Índice da linha que está sendo processada
		F_COLUMN : IN STD_LOGIC_VECTOR(10 DOWNTO 0); -- Índice da coluna que está sendo processada
		R_OUT : OUT STD_LOGIC; -- Componente R
		G_OUT : OUT STD_LOGIC; -- Componente G
		B_OUT : OUT STD_LOGIC -- Componente B
	);

END ENTITY PixelGen;

ARCHITECTURE arch OF PixelGen IS

	COMPONENT font_rom IS
		port(
			clk: in std_logic;
			addr: in std_logic_vector(10 downto 0);
			data: out std_logic_vector(7 downto 0)
		);
	END COMPONENT font_rom;
	
	--Coordenadas X e Y do pixel atual
   SIGNAL pix_x, pix_y: UNSIGNED(9 DOWNTO 0);
	
	--Endereço que será acessado na memória de caracteres
   SIGNAL rom_addr: STD_LOGIC_VECTOR(10 DOWNTO 0);
	
	--Código ASCII do caractere atual (parte do endereço)
   SIGNAL char_addr: STD_LOGIC_VECTOR(6 DOWNTO 0);
	
	--Parte do caractere (0~15) que está sendo exibida na linha atual Y
   SIGNAL row_addr: STD_LOGIC_VECTOR(3 DOWNTO 0);
	
	--Pixel relativo a coordenada X atual
   SIGNAL bit_addr: STD_LOGIC_VECTOR(2 DOWNTO 0);
	
	--Conteúdo armazenado no endereço indicado por 'rom_addr'
   SIGNAL font_word: STD_LOGIC_VECTOR(7 DOWNTO 0);
	
	--Valor do bit 'bit_addr' na palavra 'font_word'
   SIGNAL font_bit: STD_LOGIC;
	
	--Valor das componentes rgb
   SIGNAL font_rgb: STD_LOGIC_VECTOR(2 DOWNTO 0);
	
	--Flag que indica se a frase deve ser exibida
   SIGNAL txt_on: STD_LOGIC;

BEGIN

	-- Coordenadas XY atuais
	pix_x <= UNSIGNED(F_COLUMN(9 DOWNTO 0));
	pix_y <= UNSIGNED(F_ROW);
	
	-- Memória dos caracteres
	font_unit: font_rom PORT MAP(clk=>not F_CLOCK, addr=>rom_addr, data=>font_word);
	
	-- Determinação do endereço que será acessado
	row_addr <= STD_LOGIC_VECTOR(pix_y(3 DOWNTO 0));
	rom_addr <= char_addr & row_addr;
	
   txt_on <= '1' WHEN (pix_x >= 320 AND pix_x <= 455) AND (pix_y >= 292 AND pix_y <= 305) ELSE
              '0';
				  
   WITH pix_x(7 DOWNTO 3) SELECT
     char_addr <=
				"1110000" WHEN "01000", -- p
				"1101111" WHEN "01001", -- o
				"1110010" WHEN "01010", -- r
				"1110100" WHEN "01011", -- t
				"1100001" WHEN "01100", -- a
				"1101100" WHEN "01101", -- l
				"0000000" WHEN "01110", --
				"1100101" WHEN "01111", -- e
				"1101101" WHEN "10000", -- m
				"1100010" WHEN "10001", -- b
				"1100001" WHEN "10010", -- a
				"1110010" WHEN "10011", -- r
				"1100011" WHEN "10100", -- c
				"1100001" WHEN "10101", -- a
				"1100100" WHEN "10110", -- d
				"1101111" WHEN "10111", -- o
				"1110011" WHEN "11000", -- s
				"0000000" WHEN OTHERS;
   
	
	bit_addr <= NOT STD_LOGIC_VECTOR(pix_x(2 DOWNTO 0));
	font_bit <= font_word(to_integer(UNSIGNED( bit_addr))); 
	
	font_rgb <="111" WHEN font_bit='1' ELSE "000";

	PROCESS(F_ON,font_rgb,txt_on)
	BEGIN
		
		IF F_ON ='0' or txt_on='0' THEN
			 R_OUT <= '0';
			 G_OUT <= '0';
			 B_OUT <= '0';
		ELSE
			 R_OUT <= font_rgb(0);
			 G_OUT <= font_rgb(1);
			 B_OUT <= font_rgb(2);		 
		END IF;
	END PROCESS; 
	
END ARCHITECTURE arch;

Já o módulo principal:

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_ARITH.ALL;
USE IEEE.STD_LOGIC_UNSIGNED.ALL;

ENTITY DE0_NANO IS
    PORT ( 
		CLOCK_50 : IN STD_LOGIC;
		Reset		: IN STD_LOGIC;
		H_SYNC	: OUT STD_LOGIC;		
		V_SYNC	: OUT STD_LOGIC;
		R			: OUT STD_LOGIC;
		G			: OUT STD_LOGIC;
		B			: OUT STD_LOGIC);
END DE0_NANO;

ARCHITECTURE arch OF DE0_NANO IS

COMPONENT VGASync IS
	PORT(
		RESET : IN STD_LOGIC;
		F_CLOCK : IN STD_LOGIC;
		F_HSYNC : OUT STD_LOGIC;
		F_VSYNC : OUT STD_LOGIC;
		F_ROW : OUT STD_LOGIC_VECTOR(9 DOWNTO 0);
		F_COLUMN : OUT STD_LOGIC_VECTOR(10 DOWNTO 0);
		F_DISP_ENABLE : OUT STD_LOGIC
	);
END COMPONENT VGASync;

COMPONENT PixelGen IS
	PORT(
		RESET : IN STD_LOGIC;
		F_CLOCK : IN STD_LOGIC;
		F_ON : IN STD_LOGIC;
		F_ROW : IN STD_LOGIC_VECTOR(9 DOWNTO 0);
		F_COLUMN : IN STD_LOGIC_VECTOR(10 DOWNTO 0);
		R_OUT : OUT STD_LOGIC;
		G_OUT : OUT STD_LOGIC;
		B_OUT : OUT STD_LOGIC
	);
END COMPONENT PixelGen;

--Índice da linha/coluna atual
SIGNAL CURRENT_ROW : STD_LOGIC_VECTOR(9 DOWNTO 0);
SIGNAL CURRENT_COLUMN : STD_LOGIC_VECTOR(10 DOWNTO 0);
SIGNAL DISP_ENABLE : STD_LOGIC;

SIGNAL HSYNC: STD_LOGIC;
SIGNAL VSYNC: STD_LOGIC;
SIGNAL Rp: STD_LOGIC;
SIGNAL Gp: STD_LOGIC;
SIGNAL Bp: STD_LOGIC;

BEGIN

	--Módulo de sincronismo
	VGA : VGASync PORT MAP(
			RESET => RESET,
			F_CLOCK => CLOCK_50, 
			F_HSYNC => HSYNC, 
			F_VSYNC => VSYNC, 
			F_ROW => CURRENT_ROW,
			F_COLUMN => CURRENT_COLUMN,
			F_DISP_ENABLE => DISP_ENABLE);

	--Módulo para gerar os pixels
	PIXELS : PixelGen PORT MAP(
			RESET => RESET,
			F_CLOCK => CLOCK_50, 
			F_ON => DISP_ENABLE,
			F_ROW => CURRENT_ROW,
			F_COLUMN => CURRENT_COLUMN,
			R_OUT => Rp,
			G_OUT => Gp,
			B_OUT => Bp
	);

		
	PROCESS(CLOCK_50)
	BEGIN
	        IF CLOCK_50'EVENT AND CLOCK_50='1' THEN
		
			V_SYNC <= VSYNC;
			H_SYNC <= HSYNC;
			R <= Rp;	
			G <= Gp;
			B <= Bp;
		
		END IF;
	END PROCESS;
	
END arch;

Após as alterações, o novo controlador VGA é mostrado na Figura 4.

Figura 4: Controlador VGA.

Para saber mais

Para quem quiser se aprofundar, outras metodologias são propostas por Chu. Por exemplo, os caracteres que devem ser exibidos podem ser armazenados em uma segunda memória. Dessa maneira, os endereços que são acessados na memória de fonte dependem dos caracteres armazenados na outra memória além das coordenadas XY. Em outros exemplos você pode até jogar Pong! Vale a pena conferir o material.

Referências