I was taking a driver development course base on STM32F407 MCUs on Udemy and the Embedded System - Shape The World: Multi-Threaded Interfacing on Edx. As a final project, I thought it would be cool to combine all the developed drivers (GPIO, ADC, DAC, SPI...) and build a very own portable game console. The console will allow you to play a simplified version of Asteroid - the famous and fun arcade game. This game was written base on a Youtube tultorial from OneLoneCoder.
How to play?The player will need to use the joystick to change the heading direction of the spaceship (in North, South, East, West, North East, South East, North West, South West); press the B2 (lower) button to apply thrust and move the spaceship; and press the B1 (upper) button to fire rocket at the moving asteroids. As player 's spaceship collided with one of the asteroids, the game will ended.
There is some few things to notice when playing the game:
+Player 's spaceship, asteroids, rockets will all move in a special dimension: When moving and disappearing at the right edge, the object will slowly appear at the left edge of the screen, and vice versa. The same thing happen with the above and below edge, as well.
+The more user press the B2, the faster the spaceship will move in space.
+When user stop pressing the B2 button, the spaceship will keep floating with a decreasing speed for a while before reaching a full stop.
+Whenever player press the B1, 1 point will be deducted from the total score. The maximum number of rockets that player can shoot as a time is 3; this mean that player need to wait until 1 of the 3 rockets to disappear in order to fire a new one. (When 3 rockets appear on the screen and player keep pressing the B1 button, nothing will happen.)
+Whenever player hit an asteroid with rocket, 10 points will be added to the total score. If player hit a big-size asteroid, two small-size ones with double the speed of the big-size will appear.
The goal of the game is to clear all 5 waves of asteroids and earn the highest score possible.
Hardware and firmware overview: How does it works ?Fig.1 show the main components of the console: 3.2 inch TFT LCD display (driven by ILI9341), speaker, LEDs working as output and buttons, joystick working as input.
Fig.2 show the software modules used in this project and their relationship. The device 's driver layer consist of ILI9341 ( MPU that control the display), speaker, led, button and joystick modules. The peripheral 's firmware layer consist of SPI, DAC, Timer, RCC, RNG, ADC and GPIO modules. The RCC (Reset and Clock Control) module is used for setting the MCU 's system clock and bus clock, the RNG (Random Number Generator) is used to randomize the position of spaceship and asteroids at the beginning of the game.
Fig.3 show how the data within the console is processed.
The joystick is technically consist of 2 variable resistors which value will change independently according to the joystick 's X and Y position. These change in resistor values will cause change in voltage; and the 2 voltage values are read as 12 bits digital values by the ADC and fetched to the game engine at a rate of 40Hz.
The 2 buttons (B1 and B2) work as digital inputs, their state (HIGH or LOW) is read by the MCU and fetched to the game engine also at a rate of 40Hz.
The speaker module utilize on-board timer to output 12-bits digital samples of recorded sound effect to DAC at a rate of 11.025kHz. These digital values will then be converted to analog voltage, went through amplifier and go to speaker to create sound.
Game engine will call the ILI9341 module to output image (to the display); these image data will be transfered from the MCU to the display through SPI communication.
Speeding up the SPIFor STM32F4 Discovery Board, the default speed of system clock is 16MHz, and the APB1 bus is 16MHz. This lead to the fact that the maximum speed of SPI1 (Data transmission between board and display is performed through SPI1, and SPI1 is hung on APB1 bus) is 8Mhz. As can be seen from Fig.4, it took a while to fill the entire display (320x240 pixel) with yellow.
In the datasheet of ILI9341, the maximum write speed of 4-wire SPI interface is 10MHz. However, as I did some research, there are posts stating that the ILI9341 can in fact be driven as much faster speed, even as 42 MHz without problem. At first, my display work normally at 42MHz, but then after few days it stop responding, so I reduced the speed to 37.5MHz. As can be seen in Fig.5, the display is filled with yellow almost immediately at 37.5 MHz. This allow the console to render game image at much faster rate.
Fig.6 show the basic flow chart of the game. Game engine utilize 3 periodic interrupts. The first one (Timer 6) is used for telling the foreground when to read player inputs (button, joystick), update position and direction of spaceship, position of asteroids, determine whether spaceship collided with one of the asteroids, and output sound and image, etc. All of these tasks need to be performed every 25ms (40Hz). The ISR for this periodic interrupt will set a flag named frameUpdate, and the foreground will execute all of the above tasks, clear flag frameUpdate, then wait until frameUpdate to be set again by the ISR.
The ISR:
void TIM6_DAC_IRQHandler (void)
{
TIM_intrpt_handler(TIM6);
frameUpdate = SET;
}
The foreground:
int main (void)
{
RTE_init();
RTE_display_start_screen();
while(SHOOT_BUTTON_READ);
while(1){
RTE_display_black_background();
RTE_create_player_spaceship(&PlayerSpaceship);
RTE_draw_player_spaceship(&PlayerSpaceship);
RTE_create_asteroid(&AsteroidVect,Asteroid,numOfAsteroidInWave[currentWa ve],&PlayerSpaceship);
RTE_draw_asteroid(&AsteroidVect);
RNG_deinit();
RTE_start_update_frame();
while(1){
if(frameUpdate == SET){
RTE_display_score();
RTE_update_player_spaceship(&PlayerSpaceship);
RTE_draw_player_spaceship(&PlayerSpaceship);
RTE_create_rocket(&RocketVect,Rocket,&PlayerSpaceship);
RTE_update_rocket(&RocketVect,&AsteroidVect);
RTE_draw_rocket(&RocketVect);
RTE_update_asteroid(&AsteroidVect,&PlayerSpaceship);
RTE_draw_asteroid(&AsteroidVect);
if(PlayerSpaceship.Object_Property.aliveFlag == RTE_ALIVE_FALSE){
PROTOBOARD_GREEN_LED_ON;
RTE_display_game_over_screen();
while(SHOOT_BUTTON_READ);
RTE_reset_game();
PROTOBOARD_GREEN_LED_OFF;
break;
}
if(AsteroidVect.total == 0){
TIM_ctr(TIM6,STOP);
currentWave++;
RNG_init();
RTE_create_asteroid(&AsteroidVect,Asteroid,numOfAsteroidInWave[currentWave],&PlayerSpaceship);
TIM_ctr(TIM6,START);
}
frameUpdate = CLEAR;
}
}
}
}
The second periodic interrupt is utilized to output samples to DAC in order to generate sound effect.
#ifdef SPEAKER_USE_TIMER7
void TIM7_IRQHandler (void)
{
TIM_intrpt_handler(TIM7);
DAC_write(&DACxHandle,*(soundPtrGlobal++));
if(soundPtrGlobal == soundEnd){
speaker_stop_sound();
}
}
#endif
The third periodic interrupt was used for debouncing of B2 button. The process of firing rocket will be performed one when the shootButtonFirstTimeFlag is set.
Game engine: Create special space dimensionTo create special space dimension, first you will need to write a function like the RTE_wrap_cordinate below, and called it every time after an object (player spaceship, rocket, asteroid) 's position is updated.
/***********************************************************************
Private function: Wrap coordinate
***********************************************************************/
void RTE_wrap_cordinate (int16_t *xPtr, int16_t *yPtr)
{
if (*xPtr < 0){
*xPtr += ILI9341_config.width;
}
if (*xPtr >= ILI9341_config.width){
*xPtr -= ILI9341_config.width;
}
if (*yPtr < 0){
*yPtr += ILI9341_config.height;
}
if (*yPtr >= ILI9341_config.height){
*yPtr -= ILI9341_config.height;
}
}
The goal of the RTE_wrap_cordinate is simple: Let consider the case when an object moving pass the right edge of the display, this function will take the X position of object 's origin (top left corner) and subtract it with the display 's width (320 pixel). The outcome is, the object will reappear near the left edge every time it's top left corner point go past the right edge.
In the same case, to create an effect that object slowly appear from the left edge, the pixel of the object's image that was ignored by the display (because of being out of the 320x240 region) also needed to be "wrap cordinated" before drawing, as illustrated below.
The code to tell the display to draw the ignored pixel of the image:
/***********************************************************************
External function: Overwrite draw pixel function in ILI9341 driver library (in order to draw pixels going off screen)
***********************************************************************/
void ILI9341_draw_pixel (int16_t x, int16_t y, uint16_t color)
{
RTE_wrap_cordinate(&x,&y);
ILI9341_set_active_area(x,x,y,y);
ILI9341_send_command(ILI9341_MEM_WRITE);
ILI9341_send_parameter_16_bits(color);
}
Game engine: Collision detection
The game engine need to know whether spaceship collided with one of the asteroids, whether a rocket hit an asteroid, whether an asteroid collided with others asteroids to perform corresponding actions. These collision checking are done using AABB algorithm. Basically, the idea of this algorithm is to treat 2D objects as rectangular boxes and determined whether these boxes over lapsed with each others or not.
/***********************************************************************
Private function: Detect collision between 2 object using AABB algorithm
***********************************************************************/
uint8_t RTE_collision_detect (Space_Object_t *Object1Ptr, Space_Object_t *Object2Ptr)
{
int16_t Obj1BottomRight_X = Object1Ptr->Object_Property.x + Object1Ptr->Object_Image.imageWidth;
int16_t Obj1BottomRight_Y = Object1Ptr->Object_Property.y + Object1Ptr->Object_Image.imageHeight;
int16_t Obj2BottomRight_X = Object2Ptr->Object_Property.x + Object2Ptr->Obje ct_Image.imageWidth;
int16_t Obj2BottomRight_Y = Object2Ptr->Object_Property.y + Object2Ptr->Object_Image.imageHeight;
if (Object1Ptr->Object_Property.x < Obj2BottomRight_X
&& Object2Ptr->Object_Property.x < Obj1BottomRight_X
&& Object1Ptr->Object_Property.y < Obj2BottomRight_Y
&& Object2Ptr->Object_Property.y < Obj1BottomRight_Y){
return RTE_COLLISION_TRUE;
}
return RTE_COLLISION_FALSE;
}
ConclusionLink to Git Hub repo of this project is in the code section (All the code is written in C language; the game is at Game_engine_return_to_earth folder).
This handheld game console was my very first project to begin the journey into the world of embedded system. It is fun to do, and though the level is basic, it contain many aspects that I can practice and learn from: developing firmware and driver, debugging protocol, making game, and hardware designing. I hope that you can find some helpful information from this project.
Comments