"Game Of Life" mit einem OLED-Display

Video: Live-Demonstration

Verwendete Bauteile

Anschlüsse des OLED-Moduls

OLED Arduino
GND GND
VCC 3,3V bzw. 5V
SCL A5
SDA A4

Test der OLED

Zum Betrieb des OLED-Moduls (mittels I²C-Protokoll) wurden zwei vorgefertigte Software-Libraries verwendet, Adafruit_SSD1306 und Adafruit-GFX-Library. Damit kann das OLED sehr einfach angesteuert und einige Test durchgeführt werden.

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

Sketch

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

void setup()
{
  Wire.begin();

  /**
   * initialize with the I2C address:
   *    0x3C  -> 128x32 resolution
   *    0x3D  -> 128x64 resolution
   */
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  /**
   * Show image buffer on the display hardware.
   * Since the buffer is intialized with an Adafruit splashscreen
   * internally, this will display the splashscreen.
   */
  display.display();
  delay(2000);


  display.clearDisplay();
  display.drawPixel(10, 10, WHITE);
  display.display();
  delay(2000);

  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(5,0);
  display.print("Hello World!");
  display.display();
  delay(2000);
}

void loop()
{
}

"Game Of Life"

Conway´s Game Of Life hatte ich schon in "Zelluläre Automaten" implementiert und habe den Algorithmus hierfür übernommen aber noch die Feldgröße etwas erweitert. Zusätzlich wurde die Schaltung noch mit einem Reset-Knopf ausgestattet, damit man das Feld wieder mit zufälligen Zellen initialisieren kann. Außerdem gibt es noch ein Potentiometer, mit dem man die Geschwindigkeit der Generationen regeln kann.
Auf dem OLED werden natürlich die Zellen pro Generation angezeigt, aber auch die Generation ("Iter"), die Anzahl an gleichzeitig gezeigten Zellen ("Alive") und die aktuelle Verzögerungszeit/Geschwindigkeit ("Delay") der Simulation.

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

Sketch

#define FIELD_WIDTH  20
#define FIELD_HEIGHT 20

#define PIN_BUTTON        2
#define PIN_POTENTIOMETER A2

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

byte field[FIELD_WIDTH][FIELD_HEIGHT];
byte fieldLast[FIELD_WIDTH][FIELD_HEIGHT];
int iteration = 0, cellsAlive = 0;
bool isReset = false;
int gameDelay = 150;


void setup()
{
  pinMode(PIN_BUTTON, INPUT);
  pinMode(PIN_POTENTIOMETER, INPUT);

  attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), isrReset, RISING);

  Wire.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();

  randomizeField();
}

void loop()
{
  if (isReset == true) {
    isReset = false;
    randomizeField();
  }
  updateOled();
  saveFieldState();
  generateNextGeneration();

  gameDelay = round(analogRead(PIN_POTENTIOMETER) / 10) * 10;
  delay(gameDelay);
  iteration++;
}

void isrReset()
{
  isReset = true;
}

void randomizeField()
{
  iteration = 0;
  cellsAlive = 0;
  byte rnd = 0;
  randomSeed(analogRead(0));
  for(byte y=0; y<FIELD_HEIGHT; y++) {
    for(byte x=0; x<FIELD_WIDTH; x++) {
      field[y][x] = random(2);
      if (field[y][x] == 1) {
        cellsAlive++;
      }
    }
  }
}

void saveFieldState()
{
  for(byte y=0; y<FIELD_HEIGHT; y++) {
    for(byte x=0; x<FIELD_WIDTH; x++) {
      fieldLast[y][x] = field[y][x];
    }
  }
}

void generateNextGeneration()
{
  byte neighborSum = 0;
  for(byte y=0; y<FIELD_HEIGHT; y++) {
    for(byte x=0; x<FIELD_WIDTH; x++) {
      neighborSum = getNeightborSum(x, y);
      if (fieldLast[y][x] == 0) {
        if (neighborSum == 3) {
          // populate if 3 neighours around it
          field[y][x] = 1;
          cellsAlive++;
        }
      } else {
        if (neighborSum < 2 || neighborSum > 3) {
          // die if only one neighbour or 4 or more neighours
          field[y][x] = 0;
          cellsAlive--;
        }
      }
    }
  }
}

byte getNeightborSum(byte x, byte y)
{
  byte sum = 0;

  for(int j = -1; j<=1; j++) {
    for(int i = -1; i<=1; i++) {
      if (j==0 && i==0) {
        continue;
      }
      if (x+i<0 || x+i>FIELD_WIDTH-1) {
        continue;
      }
      if (y+j<0 || y+j>FIELD_HEIGHT-1) {
        continue;
      }
      sum += fieldLast[y+j][x+i];
    }
  }

  return sum;
}

void updateOled()
{
  display.fillRect(FIELD_WIDTH + 3, 0, display.width(), display.height(), BLACK);

  display.setTextColor(WHITE);
  display.setTextSize(1);

  display.setCursor(FIELD_WIDTH + 10, 0);
  display.print("Iter:");
  display.setCursor(FIELD_WIDTH + 50, 0);
  display.print(String(iteration));

  display.setCursor(FIELD_WIDTH + 10, 10);
  display.print("Alive:");
  display.setCursor(FIELD_WIDTH + 50, 10);
  display.print(String(cellsAlive));

  display.setCursor(FIELD_WIDTH + 10, 20);
  display.print("Delay:");
  display.setCursor(FIELD_WIDTH + 50, 20);
  display.print(String(gameDelay));

  for(byte y=0; y<FIELD_HEIGHT; y++) {
    for(byte x=0; x<FIELD_WIDTH; x++) {
      if (field[y][x] == 1) {
        display.drawPixel(x, y, WHITE);
      } else {
        display.drawPixel(x, y, BLACK);
      }
    }
  }
  display.display();
}

Arduino Nano RP2040 Connect

Der neue Nano RP2040 Connect besitzt durch seinen RP2040-Chip deutlich mehr RAM als der Arduino Uno und kann somit ein größeres Simulations-Feld ansteuern und außerdem ist der Chip deutlich schneller bei den notwendigen Berechnungen.

Anschlüsse

OLED Nano RP2040 Connect
GND GND
VCC +3V3
SCL A5 (GPIO13 bzw. D19)
SDA A4 (GPIO12 bzw. D18)

Sketch

Zunächst hatte ich einige Probleme, auf dem OLED überhaupt etwas darzustellen, aber letztendlich funktionierte die Variante mit der I²C-Adresse 0x3C und einer Auflösung von 128×64 Pixeln. Allerdings war es mir nicht möglich ein Spielfeld von mehr als 64×64 richtig darzustellen, obwohl der RAM-Verbrauch des Nano erst bei ca. 26% lag.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_ADDRESS 0x3C

#define FIELD_WIDTH  64
#define FIELD_HEIGHT 64

// Arduino Nano RP2040 connect: A4(SDA), A5(SCL)
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);

byte field[FIELD_WIDTH][FIELD_HEIGHT];
byte fieldLast[FIELD_WIDTH][FIELD_HEIGHT];
int iteration = 0, cellsAlive = 0;
int SIMULATION_DELAY = 10;

void setup()
{
  oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
  oled.clearDisplay();
  oled.display();
  randomizeField();
}

void loop()
{
  updateOled();
  saveFieldState();
  generateNextGeneration();
  delay(SIMULATION_DELAY);
  iteration++;
}


void randomizeField()
{
  byte rnd = 0;
  randomSeed(analogRead(0));
  for (byte y = 0; y < FIELD_HEIGHT; y++) {
    for (byte x = 0; x < FIELD_WIDTH; x++) {
      field[y][x] = random(2);
      if (field[y][x] == 1) {
        cellsAlive++;
      }
    }
  }
}

void saveFieldState()
{
  for (byte y = 0; y < FIELD_HEIGHT; y++) {
    for (byte x = 0; x < FIELD_WIDTH; x++) {
      fieldLast[y][x] = field[y][x];
    }
  }
}

void generateNextGeneration()
{
  byte neighborSum = 0;
  for (byte y = 0; y < FIELD_HEIGHT; y++) {
    for (byte x = 0; x < FIELD_WIDTH; x++) {
      neighborSum = getNeightborSum(x, y);
      if (fieldLast[y][x] == 0) {
        if (neighborSum == 3) {
          // populate if 3 neighours around it
          field[y][x] = 1;
          cellsAlive++;
        }
      } else {
        if (neighborSum < 2 || neighborSum > 3) {
          // die if only one neighbour or 4 or more neighours
          field[y][x] = 0;
          cellsAlive--;
        }
      }
    }
  }
}

byte getNeightborSum(byte x, byte y)
{
  byte sum = 0;

  for (int j = -1; j <= 1; j++) {
    for (int i = -1; i <= 1; i++) {
      if (j == 0 && i == 0) {
        continue;
      }
      if (x + i < 0 || x + i > FIELD_WIDTH - 1) {
        continue;
      }
      if (y + j < 0 || y + j > FIELD_HEIGHT - 1) {
        continue;
      }
      sum += fieldLast[y + j][x + i];
    }
  }

  return sum;
}

void updateOled()
{
  oled.fillRect(FIELD_WIDTH + 3, 0, oled.width(), oled.height(), BLACK);

  oled.setTextColor(WHITE);
  oled.setTextSize(1);

  oled.setCursor(FIELD_WIDTH + 10, 0);
  oled.print("Iter:");
  oled.setCursor(FIELD_WIDTH + 10, 10);
  oled.print(String(iteration));

  oled.setCursor(FIELD_WIDTH + 10, 30);
  oled.print("Alive:");
  oled.setCursor(FIELD_WIDTH + 10, 40);
  oled.print(String(cellsAlive));

  for (byte y = 0; y < FIELD_HEIGHT; y++) {
    for (byte x = 0; x < FIELD_WIDTH; x++) {
      if (field[y][x]==fieldLast[y][x]) {
        continue;
      }
      if (field[y][x] == 1) {
        oled.drawPixel(x, y, WHITE);
      } else {
        oled.drawPixel(x, y, BLACK);
      }
    }
  }
  oled.display();
}

Bei diesem Aufbau wurde kein Mikrotaster und Potentiometer verwendet; für ein Neustart des Spielfeldes kann einfach der RESET-Knopf des Nano betätigt werden.

zurück