FB pixel

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

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.

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 and 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 and 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 which 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 common anode depending on what's left in stock 🙂.
  • 0.49” OLED LCD with the 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 of MCU 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.
  • rduino connector. It allows you to use the Arduino shields like motor board, joystick board, LCD board 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, 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 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 type of LED matrices are produced, and you can use any one you have: 5x7 or 8x8 (Figures 5, and 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, then turn it on, keep it for several ms, and turn off.

One can ask, why 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 really simple. 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 consideration.

Program Code Description

Let’s create a new project now. I’ve called it “Game,” but you can give it any name you want. Then, create the new “main.c” file, as we are used to.

The same as in a previous tutorial, we will use the separate “config.h” file in which the configuration bits are defined. Ensure the _XTAL_FREQ macro in this file is defined as 32000000 so the delay functions will work properly.

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

#include <xc.h>

#include "config.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 __interrupt() INTERRUPT_InterruptManager (void) //Interrupt subroutine

{

if(INTCONbits.PEIE == 1) //If peripheral interrupts are enabled

{

if((PIE1bits.TMR2IE == 1) && (PIR1bits.TMR2IF == 1))//If Timer2 interrupt is enabled and Timer2 interrupt flag is set

{

PIR1bits.TMR2IF = 0; //Clear the Timer2 interrupt flag

LATB = 0xE0; //Set RB5-RB7 pins high to shutdown all the LEDs

LATC = (LATC & 0xF0) | column[col_num]; //Display the corresponding column

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

LATCbits.LATC3 = 1; //display the car in the bottom row

}

LATB = ~(1 << (col_num + 5)); //Light up the corresponding column

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) //Main function of the program

{

//GPIO configure

TRISC = 0xF0; //Configure RC0-RC3 pins as outputs

TRISB = 0x1F; //Configure RB7-RB5 pins as outputs

LATC = 0x00; //Set RC0-RC3 pins low

TRISAbits.RA4 = 1; //Configure RA4 pin as input

TRISAbits.RA5 = 1; //Configure RA5 pin as input

ANSELbits.ANS3 = 0; //Disable analog buffer at the pin RA4

ANSELHbits.ANS10 = 0; //Disable analog buffer at the pin RB4

WPUAbits.WPUA4 = 1; //Enable pull-up resistor at the pin RA4

WPUAbits.WPUA5 = 1; //Enable pull-up resistor at the pin RA5

WPUBbits.WPUB4 = 1; //Enable pull-up resistor at the pin RB4

INTCON2bits.nRABPU = 0; //Enable pull-up resistors at ports A and B

//Oscillator module configuration

OSCCONbits.IRCF = 6; //Set CPU frequency as 8 MHz

OSCTUNEbits.SPLLEN = 1; //Enable PLL

//Timer 2 configuration

T2CONbits.T2CKPS = 0b10;//Prescaler 1:16

T2CONbits.T2OUTPS = 0x0F;//Postscaler 1:16

T2CONbits.TMR2ON = 1; //Timer2 is enabled

PR2 = 155; //Set the Timer2 period as 5 ms

//Interrupts configuration

RCONbits.IPEN = 0; //Disable priority level on interrupts

PIR1bits.TMR2IF = 0; //Clear Timer2 overflow interrupt flag

PIE1bits.TMR2IE = 1; //Enable Timer2 overflow interrupt

INTCONbits.PEIE = 1; //Enable peripheral interrupts

INTCONbits.GIE = 1; //Enable all unmasked 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 (PORTBbits.RB4 == 0) //If the Start button is pressed

{

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

if (PORTBbits.RB4 == 0) //If after the delay button is still pressed

{

while (PORTBbits.RB4 == 0); //Then wait while button is pressed

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

if (PORTBbits.RB4 == 1) //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 (PORTAbits.RA5 == 0) //If the Right button is pressed

{

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

if (PORTAbits.RA5 == 0) //If after the delay button is still pressed

{

while (PORTAbits.RA5 == 0); //Then wait while button is pressed

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

if (PORTAbits.RA5 == 1) //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 (PORTAbits.RA4 == 0) //If the Left button is pressed

{

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

if (PORTAbits.RA4 == 0) //If after the delay button is still pressed

{

while (PORTAbits.RA4 == 0); //Then wait while button is pressed

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

if (PORTAbits.RA4 == 1) //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 columns 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 “xc.h” file to use the PIC MCU-related variables, functions, and macros. In line 2, we include the “config.h” file, in which the configuration bits are defined.

In lines 4-11, there are the variables declarations:

  • column array (line 4) of three elements represent 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 5) 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 6) is used in the dynamic indication and represents the number of the column which is currently displayed.
  • period (line 7) 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 of our car.
  • ticks (line 8) is the variable which increments every 5 ms.
  • shift_matrix (line 9) is the flag that allows to shift the image down when the number of ticks reaches the specified period.
  • start (line 10) is the flag which indicates whether or not the game is started.
  • shift_count (line 11) is the number of shifts of the image, actually it’s the number of the steps which we have successfully passed.

In lines 13-47, there is the INTERRUPT_InterruptManager subroutine, which we always use to process the interrupts; we’ll consider it later after the program’s main function, which is located in lines 49-175.

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

In lines 52-62, we initialize the GPIOs. In line 52, we configure pins RC0 -RC3 as outputs and RC4-RC7 as inputs. In line 53, we configure pins RB5-RB7 as outputs, and the pin RB4 as input. I hope you remember this, but I want to remind you once again that if the TRISx register is 1, the corresponding pin acts as an input, and if it’s 0, then the pin acts as an output.

In line 54, we set pins RC0-RC3 low to prevent turning on any LED at startup.

In lines 55 and 56, we configure pins RA4 and RA5 as inputs as to these pins (and to pin RB4) the buttons are connected. In lines 57 and 58, we disable analog buffers on pins RA4 and RB4, respectively.

In lines 59-61, we enable the pull-up resistors at pins RA4, RA5, and RB4, and in line 62, we allow the usage of pull-up resistance at ports RA and RB.

In lines 65-66, we set the CPU frequency as 32 MHz.

In lines 69-72, we configure Timer2. This timer 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.

Timer2 has been described in detail in tutorial 20, so here I will just provide brief information of what we do in the configuration code.

In line 69, we set the timer’s prescaler as 1:16, and in line 70, we set the postscaler as 1:16. As Timer2 is clocked with the internal source with the frequency Fcpu / 4, after the prescaler and postscaler the timer’s clocking frequency will be: 32 000 000 / 4 / 16 / 16 = 31250 Hz. So each timer’s tick will take 1000 000 us / 31250 = 32 us. Thus, to get the period of 5 ms, we need 5000 us / 32 us = 156.25 ticks. We round this value and get 156 ticks. As Timer2 starts counting from 0, we need to set the PR2 value less by one than 156, which is 155, and this is what we do in line 72. In line 71, we enable Timer2, and it starts counting immediately.

In lines 75-79, we configure the interrupts. In line 75, we disable the interrupt priorities. We have only one active interrupt - Timer2 overflow, so why enable priorities?

In line 76, we clear the Timer2 overflow interrupt flag, and in line 77, we unmask the corresponding interrupt. In line 78, we enable all unmasked interrupts; in line 79, we enable the peripheral interrupts.

In lines 81-86, we initialize the variables.

In line 81, we reset the variable col_num to start the indication from the first column. In line 82, we clear the variable start, so right after powering up the device, the game is stopped, and 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 84-86), shown in Figure 9.

Figure 9 - Startup logo of the game
Figure 9 - Startup logo of the game

As you can see in lines 84-86, 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 located, it really starts in lines 88-174.

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 4, so I will explain only the payloads of these code parts.

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

In lines 111-129, we check the “Right” button connected to pin RA5. This button is applicable only when the game is started, so we first check the start variable (line 120). If it’s 1, we check if the car position is smaller than 2 (line 122), and if yes, we increment the car position (line 124). 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 131-149, we check the “Left” button, which is connected to pin RA4. 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 140). If it’s 1, we check if the car position is greater than 0 (line 142), and if yes, we decrement the car position (line 144).

In lines 151-173, 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 151. 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 153, 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 154, we clear the shift_matrix flag, so now it can be set by the interrupt subroutine again.

In lines 155-163, 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 157), moving the whole image by one line to the bottom.

In line 158, 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 160), and if these bits are 1 we add 1 as an LSB to the corresponding column element (line 161). 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 164, we check if all three LSB of the column elements are 1. In this case, we write the weird expression (line 165):

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 166, 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 167), 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 169) and then divide it by 5 (line 170). Finally, we reset the shift_count variable (line 171).

Actually, lines 167-170 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 interrupt subroutine INTERRUPT_InterruptManager (lines 13-47).

The beginning of this function is standard. We first check if peripheral interrupts are enabled (line 15) because the Timer2 overflow interrupt belongs to the peripheral ones. Then, we check if the Timer2 overflow interrupt is unmasked and the corresponding flag is set (line 17). This means that Timer2 has overflowed, so that we can process its interrupt.

We first clear the interrupt flag (line 19), which, as you remember, is the mandatory procedure if you don’t want to be stuck in this function forever.

In line 20, we set the LATB register as 0xE0 to set pins RB5, RB6, and RB7 high and thus turn off all the LEDs. Then, we assign the LATC register with the corresponding column element (line 21). But here, the expression is a bit more complex than we used before. We could write here just LATC = column[col_num], and this won’t change the operation of our program. But I promised to show you how to do this correctly if you want the unused register bits to remain unchanged. So, in line 21, the value that we assign to the LATC register consists of two parts: (LATC & 0xF0) and column[col_num]. The first expression clears the lower four bits of the LATC register, leaving the upper four bits unchanged. Then, we add the column[col_num] value to this expression, filling the lower four bits with the corresponding column element. Using this expression, we must be sure that the upper four bits of the column value are zeros. Otherwise, making this expression even more complex would be better by performing logical AND between the column element and the 0x0F: (column[col_num] & 0x0F).

One more note about this line. It’s possible to write it like this because we connected all the rows of the LED matrix to the consequent lower pins of the port RC. If the connection is random, this expression would be way more complex.

In line 22, 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 24, 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 that the car is in the current column (checked in line 22), and their positions match, so we hit the obstacle. In this case, we clear the start flag (line 26) to indicate that the game is over and draw the cross (lines 28-30) as shown in Figure 10.

Figure 10 - Game over
Figure 10 - Game over

If there is no obstacle in the car position (line 32) (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 33). 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 line 35, we light up the corresponding column using another elegant expression

LATB = ~(1 << (col_num + 5));

which needs more explanation.

Again, this expression is possible only because we connected the columns to the consequent pins of the port RB. To light up the column, we must set the corresponding pin (RB5, RB6, or RB7) low. The col_num value can be from 0 to 2. Let’s see what we will have if its value is 0. In this case, line 35 will be considered as follows:

LATB = ~(1 << 5);

1 << 5 shifts one to the right by 5 bits, giving us the value 0b00100000.

The inversion ~ operation inverts this value, resulting in 0b11011111.

And now we assign this value to the LATB register, setting the fifth bit low and all other bits high. This will set the RB5 pin low and turn on the first column.

In the same way, you can check the operation of this line when col_num is 1 and 2 and make sure that in this case, pins RB6 and RB7 will become low, respectively.

In line 36, we increment the col_num value. If it becomes greater than 2 (line 37), we assign 0 (line 38). 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 39, we increment the ticks value. If it becomes greater than the period value (line 40), which means that the time has come to shift the image at one row to the bottom, we reset the ticks variable (line 42) and set the shift_matrix flag (line 43) which, as you remember, we process in lines 151-173. 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. And if we replace those “elegant” expressions, which I like so much, with regular if-else expressions, it would be even clearer. I think I will show this approach in the MCC-based version. So, read the next tutorial to compare the programs.

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 9. 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 of car speed. If you hit an obstacle, you will see the cross like in Figure 10, 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?