Sunday, March 11, 2012

Conway's Game of Life on the Arduino

Systems on a chip, like the ATmega328 on which the Arduino is based, provide some unique programming challenges for a modern programmer. Notably:
  • Lack of support for the entire standard library of C/C++
  • Reduced processor speed
  • Reduced amount of available RAM
  • Reduced amount of memory available for programs
All of these factored in to the problem of bringing Conway's Game of Life to the platform.



After the break, solving the embedded system problems


RAM
First off, my original program initially used two arrays, one to store whether or not a cell was occupied, and another to store the number of neighbors a cell had. When the game needed to be updated, the number of neighbors was erased, and re-totaled, then the cell array updated. This failed miserably on the Arduino, as each array had N rows and N columns, and the smallest type in C++ is 8 bits, so each bool was taking 1 byte, and a 30*30 grid would take almost 1Kb, plus another 4Kb for the other array being it was ints, and whatever the magical TVOut library was using. The amount of SRAM in the chip was 2Kb.

Rather than storing each row as 30 bytes, I decided to use a single uint32_t, giving up to 32 columns in only 4 bytes; the TVOut library maxes out at somewhere near there, as any higher and the clock on board the Arduino couldn't handle sending the TV a high enough resolution and still do a high enough FPS to make it usable in most settings.

I counted the vertical resolution of the screen at 13 rows, but knew the top one displayed stats, so I only needed an array of 8 uint32_ts to fill the screen with cells.

The second problem remained though, the original program counted the number of cells in surrounding areas then set up according to that. That couldn't be done here though, as it would also take a huge amount of space. I decided to directly decide the cells that were on if their neighbors added up to the proper number in another array of uint32_ts and simply copy it over to the original one once done. Combined my arrays used 120 bytes, almost 42 times smaller!

ROM
The third problem came, as the original program switched between a random mode and one that was the simplest known glider gun. The glider gun was chopped, as it took up too much program memory, and couldn't be shown nicely on such a small resolution.

Reduced Library
Almost all library issues can be worked around if you know the right tricks, things like the "time()" function can't be used well on an Arduino, but you can get around it by reading pins that aren't attached to anything, giving adequately random values.

Code:

 /**
*
* Copyright 2012-02-26 Joseph Lewis <joehms22@gmail.com>
*
* Conway's game of life in cpp. The world is looped (life at top can
* move to bottom &c.) Built to run on the Arduino with the TVout
* library.
*
* Apache 2.0 License
*
* http://code.google.com/p/arduino-tvout/
*
*/

#include "TVout.h"
#include "fontALL.h"
TVout TV;

const int COLS = 29;
const int ROWS = 15;

// The "Alive" cells on the board.
uint32_t alive[ROWS] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};


bool isAlive(int row, int col)
{
return alive[row] & (1<<(col));
}

void setAlive(int row, int col)
{
alive[row] |= 1 << col;
}

int boardnum = 0; // number of boards run by the game
int iteration = 0; // current round in the current board

int numberAround(int row, int col);

/**
* Sets the alive array to all falses.
*/
void blank_alive()
{
for(int i = 0; i < ROWS; ++i)
alive[i] = 0;
}

/**
* Writes output to the console.
*/
void do_output()
{
TV.clear_screen();
TV.print("Board: ");
TV.print(boardnum);
TV.print(" Iteration: ");
TV.println(iteration);

for(int i = 0; i < ROWS; i++)
{

for(int j = 0; j < COLS; j++)
{
// WIDTH, HEIGHT
if(isAlive(i,j))
TV.print("0");
else
TV.print(" ");
}
if(i != ROWS -1)
TV.print("\n");
}

}

/**
* Randomly fills the grid with alive cells after blanking.
*/
void random_fill()
{
blank_alive();
randomSeed(analogRead(0));

// Fill 30% of the cells
int numToFill = (ROWS * COLS) * 30 / 100 ;

for(int r = 0; r < numToFill; r ++)
{
int row = rand() % ROWS;
int col = rand() % COLS;

setAlive(row,col);
}
}

/**
* Returns the index of the row below the current one.
*/
int rowBelow(int row)
{
return (row + 1 < ROWS) ? row + 1 : 0;
}

/**
* Returns the index of the row above the given one
*/
int rowAbove(int row)
{
return (row > 0) ? row - 1 : ROWS - 1;
}

/** Returns the index of the col to the right of this one */
int colRight(int col)
{
return (col + 1 < COLS) ? col + 1 : 0;
}

/** Returns the index of the col to the left of this one */
int colLeft(int col)
{
return (col > 0) ? col - 1 : COLS -1;
}

/** true if the cell to the left is alive*/
bool left(int row, int col)
{
col = colLeft(col);
return isAlive(row,col);
}

/** true if the cell to the right is alive*/
bool right(int row, int col)
{
col = colRight(col);
return isAlive(row,col);
}

/** true if the cell above is alive*/
bool above(int row, int col)
{
row = rowAbove(row);
return isAlive(row,col);
}

/** true if the cell below is alive*/
bool below(int row, int col)
{
row = rowBelow(row);
return isAlive(row,col);
}

/** true if the cell NE is alive*/
bool aboveright(int row, int col)
{
row = rowAbove(row);
col = colRight(col);
return isAlive(row,col);
}

/** true if the cell SE is alive*/
bool belowright(int row, int col)
{
row = rowBelow(row);
col = colRight(col);
return isAlive(row,col);
}

/** true if the cell NW is alive*/
bool aboveleft(int row, int col)
{
row = rowAbove(row);
col = colLeft(col);
return isAlive(row,col);
}

/** true if the cell SW is alive*/
bool belowleft(int row, int col)
{
row = rowBelow(row);
col = colLeft(col);
return isAlive(row,col);
}

/**Returns the number of living cells sorrounding this one.*/
int numberAround(int row, int col)
{
int around = 0;
if(left(row,col))
around++;

if(right(row,col))
around++;

if(above(row,col))
around++;

if(below(row,col))
around++;

if(aboveright(row,col))
around++;

if(aboveleft(row,col))
around++;

if(belowright(row,col))
around++;

if(belowleft(row,col))
around++;

return around;
}

/**
* Moves all of the cells
*/
void move()
{
uint32_t nextRows[ROWS] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0};


for(int i = 0; i < ROWS; i++)
{
for(int j = 0; j < COLS; j++)
{
int na = numberAround(i,j);
if((na == 2 && isAlive(i,j)) || na == 3)
nextRows[i] |= 1 << j;
}
}


for(int i = 0; i < ROWS; i++)
alive[i] = nextRows[i];
}

void setup()
{
TV.begin(NTSC,120,96);
TV.select_font(font4x6);
}

void loop() {
boardnum++;

TV.println("Conways game of life for Arduino, Copyright 2012 Joseph Lewis <joehms22@gmail.com>");
TV.delay(2000);

random_fill();

TV.print("Doing iterations");

for(iteration = 0;iteration < 50; iteration++)
{
do_output();
move();
TV.delay(500);
}
}