VGA Character Generator - red-bote/VHDL_Demos GitHub Wiki

Renders text on a VGA display using a character generator ROM implemented in VHDL.

Generate Character ROM

Build the binary image of the zx81 character set (64 8x8 characters):

~/chargen-maker$ ./build.sh zx81.txt
~/chargen-maker$ ls -l  zx81.bin 
-rw-rw-r-- 1 xubuntu xubuntu 512 Jan 28 20:52 zx81.bin

Create new VHDL file in the project. Run hex2rom to convert character set binary image to VHDL ROM and write output to new VHDL file. The format string has the format AEDOS where:

  • A = 9 Address bits
  • E = L Endianness LSB
  • D = 8 Data bits
  • O = s for synchronous ROM

The ROM is formatted as a lookup table in VHDL in the form of a case block with 512 entries:

$ hex2rom -b ~/chargen-maker/zx81.bin charg_rom 9l8s > vga_tilemap.srcs/sources_1/imports/vga/charg_rom.vhd

$ tail -n12  vga_tilemap.srcs/sources_1/imports/vga/charg_rom.vhd
		when 000504 => D <= "00000000";	-- 0x01F8
		when 000505 => D <= "01111110";	-- 0x01F9
		when 000506 => D <= "00000100";	-- 0x01FA
		when 000507 => D <= "00001000";	-- 0x01FB
		when 000508 => D <= "00010000";	-- 0x01FC
		when 000509 => D <= "00100000";	-- 0x01FD
		when 000510 => D <= "01111110";	-- 0x01FE
		when 000511 => D <= "00000000";	-- 0x01FF
		when others => D <= "--------";
		end case;
	end process;
end;

The resulting character ROM is organized as an array 512 deep by 8-bit words. Each word is 1 row of a 8x8 pixel character with 64 tiles in all.

Character Generator Test Screen

The character ROM is addressed by the VGA scan row and column. Bits (2 downto 0) are decoded from the VGA scan column to select the pixel from the ROM data word. Decoding the ROM address from the scan row and column is simplified with 8x8 character tiles.

entity char_gen is
    Port ( clk : in STD_LOGIC;
           row_vector : in STD_LOGIC_VECTOR (9 downto 0);
           col_vector : in STD_LOGIC_VECTOR (9 downto 0);
           rgb : out STD_LOGIC_VECTOR (23 downto 0));
end char_gen;

architecture Behavioral of char_gen is
    signal charg_rom_addr : std_logic_vector(8 downto 0);
    signal charg_rom_data : std_logic_vector(7 downto 0);
    signal pixel_bit : std_logic;
begin
    -- character ROM address generator
    p_charg_rom_addrgen : process(row_vector, col_vector)
    begin
        --All 64 characters fit on 1 row of 640x480 display
        charg_rom_addr(8 downto 3) <= col_vector(8 downto 3);
        charg_rom_addr(2 downto 0) <= row_vector(2 downto 0);
    end process p_charg_rom_addrgen;

    u_charg_rom : entity work.charg_rom
	port map (
        Clk => clk,
        A => charg_rom_addr, -- in std_logic_vector(8 downto 0);
        D => charg_rom_data -- out std_logic_vector(7 downto 0)
	);

    -- "shift" the pixel of the current scan column out of the character row data
    u_char_pix_mux: entity work.multiplexers_1
    port map(
        di => charg_rom_data,
        sel => col_vector(2 downto 0),
        do => pixel_bit
    );
    rgb <= (others => '1') when pixel_bit = '1' else (others => '0') ;

end Behavioral;

Compensation for Clock Delay of Character ROM

Reading data from the ROM takes a clock cycle. The ROM is addressed by scan column but the data will be delayed 1 pixel-clock with respect to the intended destination scan column. This manifests as appearing to lose the leftmost pixel column of a character, or to have the rightmost pixel of the previous character showing up in the left-most character column.

The corrected character generator block derives the pixel-shift from bits (2..0) of the column but is registered to compensate for the clock cycle taken by reading the character data from the ROM.

    -- register the pixel shift to sync with 1-clock latency of ROM access
    p_pix_sync : process(clk)
    begin
        if rising_edge(clk) then
            pixel_shift <= col_vector(2 downto 0);
        end if;
    end process p_pix_sync;

    -- "shift" the pixel of the current scan column out of the character row data
    u_char_pix_mux: entity work.multiplexers_1
    port map(
        di => charg_rom_data,
        sel => pixel_shift, -- col_vector(2 downto 0),
        do => pixel_bit
    );

With the clock cycle delay of reading from ROM, the VGA control signals are also registered to delay 1-clock. See prior example of synchronizing VGA signals with image data from ROM.

    -- register the control signals to sync them with the RAM data
    p_video_sync : process(clk_vga)
    begin
        if rising_edge(clk_vga) then
            r_video_on <= video_on;
            r_hsync <= hsync;
            r_vsync <= vsync;
        end if;
    end process p_video_sync;

Complete project