Go Down

Topic: Conway's Game Of Life with 128x64 graphic LCD. UPDATE: also with 128x64 SPI OLED (Read 5092 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.

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


Go Up