FB pixel

CircuitBread’s Universal Board - The “Racing” Game Using MCC | Embedded C Programming - Part 27

Published


Hi! Today, I am presenting a somewhat unusual tutorial. Well, it will still be devoted to programming the PIC18F14K50 MCU, but I also want to advertise to you the universal development board that our team has designed. (Editor’s note: Sergey is being modest - he is the one who designed this development with the rest of the team simply providing a little feedback) Then, we will assemble and program a simple game using this board.

If you have already read the previous tutorial, you can skip the next two chapters and move to the “Configuration of the project using MCC” part.

Universal Development Board

The idea of this board came to mind after I developed several very similar Arduino-like boards, which only differed from the installed MCU. And then I thought, “Hey, what if I make the board without any MCU, only with some peripheral devices like LEDs, buttons, displays, sensors, etc.? And instead of an MCU, we can attach the small breadboard where you can install any MCU”. I shared this idea with Josh, and suddenly, he liked it. We discussed the list of the parts that will be present on this board, and I started designing it.

The initial result is presented in Figures 1, 2.

Figure 1 - The top side of the universal board
Figure 1 - The top side of the universal board
Figure 2 - The bottom side of the universal board
Figure 2 - The bottom side of the universal board

As you can see, all parts are located on the top side of the board, and there are many of them. I’ll provide the complete list below. On the bottom are only two long female sockets and the breadboard. The idea is that all pins of all parts are connected to these sockets. In Figure 2, you can see the legend near them, which describes the function of each pin. The assembly of the circuit is done with a regular Dupont male-to-male wire. So, you install any MCU in a DIP package into the breadboard, make the necessary connections, and explore the MCU. Some vendors don’t produce their microcontrollers in DIP packages, especially 32-bit ones. In this case, you can use special adapter boards. You can find them on Amazon or Aliexpress (I won’t provide any links; just search “SOIC to DIP” or “TQFP to DIP” etc.). The other option is to use the universal adapter I designed (Figures 3, 4).

Figure 3 - Top side of the universal adapter
Figure 3 - Top side of the universal adapter
Figure 4 - Bottom side of the universal adapter
Figure 4 - Bottom side of the universal adapter

Using this adapter you can connect:

  • any SOIC package with a pitch of 1.27 mm and between 4 and 32 pins;
  • any SSOP package with a pitch 0.65 mm and between 4 to 28 pins;
  • any SOT package with a pitch 0.65 mm and between 3 to 8 pins;
  • QFP-32 package with a 0.8 mm pitch;
  • QFN-32 package with a 0.5 mm pitch.

I have plans to design more such adapters for other QFP and QFN packages with different pin numbers.

So, if you have an ARM (or other) MCU that doesn’t have a DIP version, feel free to use such adapters; just make sure your soldering skills allow you to solder the chip properly without damaging it.

Now, let’s return to the universal board. There are the following parts installed in it:

  • Twelve LEDs. Each pin of the LED can be connected anywhere, so you can use them as follows: standalone LEDs with sinking or sourcing control; LED matrix up to 3x4; charlieplexing connection of the LEDs (like we did in the Christmas tutorial of the PIC10F200 series).
  • Ten tactile switches. You can use them as both standalone buttons or as a matrix.
  • Passive buzzer, which can be used to produce sounds.
  • Seven-segment four-digit LED indicator with dynamic indication. It can be either a common cathode or a common anode, depending on what's left in stock 🙂.
  • 0.49” OLED LCD with a resolution of 64x32 and an I2C interface.
  • DS1307 real-time clock chip with the I2C interface.
  • AT25010B EEPROM memory with a capacity of 1kbit and SPI interface (you can install a larger one if you want).
  • CH340C USB-to-UART converter, which can be used both for communication with the PC or for programming the MCU if it supports such a way. Also, all RS-232 signals of this chip (RTS, CTS, DCD, DSR, DTR, RI) are available.
  • 10 kOhm potentiometer.
  • Photoresistor from the Arduino set is used as an analog sensor to test analog comparators (together with the potentiometer) or ADC.
  • DS18B20 temperature sensor with a 1-wire interface.
  • Capacitive touch button and capacitive touch wheel. Unfortunately, the PIC18F14K50 doesn’t have the capacitive touch module, but many do. Moreover, putting these buttons on the board costs us nothing, so why not to add them?
  • 12 MHz crystal and 32.768 kHz crystal for external clocking of the MCU if needed (for example, in USB or CAN applications).
  • 5 V to 3.3 V LDO with the output current up to 3.3 V for those parts and MCUs that can’t be powered with 5 V.
  • USB type C connector is used to power the board and for USB communication through the USB-to-UART converter or the MCU.
  • Arduino connector. It allows you to use the Arduino shields like motor boards, joystick boards, LCD boards, and others. You just need to connect your MCU properly so the pin functions of the Arduino board and your MCU fit.
  • Two PMOD connectors. Well, just in case. This interface is quite popular now, and there are a lot of boards that support it.

Well, that’s everything you can find on this board. I tried to add the most flexibility and functionality at the lowest price when developing it. That’s why parts like the BLE module, CAN converter, and motor driver were excluded from consideration. You can easily attach them to the breadboard, though, if needed. But even with the current set, you can test digital and analog inputs and outputs; all the most popular digital interfaces are UART, SPI, I2C, 1-wire, USB, and capacitive interfaces.

This is just the first version of the board, and your feedback is vital, so feel free to write your comments about it under this tutorial or in our Discord channel. Maybe you have ideas of what else it would be good to add or remove as unnecessary.

This board is open source. I created it in the DipTrace program, which is not very popular, but I’ve worked in it for almost 15 years and have gotten used to it. So, the source files will be provided in the DipTrace format and as Gerber files if you want to order the board without any modifications. If there is any demand for it, I will remake it in KiCAD or Eagle, so, as I said before, please provide your feedback if there is any interest in this universal board and adapter boards.

That’s everything about the board presentation. Now, let’s switch to the demonstration. Even if you don’t have this board, you still can do this project using regular parts and a breadboard.

Project Explanation

So, as I announced at the beginning, today we will design a game. I specifically selected the tutorial topic to use only the MCU modules we are already familiar with.

I called this game “The Racing”. Probably, you have played something similar on the portable “Tetris”-like consoles if you are old enough 🙂. Let me briefly remind you of the rules and explain how it will be done using the board.

So, for the display, we will use the LED matrix 3x4 (D1-D12) in Figure 1. Also, there will be three control buttons - “Start” (S2), “Left” (S7), and “Right” (S9). A “car” will be represented by the glowing LED in the bottom line. You can move it using the “Left” and “Right” buttons. As there are only three columns, the car can have only three positions. After starting the game, your “car” begins to move, and on your way, you will meet the “obstacles” represented by glowing LEDs which will appear in the random positions and which you must avoid using the control buttons. If you hit the obstacle (the position of your “car” and the “obstacle” match), the game is over. Also, after some obstacles, the car’s speed will increase to make the game more difficult.

Now, what do you do if you don’t have the board? You can use the LED matrix and three separate buttons instead. Different types of LED matrices are produced, and you can use any one you have: 5x7 or 8x8 (Figures 5, 6). You will just use a part of the LED matrix and not the whole area. Also, you can use the separate twelve LEDs, but locating them in the breadboard will be difficult, so they form a nice matrix.

Figure 5 - 5x7 LED matrix
Figure 5 - 5x7 LED matrix
Figure 6 - 8x8 LED matrix
Figure 6 - 8x8 LED matrix

Schematic Diagram

In the schematic diagram, I will use the separate LEDs and buttons (Figure 7). If you have the board, you need to use the “Dx A” and “Dx C” pins (where “x” is the LED number from 1 to 12) for LED anode and cathode, respectively, and “Sy 1” and “Sy 2” (where “y” is the tactile switch number: 2, 7 or 9) pins for switches.

Figure 7 - Schematic diagram with the PIC18F14K50 with LED matrix and tactile switches
Figure 7 - Schematic diagram with the PIC18F14K50 with LED matrix and tactile switches

The schematic and the operation principle are similar to the 7-segment LED indicator tutorial. Here, we also use many LEDs connected in a way that requires dynamic indication. Let’s consider this in more detail.

As you see in Figure 7, there are twelve LEDs D1-D12 that are connected in matrix 3x4 LEDs. The anodes of each row and each column’s cathodes are connected (similar to a 7-segment indicator, huh?). The joint anodes through the current-limiting resistors R1-R4 are connected to the pins from RC0 (upper row) to RC3 (lower row) of the MCU. The value of resistors R1-R4 may vary depending on the supply voltage and the LED voltage drop. Here, I used 1k, but you may need to decrease it to 220-470 Ohm to provide the comfort brightness. The joint cathodes are connected to the pins from RB5 (left column) to RB7 (right column). Such a connection of the rows and columns to the consequent pins of the MCU can significantly simplify the program, which we will see soon.

The control of the LED matrix is also very similar to the control of the 7-segment LED indicator. First, we turn off all the columns by applying logical “1” to the pins RB5-RB7. Then we form logical “1” on those pins (RC0-RC3), which LEDs we want to light up in the first column. After that, we form a logical “0” at pin RB5, and the first column turns on. After some time (about 5 ms), we turn off the first column, set pins RC0-RC3 to form the image in the second column, and form the logical “0” at pin RB6. Finally, we form the image for the third column in the same way. As we switch the columns with the high frequency, our eye considers that it sees the steady image. If the number of columns or rows is greater, the principle remains the same:

  • Consequently, form the image for a column
  • Turn it on,
  • Keep it for several ms
  • Turn off.

Why do we use four current-limiting resistors if we could use just three and connect them to the joint cathodes? The answer is that the current-limiting resistors should be connected to the non-switching elements. In this case, the current through each LED will be limited by its resistor. If we connect the resistors to the joint cathodes, then the current through them will depend on the number of LEDs that are turned on simultaneously, and thus, the brightness will be different.

Actually, in the universal board, each LED has its resistor, but if you make your board, you can save eight resistors and connect the remaining, according to Figure 7.

The tactile switches S2, S7, and S9 (the numeration corresponds to the universal board) are connected as usual. The MCU pins to which they are connected were selected from the ports RA and RB because they have internal pull-up resistors. I especially didn’t put any resistors to the tactile switches in the universal board in case someone wants to try the pull-down resistor because they think it’s more convenient (actually, that’s why this board is called “universal”).

And this is everything about the schematics diagram. In Figure 8, you can see what the connections in the universal board look like.

Figure 8 - Connections of the universal board
Figure 8 - Connections of the universal board

Well, it looks tangled, but the connection process is straightforward. By the way, in Figure 8, you can see that the breadboard used in it is bigger than the one shown in Figure 2. It was a pleasant surprise that this big breadboard also fits the board when I designed it, I wasn’t counting on this. So, one more plus to the “universality” of the board. And now you can see why this board has such long “legs.” To hide everything underneath the board between it and the table.

OK, let’s now switch to the program creation and configuration.

Configuration of the Project using MCC

Let’s create a new project now. I’ve called it “Game_MCC,” but you can give it any name you want.

In this project, we will use only Timer2, which we considered in detail in Tutorial 23.

Let’s run the MCC plugin and change the MCU package. This time the MCU will operate on the 32 MHz frequency to configure thiswe need to open the System Module page, set the Internal Clock as 8MHz_HF, and set the “PLL Enabled” checkbox. Also, don’t forget to remove the check from the “Low-voltage programming enable” field.

Then we need to add the “TMR2” to the project as we did in Tutorial 23, and configure it according to Figure 9.

Figure 9 - Timer2 configuration
Figure 9 - Timer2 configuration

Timer2 will be used to generate the interrupts every 5 ms. And as in the 7-segment indicator tutorial, we used the __delay_ms functions to perform the dynamic indication, this time, we will use the timer overflow interrupt for this purpose.

Here, we set the timer’s prescaler as 1:16 and the postscaler as 1:10. With these values, we can set the timer’s period precisely as 5 ms. If the postscaler is greater than 1:10, then there will be some deviation of the actual period from 5 ms, and if it’s smaller than 1:10, we just can’t reach the 5 ms period, as the timer will run too fast. Also, we need to enable the timer interrupt and set the callback function rate as 1 to invoke it every time the timer overflows.

Now, let’s switch to the Pin Module and Pin Manager and configure all required pins according to the schematic diagram (Figure 10).

The buttons are connected to the pins RA4, RA5, and RB4 (Figure 7), so they should be configured as inputs with the pull-up resistors enabled. The LED matrix is connected to the pins RB5-RB7 and RC0-RC3, so they should be configured as outputs. Moreover, pins RB5-RB7, to which the cathodes of the LEDs are connected, should start with a high level to prevent LEDs from lightening at the startup.

Finally, let’s switch to the Interrupt Module tab and make sure that the TMR2 interrupt is enabled (Figure 11).

Figure 11 - Interrupt Module
Figure 11 - Interrupt Module

There is no need to enable the priorities of the interrupts as there is only one active interrupt.

Now everything is configured, and we can click the “Generate” button and switch to code writing.

Program Code Description

Now, let’s open the “main.c” file and write the following code.

#include "mcc_generated_files/mcc.h"

uint8_t column[3]; //Columns of the LED matrix

int8_t car_pos; //Position of our car

uint8_t col_num; //Number of the column to display

uint16_t period; //Number of 5ms intervals to shift the cars

uint8_t ticks; //Number of timer ticks

uint8_t shift_matrix; //Allows to shift the LED matrix to the bottom

uint8_t start; //The game is started

uint8_t shift_count; //Counter of the shifts

void timer2_overflow_handler (void)

{

COL_1_SetHigh(); //Set column1 high to shutdown the LEDs in it

COL_2_SetHigh(); //Set column2 high to shutdown the LEDs in it

COL_3_SetHigh(); //Set column3 high to shutdown the LEDs in it

if (column[col_num] & 0x01) //If bit0 of the current column element is 1

ROW_1_SetHigh(); //Then set the ROW_1 pin high to light up the LED

else //Otherwise

ROW_1_SetLow(); //Set the ROW_1 pin low to turn off the LED

if (column[col_num] & 0x02) //If bit1 of the current column element is 1

ROW_2_SetHigh(); //Then set the ROW_2 pin high to light up the LED

else //Otherwise

ROW_2_SetLow(); //Set the ROW_2 pin low to turn off the LED

if (column[col_num] & 0x04) //If bit2 of the current column element is 1

ROW_3_SetHigh(); //Then set the ROW_3 pin high to light up the LED

else //Otherwise

ROW_3_SetLow(); //Set the ROW_3 pin low to turn off the LED

if (column[col_num] & 0x08) //If bit3 of the current column element is 1

ROW_4_SetHigh(); //Then set the ROW_4 pin high to light up the LED

else //Otherwise

ROW_4_SetLow(); //Set the ROW_4 pin low to turn off the LED

if ((car_pos == col_num) && (start))//If the car is at the current column

{

if (column[car_pos] & 0x08)//If the obstacle is in this place

{

start = 0; //Stop the game

//Draw the cross sign

column[0] = 0x05;

column[1] = 0x02;

column[2] = 0x05;

}

else //Otherwise

ROW_4_SetHigh(); //display the car in the bottom row

}

switch (col_num) //Light up the corresponding column

{

case 0: COL_1_SetLow(); break;

case 1: COL_2_SetLow(); break;

case 2: COL_3_SetLow(); break;

}

col_num ++; //Select the next column

if (col_num > 2) //If the column number is greater than 2

col_num = 0; //Then set the column as 0

ticks ++; //Increment the ticks variable which counts 5ms intervals

if (ticks >= period)//If ticks is greater than the given period

{

ticks = 0; //then clear the ticks variable

shift_matrix = 1;//and allow to shift the image down

}

}

void main(void)

{

SYSTEM_Initialize(); // Initialize the device

TMR2_SetInterruptHandler(timer2_overflow_handler); //Set interrupt handler

INTERRUPT_GlobalInterruptEnable(); // Enable the Global Interrupts

INTERRUPT_PeripheralInterruptEnable(); // Enable the Peripheral Interrupts

col_num = 0; //First column

start = 0; //The game is stopped

//Draw the racing car shape

column[0] = 0x0A;

column[1] = 0x0F;

column[2] = 0x0A;

while (1) //Main loop of the program

{

if (BUT_START_GetValue() == LOW) //If the Start button is pressed

{

__delay_ms(20); //Then perform the debounce delay

if (BUT_START_GetValue() == LOW) //If after the delay button is still pressed

{

while (BUT_START_GetValue() == LOW); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (BUT_START_GetValue() == HIGH) //If the button is released after the delay

{

car_pos = 1; //Car is in the center

column[0] = 0; //Clear the first column

column[1] = 0; //Clear the second column

column[2] = 0; //Clear the third column

period = 200; //Start with 1 second shift

shift_matrix = 0; //Don't shift the matrix now

shift_count = 0; //Reset the shift counter

start = 1; //Start the game

}

}

}

if (BUT_RIGHT_GetValue() == LOW) //If the Right button is pressed

{

__delay_ms(20); //Then perform the debounce delay

if (BUT_RIGHT_GetValue() == LOW) //If after the delay button is still pressed

{

while (BUT_RIGHT_GetValue() == LOW); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (BUT_RIGHT_GetValue() == HIGH) //If the button is released after the delay

{

if (start) //If the game is started

{

if (car_pos < 2) //If car position is smaller than 2

{

car_pos ++; //Increment the car position

}

}

}

}

}

if (BUT_LEFT_GetValue() == LOW) //If the Left button is pressed

{

__delay_ms(20); //Then perform the debounce delay

if (BUT_LEFT_GetValue() == LOW) //If after the delay button is still pressed

{

while (BUT_LEFT_GetValue() == LOW); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (BUT_LEFT_GetValue() == HIGH) //If the button is released after the delay

{

if (start) //If the game is started

{

if (car_pos > 0) //If car position is greater than 0

{

car_pos --; //Decrement the car position

}

}

}

}

}

if (shift_matrix && start) //If the game is started and the matrix shifting is pending

{

uint8_t rnd = rand(); //Generate the random number

shift_matrix = 0; //Clear the shift_matrix flag

for (uint8_t i = 0; i < 3; i ++) //Shift the rows and add the new obstacles

{

column[i] <<= 1; //Shift the column to the bottom

if (shift_count & 0x01) //At every odd shift

{

if (rnd & (1 << i)) //Add the obstacles according to the first 3 bits of the random number

column[i] |= 1;

}

}

if ((column[0] & 0x01) && (column[1] & 0x01) && (column[2] & 0x01)) //If all columns are occupied (solid wall)

column[(rnd >> 3) % 3] &= 0xFE; //Then clear the random column

shift_count ++;

if (shift_count >= 20) //After 10 obstacles has been passed

{

period *= 4; //We multiply period by 0.8

period /= 5; //To increase the speed

shift_count = 0; //Reset the shift_count

}

}

}

}

The program is long yet quite straightforward.

In line 1, we, as usual, include the “mcc_generated_files/mcc.h” file to be able to use the MCC-generated variables, functions, and macros.

In lines 3-10, there are the variables declarations:

  • column array (line 3) of three elements represents the content of the three columns of the LED matrix. As each column consists only of four LEDs, we will use only four lower bits of the column elements.
  • car_pos (line 4) is the position of our car. This variable can take values 0 (car is in the left column), 1 (car is in the center), and 2 (car is in the right column).
  • col_num (line 5) is used in the dynamic indication and represents the number of the column that is currently displayed.
  • period (line 6) is the number of 5 ms intervals (we will talk later about how to produce them), after which the image will be shifted one row down. This will create an illusion of movement in our car.
  • ticks (line 7) is the variable that increments every 5 ms.
  • shift_matrix (line 8) is the flag that allows the shift the image down when the number of ticks reaches the specified period.
  • start (line 9) is the flag that indicates whether or not the game is started.
  • shift_count (line 10) is the number of shifts of the image, actually, it’s the number of steps which we have successfully passed.

In lines 12-61, there is the timer2_overflow_handler function, which is the callback function of the Timer2 overflow interrupt; we’ll consider it later after the program’s main function, which is located in lines 63-166.

As usual, first, we will consider the initialization part of the program (lines 65-77), which frankly doesn’t have anything new.

In line 65, there is the MCC-generated function SYSTEM_Initialize, which initializes all the hardware modules of the MCU.

In line 67, we set the already mentioned function timer2_overflow_handler as the Timer2 interrupt callback.

In lines 69 and 70, we enable global and peripheral interrupts, respectively. We need to enable both of them as the Timer2 interrupt is the peripheral one.

In line 72, we reset the variable col_num to start the indication from the first column. In line 73, we clear the variable start, so right after powering up the device, the game is stopped, and the game’s logo is shown. As the resolution of 3x4 dots is quite poor, I drew a very approximate shape of the racing car (lines 75-77), shown in Figure 12.

Figure 12 - Startup logo of the game
Figure 12 - Startup logo of the game

As you can see in lines 75-77, the logic “1” corresponds to the lit LED, and “0” corresponds to the extinguished LED. Also, you can see that we use only the lower four bits, and the upper bits are all zeros. If you use pins RC4-RC7 for other purposes, you need to take care of them in your program so they remain unchanged. I will show you later how to do this.

So, now that the initialization part is over, and the main loop is located, it really starts in lines 79-165.

Well, it’s pretty long, but mainly because of the blocking button processing algorithm for three buttons. This algorithm has been described in detail in tutorial 5, so I will explain only the payloads of these code parts.

In lines 81-100, we check the “Start” button. When we press it, the game should start, so we initialize everything needed for this. First, we set our car in the center (line 90), clear all three columns (lines 91-93), and set the initial period as 200 (line 94), which will give us the interval between car movements as 200 x 5 ms = 1 s. Then, we clear variables shift_matrix (line 95) and shift_count (line 96) to reset the number of movements. Finally, we set the start flag as 1 (line 97) to let the other program parts know that the game has begun.

In lines 102-120, we check the “Right” button. This button is applicable only when the game is started, so we first check the start variable (line 111). If it’s 1, we check if the car position is smaller than 2 (line 113), and if yes, we increment the car position (line 115). In this game, we don’t loop the game field, so you can’t get to the left position from the right position if you just press the “Right” button. To do this, you need to press “Left” twice.

In lines 122-140, we check the “Left” button. This button is also applicable only when the game is started, and its code is very similar to the previous one: first, we check the start variable (line 131). If it’s 1, we check if the car position is greater than 0 (line 133), and if yes, we decrease the car position (line 135).

In lines 142-164, the code part is responsible for obstacles and the car movement. This part is also implemented only if the game is started, so we check the start flag in line 142. Also, we check the shift_matrix flag in the same line. The latter is set inside the interrupt subroutine when the ticks value matches the period value (which we will consider very soon).

In line 144, we declare a new local variable rnd and assign the random number to it generated by the standard function rand(). This random number will be used to put the obstacles in random positions.

In line 145, we clear the shift_matrix flag, so now it can be set by the interrupt subroutine again.

In lines 146-154, there is a for-type loop from 0 to 2 to update all three columns. Inside it, we first shift the current column element by 1 bit to the left (line 148), moving the whole image by one line to the bottom.

In line 149, we check if the LSB of the shift_count variable is 1. If this is true, then the shift_count value is odd. We need this check to add the new obstacles not at every row but at every second row to leave the car room for maneuvers. So, at every odd row, we do the following: check the least three bits of variable rnd (each bit at new loop iteration) (line 151), and if these bits are 1, we add 1 as an LSB to the corresponding column element (line 152). By this, we add the obstacles at the random position of the top row, which will move towards our car after the shift_matrix flag is set the next time.

But the situation may happen when all three lower bits of the random number are 1. In this case, we will have a solid wall, which our car can’t avoid. We need to fix this situation. So, in line 155, we check if all three LSB of the column elements are 1. In this case, we write the weird expression (line 156):

column[(rnd >> 3) % 3] &= 0xFE;

Let’s figure out what it means. Here, we shift the rnd value by three bits to the right. We already used the lowest three bits of this variable, and we already know that they are all ones. So, we shift it to check the unused bits. Then, we take the modulo of the division of the remaining rnd value by 3. Well, the overall idea of this line is to set the LSB of a random column element as 0 to “break” the solid wall. As we have only three columns, we need to ensure that the array index doesn’t exceed 2, so we take this modulo. The logical AND between the column element and the number 0xFE just clears the LSB of this column element. As you see, nothing too tricky.

In line 157, we increment the shift_count variable, counting the number of car movements forward. If the shift_count value is greater or equal to 20 (which means that we have passed through 10 obstacles) (line 158), then we decrease the period value by 20% to increase the speed. We can’t write just period *= 0.8 because we’re dealing with the integer numbers. So, we split this operation into two parts. First, we multiply the period value by 4 (line 160) and then divide it by 5 (line 161). Finally, we reset the shift_count variable (line 162).

Actually, lines 158-161 are a matter of consideration. You can set them as you like. For example, you can increase the speed not by 20% but by 10% or even 5%. Also, you can do it after every 20 or 40 obstacles or whatever. So feel free to experiment with these values to make the game comfortable (as much as it can be comfortable at all 🙂).

OK, we have finished with the main function of the program. Let’s now switch to the Timer2 overflow callback function (lines 12-61). I should warn you, if you read both non-MCC and MCC-based tutorials, that here the solution is not that elegant, as according to the rules which we’ve established by ourselves, we must use the MCC-generated function wherever it’s possible, and ideally not to deal with the MCU registers directly at all.

So, in lines 14-16, we set all the column pins high and thus turn off all the LEDs.

In line 17, we check the bit #0 of the element of the array column with the number col_number using the familiar to our operation “AND.” If this bit is high, this means that the LED in the row that corresponds to bit #0 (the upper row) should be turned on, which we do in line 18. Otherwise (line 19), the LED should be turned off (20).

In lines 21-24, we do the same check but this time for bit #1, which corresponds to the second row.

In lines 25-28, we check bit #2, and in lines 29-32 we check bit #3.

In tutorial 26, lines 14-32 are replaced by just two lines of code. But in that case, there is a restriction that the LED rows should be connected to the four consequent pins of one port. In the current realization, there is no such limitation; you can connect the rows and columns of the matrix in a random way, and it still will work, unless you don’t forget to change the configuration of the pins in MCC (Figure 10).

In line 33, we check if the car is located in the current column and if the game is started. In this case, we check if the car position matches the obstacle position, and if so, the game is over.

So, in line 35, we check if the third bit of the current column element is 1. This means the obstacle is in the lowest row of the game field, where our car is also located. As we know the car is in the current column (checked in line 33), and their positions match, so we hit the obstacle. In this case, we clear the start flag (line 37) to indicate that the game is over and draw the cross (lines 39-41) as shown in Figure 13.

Figure 13 - Game over
Figure 13 - Game over

If there is no obstacle in the car position (line 43) (it still can be to the right or the left from it), then we just light up the car LED in the bottom row (line 44). So, as you see, the column array contains the whole game field but the car itself. I made it specially to simplify the checking of crashing the car.

In lines 46-51, we light up the corresponding column using the switch-case operator. If col_num is 0, then the first column is lit up (line 48); if col_num is 1, then the second column is lit up (line 49), and finally, if col_num is 2, then the third column is lit up (line 50).

In line 52, we increment the col_num value. If it becomes greater than 2 (line 53), we assign 0 (line 54). So, at each timer interrupt (every 5 ms), we turn on the next column, which makes the full matrix update cycle take 5 x 3 = 15 ms.

In line 55, we increment the ticks value. If it becomes greater than the period value (line 56), which means that the time has come to shift the image at one row to the bottom, we reset the ticks variable (line 58) and set the shift_matrix flag (line 59) which, as you remember, we process in lines 142-164. Actually, we could process it here, in the interrupt subroutine, but we should keep it as short as possible, so it’s a regular practice in the MCU world to set some flag in the interrupt subroutine and then process it in the program’s main loop.

And that’s everything about the program. As you see, it’s really not very difficult.

Testing of the Game

Let’s now assemble the device according to Figure 7, compile and build the project, and flash the PIC18F14K50 MCU. If everything is assembled and programmed correctly, you should see the startup logo like in Figure 12. When you press the “Start” button, you can see that the new obstacles appear at the top row and move towards your car. When you press the “Left” or “Right” buttons, your car should jump between the columns, avoiding the obstacles. After every 10 obstacles, you should notice the increase in car speed. If you hit an obstacle, you will see the cross like in Figure 13, and the game will be stopped until you press the “Start” button again.

Unfortunately, I can’t show you here how the game looks in motion, but I believe Josh will demonstrate it in his excellent video version of this tutorial.

As homework, I suggest you try to add some sound to the game. If you use the universal board, you can use the buzzer located in it. I leave the sound effect up to you. It can be a beep at every step; it can be a permanent roaring engine sound that will become higher when the speed increases. Or it can be something else.

Note: Chronologically, this has been written much later than the next several tutorials, so you will not see any mention of the universal board in them, which shouldn’t surprise you. We will return to the universal board when we get to learning digital interfaces.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?