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;