Style Switcher

Choose Color:

SOFTWARE IMPLEMENTATION software

Since we have multiple games, we make a separate thread for each game. We also leverage additional threads for peripherals such as timers (to keep track of system time), potentiometer inputs, and button inputs. This project has 8 protothreads in total along with the main function.

void main(void)


The Main function initializes the display, set up RB7 and RB8 as digital inputs to read the button press, configures and enables the ADC, defines setup parameters for openADC ports, and configures ports to sample AN11 and AN5 as the analog inputs from the two potentiometers. All the threads are added and scheduled using round robin fashion.

static PT_THREAD (protothread_timer(struct pt *pt))


The Timer Thread increments a variable called sys_time_seconds every 250 milliseconds. The sys_time_seconds variable is used in the snake game to only update the graphics every 250ms. By doing that, we can make sure that the snake will not go too fast and players will not see flickering screen because the TFT is updating too fast. This thread runs all the time.

static PT_THREAD (protothread_game(struct pt *pt))


The Game Thread draws the main menu and calls other threads to begin when a certain game is selected. Here, the ADC values from each of the potentiometers are read and used in menu selection. Our yield time here (using PT_YIELD_TIME_msec) is set to 33 milliseconds.

We have a value called init that is initialized as 0. Its value is read to determine which thread’s initialization section to run. A value of 0 runs the initialization section of the Game Thread. This initialization section writes a "CHOOSE A GAME!" header and rectangles with the game names one under the other.

We have two “ADC” values that corresponds to the left and right potentiometers connected to the analog channels of the PIC32. The left and right potentiometers are represented by ADC5 and ADC11 respectively. For the main menu we use ADC11 only for selection. Potentiometer values will henceforth be referred to as “ADC values”. We also have a button_read variable that reads the button connected to BIT_7. These allow players to choose the highlighted game to play.

Since the ADC values can range from 0 to 1024, we divide 1024 by the number of games (5) to determine which game to highlight on the menu. We also check to make sure this game is not already highlighted with a variable called ADC_menu. For example, (ADC11 >= 0 && ADC11 < 204 && ADC_menu != 1) is the conditional for highlighting Etch-A-Sketch. Within this conditional, the highlightings for all other games are erased. The main menu is implemented in a way such that the screen will only redraw if a change in the threshold conditions has been detected to avoid flickering of the TFT screen. Afterward, if button_read == 128 and the highlight condition is met, we set a variable called game to the game’s corresponding value and init to the game’s value as well. This would be 0 for the main menu, 1 for Etch-A-Sketch, 2 for Snake, 3 for Boids, 4 for Brick-Breaker, and 5 for Pong.

static PT_THREAD (protothread_input(struct pt *pt))


The Input Thread reads the digital inputs of RB7 and RB8 on the development board, which are connected to the buttons. The value of RB7 is stored in a variable called button_read. We use this button for selecting games and resetting each game. The value of RB8 is stored in ret_menu, which is used for returning to the main menu. If ret_menu is high, the screen is erased, game is set to 0, init is set to 0, and ADC_menu is set to 0.

Potentiometer ADC values (ADC11 and ADC5) are also updated here. This thread runs all the time.

static PT_THREAD (protothread_EtchASketch(struct pt *pt))


Our Etch-A-Sketch Thread runs when game is set to 1. At initialization, we read the initial ADC values for each potentiometer to store an offset and “zero” calibrate the values. We then erase and clear the screen and set the init variable to -1.

The position of the pen is directly mapped to the two ADC values. Every time the thread updates, the initial ADC value is subtracted from the current ADC value to get the relative change. Since we subtracted the offset earlier to zero the value, negative values are possible. If the ADC value is less than 0, 1024 is added to the value to account for the potentiometer discontinuity and screen wraparound. This will ensure that even when the cursor goes off the screen, it will reappear on the other side to continue drawing. We multiply the corrected ADC5 by 0.234375 to map it as an x value on the screen (240 pixels wide). We multiply the corrected ADC11 by 0.3125 to map it as a y value on the screen (320 pixels tall). Then, if the ADC values are at least 4 pixels within the edge of the screen and are within 4 pixels from the last drawn ADC value, we draw it as a circle of a 4-pixel radius. If the BIT_7 button is read to be high, the screen will be erased.

We record the time at the beginning and end of this thread. Our PT_YIELD_TIME_msec is set to 5 - (end_time - begin_time) to make sure it only draws at this interval so that random noise of the ADC values could be filtered out.

static PT_THREAD (protothread_snake(struct pt *pt))


The Snake Thread runs when game is set to 2. At initialization, the screen is erased, a dot is drawn, and a snake of length 3 is drawn going down the screen. The snake is implemented using a queue (first-in-first-out array) where each index represents a part of the snake, and its corresponding value represents the location on the screen. Since the queue was initialized using arrays and indices (instead of an actual queue), overflow and wraparound must be taken into account since the array only has a length of 768 (the number of possible locations on the screen). We only use ADC11 here and record both the current value as cur_ADC11 and the previous value as old_ADC11. If the difference between the old and new ADC values is greater than 100, we turn right. If it is less than -100, we turn left. Since the potentiometer is physically continuous, but the values are not, we need to check for the discontinuity. If the difference is greater than 600, we assume the new ADC value has jumped to a small value. Thus, we read that as a left turn. If the difference is smaller than -600, we read it as a right turn using the same logic. We use a variable to store the direction that the snake is traveling in so that we know which direction to enqueue the new node.

The position of the snake updates every 250 milliseconds. We normally enqueue and dequeue to move the snake. However, if the snake eats the dot, it will only enqueue to lengthen the snake. We also only redraw the score if a dot is eaten to prevent screen flickering. Every time the snake eats the dot or the game is restarted, a dot will be generated in a random location on the screen (within the bounds of the grid). We also use enqueue and dequeue helper functions to aid our logic.

Self-collision is checked by a for loop that checks whether the front of the snake equals any location in the snake’s body. Similarly, wall collision checks if the front of the snake matches any wall coordinates. A game over screen appears once a collision has occurred.

static PT_THREAD (protothread_boids(struct pt *pt))


Our Boids Thread programs the predator that the user controls with potentiometers as well as the boids that flock and fly away from it. It runs when game is set to 3. Much of the code is inspired by pseudocode for the boids project on the ECE 4760 website. We resued most of the code from the BOIDS lab and modified the thread to take in the potentiometer input and control the predator motions. The boids are represented by an x and y location and velocity arrays that are both initialized to random values. Similar variables are initialized for the predator.

For every boid, we check how far away the other boids are. If the differences are less than the visual range, we check if the squared distance is less than the protected range. We accumulate the distances from boids that are less than the protected range. This is then used to multiply by an avoidance factor and added to the velocity to fly away from other boids that are too close for comfort. However, if they are within the visual range but outside the protected range, the boids will fly towards each other. Centering contributions are also added to velocity to ensure the boids have a center point that they all want to flock towards. The boids react similarly to the predator except the protected range is the same size as the visual range. Thus, the boids will always avoid the predator.

If either the boid or the predator reaches the edge of the screen, a turn factor will be subtracted from the velocity in the opposite direction. Minimum and maximum speeds are also enforced by dividing the x and y speeds individually by the total speed, multiplying it by the max speed, and setting this value as the new speed.

ADC values are read and mapped to the predator’s velocity. The predator’s x velocity is updated by adding the current x velocity to the change in ADC5 multiplied by 0.1. The predator’s y velocity is updated by adding the current y velocity to the change in ADC11 multiplied by 0.1.

Predators are drawn as red dots with a 4 pixel radius and boids are drawn as single pixels. The initialization section is run and the game is reset when the BIT_7 button is pressed.

static PT_THREAD (protothread_brick_breaker(struct pt *pt))


The brick breaker thread is run when the game variable is equal to 4. During initialization, we erase the screen, draw the paddle, initialize an array for the brick positions, draw the ball, set the ball’s velocity to (5, -5), and draw all the bricks. When the BIT_7 button is pressed, the game is reset and the initialization section is run.

Similar to the boids algorithm, we use the change in the ADC11 value to determine the paddle’s velocity for a smoother paddle movement. However, we also add a threshold such that if the change is not larger than 5 ADC units, the velocity is set to 0. This gives the difference in effect between boids (where the predator will continue to travel) and brick breaker (where the paddle will stop if no input is given). Additionally, when the paddle reaches the edge of the screen, we set the velocity to 0 such that the paddle will not go off the screen.

When the ball is within the paddle’s boundaries, the ball’s y position is negated so it bounces back up. We implemented a checkCollision helper function which will return a 0 if the ball is outside the obstacle’s boundaries, but will return a 1 if the ball is within the obstacle’s boundaries. We use this function to check for brick collisions as well. Pseudocode for collision checking was inspired from PasteBin.

The ball’s x and y values are updated separately. The x value is updated, and if a collision is detected, the x velocity is negated. The same process occurs for the y value afterwards. Bricks are erased once a collision occurs. A game over condition appears once the ball hits the bottom of the screen. If all bricks are deallocated, a win condition appears on the screen.

static PT_THREAD (protothread_pong(struct pt *pt))


The Pong Thread operates in a very similar manner to the Brick Breaker Thread. It will run when game is equal to 5. The ball and paddles are drawn in the same manner as the brick breaker game. The ball is also given the same velocity during initialization. The ADC values are read for both ADC11 and ADC5. The change in ADC11 (right potentiometer) is mapped to the velocity of the bottom red paddle and the change in ADC5 (left potentiometer) is mapped to the velocity of the top green paddle. In the same way as brick breaker, the velocity is set to 0 if the ADC values do not change or the paddle has reached the edge of the screen. Collisions are checked for both paddles and walls for the ball while checking for game scoring conditions (the ball reaches the top or bottom of the screen). Once one player has hit the ball to the other side, a win message appears and the game can be restarted or the players may choose to return to the main menu.