This program was my attempt recreate the Conway's Game of Life on the RaspberryPi Pico using Prof. Hunter Adam's VGA Library for the RaspberryPi Pico. The program starts with randomly placing alive and dead cells on a board and calculating the next generation of the based on the current generation. For this purpose, I used two boards, one to hold the current generation and the other to hold the next generation. Once the new generation is completely worked out, it is overwritten into the old board. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.
/*
* Parth Sarthi Sharma (pss242@cornell.edu)
* Code based on examples from Raspberry Pi Foundation.
* This code is an implementation of the Conway's Game of Life
* on the Raspberry Pi Pico.
*/
#include <stdio.h> //The standard C library
#include <math.h> //The standard math library
#include "pico/stdlib.h" //Standard library for Pico
#include "hardware/pio.h" //The hardware PIO library
#include "hardware/dma.h" //The hardware DMA library
#include "pico/time.h" //The pico time library
#include "hardware/gpio.h" //The hardware GPIO library
#include "Bitmaps.h" //The bitmaps library
#include "vga_graphics.h" //The graphics library
#define HEIGHT 480 //Height of the VGA screen
#define WIDTH 640 //Width of the VGA screen
volatile char GameBoard[HEIGHT / 4][WIDTH / 4], flag = 0; //The game board and the flag to synchronise changes made for the next generation and drawing the next generation
char NewBoard[HEIGHT / 4][WIDTH / 4]; //A copy of the game board to make changes to
unsigned int generation = 1; //The generation
int checkNeighbours(int x, int y){ //Check to see how many neighbours of a particular cell are alive
return GameBoard[x - 1][y - 1] + GameBoard[x - 1][y] + GameBoard[x - 1][y + 1] + GameBoard[x][y - 1] + GameBoard[x][y + 1] + GameBoard[x + 1][y - 1] + GameBoard[x + 1][y] + GameBoard[x + 1][y + 1];
}
inline char randomRange(int min, int max){ //Function to generate a random number between min and max
return (rand() % (max - min)) + min;
}
void core1_entry() { //The program running on core 1
short i, j; //The iterators
while(1){ //While eternity
while(flag); //Wait here while flag is set
for(i = (HEIGHT / 8); i < (HEIGHT / 4) - 1; i++){ //For the bottom half of the board
for(j = 1; j < (WIDTH / 4) - 1; j++){ //For all elements from left to right
if(GameBoard[i][j]){ //If the element is alive
fillRect(j << 2, i << 2, 4, 4, WHITE); //Draw the cell with white
}
else{ //If the element is dead
fillRect(j << 2, i << 2, 4, 4, BLACK); //Draw the cell with black
}
}
}
flag = 1; //Set the flag once done
}
}
int main(){ //The program running on core 1
short i, j; //The iterators
stdio_init_all(); //Initialize all of the present standard stdio types that are linked into the binary
initVGA(); //Initialize the VGA screen and functions
srand(1); //Random seed
//For all the cells, set the cells randomly to either alive or dead
for(i = 1; i < (HEIGHT / 4) - 1; i++){
for(j = 1; j < (WIDTH / 4) - 1; j++){
GameBoard[i][j] = randomRange(0, 2);
}
}
multicore_launch_core1(core1_entry); //Reset core1 and enter the core1_entry function on core 1 using the default core 1 stack
while(1){ //While eternity
while(!flag); //Wait here until flag is set
printf("Generation: %u\n", generation); //Print the current generation
generation++; //Increment the generation
for(i = 1; i < (HEIGHT / 4) - 1; i++){ //For the top half of the board
for(j = 1; j < (WIDTH / 4) - 1; j++){ //For all elements from left to right
char neighbours = checkNeighbours(i, j); //Check the number of neighbours alive for the cell
if(GameBoard[i][j]){ //If the current cell is alive
if(neighbours == 2 || neighbours == 3){ //If the number of alive neighbours is 2 or 3
NewBoard[i][j] = 1; //The cell lives
}
else{ //Else
NewBoard[i][j] = 0; //The cell dies
}
}
else{ //If the current cell is dead
if(neighbours == 3){ //If the number of alive neighbours is 3
NewBoard[i][j] = 1; //The cell comes alive
}
else{ //Else
NewBoard[i][j] = 0; //The cell remains dead
}
}
}
}
memcpy(GameBoard, NewBoard, 19200); //Copy the new board to the gameboard
flag = 0; //Clear the flag
for(i = 1; i < (HEIGHT / 8); i++){ //For the top half of the board
for(j = 1; j < (WIDTH / 4) - 1; j++){ //For all elements from left to right
if(GameBoard[i][j]){ //If the element is alive
fillRect(j << 2, i << 2, 4, 4, WHITE); //Draw the cell with white
}
else{ //If the element is dead
fillRect(j << 2, i << 2, 4, 4, BLACK); //Draw the cell with black
}
}
}
}
}
The code has a VGA library which has been explained really nicely by Prof. Adams and the main file. Most of the code is self explanatory, however, the main components of the game are explained in the following subsections.
I have initialized two game boards, GameBoard[][]
and NewBoard[][]
. The GameBoard
holds the current state of the life to be drawn while the NewBoard
is used to hold the next generation of the game. Once the next generation is figured out, it is overwritten into the old board using memcpy(GameBoard, NewBoard, 19200)
where 19200 is the number of bytes to be transferred.
In order to set up the board, I iterated over through all the cells and randomly set them to be either alive or dead. This is the first generation of the board and signifies the initial state of the system.
Note: The generation of the board is pseudo-random. This means that even though it may seem random, it will always initialize in the same state every time the program is reset.
for(i = 1; i < (HEIGHT / 4) - 1; i++){
for(j = 1; j < (WIDTH / 4) - 1; j++){
GameBoard[i][j] = randomRange(0, 2);
}
}
According to Wikipedia, the universe of the Game of Life is an infinite, two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, live or dead, (or populated and unpopulated, respectively). Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed, live or dead; births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick. Each generation is a pure function of the preceding one. The rules continue to be applied repeatedly to create further generations.
I these exact set of rules to calculate the next generation in my implementation.
for(i = 1; i < (HEIGHT / 4) - 1; i++){
for(j = 1; j < (WIDTH / 4) - 1; j++){
char neighbours = checkNeighbours(i, j);
if(GameBoard[i][j]){
if(neighbours == 2 || neighbours == 3){
NewBoard[i][j] = 1;
}
else{
NewBoard[i][j] = 0;
}
}
else{
if(neighbours == 3){
NewBoard[i][j] = 1;
}
else{
NewBoard[i][j] = 0;
}
}
}
}
Drawing the boards is the most computationally expensive part of the implementation. Drawing the board using just 1 core takes about 1100ms whereas all the other computations take about 81ms. Therefore, I equally divided the task of drawing the board between the two cores.
//Core 0
for(i = 1; i < (HEIGHT / 8); i++){
for(j = 1; j < (WIDTH / 4) - 1; j++){
if(GameBoard[i][j]){
fillRect(j << 2, i << 2, 4, 4, WHITE);
}
else{
fillRect(j << 2, i << 2, 4, 4, BLACK);
}
}
}
//Core 1
for(i = (HEIGHT / 8); i < (HEIGHT / 4) - 1; i++){
for(j = 1; j < (WIDTH / 4) - 1; j++){
if(GameBoard[i][j]){
fillRect(j << 2, i << 2, 4, 4, WHITE);
}
else{
fillRect(j << 2, i << 2, 4, 4, BLACK);
}
}
}
The video shows the implementation of Conway's Game of Life on the RaspberryPi Pico.
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(GameOfLife-project)
pico_sdk_init()
add_executable(GameOfLife)
pico_enable_stdio_usb(GameOfLife 1)
pico_enable_stdio_uart(GameOfLife 1)
pico_generate_pio_header(GameOfLife ${CMAKE_CURRENT_LIST_DIR}/hsync.pio)
pico_generate_pio_header(GameOfLife ${CMAKE_CURRENT_LIST_DIR}/vsync.pio)
pico_generate_pio_header(GameOfLife ${CMAKE_CURRENT_LIST_DIR}/rgb.pio)
target_sources(GameOfLife PRIVATE GameOfLife.c vga_graphics.c)
target_link_libraries(GameOfLife PRIVATE pico_stdlib hardware_pio hardware_dma hardware_adc hardware_irq pico_time pico_multicore)
pico_add_extra_outputs(GameOfLife)