FPGA Snake Game
INTRODUCTION
In the previous lab, we have found that using the Cortex Microcontroller Software Interface Standard (CMSIS) and developed drivers have made controlling the peripherals a lot easier. Therefore in this lab exercise, we will be developing an Application Programming Interface (API) that has even more generic and easy-to-use functions. With the API and implemented SoC, we will be creating a Snake Game, which will be the project for the course. This will include synthesizing and implementing the Verilog codes, generating a bitstream, modifying the main C file, API, and device drivers, and programming the FPGA board. It is important that students become familiar with API and understand how it can ease application development.
​
BACKGROUND​
​
An API is a software abstract layer that can provide a standard programming interface for application developers (Figure 1). It is a set of definitions and protocols for building and integrating application software or in other words, a tool that allows two different systems to communicate. The API can be developed by combining the functions provided by the CMSIS and peripheral drivers.
Figure 1: API Implementation Diagram.
API has a number of advantages over low-level programming such as being easy to read and reuse by different users, having better code density and performance, and being portable from device to device. However, since it is generic, that results in having less control over the low-level hardware. To solve this issue, APIs offer call-back functions (Figure 2) that allow users to control or specify low-level hardware devices in their application code.
Figure 2: Call-Back Function Diagram.
In the previous lab, we created a file structure that included Application, Device, and Core folders. For this exercise, we will be adding three more files to the Device folder which are the edk_api.h, edk_api.c, and retarget.c files. The edk_api.c file is where the functions will be written and the edk_api.h file will include all the function calls. The retarget.c file is needed to define hardware-dependent functions, such as the “printf”, “int fputc(int ch, FILE *f)” and “int fgetc(FILE *f)” functions.
SNAKE GAME DESIGN​
​
In this lab, we implemented a high level software application using low-level software drivers to interact with the various peripherals implemented on the FPGA board (Figure 3). The game application is a two player snake game, we used the Timer and UART interrupts as the primary drivers for the program flow. Using interrupts is a power saving technique because the processor can be set to idle whenever there is no input from the user (Figure 4). Another less power efficient method would be to use polling to check for user inputs, this would require that the processor to be consistently checking the peripherals for inputs. The UART ISR is used to get the keyboard input from the user and the Timer ISR is used for the general program flow of the snake game.
Figure 3: Cortex-M0 Block Diagram.
Figure 4: Interrupt Driven Mode Diagram.
The snake game is displayed on a monitor using the VGA peripheral, the game instructions and scores are printed on the text region and the gameplay is displayed on the image region. We used two different colors for the snakes, for Player 1 the snake is red and for Player 2 the snake is green. The general rules for the games are as follows:
​
-
Each time a snake eats a target a point is added to the player score
-
When a snake crashes with a boundary the player loses and the opponent gets 5 points added to their score
-
When a snake collides with the body of the opponent’s snake, the snake dies and the opponent gets 5 points added to their score
-
When a snake collides with itself, it dies and the opponent gets 5 points added to their score
-
When both snakes collide head on neither player gets points added and both snakes die
-
When a snake eats a target only its speed increases but the other snake’s does not, each snake must eat targets to increase their individual speed
​
GAME RULES AND IMPLEMENTATION
The application software for this game was programmed using the C programming language. The main data structure for this game was a Snake struct (Figure 5). This struct consisted of 9 members:
​
-
x[N] is an N sized array, holds the x coordinates of ith block of the snake
-
y[N] is an N sized array, holds the y coordinates of ith block of the snake
-
node is an integer variable, holds the length of the snake’s body
-
direction is an integer variable, holds the current direction of the snake which can range from 1-4 i.e. Up, Down, Left, Right
-
color is an integer variable, holds the value that represents the color of the snake initialized by the various macro defined colors
-
alive is an integer variable, holds whether snake is alive (1) or dead (0)
-
count is an integer variable, holds the count which is incremented until the snake reaches the refresh rate when the snake can updated
-
level is an integer, is the index for the speed table, as this is incremented so is the speed of the snake (0 to 9)
Figure 5: Definition of Struct Snake.
For the target, another struct was used which had an x and y variable for the coordinate location of the target and a target reached variable which is set to 1 when the game needs to generate a new target. As mentioned above the UART ISR was used to read input from the keyboard.
​
UART AND TIME ISR FUNCTIONS
​
We used TeraTerm with a baud rate of 19200 to read from the keyboard. This ISR takes the keyboard inputs and updates the snake direction for each player. For player 1, there are four designated directional keys for Up, Down, Left, Right which are ‘w’, ‘s’, ‘a’, ‘d’ in that order. For player two the designated keys are ‘i’,’k’, ‘j’, and ‘l’ in that same order. The ASCII values for these lower case letters correspond to the values that are checked for in the application software. For example, 0x69 is the value checked for ‘i’ which would be Up for player 2. There are other important keys for example the space bar is used to pause the game and the ‘q’ is used for quitting and ‘r’ for restarting once the game has ended.
​
The Timer ISR is used to control the program flow of the game, in this implementation the main game loop is contained here. Each time the timer interrupts a new game loop iteration is executed. The timer is initialized so that it will interrupt every second divided by the gamespeed. The gamespeed value is held at a constant value of 50 which means that 50 game loop interactions are executed every second. Although this game speed is held to be constant, the snakes are still sped up as they eat new targets. This is done by keeping a count variable for each snake i.e. snake.count (see struct snake) which is incremented at every game loop iteration until it reaches a threshold. This threshold is set depending on the speed table value indexed by the snake.level variable. Once the snake.count reaches the threshold that snake updated, meaning that the snake can move. As the threshold value decreases, the snakes move faster, hence the snakes are refreshed at a certain rate relative to the constant game speed. When the snakes reach the highest level which is 9, they begin to be updated at the constant speed which is 1 sec/50.
​
SNAKE GAME FUNCTIONS​
​
The table of Snake functions (Table 1) provides the function signature and the description of these functions. The programming tasks in the original sampling code are broken down into smaller functions to avoid repetitive code and to modularize the program. The input to these functions are struct Snake pointers because it is necessary to pass these structs by reference to ensure structs are updated properly. These functions contain the code for the various collision checks, target checks and for moving and drawing the snake.
Table 1: Snake Functions.
Figure 6: Snake Game Loop in Timer ISR Flow Diagram.
The program flow of the game loop is displayed in the Figure above (Figure 6). The first value that is checked is whether the game is paused or not. If the game is paused, we jump to the end which consists of displaying the current score on the 7 Segment display and clearing the interrupt request. If the game is not paused then we check if a new target needs to be generated. A new target is generated and checked to ensure that the new target is not placed on top of an existing snake. When the new target is generated then, the update_snake_val() function is called which checks whether the snake counts have reached the threshold to refresh in this iteration. Then the snake count is incremented, which is done at every loop. Next, if the snake is to be refreshed in the interaction, then the snake is shifted. After this we check whether the snake has eaten a target. The snake is then drawn in its new location after it has been shifted. Finally, all the collision checks are done, which include head collision, body collision, self collision, and boundary collision. At the end of the game loop, we check whether both snakes are dead. If either one is alive we continue the game, if both are dead the GameOver() function is called.
​
INTERFACING WITH 7-SEGMENTS DISPLAY AND LEDS​
​
We implemented two API functions to interface with the 7 Segment Display and LEDs/GPIO. These functions each call the respective device driver functions to write to the peripherals. The disp_scores() function displays both players’ current scores on the 7 segment display and the LED_Winnder() function updates the LED values and assigns a certain lighting pattern to indicate which player is winning the game.
Table 2: API Functions.
IMPLEMENTATION RESULTS​
​
Initially, the monitor displays the Snake Game title, Player 1 and 2 controls, and a few messages regarding how to ensure the game runs smoothly (Figure 7). At this state, the 7-segment displays on the FPGA board show the score as 0 to 0 and LEDs OFF. Once the key on the keyboard is pressed, both of the snakes appear in the image region of the monitor where the red snake is Player 1 and the green snake is Player 2 (Figure 8). In this example the two snakes collided head to head meaning that both snakes died and no points were given to either of the snakes. Since both of the snakes had a score of 0, a message saying “It’s a Tie!” is displayed. We have the LEDs programmed to have every other LED ON when the score is a tie, the left 8 LEDs when Player 1 is winning, and the right 8 LEDs ON when Player 2 is winning.
a)
a)
b)
Figure 7: Snake Game Demo 1: a) VGA monitor b) FPGA board.
a)
b)
Figure 8: Snake Game Demo 2: a) VGA monitor b) FPGA board.
Figure 9 shows the results after playing for a few seconds. Player 1’s snake was the first to eat the food which gave him 1 point, but collided with the boundary shortly after, awarding Player 2 5 points. Player 2’s snake continued to try to eat the food and managed to gather a total of 16 points before running into the boundary. Since Player 2 had the most points, the text region displays that Player 2 is the winner. The FPGA board displays the final score of both players on the 7-segment displays (1 to 16) and enables the 8 right LEDs to show that Player 2 won.
a)
b)
Figure 9: Snake Game Demo 3: a) VGA monitor b) FPGA board.
CONCLUSION​
​
This lab exercise is a great opportunity for students to use the SoC that we have been implementing throughout the semester and develop a game application to see how everything comes together. Although learning the concepts of the SoC in class are useful, being able to implement it in Verilog, Assembly code, and C language, and programming it onto the FPGA board ensures a full understanding. We found this project very fun to do because we were able to put everything that we have learned into one design to develop a Snake Game. We liked how easier it was to program using API, especially when controlling the peripherals. Although we liked developing the Snake Game, we wish we could've had more time to get a chance to try to develop one of the other games such as Tetris, Pacman, or Tic-Tac-Toe. Overall, we enjoyed working on this lab and believe this lab is essential because it helped us get a better understanding on how to use API.