Software Design
-
Basic Idea
The software design is the second part of this project. Although we did not spend
nearly as much time on this part when compared to the hardware design; however,
this is the most critical part of the design for it allows one the control of where
color is displayed on a screen.
The game was implemented in steps.
The first task was to be able to display color on the screen. So, the very first
task was to design the RGB output to the AD724. Since the Mega32 runs on a 16MHz
clock and the AD724 is synchronized to a 3.58MHz clock, an interrupt was needed.
This interrupt would trigger on the sync signal generated by the ELM304.
The idea is to be able to distinguish vertical sync pulses from the horizontal
sync pulses. Vertical sync pulses mark the beginning of the screen while horizontal
sync pulses iterate over the printable lines on the screen, until the next vertical
sync is found.
The vertical sync is perhaps the trickiest to detect. The vertical sync is marked
distinctively by the sync signal. It looks like an inverted horizontal sync. This
signal has pulses that are approximately 5.7us wide. Then the signal stays low for
about 20-25us. The trick is to detect this pattern. We employed an external interrupt,
namely EXT_INT0 that corresponds to 3rd pin of PORTD (PORTD.2). This interrupt would
trigger on the 15KHz signal from the ELM304 chip. To detect the vertical sync pattern,
one must count the number of vertical sync pulses, to be sure. Thus, we used a counter
that counts at most five vertical sync pulses. Basically, the interrupt is started as
being rising edge trigged. Once a vertical sync pulse is found the interrupt is entered
on the rising edge, at which time a timer--TCNT0--is reset. Then the interrupt is switched
to become falling edge triggered. This is done by modifying bits 0 and 1 of the MCUCR.
So, now when the vertical sync pulse ends, the interrupt can detect the falling edge.
Meanwhile, the counter was still counting. So, one can compare this counter value
to the known width of the vertical sync pulse (~6us). This behavior can be used to
detect vertical sync since the horizontal sync pulses are much wider. So, using this
technique, one must count about five vertical sync pulses that mark the beginning of
the screen.
Once the vertical sync is detected, the horizontal sync must be started in order to
iterate over the screen. It is apparent that the writable portion of the screen does
not include the first 30 lines and the last 10-15 lines (depending upon the size of
the interrupt). So once the horizontal line count becomes within range, we can start
writing to the screen. This is achieved by simply assigning PORTC to a predefined color.
Since there is also a portion to the left of the screen that cannot be visible if written
to, there must be a short delay before one can begin writing. The horizontal length of
each line depends on the amount of delay encountered since the beginning of the assignment
statement to PORTC, until another similar assignment statement with a different color.
In our experience, a 1us delay corresponds to about a 3/4 inch line segment on the screen.
Once we are done writing to one line, we can exit the interrupt and write another line when
the interrupt next enters. One must be careful not to include too many time/memory-intensive
calculations within the interrupt. This is neccessary to ensure uniform lines on the screen.
The rest of the interrupt involves TET related code. For our convenience, we decided to
define a 10 block x 18 block array that we consider the playable area. Here, the vertical
and horizontal dimensions of each TET block are 11 horizontal sync lines and 1us delays,
respectively. We have allocated an array of size 200 to represent the screen. Although
only 180 elements are written to, the other 20 elements are for debugging purposes for
the top and bottom parts of the screen. The interrupt then simply reads each line, or 10
elements of the array, at a time and prints the same data for a period of 11 lines. Then,
this line counter is reset and the next "row" of the array can be read and output onto the
screen.
The rest of the code defines the TET gameplay. There exist 7 basic shapes in "Tetris", which,
along with their rotated images, constitute about 23 different shapes. We decided to write
23 different code segments to represent each one of these shapes. Each function defines
an area where a block will be written to. In addition, it checks and updates information
regarding where it is safe to write a block. For example, the block cannot move down, left,
or right if there are other blocks present in those locations. Furthermore, since each shape
can rotate left and right, each function defines whether it is safe to rotate in either
direction, given the current position of the shape as well as the surrounding pieces. Each
function takes as its parameters the position to be drawn on the screen (the linear array
element), and the direction in which to move. Each function also reserves a color for the
specific piece. This works fine since there are 7 pieces and 8 different colors that we can
produce with the 3 red, green, and blue bits. The color black is reserved for the background.
At the heart of the TET code is a state machine that keeps track of which piece is being
drawn and how it is being drawn. As long as the current piece's function has declared that it
is safe to move in a desired position, this state machine re-calls that function with
and updated position. It is trivial to move the same type of piece within the screen since
the corresponding function has the erase and re-write positions pre-defined. However, it is
less trivial to erase and re-write when changing from one type of piece to another. For example,
if a T-piece is being rotated, the older T-piece must be erased and the new rotated piece must
be drawn. For this, there exists a 'draw' flag which lets the corresponding function know
whether it must erase or re-draw its designated piece. So while rotating pieces, the state
machine switches on the 'rotate' variable and determines which piece must be erased with the
draw flag and which piece must be drawn with a reset draw flag. The state machine also verifies
whether the game is over. Once all pieces have reached the top of the screen array, no more
pieces are allowed to fall and the game is considered to be over. In addition, the state machine
calls the routine 'clear_line' every time a piece lands. This is to check whether a line has
been completely filled with color and to erase that line while dropping the rest of the screen
down by one line.
The controller interface is managed by the function 'control' which checks which buttons are
being pressed and modifies the speed, rotatation, and movement variables accordingly.
The pieces drop down the screen at a pre-defined rate, which is much slower than the rate
at which 'control' is being called. This is to allow the user more freedom with the behavior
of the pieces within each falling line. The speed of the game increments linearly as the user
clears a certain number of lines.
Code