Using a PIC32 MCU
In today’s digital world, hardly anyone uses analog televisions anymore. In this project article, learn how three Cornell students make use of this antiquated technology in conjunction with the Microchip PIC32 MCU to create their own racing video game.
As a group of avid game enthusiasts, the choice to design our own game for our final project was obvious. Our intent was to use Microchip Technology’s PIC32 microcontroller (MCU) to develop a complete game from beginning to end in the designated time period. The objective of our game is to move a car across the finish line in the shortest amount of time, using controls that simulate the experience of driving. The reason behind our game choice was that we wanted to make a game that could be feasibly completed within the time constraints and still be entertaining. We also realized that if we decided to create a racing game, there would be many different reference materials we could draw from for the game physics.
The central motivation behind our design was game playability. Rather than designing an elaborately complex racing game full of bugs, we opted to create a simpler project with higher quality game play. With that in mind, we focused on ensuring a smooth interface between our hardware components and the actual movement of the pixelated car.
In this article, we further discuss the thought process and rationale behind the design choices made throughout the project. Our hope is that others can draw inspiration and advice from our work as they develop their own games.
The primary focus of the hardware design were the pedals and the steering wheel that enable the user to control the car’s movement. To best simulate the experience of driving, we wanted the pedals to be as realistic as possible. Specifically, we wanted the user to be able to adjust the compression of the pedals and have the car move forward and backward accordingly.
We built two pedals—accelerator and brake—by connecting two pieces of wood with a metal joint at one end and a compressible spring at the other (Figure 1). This enables users to control the degree of compression, and thereby the motion of the car, by exerting different amounts of pressure with their feet.
To measure the position of each pedal, we attached a slide potentiometer to the spring, so the degree of compression would be proportional to the amount of movement of the potentiometer. The pedals were then attached to a baseboard to prevent them from moving when pressed. In the setup, a PIC32 MCU was located next to the pedals on the base (Figure 1), so we simply connected them with wires to an analog pin.
To control left and right movement, we designed and 3D-printed a steering wheel (Figure 2). We used a simple circular design with a connecting piece across the diameter. To communicate between the wheel and the CRT, we used an Adafruit MMA8451 accelerometer, which we hot glued to the backside. We chose to use an accelerometer as opposed to a rotary potentiometer because it did not require the wheel to be mounted to a stand. This meant that the wheel could be held and turned in mid-air like the controller in the popular racing game Mario Kart. To allow the user a better driving experience, we connected long wires to the MCU, allowing the wheel to be relatively mobile.
The main output of our game was the CRT display, which we connected to the MCU using an RCA connector. We chose a CRT display because it was easy to drive from the PIC32 with SPI, and because it had a low cost and high refresh rate. We used an adaptation of Di Jasio’s method of generating sync pulses , using one output-compare unit to output to the screen with easy control over the video content timing. The CRT operates by combining the video and sync signal into one output signal. The voltage of the nominal output is either at 0, 1.0 or -0.3V. When it is at 0V, black is drawn to the screen, and when the output is at 1V, white is drawn to the screen. An image of the voltage signal is shown in Figure 3. We connected the CRT sync (SYNC pin 14) and input (VIDEO pin 3) to ports RB5 and RA1, respectively, as illustrated in our full schematic (Figure 4).
We also decided to have a Start-Reset button so the game could be replayed. This button allowed the user to go between the instruction and the game screen when pressed. It was hot-glued to the steering wheel to keep the controls consolidated and close to the user. The input was connected to RB4 on the MCU (Figure 4).
The last major hardware component added to our project was a DAC for sound effects. The SPI DAC we used was the Microchip Technology’s MCP4822. We connected DAC SPI Sclock to RB15, chip select to RB13, and Sdata to RB11. The main motivation for adding the DAC hardware was to allow the user to have a more immersive sensory experience.
A big part of our work on this project was determining the game design and its programming. To display to the CRT, we used an NTSC library . This library implemented the SPI interface used to communicate to the TV, and provided functions for drawing letters, lines and pixels. The driver code, which we used as the base of our project, operates by displaying through DMA channel 1, and it triggers the ISR operating at 15,725MHz, the NTSC/line rate. For each frame, the ISR will output 262 lines, at which point it will reset the image memory pointer and begin the next frame.
Because the rest of the game was made from scratch, we had to design our own illustrations in pixel art form. To facilitate this process, we wrote multiple video functions that covered more area than just a line or a point. We drew an aerial view of the car, so that when playing the game, the user could better perceive the vehicle’s speed. After determining the relative size of the car to the screen, we created the background, which consisted of a four-lane road with grass on both sides.
The final illustration we added to the playing screen was the rectangles that appeared in the middle of each lane to obstruct the car from going forward. To simulate the movement of the car and obstacles, we erased the image of the car at the beginning of each frame, and then recalculated and updated the new positions of the cars and obstacles. Finally, to ensure that the car appeared at the top layer of the screen, we redrew the entire background and then the car over it.
A final touch we added to our game was a start screen containing the game rules, which appears upon reset. Upon pressing the Start button from the start screen, the program counts down from three before the game actually begins. We had to draw these numbers pixel by pixel, because the provided video text functions made the numbers too small. The initial start screen and the main playing field are shown in Figure 5.
For the sound output, we used the second SPI channel to connect the DAC. We used RB13, and sent sine waves through an additional ISR. We created three different notes in particular, all at different frequencies and/or durations. The most common sound is the sound of the user hitting an obstacle. We set the direct digital synthesis (DDS) increment  to the lowest frequency for this note. A high-pitched tone was used for reaching the finish line, and a tone in the middle was for losing all the player’s lives (see DESIGN/GAME MECHANICS section). The sine wave outputted to the DAC would change based on the DDS increment value.
The other key software component of our project was programming the steering wheel controls. For this, we used an Arduino Nano that functioned as a device driver to transmit data from the MMA8451 accelerometer on the wheel. We used the Adafruit Arduino device library  for the MMA8451, to easily connect to the Arduino and record orientation of the wheel, and a simple 2-bit digital parallel interface between the Arduino and the PIC32.
We chose to use this 2-bit interface over Serial because we wanted to keep the game controls as simple as possible. There are only four possible steering positions (one left, one right and two center positions). Despite this rough translation, the game was still extremely playable, and the change in sideways direction was smooth due to a numerical integration calculation. Although this 2-bit interface prevents the player from executing sharp turns, we found this level of detail was not necessary due to the nature of our game.
Our game was a simple top-down racing game. The main objective was to reach the finish line in the quickest time, while avoiding the randomly generated obstacles along the way. Since the obstacles were sometimes hard to avoid, we gave the player nine lives so that winning would be possible. Hitting an obstacle would result in a time penalty and the loss of one life. When the game was over, it would display whether the player won or lost and the score. To start and restart the game, the user presses the Start-Reset button attached to the steering wheel, which brings the game back to the starting instruction screen.
Our program had multiple protothreads , but when we tried to use a round-robin scheduler, we found that it interfered with the timing of the DMA writing to the CRT. To deal with this issue, instead of running multiple threads at once, we wrote our own custom scheduler. This had a single main thread, from which we spawned an additional thread when needed. The separate thread was spawned in the case of collision, causing the main thread to stop, yield to the spawned thread and only resume after the spawned thread was finished. By doing this, we satisfied the timing of the video generation.
A large portion of the project was spent writing the logic for car movement and its interaction with the environment. The first objective we tackled was associating the user’s interaction with the pedals and wheel with the movement of the car. We figured that both acceleration and velocity values are necessary for a complete game.
We controlled the vertical motion of the car with an acceleration value that directly corresponded to the degree the pedal is pressed. From the acceleration pedal, the ADC reads in values approximately between 0 and 300 ADC units, where 0 is the resting state of the pedal and 300 is the fully compressed state. In our mapping, we had this range map to a value between -0.08 and 0.15 pixels/frame2. This is because in the resting state, the car should slowly decelerate. The reason for this decision was that it more closely imitates how a car actually works.
At first, we considered mapping the pedal ADC directly to the acceleration and thus the position of the car on the screen, but we soon realized that mapping to acceleration was better. This is because using a direct map to velocity, the car would just stop if the acceleration pedal were released. This does not reflect the behavior of a real car, which would begin to slow down. For the movement of the wheel, however, we mapped the left and right values to the directions sensed from the Arduino. This is because we wanted the user to be able to quickly dodge obstacles. It doesn’t make much sense for a user to put the wheel in a neutral position and continue drifting, because it was turning previously.
To add more complexity to the game, we designed obstacles that randomly block different areas of each lane. To create these obstacles, we made a struct that grouped together the X-position, Y-position and valid bit variables. We also made an array of size four, corresponding to the X-position of the middle of each lane in the playing field. Based on a random seed, obstacles were generated at the top of the screen at one of the four positions in the array. The valid bit was used to control whether the obstacle should be drawn or not. When the valid bit is 0, the obstacle is not drawn. However, after a certain number of ticks based on a modulus calculation, the obstacle is made valid and thus appears in the playing screen. When the obstacle reaches the bottom of the screen or collides with the car, the obstacle is erased and made invalid, and its Y-position is reset to the top of the screen.
We designed the obstacles as squares that descended from the top of the screen. The square design allowed for an easy hitbox detection with the car. We also based the movement off that of the car. This gave the car a semblance of velocity relative to the objects around it. We made this design decision because as the car speeds up, it should be harder to avoid the obstacles. Naturally, their velocities should be relative to each other, so we did a one-to-one mapping.
To encourage the user to stay on the main road, we incorporated grass that was next to the roads. We thought this would be a good decision, because it prevents the playing field from getting too large. Besides this, grass is a common aspect in many popular racing games. A user who decides to drive in the grass will decelerate and end up not moving at all. Therefore, the user may choose to go into the grass, despite this deceleration, to save some time if collision with an obstacle is anticipated. Generally, though, it is wiser for the player to stay on the road.
We implemented a nine-lives system to give the player a better chance to beat the game. For the life system, we simply added a counter to the bottom corner of the screen. Each collision subtracts one life, and when the player has 0 lives left, the game immediately ends and displays the losing end screen that reads “Game Over.” Every time the game restarted, the number of lives resets to nine.
To win the game, the player must reach a specified distance of 18,000 pixels without losing all nine lives. This distance was calculated by taking the double integral of the acceleration values from the accelerator pedal. We made this calculation easier by simply summing the velocities we already calculated. Once this distance is reached, a finish line spawns at the top of the road and moves towards the car at the same velocity as that of the obstacles. When the front of the car and the finish line meet, the user has effectively “crossed the finish line” and wins the game. A final end-game screen appears, displaying the user’s total time to cross that distance.
In just over one month, we were able to complete a fully functioning game with minimal bugs. It contained responsive hardware components along with software that allowed the user smooth control of the game. With regard to game logic, there were few bugs that affected the user experience. The final result was a game with good playability. The controls were smoothly integrated with the CRT, and there was no visible trouble between the game controls and the display on the screen. Even though the wheel has only four levels, turning the wheel feels smooth.
Because there is limited space for the player to turn left and right, it is not necessary for there to be extremely precise controls for the wheel. However, we wanted the controls for the pedals to feel much more responsive. Accordingly, after a certain degree of pedal compression, any small additional difference causes the car to speed up.
In addition to controls for accelerating, we have additional layers of complexity that make the game more interesting. The features give the users motivation to accelerate and turn to improve their scores. The obstacles are also randomly generated, which makes the game less static. Each time a game is played, the path that the user takes to get a better score is always changing. By not having one set optimal path, the game becomes more interesting and dynamic.
Overall, we were able to design a complete game. We were able to produce an entire base game with a clear goal and simple controls. To get a better feel of our game, please check out our video of the project below.
A lot of improvements could be added to make the game more functional and more entertaining. The main bug in the existing project came from using two separate interrupts. Because the DMA for the CRT requires strict timing from the ISR, using an extra interrupt caused slight static in the game screen. This static did not interfere with the game play, however.
Another possible change concerns the complexity of the obstacles. They were designed as simple squares that would come down with the same velocity as the car. However, the obstacles could be made slightly more dynamic and possibly have different shapes or different velocities relative to the car’s velocity.
During the project, we intentionally chose to avoid some options, due to difficulty and time constraints. However, there definitely could be possible improvements. One such idea would be to make the steering wheel and the baseboard wireless by implementing radio communication between the modules. We considered using XBee radios to link the wheel with the rest of the project, but decided against this in the interest of saving time. Also, we could remove the sound effects to add the wireless radio communication, which might further improve the project. We hope that if readers decide to implement a game of their own, they will incorporate these suggestions.
 Programming 32-bit Microcontrollers in C: Exploring the PIC32 by Lucio Di Jasio (book)
 Sean Carroll’s PIC32 Small Dev Board:
 NTSC library: http://people.ece.cornell.edu/land/courses/ece4760/PIC32/index_NTSC_video.html
 The direct digital synthesis (DDS) increment: https://en.wikipedia.org/wiki/Direct_digital_synthesis
 Adafruit Arduino device library:
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JULY 2020 #360 – Get a PDF of the issueSponsor this Article
Haley Lee is a senior studying Electrical and Computer Engineering at Cornell University. She plans on graduating early and immediately start working.
Dustin Hwang is a senior studying Computer Science at Cornell University. He plans to complete his Masters of Engineering degree and enter industry afterwards.
Brandon Guo is a senior studying Computer Science. He wants to work on the West Coast after graduation and spend his time cooking and playing volleyball.