Email Print

Ive been sorting out the workshop here and found a project that I started around 12 years ago. I thought I would finish it and make it do something fun.

The project was an 8 x 8 LED grid. I used high brightness blue LEDs.

I wanted it to display something interesting and varying. I chose Conway's Game of Life as it is interesting, random, relatively simple rules and keeps modulating.

 There are loads of other projects out there doing much cooler things, but I felt like I ought to finish this project as it had been sitting around for sooo long.

Here is a video of the unit in action:

No image

The LEDs were connected together using waste 1mm copper wire and arranged in a grid so that all the anodes from one column of 8 are connected together and all the cathodes from one row of 8 are connected together.

No image

No image

The board is controlled by an AVR ATmega 328, programmed using the Arduino IDE. I wanted to use fewer pins, so rather than use 8 pins for the anodes and 8 pins for the cathode (16 pins in total), I used a 4017 decade counter to provide an output for the anodes. This has 10 outputs which go high in order when it is clocked. There is a reset pin to return the unit to its initial condition. I only needed 8 outputs, so I connected the 9th output pin to the reset. I also added a clock line and a reset line back to the arduino. Hence I could display everything with 10 pins (8 data, 1 clock, 1 reset). The reset is required to ensure that the unit always starts in the correct position, otherwise the starting line was slightly arbitrary.

This display could be made to display any 8x8 graphic. One day I'll turn it into a scrolling display.

No image

No image

The home-made Arduino PCB is stuck to the back. The front has been covered with diffuser polypropylene.

Conway's Game of Life is a mathematical puzzle with a few simple rules. Each 'cell' within the grid lives or dies depending upon four simple mathematical rules. These are based upon its neighbours:

  1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overcrowding.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

This can give rise to some interesting patterns.

I used the code from Jimmie P. Rogers LoL Shield, which already had Life programmed in (although his board uses Charlieplexing in order to control even more LEDs from hardly any output pins).

The Arduino code is here:

/*
 LED 8x8 display board
 by Matt Little
 This email address is being protected from spambots. You need JavaScript enabled to view it.
 
 www.re-innovation.co.uk
 
 Controls an 8x8 LED grid using 8 digital pins and a decade counter.
 
 The grid is connected via digital pins 2-9 for the cathodes 
 and via a 4017 decade counter for the anodes.
 Hence clocking through the decade counter while changing pins 2-9
 will give an output to the display.
 
 4017 decade counter data sheet is available from:
 http://www.doctronics.co.uk/pdf_files/4017.pdf
 
 It is wired so that the clock connects with pin 10 and 
 the reset is wired to output Q8, so only Q0-Q7 are used 
 (8 outputs for the 8 lines).
 
 The idea is that we clock through each segment of the display and upload
 a digital number for the pixels within the segment.
 If we do this quick enough, we cannot tell that there is any pause and out
 brain builds up an image (persistance of vision (POV)).
 
 
 We use a lot of port mainipulation here, as it speeds up the code.
 PortB = Pins 8-13
 PortC = Analog input pins
 PortD = Pins 0-7  
 
 This example code is in the public domain.
 */

// 1-dimensional array of row pin numbers:
const int col[8] = { 2,3,4,5,6,7,8,9 };
  
// The clock pin:  
int clock = 10;  

//The reset pin:
int resetCounter = 11;  // This is used to reset the decade counter and ensure it starts correctly


// Array of pixels - these are bytes (8 bit numbers):
byte pixels[8] = {B00000000,
                  B00000000,
                  B00000000,
                  B00000000,
                  B00000000,
                  B00000000,
                  B00000000,
                  B00000000};
                                   
byte dataD;
byte dataB;

long int delayCounter = 0;  // Used to set the refresh rate for the display

// Data for LIFE - 
#define DELAY 0             //Sets the time each generation is shown
#define RESEEDRATE 1000       //Sets the rate the world is re-seeded
#define SIZEX 8              //Sets the X axis size
#define SIZEY 8               //Sets the Y axis size
byte world[SIZEX][SIZEY][2];  //Creates a double buffer world
long density = 50;            //Sets density % during seeding
int geck = 0;                 //Counter for re-seeding


// the setup routine runs once when you press reset:
void setup() {
  
 // initialize the I/O pins as outputs
  // iterate over the pins:
  for (int thisPin = 0; thisPin < 8; thisPin++) 
  {
    // initialize the output pins:
    pinMode(col[thisPin], OUTPUT);  
    // take the col pins (i.e. the cathodes) high to ensure that
    // the LEDS are off:
    digitalWrite(col[thisPin], HIGH);    
  }  
    
  pinMode(clock, OUTPUT);  
  digitalWrite(clock, HIGH);   // turn the LED on (HIGH is the voltage level) 

  pinMode(resetCounter, OUTPUT);  
  digitalWrite(resetCounter, LOW);   // turn the LED on (HIGH is the voltage level) 
  delay(10);  
  digitalWrite(resetCounter, HIGH);   // turn the LED on (HIGH is the voltage level) 
  pinMode(resetCounter, INPUT);    // Set this pin to be a high impedance input - allows external reset to work
  
  for(int y=0;y<7;y++)  // This loop just clock through to the correct starting point
  {
    digitalWrite(clock, LOW);  // turn the LED on (HIGH is the voltage level)  
    delay(1);                  // very slight delay betwewen segments
    digitalWrite(clock, HIGH); // turn the LED on (HIGH is the voltage level)
  }
  
    // Initialise the LIFE
  randomSeed(analogRead(5));
  //Builds the world with an initial seed.
  for (int i = 0; i < SIZEX; i++) {
    for (int j = 0; j < SIZEY; j++) {
      if (random(100) < density) {
        world[i][j][0] = 1;
      }
      else {
        world[i][j][0] = 0;
      }
      world[i][j][1] = 0;
    }
  }
}

// the loop routine runs over and over again forever:
void loop() {
  
  for (int c=0;c<=7;c++)
  {
    digitalWrite(clock, LOW);  // turn the LED on (HIGH is the voltage level)  
    delayMicroseconds(100);
    digitalWrite(clock, HIGH); // turn the LED on (HIGH is the voltage level) 
    writeData(pixels[c]);
  }
  
  if(delayCounter>=500)
  {
     // Data for LIFE
     // Birth and death cycle 
    for (int x = 0; x < SIZEX; x++) { 
      for (int y = 0; y < SIZEY; y++) {
        // Default is for cell to stay the same
        world[x][y][1] = world[x][y][0];
        int count = neighbours(x, y); 
        geck++;
        if (count == 3 && world[x][y][0] == 0) {
          // A new cell is born
          world[x][y][1] = 1; 
          
          // Here we need to give birth to a life
          writePixels(x,y,HIGH);
        } 
        else if ((count < 2 || count > 3) && world[x][y][0] == 1) {
          // Cell dies
          world[x][y][1] = 0;
          // Here we remove a life
          writePixels(x,y,LOW);
        }
      }
    }
    
    //Counts and then checks for re-seeding
    //Otherwise the display will die out at some point
    geck++;
    if (geck > RESEEDRATE){
      seedWorld();
      geck = 0;
    }
  
    // Copy next generation into place
    for (int x = 0; x < SIZEX; x++) { 
      for (int y = 0; y < SIZEY; y++) {
        world[x][y][0] = world[x][y][1];
      }
    }
    
    delayCounter = 0;
  }
  delayCounter++;  // We use a delay loop (rather than a delay), as it keeps th display refreshed.
}

void writeData(byte data)
{
    // We need to write the bottom 6 bits of data to PORTD (PINS 2-7)
    // We need to write the top 2 bits of data to PORTB (PINS 8-9)
    data = data ^ B11111111;  // Invert the data (as it displays the opposite.
    dataD = data<<2;
    dataB = (data>>6)&B00000011;
    
    PORTD = dataD + (PORTD&B00000011);
    PORTB = dataB + (PORTB&B11111100);
   
}

void writePixels(int xx, int yy, boolean on)
{
    // We need to be able toe write to specific pixels to run the game of life
    // This is done by updating the pixels array
    // We need to find column y then replace row x with the boolean
    
    // Finding the column is easy = pixels[y]
    // Finding the row is harder. We need to take the 8 bit number in pixels[y]
    // Then alter just the bit that x relates to.
    if(on == HIGH)
    {
      // We want to set the bit
      bitSet(pixels[yy],xx);
    }
    else
    {
       // We want to clear the bit
      bitClear(pixels[yy],xx);
    }      
}


//Re-seeds based off of RESEEDRATE
void seedWorld(){
  randomSeed(analogRead(5));
  for (int i = 0; i < SIZEX; i++) {
    for (int j = 0; j < SIZEY; j++) {
      if (random(100) < density) {
        world[i][j][1] = 1;
      }
    } 
  }
}

//Runs the rule checks, including screen wrap
int neighbours(int x, int y) {
  return world[(x + 1) % SIZEX][y][0] + 
    world[x][(y + 1) % SIZEY][0] + 
    world[(x + SIZEX - 1) % SIZEX][y][0] + 
    world[x][(y + SIZEY - 1) % SIZEY][0] + 
    world[(x + 1) % SIZEX][(y + 1) % SIZEY][0] + 
    world[(x + SIZEX - 1) % SIZEX][(y + 1) % SIZEY][0] + 
    world[(x + SIZEX - 1) % SIZEX][(y + SIZEY - 1) % SIZEY][0] + 
    world[(x + 1) % SIZEX][(y + SIZEY - 1) % SIZEY][0]; 
}

I know there are much better and more complicated LED display boards out there, but I really wanted to finish this project and I learnt quite a lot about bit-wise programming (Cheers for your help, lwk).