LED Matrix Game

Thursday, August 6, 2009 12 comments
Making games is cool and making them in hardware with blinking LEDs is cooler still. If you ever wondered how to make an LED matrix game yourself, this article is for you. I have discussed the most basic 8x8 LED matrix based design and the code samples should be adequate to prepare you to write your own game. I assume you have familiarity with Atmel's ATMEGA microcontrollers and that you can follow circuit schematics and assemble the necessary parts yourself.  I have not used any ready-made 8x8 modules. The LEDs have been meticulously assembled on a veroboard by wiring and soldering on the back. I have used WinAVR for development and the source code and various snippets are provided in C for the same.

The game system has three parts as shown below:


The user pushes the buttons in order to select menu options and play the game(s). The inputs are interpreted by the microcontroller and the LED matrix display is updated as the game logic dictates. Displaying 8x8 = 64 LEDs simultaneously can be daunting. So lets go step by step. Following schematic shows how you connect a single LED - a staple Hello World!

#include<avr/io.h>
#include<util/delay.h>

int main(void)
{
 DDRA=0xFF;
 while(1)
 {
  PORTA=0X00;
  _delay_ms(200);
  PORTA=0x01;
  _delay_ms(200);
 }
 return(0);
}

This, I guess you already know. A single LED is connected to PORTA PIN0, through a current limiting resistor from anode side and the cathode is made ground. In the program you declare the PORTA pins to be output and then in an infinite loop you make the PIN0 on PORTA go high and low, making the LED blink. Now think for a moment: In an 8x8 matrix there are 64 LEDs. If you are using ATMEGA16/32 you have 40 pins out of which only 32 are IO pins grouped as 4 ports of 8 pins each. So even if you use all the 32 pins in the manner shown above, you are able to light up only 32 LEDs, not the whole matrix. More importantly in doing so you have run out of the pins that you could possibly use for push-button inputs. So how can we overcome this stumbling block? A solution that is found in several designs is as shown below:

 
Here, LEDs in the same column have the anodes on a common line, running top to bottom. Similarly for LEDs in a single row have cathodes on a common line, running left to right. This means that if on the wires that make up the column an 8 bit pattern say 10111110 is set and then the common line for some row is grounded, then the LEDs in that particular row will glow as dictated by this bit-pattern. With this bit-pattern, for example, if you ground the common line for the topmost row then the LED1 and LED7 will be OFF and rest in that row will be ON. You may ground the common line on any row and the bit-pattern set on the column wires will appear as glowing LEDs in that particular row. 

This design, however, is not the complete solution. The reason is that different rows cannot display different bit patterns. They will all display the same pattern that has been set on the column wires, once they are grounded. In order to overcome this problem, we shall use a clever trick - persistence of vision. The idea is that if changes are made in rapid succession, the human eye is unable to sense the small incrementals and perceives the event as smooth continuous motion. In practice we display one row at a time - we set the bit pattern on the column for the topmost row and ground that row keeping all other rows un-grounded. Next, we set the bit pattern for the second row on the column wires and then ground the second row, keeping all other rows un-grounded and so on till we have displayed all the rows, one at a time, in succession from top to bottom.  We repeat the same process over and over again, just like scan-lines in a TV. We make the switching between the rows so fast that it appears that you are viewing the entire LED matrix in one go.

Now we can attach the 8x8 LED module shown above to two different ports - one to set bit patterns for the column wires and other to ground different rows when desired. In the following figure PORTC is used to set patterns on column wires and PORTA is used to ground rows:


This type of connection has some drawbacks too. The thing is when a row is grounded then current from all the LEDs that are ON in that row sinks collectively at this pin and this may be dangerous for the microcontroller. That's where the driver IC shown in the figure on the top comes into picture. We use ULN2803 to sink the current safely from all 8 rows. The ULN2803 IC is shown below:

Pin 9 is connected to ground. Pins 1-8 are connected to the PORTA[0:7] which we use to control the selective sinking of the rows. The row [1:8] wires are connected to pins [18:11] so that when PORTA PIN0 is set ON, the row 1 is connected to the ground. So keeping these factors in mind, the block diagram can be modified as:


Which results into the schematic:

If you have understood everything so far, you will prepare the working hardware without any problems. Only programming remains to be done. I just give some examples and explain them:

/* Glow all LEDs */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include <avr/delay.h>
#include <avr/wdt.h>

unsigned char     matrix[8];  // The matrix that is directly copied to the LEDs
static int      current_row;  // The current row being sinked by 2803 IC
unsigned char    PORTD_Val;


void clearmatrix(){
 for(int i=0; i<8; i++) 
  matrix[i] = 0x00;
}

void setpixel(unsigned char *s, int x, int y){
    unsigned char ch;
 ch = *(s+y);
 *(s+y) = (ch | (128>>x));
}


void resetpixel(unsigned char *s, int x, int y){
    unsigned char ch;
 ch = *(s+y);
 *(s+y) = (ch & ~(128>>x));
}

int ispixel(unsigned char *s, int x, int y){
    unsigned char ch;
 ch = *(s+y);
 if((ch & (128>>x))!=0) 
  return 1;
 else 
  return 0;
}


int main (void)
{
 wdt_disable();
 
 /* Initialise GPIO */
 DDRA |= 0xFF;
 DDRC |= 0xFF;
 DDRD &= 0x00;      // The port used as input
 
 PORTA = 0x00;      // No initial value on PORTA
 PORTC = 0x00;      // No initial value on PORTC
 PORTD = 0xFF;      // Enable internal pull up resistors
    
 
 /* Initialize timer */
 TCCR1B |= (1 << WGM12);    // Configure timer 1 for CTC mode 
 TIMSK  |= (1 << OCIE1A);    // Enable CTC interrupt
 sei();        // Enable global interrupts
 OCR1A   = 3000;      // Set CTC compare value 
 TCCR1B |= (1 << CS10);    // Div by 1
 
 
 clearmatrix();
  
 int i, j;
 
 for (;;)
 {
   for(i=0; i<8; i++)
    for(j=0; j<8; j++)
    { 
     setpixel(matrix, i, j);
     _delay_ms(100);
    }
  
 }
}

ISR(TIMER1_COMPA_vect)
{
 PORTC = matrix[current_row];
 
 PORTA = 1<<current_row;
 
 current_row++;
  
 if (current_row == 8)
  current_row = 0;
 
}

I maintain a global unsigned char called matrix of 8 bytes. This is the display screen for the rest of the program to use. Clearly 8 bytes means 64 bits representing ON/OFF state of all the LEDs in the matrix. matrix[0] is supposed to mean the top row, matrix[1] the next row and so on till matrix[7] - the row at the bottom.

Another global is current_row to track which row you are supposed to sink current at through ULN2803 IC attached at PORTA. The sinking of the row is timed through TIMER1 ISR shown at the end of the program. This sets the column bits on PORTC based on the current_row value and also makes the appropriate row sink current. This done it cycles through other values of current_row.

With the matrix so defined and the ISR automatically taking care when to update the display, the rest of the program can focus on the application. I have made functions setpixel(), resetpixel(), clearmatrix() and ispixel() which work as if the 8x8 matrix were a graphical screen with top left corner of coordinates (0,0) and x increasing column-wise from 0 to 7 and y increasing from top to bottom row as 0 to 7. The functions are used to turn on, turn off an LED given (x,y), then clear entire matrix and also to inspect whether a particular (x,y) pixel is on or not respectively. You can clearly see how in the main loop we have conveniently made use of setpixel().

The following snippet shows manipulation of a pixel on the matrix. Pressing four keys attached to the microcontroller designated UP, DOWN, LEFT and RIGHT moves a single glowing LED across the display. in response This is the simplest idea that is exploited to make more complete games:

.
.
int main (void)
{
 wdt_disable();
 
 /* Initialise GPIO */
 DDRA |= 0xFF;
 DDRC |= 0xFF;
 DDRD &= 0x00;      // The port used as input
 
 PORTA = 0x00;      // No initial value on PORTA
 PORTC = 0x00;      // No initial value on PORTC
 PORTD = 0xFF;      // Enable internal pull up resistors
    
 
 /* Initialize timer */
 TCCR1B |= (1 << WGM12);    // Configure timer 1 for CTC mode 
 TIMSK  |= (1 << OCIE1A);    // Enable CTC interrupt
 sei();        // Enable global interrupts
 OCR1A   = 3000;      // Set CTC compare value 
 TCCR1B |= (1 << CS10);    // Div by 1
 
 
 clearmatrix();
  
 int x=0, y=0;
 
 
 for (;;)
 {
  PORTD_Val = PIND;
  if (!(PORTD_Val&(1<<0))) // Detect pushbutton pressed at the pin PORTD0 (left)
   x--;
  if (!(PORTD_Val&(1<<1))) // Detect pushbutton pressed at the pin PORTD1 (right)
   x++;
  
  if (!(PORTD_Val&(1<<2))) // Detect pushbutton pressed at the pin PORTD2 (up)
   y--;
  
  if (!(PORTD_Val&(1<<3))) // Detect pushbutton pressed at the pin PORTD3 (down)
   y++;
  
  // Check range of x and y
  if (x<0) x=0;
  if (x>7) x=7;
  if (y<0) y=0;
  if (y>7) y=7;
  
  // Draw a pixel with these positions of x and y
  clearmatrix();    // Clear all previously done drawing
  setpixel(matrix, x, y); // Draw at the current x and y location
  _delay_ms(100);
  
 }
}
.
.

Finally, a word on using fonts. Fonts are just 8x8 bit patterns. You can lay them out on a grid and obtain hexadecimal values for each row and then put them in an array for reference. I once found this 8x8 pixel font editor that you may find interesting. More advanced users may choose to create all the letters of the alphabet and store them on EEPROM. Anyway, here is the snippet that shows how to make fonts and display them:

.
.
//global
unsigned char font_T[8] = {0x7f, 0x49, 0x08, 0x08, 0x08, 0x08, 0x1c, 0x00};
unsigned char font_O[8] = {0x1e, 0x21, 0x21, 0x21, 0x21, 0x21, 0x1e, 0x00};
unsigned char font_M[8] = {0x21, 0x33, 0x2d, 0x21, 0x21, 0x21, 0x21, 0x00};
.
.
.
int main(void)
.
.
 clearmatrix();
  
 int i;
 
 for (;;)
 {
  PORTD_Val = PIND;
    
  if (!(PORTD_Val&(1<<2))) // Detect pushbutton pressed at the pin PORTD2 (show T)
  {
   for(i=0; i<7; i++)
    matrix[i] = font_T[i];
  }
  
  if (!(PORTD_Val&(1<<1))) // Detect pushbutton pressed at the pin PORTD1 (show O)
  {
   for(i=0; i<7; i++)
    matrix[i] = font_O[i];
  }
  
  if (!(PORTD_Val&(1<<0))) // Detect pushbutton pressed at the pin PORTD0 (show M)
  {
   for(i=0; i<7; i++)
    matrix[i] = font_M[i];
  }
 }
.
.

I will leave you with a video of the game system I built. This one was made for the college competition with teammates Navneet and Neelav. As you can see I have managed four games and a menu based on scrolling text. I did not cover these in this article. Hope you are able to figure out yourselves :)