Arduinoで遊ぶページ

Arduinoで遊んだ結果を残すページです。
garretlab
ライフゲーム

概要

Arduinoにグラフィック液晶ディスプレイ(SG12864ASLB-GB)をつないで、ライフゲームを作成します。グラフィック液晶ディスプレイそのものをArduinoに接続する方法は、こちらのページを参照してください。GLCD用のライブラリにもライフゲームのプログラムがついていましたが、1ドットを1セルとみなしたものではないので、1ドットを1セルとみなしたライフゲームを作成してみました。

作るもの

ライフゲームを作成します。ここでは簡単にルールを説明します。ライフゲームの詳細は、例えば、ウィキペディアに書かれているのでそちらを参照してください。

ライフゲームでは、Mマス×Nマスのセルが並んでいます。各セルは生きているか死んでいるかのどちらかの状態を持っています。それぞれのマスの次の世代の状態は現在の状態で決まります。以下に、現在の状態と次の世代の状態の関係を示します。現在生きているセルは、周囲の生きているセルの数が2か3のとき生き続けます。現在死んでいるセルは、周囲の生きているセルの数が3のときに誕生します。その他の場合はすべて死にます。

現在の状態 生きている周囲のセルの数 
0 1 2 3 4 5 6 7 8
Alive D D A A D D D D D
Dead D D D A D D D D D

境界条件は、周期境界条件としました。つまり、一番右側の右は一番左側、一番下側の下は一番上です。

用意するもの

以下のものを用意します。

  • Arduino
  • USBケーブル
  • PC
  • 電源
  • グラフィック液晶ディスプレイ(SUNLIKE社製SG12864ASLB-GBを利用)
  • 可変抵抗10kΩ(液晶ディスプレイの輝度調整に使います)
  • バックライト用抵抗
  • ブレッドボード(可変抵抗、バックライト用抵抗を配線するために使いました)
  • ジャンパーワイヤ

利用機器

グラフィック液晶ディスプレイを利用します。Arduinoでは、グラフィック液晶ディスプレイを利用するためのライブラリがすでに用意されています。詳細はこちらのページを参照してください。

基本的な考え方

状況把握

現在の各セルの状態(生きているか死んでいるか)と、周囲のセルの状態(生きている周囲のセルの数)を取得します。

処理決定

現在の各セルの状態と、周囲のセルの状態とで、次世代の状態を決定します。

機器操作

次世代の状態をグラフィック液晶に表示します。

設計

ハードウェアの設計

液晶ディスプレイをそのまま使います。他に接続するものはありません。

プログラムの設計

基本的な考え方

ライフゲームの基本的な考え方は、以下の通りです。

  1. 各セルについて、周囲8個のセルの状態を調べる。
  2. 対象のセルが現在生きている場合、周囲の生きているセルの個数が2もしくは3の場合、そのセルは次の世代も生き続ける。そうでなければ死ぬ。
  3. 対象のセルが死んでいる場合、周囲の生きているセルの個数が3の場合、そのセルは生きる。

メモリの使い方

グラフィック液晶ディスプレイの大きさが128×64ドットなので、1ドットを1セルとみなします。各セルの状態は生きているか死んでいるかの2つの状態なので、1ビットであらわすことができます。本来であれば、2世代分のメモリが必要ですが、128×64=8192(ビット)=1024バイト=1キロバイトとなり、2世代分のメモリを使用すると2キロバイトとなります。一方、Arduino UnoのSRAMは2キロバイトしかないので、メモリが足りません。問題なくコンパイルできるので、試しにスケッチをアップロードして動作させてみましたが、当然のことながら、動きませんでした。読み込み専用のメモリであれば、PROGMEMを利用する方法もありますが、今回は読み書きする必要があるため、この方法は使えません。

GLCDのマニュアルを読んでいると、ReadData()という関数が用意されていました。試してみると、GotoXY()で座標を設定した後、その座標から1バイト(=8ビット)を、Y軸方向に読み取る関数のようでした。

今回は、次世代の状態をSRAMに格納し、現在の状態はグラフィック液晶ディスプレイから読み取ることにします。

次世代の状態と現在の状態

次世代の状態は単純にchar型の配列を利用します。char型の変数は大きさが1バイトと決まっています。Arduino(ATMega328)では、1バイトは8ビットなので、X軸もしくはY軸方向は、液晶のサイズの8分の1の大きさを確保すれば十分です。GLCDライブラリには、WriteData()という関数もあり、これは、設定した座標からY軸方向に8ビット分を書き込む関数のようでした。このため、X軸方向は液晶ディスプレイのサイズ(=128)、Y軸方向は液晶ディスプレイのサイズ(64)÷8=8としました。スケッチでは、fieldという変数です。

現在の状態は、グラフィック液晶ディスプレイから読み取りますが、ライフゲームの性質上、セルの上下左右(斜めも含む)のセルが必要です。また、Y軸方向は8バイトと少なくて済むため、Y軸方向は8バイト固定、X軸方向は可変(最低3バイト必要です)としました。スケッチでは、bufferという変数です。

セルの読み書き

次世代の状態を格納している配列はバイト単位で、Y軸方向に詰まった状態になっています。スケッチでは、field[field_width][field_height_in_byte]と定義しています。(X, Y)座標のセルは、field[X][Y/8]に含まれています。この変数から、実際に必要なセルの値を読み取るには、field[X][Y/8] & ((1 << (Y % 8))と表すことができます。Y/8は、Yを8で割った商で整数となります。また、Y%8は、Yを8で割った余りを表します。例えば、(X, 26)は、field[X][3]の第2ビットです。

スケッチ

#include <glcd.h>

const int field_width = DISPLAY_WIDTH;
const int field_height = DISPLAY_HEIGHT;
const int field_height_in_byte = (DISPLAY_HEIGHT / 8);
unsigned char field[field_width][field_height_in_byte];

const int buffer_width = 34;
const int buffer_height_in_byte = field_height_in_byte;
unsigned char buffer[buffer_width][buffer_height_in_byte];

int buffer_x;

void setup () {
  randomSeed(analogRead(0));
  GLCD.Init();

  /* Initialize the field randomly */
  for(int x = 0; x < field_width; x++) {
    for(int y = 0; y < field_height_in_byte; y++) {
      field[x][y] = random(255);
    }
  }
  
  write_field();
}

void loop () {
  int live_cells;
  int loop_num = buffer_width - 2;
  
  for(int x = 0; x < field_width; x+= loop_num){
    refresh_buffer(x - 1);
    for(int y = 0; y < field_height; y++) {
      for(int z = 0; z < loop_num; z++) {
        if(get_status(x + z, y, &live_cells)) {
            set_status(x + z, y, ((live_cells == 2) || (live_cells == 3)));
        } else {
            set_status(x + z, y, (live_cells == 3));
        }
      }
    }
  }
  
  write_field();
}

/*
 * Write the field to the GLCD.
 */
void write_field() {
  for(int x = 0; x < field_width; x++) {
    for(int y = 0; y < field_height_in_byte; y++) {
      GLCD.GotoXY(x, (y * 8));
      GLCD.WriteData(field[x][y]);
    }
  }
}

/*
 * Copy status of the GLCD into the buffer.
 */
void refresh_buffer(int x) {
  static int initialized = 0;
  int i, j;
  
  if ((x != buffer_x ) || (!initialized)) {
    for(i = 0; i < buffer_width; i++) {
      for(j = 0; j < buffer_height_in_byte; j++) {
        buffer[i][j] = read_data((x + i), j);
      }
    }
    buffer_x = x;
    initialized = 1;
  }
}

/*
 * Read GLCD Data.
 */
char read_data(int x, int y_in_byte) {
  if (x < 0) {
    x += field_width;
  } else if (x > (field_width - 1)) {
    x -= field_width;
  }
  
  GLCD.GotoXY(x, y_in_byte * 8);
  return GLCD.ReadData();
}

/*
 * Count the number of live cells and returns the status of the specified cell.
 */
int get_status(int x, int y, int *num) {
  *num = get_cell(x - 1, y - 1) + get_cell(x - 1, y) + get_cell(x - 1, y + 1) +
         get_cell(x,     y - 1) /* target cell */    + get_cell(x,     y + 1) +
         get_cell(x + 1, y - 1) + get_cell(x + 1, y) + get_cell(x + 1, y + 1);
  
  return get_cell(x, y);
}

/*
 * Get the status of a cell. Returns 1 when alive, 0 when dead.
 */
inline int get_cell(int x, int y) {
  if (y < 0) {
    y += field_height;
  } else if (y > (field_height - 1)) {
    y -= field_height;
  }
  
  if (buffer[x - buffer_x][y / 8] & (1 << (y % 8))) {
    return 1;
  } else {
    return 0;
  }
}

/*
 * Set the status of a cell.
 */
void set_status(int x, int y, int state) {
  if (state) { // Alive
    field[x][y / 8] |= (1 << (y % 8));
  } else {     // Dead
    field[x][y / 8] &= ~((1 << (y % 8)));
  }
}

組立

こちらのページを参照してください。

動作している様子

以下に写真を示します。上の写真が開始直後、下の写真がしばらくたってからの写真です。いわゆるグライダーとかも見ることができます。

バージョン

Arduino 0022



メニューを表示するためにJavaScriptを有効にしてください。

Arduinoで遊ぶページ
Copyright © 2011 garretlab all rights reserved.
inserted by FC2 system