Conway's Game Of Life with 128x64 graphic LCD. UPDATE: also with 128x64 SPI OLED

This is a bit of an "unfinished work", but I though I should write it up before I forget the details!

Conway's game of life, in case you don't know, is a simulation of colonies of cells living on a rectangular grid. Each position on the grid can contain a cell or be empty, and over each generation, a cell can continue to live, or die, or a new cell can be born, depending on a few very simple rules to do with how many cells are living in the immediate neighbourhood.

I originally developed the code on a Nano 3, then transferred it to a stand-alone ATmega328, using the Nano running ArduinoISP to program it. It should be easy to get running on any Arduino with a similar display connected.

Video: Conway's Game of Life Arduino (digitalWriteFast) - YouTube

// Conway's Game Of Life 128x64
// PaulRB
// Jun 2013
// Sept 2013 updated to use digitalWriteFast

#include <digitalWriteFast.h>

//Pins controlling KS0108 Graphic LCD
const byte GLCD_RS = 12; // H = data, L = instruction
const byte GLCD_E = 8;
const byte GLCD_DATA0 = 7;
const byte GLCD_DATA1 = 6;
const byte GLCD_DATA2 = 5;
const byte GLCD_DATA3 = 9;
const byte GLCD_DATA4 = 10;
const byte GLCD_DATA5 = 11;
const byte GLCD_DATA6 = 13;
const byte GLCD_DATA7 = 4;
const byte GLCD_CS1 = 3; // left half of screen
const byte GLCD_CS2 = 2; // right half of screen

const byte GLCDDelay = 3; // Enable GLCD to keep up with Arduino
const int redrawMethodBoundary = 320;
const byte SpeedPot = A6;

unsigned long Matrix[129][2]; // Cell data in ram

void setup() {
  
  pinModeFast(GLCD_CS1, OUTPUT);
  pinModeFast(GLCD_CS2, OUTPUT);
  pinModeFast(GLCD_RS, OUTPUT);
  pinModeFast(GLCD_E, OUTPUT);
  pinModeFast(GLCD_DATA0, OUTPUT);
  pinModeFast(GLCD_DATA1, OUTPUT);
  pinModeFast(GLCD_DATA2, OUTPUT);
  pinModeFast(GLCD_DATA3, OUTPUT);
  pinModeFast(GLCD_DATA4, OUTPUT);
  pinModeFast(GLCD_DATA5, OUTPUT);
  pinModeFast(GLCD_DATA6, OUTPUT);
  pinModeFast(GLCD_DATA7, OUTPUT);

  digitalWriteFast(GLCD_CS1, HIGH);
  digitalWriteFast(GLCD_CS2, HIGH);
  digitalWriteFast(GLCD_RS, LOW);
  digitalWriteFast(GLCD_E, LOW);

  sendGLCD(B00111111); // Enable display
  sendGLCD(B11000000); // Set Scroll Position = 0
        
  randomiseMatrix();
  outputMatrix();
  
  //Serial.begin(38400);
	
}

void loop() {
  generateMatrix();
  //delay(analogRead(SpeedPot));
}

void sendGLCD(byte b) { //Send a byte to the GLCD

  static byte oldb; // static variables keep their value between calls

  digitalWriteFast(GLCD_E, HIGH);
  delayMicroseconds(GLCDDelay); 
  
  if (b != oldb) { // do we need to change the pins?
    if (b & B00000001) digitalWriteFast(GLCD_DATA0, HIGH); else digitalWriteFast(GLCD_DATA0, LOW);
    if (b & B00000010) digitalWriteFast(GLCD_DATA1, HIGH); else digitalWriteFast(GLCD_DATA1, LOW);
    if (b & B00000100) digitalWriteFast(GLCD_DATA2, HIGH); else digitalWriteFast(GLCD_DATA2, LOW);
    if (b & B00001000) digitalWriteFast(GLCD_DATA3, HIGH); else digitalWriteFast(GLCD_DATA3, LOW);
    if (b & B00010000) digitalWriteFast(GLCD_DATA4, HIGH); else digitalWriteFast(GLCD_DATA4, LOW);
    if (b & B00100000) digitalWriteFast(GLCD_DATA5, HIGH); else digitalWriteFast(GLCD_DATA5, LOW);
    if (b & B01000000) digitalWriteFast(GLCD_DATA6, HIGH); else digitalWriteFast(GLCD_DATA6, LOW);
    if (b & B10000000) digitalWriteFast(GLCD_DATA7, HIGH); else digitalWriteFast(GLCD_DATA7, LOW);
    oldb = b;
  }
  
  digitalWriteFast(GLCD_E, LOW); // data is read by glcd on falling edge
  delayMicroseconds(GLCDDelay);

}

void outputMatrix() {
  
  //Send matrix data for display on GLCD
  for (byte x = 0; x <= 7; x++) {
    
    byte col = x >> 2;
    byte b = (x & 3) << 3;
    digitalWriteFast(GLCD_CS1, HIGH);
    digitalWriteFast(GLCD_CS2, HIGH);
    digitalWriteFast(GLCD_RS, LOW);
    sendGLCD(B01000000); // Set Y = 0
    sendGLCD(B10111000 | x); // set X
    digitalWriteFast(GLCD_RS, HIGH);
    digitalWriteFast(GLCD_CS1, HIGH);
    digitalWriteFast(GLCD_CS2, LOW);
    for (byte row = 0; row <= 127; row++) {
      sendGLCD(Matrix[row][col] >> b);
      if (row == 63) {
        digitalWriteFast(GLCD_CS1, LOW);
        digitalWriteFast(GLCD_CS2, HIGH);
      }
    }	
  }
}
	
void randomiseMatrix() {

  //Set up initial cells in matrix
  randomSeed(analogRead(0));
  for (byte row = 0; row <= 127; row++) {
    for (byte col = 0; col <= 1; col++) {
      Matrix[row][col] = random(0xffff) << 16 | random(0xffff);
    }
  }
}
	
void injectGlider() {

  byte col = random(127);
  byte row = random(1);
  Matrix[col++][row] |= B0000111;
  Matrix[col++][row] |= B0000001;
  Matrix[col++][row] |= B0000010;

}
	
int generateMatrix() {

  //Variables holding data on neighbouring cells
  unsigned long NeighbourN[2], NeighbourNW[2], NeighbourNE[2], CurrCells[2], NeighbourW[2], NeighbourE[2], NeighbourS[2], NeighbourSW[2], NeighbourSE[2];
	
  //Variables used in calculating new cells
  unsigned long tot1[2], carry[2], tot2[2], tot4[2], NewCells[2];
  
  int changes = 0; // counts the changes in the matrix
  static int prevChanges = 256; // counts the changes in the matrix on prev generation
  static int staleCount = 0; // counts the consecutive occurrances of the same number of changes in the matrix

  //set up N, NW, NE, W & E neighbour data
  for (byte b = 0; b <= 1; b++) {
    NeighbourN[b] = Matrix[127][b];
    CurrCells[b] = Matrix[0][b];
    Matrix[128][b] = CurrCells[b];  // copy row 0 to location after last row to remove need for wrap-around code in the loop
  }

  for (byte b = 0; b <= 1; b++) {
    NeighbourNW[b] = NeighbourN[b] >> 1; 
    bitWrite(NeighbourNW[b], 31, bitRead(NeighbourN[1-b], 0));
    NeighbourNE[b] = NeighbourN[b] << 1;
    bitWrite(NeighbourNE[b], 0, bitRead(NeighbourN[1-b], 31));
	
    NeighbourW[b] = CurrCells[b] >> 1;
    bitWrite(NeighbourW[b], 31, bitRead(CurrCells[1-b], 0));
    NeighbourE[b] = CurrCells[b] << 1;
    bitWrite(NeighbourE[b], 0, bitRead(CurrCells[1-b], 31));

  }
  
  digitalWriteFast(GLCD_CS1, HIGH);
  digitalWriteFast(GLCD_CS2, LOW);
  
  //Process each row of the matrix
  for (byte row = 0; row <= 127; row++) {
		
    //Pick up new S, SW & SE neighbours
    for (byte b = 0; b <= 1; b++) {
      NeighbourS[b] = Matrix[row + 1][b];
      }
  
    for (byte b = 0; b <= 1; b++) {
      NeighbourSW[b] = NeighbourS[b] >> 1;
      bitWrite(NeighbourSW[b], 31, bitRead(NeighbourS[1-b], 0));
      NeighbourSE[b] = NeighbourS[b] << 1;
      bitWrite(NeighbourSE[b], 0, bitRead(NeighbourS[1-b], 31));
  
      //Any live cells at all in this region?
      if (CurrCells[b] | NeighbourN[b] | NeighbourS[b] | NeighbourE[b] | NeighbourW[b] | NeighbourNE[b] | NeighbourNW[b] | NeighbourSE[b] | NeighbourSW[b] > 0) {
      
        //Count the live neighbours (in parallel) for the current row of cells
        //However, if total goes over 3, we don't care (see below), so counting stops at 4
        tot1[b] = NeighbourN[b];
        tot2[b] = tot1[b] & NeighbourNW[b]; tot1[b] = tot1[b] ^ NeighbourNW[b];
        carry[b] = tot1[b] & NeighbourNE[b]; tot1[b] = tot1[b] ^ NeighbourNE[b]; tot4[b] = tot2[b] & carry[b]; tot2[b] = tot2[b] ^ carry[b];
        carry[b] = tot1[b] & NeighbourW[b]; tot1[b] = tot1[b] ^ NeighbourW[b]; tot4[b] = tot2[b] & carry[b] | tot4[b]; tot2[b] = tot2[b] ^ carry[b];
        carry[b] = tot1[b] & NeighbourE[b]; tot1[b] = tot1[b] ^ NeighbourE[b]; tot4[b] = tot2[b] & carry[b] | tot4[b]; tot2[b] = tot2[b] ^ carry[b];
        carry[b] = tot1[b] & NeighbourS[b]; tot1[b] = tot1[b] ^ NeighbourS[b]; tot4[b] = tot2[b] & carry[b] | tot4[b]; tot2[b] = tot2[b] ^ carry[b];
        carry[b] = tot1[b] & NeighbourSW[b]; tot1[b] = tot1[b] ^ NeighbourSW[b]; tot4[b] = tot2[b] & carry[b] | tot4[b]; tot2[b] = tot2[b] ^ carry[b];
        carry[b] = tot1[b] & NeighbourSE[b]; tot1[b] = tot1[b] ^ NeighbourSE[b]; tot4[b] = tot2[b] & carry[b] | tot4[b]; tot2[b] = tot2[b] ^ carry[b];
		
        //Calculate the updated cells:
        // <2 or >3 neighbours, cell dies
        // =2 neighbours, cell continues to live
        // =3 neighbours, new cell born
        NewCells[b] = (CurrCells[b] | tot1[b]) & tot2[b] & ~ tot4[b];
        
        //Have any cells changed?
        if (NewCells[b] != CurrCells[b]) {
          
          //Count the change for "stale" test
          changes++;
         
          if (prevChanges <= redrawMethodBoundary) {
            
            //Faster to redraw just changed parts of screen at this low level of changes
            unsigned long before = CurrCells[b];
            unsigned long after = NewCells[b];
            byte col = b << 2;
          
            for (byte x = 0; x <= 3; x++) {
            
              if ((before & 0xFF) != (after & 0xFF)) {
              
                digitalWriteFast(GLCD_RS, LOW);
                sendGLCD(B01000000 | row); // Set y
                sendGLCD(B10111000 | col); // set X
                digitalWriteFast(GLCD_RS, HIGH);
                sendGLCD(after & 0xFF);
              
              }
              before = before >> 8;
              after = after >> 8;
              col++;
            }
          }	
        }

        Matrix[row][b] = NewCells[b];
        }
  
      //Current cells (before update), E , W, SE, SW and S neighbours become
      //new N, NW, NE, E, W neighbours and current cells for next loop
      NeighbourN[b] = CurrCells[b];
      NeighbourNW[b] = NeighbourW[b];
      NeighbourNE[b] = NeighbourE[b];
      NeighbourE[b] = NeighbourSE[b];
      NeighbourW[b] = NeighbourSW[b];
      CurrCells[b] = NeighbourS[b];
      }
    if (row == 63) {
      digitalWriteFast(GLCD_CS1, LOW);
      digitalWriteFast(GLCD_CS2, HIGH);
      }
    } //next row
    
    if (changes != prevChanges) staleCount = 0; else staleCount++; //Detect "stale" matrix
    if (staleCount > 32) injectGlider(); //Inject a glider

    if (prevChanges > redrawMethodBoundary) outputMatrix(); //Faster to redraw entire screen at this high level of changes

    prevChanges = changes;
  }

Nice project, I've always been interested in cellular automata and I may be tempted to knock this up just for the hell of it. :slight_smile:

Joe Brand...

She's married. You've got no chance.

Seriously, how come you even know that name fungus? Are you not a real Spaniard?

PaulRB:
She's married. You've got no chance.

Seriously, how come you even know that name fungus? Are you not a real Spaniard?

She was on QI. Everybody watches QI...

(and Top Gear)

fungus:
Everybody watches QI...

I did not know that. I know Top Gear is one of the most popular programmes in the world and must make a lot of money for the BBC (and Clarkson), but I imagined QI would only appeal to the Brits... Anyway, we are getting off topic!

I would like to take the Game of Life toy/ornament idea to a new level. Its a bit of a pipe dream, I have no real idea how to achieve it. I would like to project a larger grid, perhaps 256 x 256, onto the walls/ceiling of a darkened room. Any thoughts how this could be achieved on a budget with off-the-shelf components?

PaulRB:
I would like to take the Game of Life toy/ornament idea to a new level. Its a bit of a pipe dream, I have no real idea how to achieve it. I would like to project a larger grid, perhaps 256 x 256, onto the walls/ceiling of a darkened room. Any thoughts how this could be achieved on a budget with off-the-shelf components?

I think the bedside projection alarm clocks just shine a focused light through a LCD panel. Maybe you could prove the idea by hardware hacking a cheap LCD matrix display and see if you can work out the required optics to focus a projection.
A more expensive (though > £120) idea would be to use a Raspberry Pi and a pico projector

Yeah, those are the lines I've been thinking along too. The RasPi/Pico projector is more than I want to spend, and its a bit too ready made anyway. Projecting through a graphic LCD is one I though of too. The concern for me with that idea may be that "hacking" them may well destroy them. You would have to remove the reflective backing and the PCB...

Another idea I'm mulling over is a POV-type solution involving a densely-packed line of LEDs, such as 8 of these http://www.ebay.co.uk/itm/5V-LED-TEST-PCB-Arduino-Basic-Stamp-Digital-Electronics-Prototype-Breadboard-PIC-/321240815778?pt=UK_BOI_Electrical_Components_Supplies_ET&var=&hash=item4acb71d8a2, a lens and a rotating mirror?

PaulRB:
Projecting through a graphic LCD is one I though of too.

Very unlikely to work. It will be almost impossible to focus the image.

PaulRB:
Another idea I'm mulling over is a POV-type solution involving a densely-packed line of LEDs, such as 8 of these a lens and a rotating mirror?

Same problem. The light from a LED isn't going to form a tiny dot on a wall/ceiling.

PaulRB:
Projecting through a graphic LCD is one I though of too. The concern for me with that idea may be that "hacking" them may well destroy them. You would have to remove the reflective backing and the PCB...
But for £1.89 it might be worth testing.

Another idea I'm mulling over is a POV-type solution involving a densely-packed line of LEDs, such as 8 of these http://www.ebay.co.uk/itm/5V-LED-TEST-PCB-Arduino-Basic-Stamp-Digital-Electronics-Prototype-Breadboard-PIC-/321240815778?pt=UK_BOI_Electrical_Components_Supplies_ET&var=&hash=item4acb71d8a2, a lens and a rotating mirror?
They look remarkably like Majenco's LED panels. I wonder if they would be bright enough. How far are you hoping to project and to what size?

fungus:
It will be almost impossible to focus the image... The light from a LED isn't going to form a tiny dot on a wall/ceiling.

Sharp focus is not critical. In fact, soft focused patches might look quite good, as long as they don't overlap too much.

Riva:
£1.89 it might be worth testing.

Yes I suppose.

Riva:
They look remarkably like Majenco's LED panels. I wonder if they would be bright enough. How far are you hoping to project and to what size?

Who or what is Majenco? Did you mean Jameco? I just imagined projecting the image onto the ceiling and perhaps the walls also in an averaged size room, so distances 2~4m.

I should really move this discussion to Project Guidance.

The penny has dropped!

Majenco is this guy. He helped me with a sprintf question recently.

So you must mean this or this.

Yes that Majenco and the latter of the two ebay links you posted.

I wonder if you could do this on Arduino with a 2D MEMS chip and laser(s)

Riva:
I wonder if you could do this on Arduino with a 2D MEMS chip and laser(s)

Sure... that's what some of those pico-projectors use to generate the image. It won't be cheap though.

Very cool, Riva, unfortunately does not (yet) fit the criteria of "budget" or "off-the-shelf" !

I think I have an answer!

I just tried shining a LED through a black drinking straw, you get quite a nice little circle of light on the ceiling of a dark room.

The 'projector' would be quite big.... it won't be easy to get a nice regular grid of pixels to cover a large area...but it can be done!

Use a magnifying glass and put the leds at the focal point.

Easy and makes a very simple projector.

fungus:
I just tried shining a LED through a black drinking straw, you get quite a nice little circle of light on the ceiling of a dark room.

Interesting... how long does the straw need to be to get the effect, or rather how short can you make it and still get the effect?

janost:
Use a magnifying glass and put the leds at the focal point.

Easy and makes a very simple projector.

Yes, that's the kind of thing.

Remember I want to achieve a high pixel count, at least 64x64 but ideally higher than the 128x64 I have now. Clearly 4,096 individual LEDs is impractical, so some POV/scanning technique would be needed.

Another idea I had was to use a 128x64 OLED display and put a magnifying glass above that .