Introduction: Obstacle Avoidance Game
For this assignment we were given an Altera DE2-115 board with a Cyclone IV E FPGA and told to create a project that utilizes the hardware of the board and the Verilog HDL language that we have learned in class. We thought it would be interesting to have a project that incorporates a screen and inputs from a user. With this criteria in mind, we decided on creating a game in which blocks fall down and you must move an object at the bottom of the screen to avoid them.
In the main module, we instantiated other modules to do the various tasks needed. There are modules that takes button inputs and creates a position array, that creates obstacles using a random number generator, that combines the position array and obstacle array while adding new obstacles, that checks if there is a collision between the obstacles and the position, that coverts the combined obstacle and position into an graphics array and a module that takes the graphics array and puts it onto the screen through the VGA output.
In this Group are:
- Cole Bouchard
- Bryce Dunn
- Trevor Smith (smitht35@myumanitoba.ca)
Attachments
Step 1: Preperation and Planning
The team met to plan tasks which would be required in the final project. There are nine major sections.
- Start Process / Begin Game
Take In User Input
Update Car Position
Generate a Pseudo-Random Array for Obstacle Array
Test Random Array for Criteria
Check for Collisions
Update
Game StateGenerate Display
Step 2: Buttons and Position Array
For the push button and position array module,there are two user inputs, one for moving the position to the left and one for moving the position to the right. This was accomplished by using the push buttons on the DE2-115. To compensate for the inaccuracies caused by the mechanics of the buttons, we needed to debounce them. When a button is pushed, it will oscillate until it levels out causing many positive edges. This is corrected by debouncing which will only create one positive edge per push. The debouncing code was adapted from code on the website FPGA 4 Fun. We used a finite state machine to assign where the position is in a 3 bit array. The 3 bit array is the output of the module and corresponds to the position of our movable object.
Debouncer Source: http://www.fpga4fun.com/Debouncer2.html
Code:
wire L_idle = (L_state==L_sync_1);
wire L_cnt_max = &L_cnt; // true when all bits of PB_cnt are 1's
reg [15:0] R_cnt;
wire R_idle = (R_state==R_sync_1);
wire R_cnt_max = &R_cnt;
always @(posedge clk)
if(L_idle)
L_cnt <= 0; // nothing's going on
else
begin
L_cnt <= L_cnt + 16'd1; // something's going on, increment the counter
if(L_cnt_max) L_state <= ~L_state; // if the counter is maxed out, PB changed!
end
always @(posedge clk)
if(R_idle)
R_cnt <= 0; // nothing's going on
else
begin
R_cnt <=R_cnt + 16'd1; // something's going on, increment the counter
if(R_cnt_max) R_state <= ~R_state; // if the counter is maxed out, PB changed!
end
assign L_down = ~L_idle & L_cnt_max & ~L_state;
assign R_down = ~R_idle & R_cnt_max & ~R_state;
reg [0:3] Y1;
parameter [3:0] A=3'b001, B=3'b010, C=3'b100, D=3'b000;
always @ (posedge clk)
begin
case(Y1)
A:
if(L_down)
begin
out=B;
Y1=B;
end
B: begin
if (R_down)
begin
out=A;
Y1=A;
end
if(L_down)
begin
out=C;
Y1=C;
end
end
C:
if (R_down)
begin
out=B;
Y1=B;
end
D: begin
Y1=B;
out=B;
end
endcase
end
endmodule
Step 3: VGA
For the visual aspect of the game, we thought about using the LEDs but decided on using the VGA output to display the game on a screen. To accomplish this, we researched how a VGA works and how the board takes input for digital to analog converter (DAC). VGA uses three inputs for 3 different colors which are then combined to create a range of colors and it uses a vertical and horizontal syncs which tells which pixels are lit up for the RGB signal.
The DE2-115 board has ADV7123 video DAC which takes an 8 bit signal for red, green and blue, a Sync-N signal, a Blank-N signal and the vertical and horizontal syncs. The red, green and blue, a Sync-N and a Blank-N signal are used to create the analog rgb signal with a component signal on the green wire. The h-sync and v-sync are converted to their analog equivalents.
Depending on the resolution, a different dot rate(the amount pixels turned on per second) will be need. To create this. We used a phase lock loop(PLL) mega function built into Quartus. This allowed us to choose any clock frequency. With this, the h-sync and v-sync can be created with knowing how many pixels wide and tall your resolution and using a counter.
This module takes in the clock, dot rate and graphics array and outputs the necessary VGA inputs.
//CODE ADAPTED FROM "FPGA Prototyping by Verilog Examples" by Pong P. Chu//
module vgaout(sw,clk,R,G,B,vga_clk,sync_n,blank_n,vga_HS,vga_VS);
input [2:0]sw;
input clk;
output [7:0] R,G,B;
output vga_clk,sync_n,blank_n,vga_HS,vga_VS;
reg [7:0] R_temp,G_temp,B_temp;
wire write, test;
wire xpixel,ypixel;
clksrc clksrc1(clk, vga_clk); //PLL that is used to create 40MHz clock needed for resolution
vgaSync vgaSync1(.clk(clk), .pixel_tick(vga_clk),.hsync(vga_HS),.vsync(vga_VS),.xpixel(xpixel),.ypixel(ypixel), .video_on(write));
assign sync_n = 1; //vga_HS^vga_VS; from DAC manual, the sync and blank are used to help with the sync of RGB. They can just 1 causing RGB to always be on.
assign blank_n = 1; //write;
//would be input from graphics array where to put objects
assign test = 1;//((xpixel>0) && (xpixel<1055));
always @ (posedge clk)
begin
R_temp = {8{(sw[0] & write & test)}};
G_temp = {8{(sw[1] & write & test)}};
B_temp = {8{(sw[2] & write & test)}};
end
assign R = R_temp;
assign G = G_temp;
assign B = B_temp;
endmodule
module vgaSync(clk,pixel_tick,hsync,vsync,xpixel,ypixel,video_on);
input clk, pixel_tick;
output wire hsync,vsync,video_on;
output [10:0]xpixel,ypixel;
reg [10:0] hcount,vcount;
reg [10:0] hcount_temp,vcount_temp;
reg vsync_reg,hsync_reg;
wire hsync_temp,vsync_temp;
//VGA 1920 by 1080 sync parameter
localparam HD = 800; //horizontal display
localparam RB = 40; //right border/front porch
localparam HR = 128; //h. retrace
localparam LB = 88; //left border/back porch
localparam VD = 600; //v. display
localparam BB = 1; //bottom border/front porck
localparam VR = 4; //v. retrace
localparam TB = 23; //top border/back porch
wire hend,vend;
//register
always @ (posedge clk)
begin
vcount = vcount_temp;
hcount = hcount_temp;
vsync_reg = vsync_temp;
hsync_reg = hsync_temp;
end
//end of screen
assign hend = (hcount == (HD+RB+HR+LB-1));
assign vend = (vcount == (VD+BB+VR+TB-1));
always @*
if (pixel_tick)
if (hend)
hcount_temp = 0;
else
hcount_temp = hcount + 1;
else
hcount_temp = hcount;
always @*
if (pixel_tick & hend)
if (vend)
vcount_temp = 0;
else
vcount_temp = vcount + 1;
else
vcount_temp = vcount;
assign video_on = ((hcount=(HD+RB)) && (hcount<=(HD+RB+HR+LB-1)))); //horizontal and vertical sync must be inverted for 800x600 resolution
assign vsync_temp = ~(((vcount>=(VD+BB)) && (vcount<=(VD+BB+VR+TB-1))));
//output
assign xpixel = hcount;
assign ypixel = vcount;
assign hsync = hsync_temp;
assign vsync = vsync_temp;
Step 4: VGA Graphics Array
Once a module is created that can display colors on the screen, a module must be made to convert the total position and obstacle array into another array which can be read by the VGA module to display the position and obstacle array. This is done by using the the x and y count/horizontal and vertical pixels. By setting inequalities for between 2 horizontal and 2 vertical values to designate a rectangular area to light up to the specific colors. This module basically takes the position and obstacle array and expands it for the entire game play area and designates a color of each section.
Step 5: Psuedo Random Obstacle Generator
A module is needed that creates a random three bit number but not the 3'b111 case. A clock can be used to create random numbers. We chose to use the PLL 40 MHz clock because it will be out of sync with the 50 MHz clock of the board causing the counter to be out of sync with the game. Using that, we a have a counter that goes from 0 to 6 with a start counter switch allowing for a different order of the obstacles. The obstacles might be in the same order but by starting the counter at different times before the player begins the game, it will seem random.
module random(start,clk,outOb);
input start,clk; output reg [2:0]outOb;
always @ (posedge clk)
if (start)
outOb <= 3'b000;
else i
f (outOb < 6) outOb <= outOb + 1;
else outOb = 3'b000;
endmodule
Step 6: Obstacle Update Array
What makes this project a game is that you have to avoid the obstacles that can come down in the 3 possible positions (left, middle and right positions). This means we have to keep track of our obstacles and send new ones after a certain period of time passes.
This means we will need the following:
1) A reg array to store the obstacles.
2) A counter to keep track of whether or not enough time has elapsed to know if we have to send a new obstacle.
3) A way to generate new obstacles. (we will use our random generator)
4) A way to move those "obstacles" around. We'll have to be able to get rid of the bottom row that "passes" our position vector, and also insert our new obstacle into the top of the obstacle array.
1) To keep track of the obstacles, we use a reg called "TotalArray". TotalArray uses the indexes from 0 to the index 3 from the end for the obstacles, and those last 3 for what we refer to as our "Position". For example: In our project we use a TotalArray with indexes from [0:20], so everything from 0:17 will be our obstacles, and everything from 18:20 will be our current position.
It is important to know 2 things specific to our implementation.
a) You must know that obstacles are interpreted in groups of three. So when we are writing to the screen or checking to see if an obstacle has collided with our position, we have to think of the obstacles 3 indexes at a time.
So the first 3 obstacles are TotalArray[0:2], the next three would be TotalArray[3:5] and so on.
b) Obstacles never make it to the last row where the Position indexes are. This means that the Position indexes 18:20 will only ever have one bit be a 1.
2) This counter counts up on the posedge of the board's built in clock which is taken from PIN Y2. A parameter must also be instantiated to state what the value of counter must be in order to know that the time has elapsed signaling for a new obstacle to be generated and then put into the Obstacles portion of the array. The larger the value of this parameter, the longer the time between new obstacles being generated, and the easier the game will play.
3) Our random number generator must "return" a 3 bit binary value. We effect this by giving the random generator module a reg to access and change whenever it generates a new value. We can take this reg and use it for our updateObstacleArray module. This 3 bit value will correspond to the new obstacle that will show up in the 0:2 indexes. One thing to consider here is that we cannot have the new obstacle be the case: 3'b111 because that would mean the user would have no way of avoiding the obstacle and that's no fun! (We're nice people!) To take care of this in the random generate module.
There is another case that we cannot have happen and that is the case where a 3'b110 obstacle is followed by a 3'b011 obstacle because this also makes an auto loss scenario. Instead of checking for this case, we instead create a line of 3'b000 between every obstacle so that the user can always find a way through the obstacles.
4) When the time elapses the pre-defined period constant parameter (from 2), we need to move our obstacles around. Assuming their is no collision between an obstacle and a postion value, then we will update the array to reflect this passing of time with a new obstacle. (We will come back to this collision check.)
We need to make a new obstacle appear in the top row of the TotalArray, get rid of the bottom row of the obstacles, and push everything from the top part of the obstacles down one row.
This is accomplished by using a tempArray.
...
...
// Assuming Time Elapses so that an obstacle needs to be generated.
reg TotalAr[0:20]
reg tempArray[0:17]
input randSig[0:2];
if (obsAr[0:2] == 3'b000)
begin
//if first/top of array is empty gen new obs and push original obsAr "down"
// implies each obstacle is seperated by an empty horizontal line between obstacles
if (randSig != 3'b111)
begin
tempArray[3:17] <= TotalAr[0:14]; // storing the top 15 elements
tempArray[0:2] = randSig; // Creating the new obstacle from the randomsignal
TotalAr[0:17] = tempArray[0:17]; // putting the new obstacle array back into the TotalArray
end
else
begin
tempArray[3:17] <= TotalAr[0:14];
tempArray[0:2] = 3'b000; // Make the new obstacle a blank row if there was an obstacle in the top row to begin.
TotalAr[0:17] = tempArray[0:17];
end
end
...
...
This code is what would be used to put the new obstacle at the top as well as move the old obstacles down one row.
Step 7: Collision Check/Game Update
Checking for collisions is one of the more simple parts of the project. On every clock edge of the board's internal clock, a comparison is made between the second last "row" of the obstacle portion of the TotalArray and the "row" of the TotalArray containing the "Position". The two rows are ANDed together using something like:
if (TotalArray[0:17] && TotalArray[18:20]) // == true; then...
// update gamestate register, where a gamestate = 1 means the game is over and the player lost.
assign gamestate = 1;
We can then use gamestate inside of our visuals generator and generate a "Game Over" screen when gamestate = 1.
Step 8: Game Generation
There are some ( detail oriented / nit-picky ) fundamentals that go into creating the visuals for this game via VGA.
We reccommend http://www.fpga4fun.com/PongGame.html for some of the neccessary background that goes into understanding how send signals to the VGA pins of the DE-2 board.
Essentially, you send values to the R G B pins of the VGA pins on the board. The key to making anything relevant show up on the screen, you need to be sending a signal to the RGB pins when the screen is refreshing the pixel you want. To get this, we have to generate a sort of logic that goes through all the pixels on the screen and evaluate to true when either an obstacle or our position are true in the TotalArray.
To get this, we nest a few if statements together to iterate through the differents parts of the TotalArray.
(This part of the code is inside the attached code....)
Step 9: What We Would Do Differently
This project had many hurdles and challenges to overcome. Lots of time was spent in creating a module that would create VGA outputs. This was a great idea but it ended up taking a lot of time to create and in the end the team decided on using a perviously created code. 50+ hours of combined team members time was used for research that would assist in using verilog and understanding how the mechanisms of our program would work.
We hope that this instructable has given you an idea of how you would go about implementing a game similar to the one we created.