Hao Luan , Bo Liu and Albert Chan
For our EE552 project, we design a video game. For the Application Notes, we describe in detail the different steps required to realise a video display.
We ask that any parts of our application notes used in any project be clearly put as reference in the reference section or wherever is appropriate.
The first requirement in manipulating a monitor is the synchronization of the pixels on the monitor with the system clock(25.175Mhz). The following explanation goes side by side with the file "syncgen.vhd". Since this is not our file, we do not have the right to use it here. We ask the readers to refer to Pong.
,
To display anything on a monitor, two synchronization signals are required. One is used to synchronize the horizontal direction of the monitor, the other the vertical direction. The synchronization signals are used to synchronize the pixels on the monitor with the system clock that is being used for the project. Since all the pixels on a monitor cannot be available to be displayed simultaneously at any time, a clock is therefore used to update the availability of the pixels in such a way that only one pixel can be displayed at anytime. The order in which the pixels are available is from the first line of the pixels at the top left corner of the monitor to the right of the same line in the horizontal direction. The logic high of the signal HSync is used for the horizontal synchronization. After the availability of the first line of pixels is updated, the second line is updated from left to right, and then the third line and so on. When the last line of pixels is updated, the end of the monitor is reached. At this moment the signal VSync produces a logic high to reset the synchronization so that the synchronization starts again at the top left corner of the monitor. The process repeats itself indefinitely.
We have now synchronized the pixels of a monitor with the system clock. However we still have no access to the pixels. What we need now is to count the pixels as the pixels are synchronized. The following segment of VHDL codes count the pixels as the pixels are synchronized.
These are two files required to generate the coordinates of a VGA monitor : count_xy.vhd and syncgen.vhd
Clearly we see that the vector "countX" counts the pixels in the horizontal direction, while "countY" counts the pixels in the vertical direction. Now we see that every pixel has a unique set of values described by countX and countY. We can use these values to pick out the pixel that we want. Effectively we have set up a coordinate system with the origin at the top left corner of the monitor.
These files are required to run the demonstration: line.vhd, count_xy.vhd and syncgen.vhd
Using the values of countX and countY we can draw various shapes of various colors at the specified location on the monitor. The following segment of VHDL codes describes how to draw a vertical red line at the specified location "line_position". The signals vga_red, vga_green and vga_blue are the signals to be connected to the pixels on the monitor. They respectively represent the red, green and blue signals for the pixel.
,
First the meanings of the parameters are as follows.
Depth : the total number of characters in this file, which is five hundred and twelve (512) in this case.
Width : the number of bits in a row of the eight rows for a character
Address_radix : the numeral system used for the address in this file for the characters, which is octal. For example, "010" is the address for the first row with bits "00001000".
Data_radix : the numeral system used for the bits for mapping out the character.
Comments are written between percentage signs (%).
The file begins with the key words "content" and "begin" and ends with "end" as shown.
As can be seen the description is divided into three columns. The first column contains three digits, the second eight and the third is a comment to show the appearance of the character.
The first column uses the octal system, the reason for which will be clear later. These octal numbers are basically the addresses for different parts of the letter A. Please see the file char_set.mif in the reference and it is clear that every character has a unique address.
The second column is in binary system. These bits describe the states of the pixels on the screen; "1" for high and "0" for low. We can match the 1's in the second column with the asterisks in the third and it becomes obvious how the character is described.
The numeral systems used are declared at the beginning of the file.
The following codes display the arrangement of the letters to obtain the messages as found in dis_con2.mif.
The meanings of the parameters are similar to those described above.
As can be seen in the figure the declarations of the characters are again divided into three columns. The first column describes the address, the second describes the arrangement of the characters and the third the comment to indicate the appearance as a result of the arrangement.
The first column describes the address for the first character in binary system. To obtain the character other than the first, a number is added to the address for the first character. For example, if the character A is to be displayed, "00000100" is added to "01010000" to give address "01010100" which is the address for A in the figure above. As we step down the first row we see that the last character has address "01011111", and therefore the first character in the next row has address "01100000" as indicated.
The function used to map characters into the memory in the chip is the "lpm_rom" function. Shown below is the function.
-- Character Format ROM for Video Display
-- Displays constant format character data
format_rom: lpm_rom
GENERIC MAP ( lpm_widthad => 6,
lpm_numwords => "48",
lpm_outdata => "UNREGISTERED",
lpm_address_control => "UNREGISTERED",
-- Reads in mif file for data display format
lpm_file => "dis_con2.mif",
lpm_width => 6)
PORT MAP ( address => format_address, q => format_data);
The parameters are explained as follows.
lpm_widthad : specifies the size of the address, which in this case is six (6). Notice that the size matches the number of bits for the address in dis_con2.mif shown in the figure above.
lpm_numwords : specifies the number of characters to be stored in the memory.
lpm_outdata : indicates whether or not the q port is registered. We do not need it.
lpm_address_control : indicates whether or not the address port is registered. We do not need it.
lpm_file : the file name.
lpm_width : the size of the output port; i.e. the number of bits required to represent the octal number in binary format.
Address : the address of the row of characters that we want.
q : the output data, which are the octal numbers converted to binary format representing the characters in a row
Now the strategy used to obtain the character as described in the second column is tricky. First the numbers in the second column are in octal system. If we examine one number and match it with the character in the third column and then if we go back to "char_set.mif", we see that the number is actually the first two digits of the addresses for the corresponding character. We will see later how the third digit is obtained. Notice the reason for the use of octal system for the addresses in the second column above; so it is compatible with the system used in "char_set.mif".
The following figure should be helpful in understanding the relations between the screen, the file dice_con.mif and char_set.mif. What the figure is showing is that using the positions on the screen, we pick out the set of characters in a row defined in dice_con.mif, each character from the set is then obtained from char_set.mif. The following figure and the explanations below the figure will clearly illustrate the methods of message display.
,
To understand how to display messages, we first have to divide the screen into columns and rows, each of which consists of a certain number of pixels. The number of pixels in each column and row will determine the size of the characters. Recall that the characters in char_set.mif are defined by eight by eight pixels. Therefore in order that the characters are not distorted when they are displayed on the screen, it is obvious that we want to define the size of the columns and rows as a multiple of eight; that is eight by eight, sixteen by sixteen, etc. To define the size of the columns and rows we use the signals "countX" and "countY" given by "count_xy.vhd". Notice that both "countX" and "countY" are vectors of size 10 (9 downto 0). The trick in obtaining columns and rows of size sixteen, to display the message "PLAYER 2" for example, is to ignore the four least significant bits of "countX" and "countY". That is we declare two local signals each of size 6 and store the six most significant bits of "countX" and "countY" into the local signals. For example in our programs we call the signals representing the columns and rows of size 16 as col_address and row_address. In the following assignment we effectively obtain columns and rows of size 16.
col_address <= countX(9 downto 4);
row_address <= countY(9 downto 4);
So now as "countX" and "countY" increase, col_address and row_address also increase, but only once every 16 counts of "countX" and "countY".
Now that we have divided the screen into columns and rows, we will attempt to pick out the columns and rows in which we want to display the characters. We need to match the columns and rows that we have picked with the addresses of the characters described in dice_con.mif or dis_con2.mif.
Before we are able to match the columns and rows with the addresses in dis_con2.mif, we need to divide each of col_address and row_address into two halves. The half corresponding to the least significant bits is called "con_col" and will match with the columns. The other half corresponding to the most significant bits is called "con_row" and will match with the rows. So to pick out the address for "PLAYER 2" to display it between column 3 and column 10 and in row 10, we write,
if col_address >= "000011" and col_address <= "001010" and row_address = "001100" then
con_col <= col_address - "000011";
con_row <= row_address - "001011";
rom_address(8 downto 3) <= format_data;
end if;
The statement "rom_address(8 downto 3) <= format_data;" will be explained later.
When col_address = "000011", con_col = "000000" and con_row = "000001". So if we declare a signal "format_address" of size 6 and if we do the following assignment,
-- Address for Constant Character Data ROM
format_address(2 Downto 0) <= con_col(2 Downto 0);
format_address(5 Downto 3) <= con_row(2 Downto 0);
we see that this effectively gives us
format_address = "001000"
when col_address = "000011", which picks out the first two digits of the address for the letter P in dis_con.mif. As col_address increases the successive characters are chosen.
Now if we examine the lpm_rom function for dis_con2.mif we see that format_address is assigned to address so that the chip knows which character we want to obtain. The output data are then given by q and are assigned to format_data, which is another local signal of size six (6) in this case. format_data contains the bits obtained by converting the octal numbers in dis_con2.mif.
Half a page before this line, we mentioned the following assignment,
rom_address(8 downto 3) <= format_data;
What this does is the content of format_data is assigned to the local signal
rom_address of size 9 (8 downto 0). Since format_address = "001000", we have effectively chosen the letter P from the second row of messages in dis_con2.mif. Therefore format_data = 010000(base 2) = 20(base 8) = rom_address(8 downto 3).
Now in order to know what happens to the three least significant bits of rom_address, we need to mention another two local signals. One is pixel_col_count and the other pixel_row_count, both of which are std_logic_vector(3 downto 0). Now we do the following assignments.
pixel_col_count <= countX(3 downto 0);
pixel_row_count <= countY(3 downto 0);
Now since the columns and rows are of size sixteen (16), every time we start at new block, defined by col_address and row_address, of dimensions sixteen by sixteen (16 X 16), the four least significant bits of countX and countY are zeros. So at the beginning we assign zeros into pixel_col_count and pixel_row_count. Now we do the following assignment.
rom_address(2 Downto 0) <= pixel_row_count(3 Downto 1);
Notice that we are ignoring the least significant bit of pixel_row_count. What this effectively does is that the display in the vertical direction is doubled. Now we see that now we have,
rom_address(8 Downto 0) = 010000000(base 2) = 200(base 8).
Now we assign rom_address into the lpm_rom function for the file char_set.mif as. This tells the chip which address to look for the character we want.
Now we examine pixel_col_count. Recall first of all that countY increases by one only when countX finishes counting the pixels in a row of the screen. So when countY(3 downto 0) = 0000, countX((3 downto 0) keep increasing. Now if we do the following assignment,
rom_mux_output <= rom_data ( (CONV_INTEGER(NOT pixel_col_count (3 downto 1))));
we effectively choose all the bits in the row with address 200(base 8) in the file char_set.mif. Since countX counts the pixels in a row, or in the horizontal direction, and since we ignore the least significant bit of pixel_col_count, we have effectively double the display of the character in the horizontal direction also. Since the character is defined to be eight pixels by eight pixels in char_set.mif, it is now sixteen by sixteen.
When the signal rom_mux_output is connected to a pixel of a certain color, it is then lit if the signal is high.
When countX finishes counting two rows countY increases by two and is now countY(3 downto 0) = 0010 so that rom_address is now
rom_address(8 Downto 0) = 010000001(base 2) = 201(base 8).
Therefore the second row of pixels of the character P in char_set.mif is chosen. The process continues until the character is written onto the block on the screen.
The other characters are similarly written onto the screen.
To write larger characters the columns and the rows have to be defined to the appropriate size and the bits in pixel_row_count and pixel_col_count are to be appropriately chosen. For example, to write a character of size thirty two pixels by thirty two pixels, as we do for the title of the game, we define the columns and the rows to be thirty two by thirty two and ignore the two least significant bits of pixel_row_count and pixel_col_count. Here is an example of the message display without extraneous "vertical bars" beside the characters demo_ms.vhd.
To display messages that circulate so that as one disappears a different one appears, we need to decide the length of the display of a message. Suppose we want to display a message for approximately four seconds before it disappears and a different one appears, we need a new clock. From the program syncgen.vhd we know that the signal VSync has a period of 16.6 milliseconds. We assign this signal to one on the port that we call vclock so we can use in the program message.vhd. Using vclock, we make a new clock, called fresh_clock, under the condition of a counter so that every time the counter counts sixty four cycles of vclock, fresh_clock stays high for a period of vclock. Before the counter reaches two hundred and fifty-five , fresh_clock is assigned a zero. As a result the period of fresh_clock is around 4 seconds.
Using the fresh clock, we circulate our messages once every 4 seconds. In the meantime, the relative_clock will shift the displayed message to the left every half a second. The methods for displaying alternating messages are explained above. please see message.vhd for details.
We give an example here to demostrate how to use and interface the display components we discuss above. The main file is dicedemo.vhd. You need to download syncgen.vhd, count_xy.vhd message.vhd, demo_ms.vhd char_set.mif, dice_con.mif and dis_con2.mif to finish compilation. A compiled ".sof" file is also available here dicedemo.sof,
You can find more information you need from following sites:
The files on which this project is based are called PONG and MIPS. which are obtained from Georgia Institute of Technology.
,