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

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 .

The magnifying glass is a projector without the collimator so the picture will be upside down.
The focalpoint is actually a plane and not a point so a ledmatrix will be projected as an image.

It works better than a straw because it collects the light instead of blocking it.
The projected image brightness will be dependent on the distance since all of the light in a single led is averaged over the size of the projected dot of the led.

I have built a number of led discolight effects using this and it works even if the room is not dark.

PaulRB:
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?

That's up to you. If you haven't got a straw then try rolling some paper around a pencil or something.

Next idea: this display placed at a 45 degree angle, illuminated by a bright LED, perhaps a colour-cycling RGB, with a focussing lens placed above. What do you think?

PaulRB:
Next idea: this display placed at a 45 degree angle, illuminated by a bright LED, perhaps a colour-cycling RGB, with a focussing lens placed above. What do you think?

I don't think reflection will work very well but you never know.
I pulled apart one of the little Nokia LCD's and as suspected it does use the horrible rubberised connection to the PCB but I think it would be possible to cut out the centre of the PCB under the display matrix and shine a light through (I did not remove the reflective backing) the LCD mounted in a clear plastic/glass package.
Another idea would be a LCD like this (or this) that uses a flat ribbon connector so may lift off the backing board and work with light shone through it.

UPDATE: Recently purchased an inexpensive 128 x 64 OLED display on eBay which uses SPI bus. Inevitably, I just had to get Game Of Life running on that too...

// Conway's Game Of Life 128x64
// PaulRB
// Jun 2014

#include <SPI.h>

//Pins controlling SSD1306 Graphic OLED
#define OLED_DC     10
#define OLED_CS     9
#define OLED_RESET  7

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

void setup() {
  
  pinMode(OLED_DC, OUTPUT);
  pinMode(OLED_CS, OUTPUT);
  pinMode(OLED_RESET, OUTPUT);

  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2); // 8 MHz

  digitalWrite(OLED_RESET, HIGH);
  delay(1);
  digitalWrite(OLED_RESET, LOW);
  delay(10);
  digitalWrite(OLED_RESET, HIGH);

  digitalWrite(OLED_DC, LOW);
  digitalWrite(OLED_CS, LOW);
  
  SPI.transfer(0xAE); // Display off
  SPI.transfer(0xD5); // Set display clock divider
  SPI.transfer(0x80);
  SPI.transfer(0xA8); // Set multiplex 
  SPI.transfer(0x3F);
  SPI.transfer(0xD3); // Set display offset
  SPI.transfer(0x00);
  SPI.transfer(0x40); // Set start line to zero
  SPI.transfer(0x8D); // Set charge pump
  SPI.transfer(0x14);
  SPI.transfer(0x20); // Set memory mode
  SPI.transfer(0x00);
  SPI.transfer(0xA0 | 0x1); // Set segment remapping
  SPI.transfer(0xC8); // Set command Scan decode
  SPI.transfer(0xDA); // Set Comm pins
  SPI.transfer(0x12);
  SPI.transfer(0x81); // Set contrast
  SPI.transfer(0xCF);
  SPI.transfer(0xd9); // Set precharge
  SPI.transfer(0xF1);
  SPI.transfer(0xDB); // Set Vcom detect
  SPI.transfer(0x40);
  SPI.transfer(0xA4); // Allow display resume
  SPI.transfer(0xA6); // Set normal display
  SPI.transfer(0xAF); // Display On
  
  digitalWrite(OLED_CS, HIGH);

  //R-pentomino
  Matrix[64] = B0000010; Matrix[64] = Matrix[64] << 32;
  Matrix[65] = B0000111; Matrix[65] = Matrix[65] << 32;
  Matrix[66] = B0000100; Matrix[66] = Matrix[66] << 32;
  outputMatrix();

  Serial.begin(115200);
  	
}

void loop() {
  unsigned long start = millis();
  for (int i=0; i<=500; i++) {
    generateMatrix();
    outputMatrix();
  }
  Serial.print("Gens/s:"); Serial.println((millis() - start)/500);

}

void outputMatrix() {
  
  digitalWrite(OLED_DC, LOW); //Command mode
  digitalWrite(OLED_CS, LOW); //Enable display on SPI bus
  
  SPI.transfer(0x21); // Set column address
  SPI.transfer(0);
  SPI.transfer(127);

  SPI.transfer(0x22); // Set page address
  SPI.transfer(0);
  SPI.transfer(7);

  digitalWrite(OLED_CS, HIGH); //Disable display on SPI bus
  
  digitalWrite(OLED_DC, HIGH); // Data mode
  digitalWrite(OLED_CS, LOW); //Enable display on SPI bus

  //Send matrix data for display on OLED
  for (byte x = 0; x < 64; x+=8) {
    
    for (byte row = 0; row <= 127; row++) {
      SPI.transfer(Matrix[row] >> x);
    }	
    
  }
  digitalWrite(OLED_CS, 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] = random(0xffff) << 16 | random(0xffff);
    }
  }
}
	
void injectGlider() {

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

}
	
int generateMatrix() {

  //Variables holding data on neighbouring cells
  unsigned long long NeighbourN, NeighbourNW, NeighbourNE, CurrCells, NeighbourW, NeighbourE, NeighbourS, NeighbourSW, NeighbourSE;
	
  //Variables used in calculating new cells
  unsigned long long tot1, carry, tot2, tot4, NewCells;
  
  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
  NeighbourN = Matrix[127];
  CurrCells = Matrix[0];
  Matrix[128] = CurrCells;  // copy row 0 to location after last row to remove need for wrap-around code in the loop

  NeighbourNW = NeighbourN >> 1 | NeighbourN << 63; 
  NeighbourNE = NeighbourN << 1 | NeighbourN >> 63;
	
  NeighbourW = CurrCells >> 1 | CurrCells << 63;
  NeighbourE = CurrCells << 1 | CurrCells >> 63;
  
  //Process each row of the matrix
  for (byte row = 0; row <= 127; row++) {
		
    //Pick up new S, SW & SE neighbours
    NeighbourS = Matrix[row + 1];
  
    NeighbourSW = NeighbourS >> 1 | NeighbourS << 63;
    NeighbourSE = NeighbourS << 1 | NeighbourS >> 63;

    //Any live cells at all in this region?
    if (CurrCells | NeighbourN | NeighbourS | NeighbourE | NeighbourW | NeighbourNE | NeighbourNW | NeighbourSE | NeighbourSW > 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 = NeighbourN;
      tot2 = tot1 & NeighbourNW; tot1 = tot1 ^ NeighbourNW;
      carry = tot1 & NeighbourNE; tot1 = tot1 ^ NeighbourNE; tot4 = tot2 & carry; tot2 = tot2 ^ carry;
      carry = tot1 & NeighbourW; tot1 = tot1 ^ NeighbourW; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
      carry = tot1 & NeighbourE; tot1 = tot1 ^ NeighbourE; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
      carry = tot1 & NeighbourS; tot1 = tot1 ^ NeighbourS; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
      carry = tot1 & NeighbourSW; tot1 = tot1 ^ NeighbourSW; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
      carry = tot1 & NeighbourSE; tot1 = tot1 ^ NeighbourSE; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
		
      //Calculate the updated cells:
      // <2 or >3 neighbours, cell dies
      // =2 neighbours, cell continues to live
      // =3 neighbours, new cell born
      NewCells = (CurrCells | tot1) & tot2 & ~ tot4;
      
      //Have any cells changed?
      if (NewCells != CurrCells) {       
        //Count the change for "stale" test
        changes++;
      }

      Matrix[row] = NewCells;
    }

    //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 = CurrCells;
    NeighbourNW = NeighbourW;
    NeighbourNE = NeighbourE;
    NeighbourE = NeighbourSE;
    NeighbourW = NeighbourSW;
    CurrCells = NeighbourS;
    }
    
    if (changes != prevChanges) staleCount = 0; else staleCount++; //Detect "stale" matrix
    if (staleCount > 32) injectGlider(); //Inject a glider

    prevChanges = changes;
  }

In case anyone is interested, I'm getting around 45 generations per second with this setup (16MHz Pro Micro, updating 128x64 OLED using hardware SPI at max available speed of 8MHz). Each life matrix generation is taking roughly 12ms and updating the OLED (every pixel) is taking around 10ms.

Recently purchased a Maple Mini clone on eBay, for very little money. This is a 32-bit ARM based micro controller running at 72MHz.

Converting the sketch for the Maple was very straightforward. The only changes were those around the Serial connection to the PC (only used for reporting the generations per second achieved) and the SPI connection to the OLED display. I did no performance tuning except to increase the SPI clock speed from 8MHz to 18MHz.

New speed record: 392 generations per second!

The increase in clock speed from 16MHz to 72 MHz (x4.5) should have boosted the generations per second from around 45 to 200. The 32-bit processor seems to be doubling the speed again versus the 8 bit in the Arduino.

// Conway's Game Of Life 128x64
// PaulRB
// Jun 2014

HardwareSPI spi(1);

//Pins controlling SSD1306 Graphic OLED
#define OLED_DC     1
#define OLED_CS     0
#define OLED_RESET  2

union MatrixData {
  unsigned long long l;
  byte b[8];
};

MatrixData Matrix[129]; // Cell data in ram

void setup() {
  
  pinMode(OLED_DC, OUTPUT);
  pinMode(OLED_CS, OUTPUT);
  pinMode(OLED_RESET, OUTPUT);

  spi.begin(SPI_18MHZ, MSBFIRST, 0);

  digitalWrite(OLED_RESET, HIGH);
  delay(1);
  digitalWrite(OLED_RESET, LOW);
  delay(10);
  digitalWrite(OLED_RESET, HIGH);

  digitalWrite(OLED_DC, LOW);
  digitalWrite(OLED_CS, LOW);
  
  spi.write(0xAE); // Display off
  spi.write(0xD5); // Set display clock divider
  spi.write(0x80);
  spi.write(0xA8); // Set multiplex 
  spi.write(0x3F);
  spi.write(0xD3); // Set display offset
  spi.write(0x00);
  spi.write(0x40); // Set start line to zero
  spi.write(0x8D); // Set charge pump
  spi.write(0x14);
  spi.write(0x20); // Set memory mode
  spi.write(0x00);
  spi.write(0xA0 | 0x1); // Set segment remapping
  spi.write(0xC8); // Set command Scan decode
  spi.write(0xDA); // Set Comm pins
  spi.write(0x12);
  spi.write(0x81); // Set contrast
  spi.write(0xCF);
  spi.write(0xd9); // Set precharge
  spi.write(0xF1);
  spi.write(0xDB); // Set Vcom detect
  spi.write(0x40);
  spi.write(0xA4); // Allow display resume
  spi.write(0xA6); // Set normal display
  spi.write(0xAF); // Display On
  
  digitalWrite(OLED_CS, HIGH);

  //R-pentomino
  Matrix[64].l = B0000010; Matrix[64].l = Matrix[64].l << 32;
  Matrix[65].l = B0000111; Matrix[65].l = Matrix[65].l << 32;
  Matrix[66].l = B0000100; Matrix[66].l = Matrix[66].l << 32;
  
  //randomiseMatrix();
  outputMatrix();

}

void loop() {
  unsigned long start = millis();
  for (int i=0; i<1000; i++) {
    generateMatrix();
    outputMatrix();
  }
  SerialUSB.print("Gens/s:"); SerialUSB.println(1000000/(millis() - start));

}

void outputMatrix() {
  
  digitalWrite(OLED_DC, LOW); //Command mode
  digitalWrite(OLED_CS, LOW); //Enable display on SPI bus
  
  spi.write(0x21); // Set column address
  spi.write(0);
  spi.write(127);

  spi.write(0x22); // Set page address
  spi.write(0);
  spi.write(7);

  digitalWrite(OLED_CS, HIGH); //Disable display on SPI bus
  
  digitalWrite(OLED_DC, HIGH); // Data mode
  digitalWrite(OLED_CS, LOW); //Enable display on SPI bus

  //Send matrix data for display on OLED
  for (byte col = 0; col < 8; col++) {
    
    for (byte row = 0; row <= 127; row++) {
      spi.write(Matrix[row].b[col]);
    }	
    
  }
  digitalWrite(OLED_CS, HIGH);
}
	
void randomiseMatrix() {

  //Set up initial cells in matrix
  randomSeed(analogRead(0));
  for (byte row = 0; row <= 127; row++) {
    for (byte col = 0; col <= 8; col++) {
      Matrix[row].b[col] = random(0xff);
    }
  }
}
	
void injectGlider() {

  byte col = random(127);
  byte row = random(63);
  Matrix[col++].l |= ((unsigned long long) B0000111) << row;
  Matrix[col++].l |= ((unsigned long long) B0000001) << row;
  Matrix[col++].l |= ((unsigned long long) B0000010) << row;

}
	
int generateMatrix() {

  //Variables holding data on neighbouring cells
  unsigned long long NeighbourN, NeighbourNW, NeighbourNE, CurrCells, NeighbourW, NeighbourE, NeighbourS, NeighbourSW, NeighbourSE;
	
  //Variables used in calculating new cells
  unsigned long long tot1, carry, tot2, tot4, NewCells;
  
  int changes = 0; // counts the changes in the matrix
  static int prevChanges[4]; // counts the changes in the matrix on prev 4 generations
  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
  NeighbourN = Matrix[127].l;
  CurrCells = Matrix[0].l;
  Matrix[128].l = CurrCells;  // copy row 0 to location after last row to remove need for wrap-around code in the loop

  NeighbourNW = NeighbourN >> 1 | NeighbourN << 63; 
  NeighbourNE = NeighbourN << 1 | NeighbourN >> 63;
	
  NeighbourW = CurrCells >> 1 | CurrCells << 63;
  NeighbourE = CurrCells << 1 | CurrCells >> 63;
  
  //Process each row of the matrix
  for (byte row = 0; row <= 127; row++) {
		
    //Pick up new S, SW & SE neighbours
    NeighbourS = Matrix[row + 1].l;
    
    NeighbourSW = NeighbourS >> 1 | NeighbourS << 63;

    NeighbourSE = NeighbourS << 1 | NeighbourS >> 63;

    //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 = NeighbourN;
    tot2 = tot1 & NeighbourNW; tot1 = tot1 ^ NeighbourNW;
    carry = tot1 & NeighbourNE; tot1 = tot1 ^ NeighbourNE; tot4 = tot2 & carry; tot2 = tot2 ^ carry;
    carry = tot1 & NeighbourW; tot1 = tot1 ^ NeighbourW; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
    carry = tot1 & NeighbourE; tot1 = tot1 ^ NeighbourE; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
    carry = tot1 & NeighbourS; tot1 = tot1 ^ NeighbourS; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
    carry = tot1 & NeighbourSW; tot1 = tot1 ^ NeighbourSW; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
    carry = tot1 & NeighbourSE; tot1 = tot1 ^ NeighbourSE; tot4 = tot2 & carry | tot4; tot2 = tot2 ^ carry;
		
    //Calculate the updated cells:
    // <2 or >3 neighbours, cell dies
    // =2 neighbours, cell continues to live
    // =3 neighbours, new cell born
    NewCells = (CurrCells | tot1) & tot2 & ~ tot4;
    
    //Have any cells changed?
    if (NewCells != CurrCells) {       
      //Count the change for "stale" test
      changes++;
      Matrix[row].l = NewCells;
    }

    //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 = CurrCells;
    NeighbourNW = NeighbourW;
    NeighbourNE = NeighbourE;
    NeighbourE = NeighbourSE;
    NeighbourW = NeighbourSW;
    CurrCells = NeighbourS;
  }
    
  if (changes != prevChanges[0] && changes != prevChanges[1] && changes != prevChanges[2] && changes != prevChanges[3]) {
    staleCount = 0;
  }
  else {
    staleCount++; //Detect "stale" matrix
  }
    
  if (staleCount > 64) injectGlider(); //Inject a glider
  //SerialUSB.println(changes);

  for (int i=3; i>0; i--) {
    prevChanges[i] = prevChanges[i-1];
  }

  prevChanges[0] = changes;
}

Paul

Just wanted to say thanks for posting the Game of Life sketch for the oled. I'm stricktly a hardware guy and it inspired me to create a super tiny version. Also an excuse to try out a few hardware ideas like a 50mil pitch ISP.

IMG_0196.JPG

I have Brian's Brain - another cellular automata - running at 250x250 via a Gameduino and a Maple.

On the plus side, Brain doesn't need any prodding to stay alive: once your area is large enough, it will keep going forever without settling down, like Life.

On the minus side, having three states per cell and a large area means it only fits in memory because of some compression and that slows down things a bit. The tricky way you get to do bitmap pixel addressing on the Gameduino doesn't help either.

A fairly simple implementation is running at about 300-350ms/generation, depending on how busy it is. About 110ms is drawing changed pixels, the rest is working out which those are! :slight_smile:

I'd spend time making it quicker, but I suspect that it'd just be much easier to get it running on a Raspberry Pi.

(Having said that, one quick twiddle means the average over a 1,000 generations is 307ms each, so that leaves about 200ms of calculating to reduce. I'm now wondering what a Gameduino 2 would run it at.

Doing that twiddle to the outer columns takes the average for the same starting position down to 269ms, even though they were only 1/125th of cells done the old way! Clearly having a sequence of jumps taken through 'if's conditions being false is very slow on the ARM.)

It's worth experimenting with the length of the 'bunch of cells' variables.

Using the 'do multiple neighbour counts at once' approach with another alive/dead CA on the Maple works out to take

.. 30 ms/gen with byte variables for the bunches
.. 24 ms/gen with uint16
.. 20 ms/gen unsigned long
.. 101 ms/gen unsigned long long

for updating a 256x256 cell world. Clearly, 32 bit variables are faster for the Maple's ARM, even though you need to deal with twice as many as when you're using 64 bit ones.

(Ooh, adding something that tests if the neighbourhood bunches are all zero - or, for this one, ones, which would be pointless for Life - and acting accordingly takes the byte / uint16 / unsigned long times down to 23 ms / 19 ms / 9 ms = over 100 gens/second for 256x256. Although the longer sizes trigger the test less frequently, they save more time when they do.)

Ooh2: If you test for being all zero, it looks like the size of bunch length is almost irrelevant for Life. 'Obviously' 64 bits stays by far the worst, but starting with a random 50% alive state, bytes are a small fraction quicker than uint16s which are a tiny fraction quicker than unsigned longs. There's enough stable/blinking debris to mean that the longer lengths are all zero much less of the time, so the overhead of the test wipes out more of the savings.

Hello everyone, first post here.

As these things usually go, I've registered to ask a newbie question. Sorry if that sounds like just being lazy, but I've tried to search for anwser first and my confusion didn't really go down anyhow, hehe.

I've recently started my adventure with Arduino and my focus was on controlling a small display. Got myself an OLED 128x64 from a chinese manufacturer and surprisingly it has correctly marked ISP pins.
They go like this: GND, VCC, CLK, MOSI, DC, CS.

It works perfectly with Adafruit library, using software SPI, even with no Reset connected.

// If using software SPI (the default case):
#define OLED_MOSI   9
#define OLED_CLK   10
#define OLED_DC    11
#define OLED_CS    12
#define OLED_RESET 13

However I'm totally lost on how to use Hardware SPI like this:

/* Uncomment this block to use hardware SPI
#define OLED_DC     6
#define OLED_CS     7
#define OLED_RESET  8

That wouldn't bother me the slightest with Adafruit libraries, because they work fine on SW SPI, but in the code for GoL presented here I find:

HardwareSPI spi(1);

//Pins controlling SSD1306 Graphic OLED
#define OLED_DC     1
#define OLED_CS     0
#define OLED_RESET  2

...and that's such a joykiller. Can anyone explain me if I can convert the code posted by **PaulRB ** to use my setup, or am I in complete loss due to the lack of Reset line?

Hi Mef82,

If I were you I would use the previous version of the GoL sketch I posted. The version you have there was translated to work in the Maple IDE rather than the Arduino IDE.

However, that won't fix your lack of a reset line. I would try simply removing all lines in the sketch that refer to the reset line and see what happens. There is a reasonably good chance it will work.

If it does not, it may be because the display does not get reset when the Arduino gets reset, so try "cycling the power" to the display and Arduino together (in other words force both to reset together by cutting the power and reconnecting it again).

Paul

Newbie here. How would one convert this to work on a display with I2C instead of SPI such as this one?
http://www.ebay.com/itm/0-96-I2C-IIC-SPI-Serial-128X64-White-OLED-LCD-LED-Display-Module-for-Arduino-/181325476747

Hi, I have one of those too. But I never bothered to get the GOL sketch working with it because I assume it would be considerably slower than the spi version. If you get it working, let me know. I could then measure the speed relative to the spi version.

I have thought about buying 7 more spi oleds and tiling them to make a 256x256 GOL grid...

PaulRB:
I have thought about buying 7 more spi oleds and tiling them to make a 256x256 GOL grid...

Well, I did make a 256x256 GOL grid. I used a Maple Mini clone, but now I am uploading to it using the Arduino IDE instead of the Maple IDE.

I'm still using one 128x64 SPI OLED display, which shows a view on the larger grid. The view pans around automatically to try to show a part of the grid where something hopefully interesting is going on.

Video

Code attached (now too long to post).

Next idea I am toying with is a large led matrix, to have more "visual impact" than the tiny OLED screen. I don't want to spend too much, so a 64x64 matrix looks reasonably affordable. I could build this from 4 of these 64x16 modules. The MM would need to perform the multiplexing with this type of display, but I don't think that will be a problem.

Life256x256OLED64bitMaple.ino (8.78 KB)