December 6, 2000 (updated December 11, 2000)
In order to program the VGA display for the EM Probe project I encountered quite a bit of difficulty in understanding how to implement characters on a VGA monitor. Other projects have used this idea (the Dice Race and Logic Analyzer teams) but their implementation, though it provided me with valuable ideas and starting points, seemed cryptic to me. In an attempt to clear things up a bit, I have created this guide. It does require some prior knowledge of VGA setup and none of the code is strictly workable without a number of extra processes, but if a full-blown, functional example is needed I encourage you to look at the EM Probe files, specifically the file probedisplay.vhd.
This application note assumes that the reader knows how to set up a VGA display, with HSync and VSync signals governed by horizontal and vertical counters (here called XCount and YCount).
The principle behind displaying 8 x 8 pixel characters on screen is that a 640 x 480 pixel monitor can be broken up into a grid of 80 x 60 characters. To do this, create new horizontal and vertical signals (call them col_address and row_address) which comprise all but the least significant three bits of XCount and YCount respectively.
Now that the screen is divided up into character blocks, it's possible to begin defining what will fill those blocks. A
straightforward way to define display locations is to define the rows using a case statement, and then lay out any messages on a
column by column basis. As an example, if we wanted to display a message composed of seven characters on row 13, beginning at
column 32, we would write:
We will need two ROMs, one to define words and character combinations and one to define the characters. For an example
of a word ROM, see the file screenwords.mif and for an example of a character ROM, see the
file chars.mif.
The interfaces to the two ROMS are defined like this:
reg_character: lpm_rom-- loading the character set into rom
GENERIC MAP ( lpm_widthad => 9,
lpm_numwords => 384,
lpm_outdata => "UNREGISTERED",
lpm_address_control => "UNREGISTERED",
-- Reads in mif file for character generator data
lpm_file => "chars.mif",
lpm_width => 8)
PORT MAP ( address => rom_address, q => rom_data);
-- word ROM for determining character layout on screen
format_rom: lpm_rom
GENERIC MAP ( lpm_widthad => 8,
lpm_numwords => 185,
lpm_outdata => "UNREGISTERED",
lpm_address_control => "UNREGISTERED",
-- Reads in mif file for character display format
lpm_file => "screenwords.mif",
lpm_width => 6)
PORT MAP ( address => format_address, q => format_data);
The way to accomplish this is to calculate the format address based on the column address in which the character is to appear. For example, to display the "HI-4422 PROBE" message beginning at column 15, we could write
Each row of the character begins with 07, which is what is used by the word ROM. For the least digit, we'll need to use the least three bits of the YCount signal which we had set aside earlier. Having defined the character row in which the character is to appear, we now need to define the individual pixels. The individual rows in the character ROM are accessed the same way as they were in the word ROM. The format_data signal defines six of the bits in the character ROM addresses, as we've seen, while the least three bits of the YCount signal define the other three bits. So:
Having defined rom_address completely, the character ROM returns the requested row of "pixels" in the rom_data signal. These "pixel" values will determine what to display, but they need to be reversed first. The problem is that the left-most bit in the rom_data signal is its most significant bit but the XCount signal counts up from the left. Thus, when the least three bits of XCount are, for example, "000", we want to display the most significant bit, bit "111", of rom_data. If this doesn't make sense, look at the character ROM "H" above: if the bit vector were displayed in count order from lowest bit to highest, the image will be backwards (admittedly not a problem for "H", but it will affect other characters more obviously).
We want to take the rom_data signal one bit at a time, in the right order, so that we can fix the pixel colour one pixel at a time. The following signal assignment will do this:
All of the above discussion boils down to determining the status of an individual pixel by examining rom_mux_output. Use the following logical conditions to define a white background or black characters (this requires the colour signals to be BOOLEAN):
However, if more than one colour is desired, it is necessary to define the colour according to location on screen. The general structure is the same as that for determining the value of format_address from col_address: define all necessary rows with a case statement, then individual blocks of columns within that row. For example, to select a blue colour for the "HI-4422 PROBE" message beginning at column 15 of row 13, with nothing else in the row, we could write
The complete example would look (the process header is rough pseudocode only) like:
case row_address is
rom_address(8 downto 3)<= format_data
end process1;
process2:
rom_address(2 downto 0) <= YCount(2 downto 0);
rom_mux_output <= rom_data(CONV_INTEGER(NOT XCount(2 downto 0)));
case row_address is
Graeme Fricke, EM Probe project group