Go Down

Topic: Conway's Game Of Life with 128x64 graphic LCD. UPDATE: also with 128x64 SPI OLED (Read 6815 times) previous topic - next topic

PaulRB

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

fungus

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!

No, I don't answer questions sent in private messages (but I do accept thank-you notes...)

janost

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

Easy and makes a very simple projector.

PaulRB


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?

PaulRB

#19
Feb 12, 2014, 01:35 pm Last Edit: Feb 12, 2014, 01:37 pm by PaulRB Reason: 1

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 .

janost

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.

fungus


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.



No, I don't answer questions sent in private messages (but I do accept thank-you notes...)

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?

Riva


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.
Once I almost saw Elvis but then my shovel broke. :(

PaulRB

#24
Jun 05, 2014, 01:30 am Last Edit: Jun 06, 2014, 12:43 am by PaulRB Reason: 1
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...

http://youtu.be/6-LDtmLk0iE

Code: [Select]


// 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;
  }


PaulRB

#25
Jun 05, 2014, 10:34 pm Last Edit: Jun 06, 2014, 12:45 am by PaulRB Reason: 1
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.

PaulRB

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.

http://youtu.be/mHleIPZmlk0

Code: [Select]

// 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

0miker0

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.

http://youtu.be/e0BtHy5XiqE?list=UUXpjUx31kBU96EtzbuK0xkQ


unprinted

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! :)

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.)

unprinted

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.

Go Up