Wooohoo! I can now officially cross something off my bucket list:

  • run marathon
  • present project at Makerfaire
  • world domination?

So, what was my project? Here’s a pic of it

my project set up at makerfaire

What do you mean you don’t know what is from that?

WELL.

What I built was basically part art, part math and part eletronics. I hooked up an RGB LED Matrix (specifically this one from adafruit) to an Arduino Mega along with 32 switches. (Note to future self: don’t ever try and wire up 32 switches.) These switches provided the starting row for generating a pattern on the led matrix based on rule 110: a 1 dimensional cellular automaton that generates a pattern like this:

rule 110 pattern (from @rule110_bot)

but on an RGB LED Matrix, so it looks more like

RGB panel product shot

Why??

This all started out when I came across this fabulous kickstarter: KnitYak: Custom mathematical knit scarves by fbz, which was for rule 110 scarfs. I was immediatly struck by how beautiful the pattern was, and intriguied by the fact that it was being generated algorithmically. After reading several wikipedia articles, my interest in the pattern had only grown further. I ended up writing a ruby implementation of rule 110 that could output patterns to your terminal, and then to pngs, and then finally to twitter.

But it was hard to mess around with the starting row and explore the patterns that got generated. I wanted something you could interact with, so when I saw that the Ottawa Makerfaire was looking for projects I figured it would be a perfect oppurtunity to force myself to build something and to have something interactive for attendees to play with.

Hardware

me being creepy

my rats nest of a project

Here’s what inside:

  • Arduino Mega
  • 64x32 RGB LED matrix
  • Shift Registers (SN74HC165N) x 6
  • Rocker switches x 32
  • 8 position rotary switches x 2
  • 10K resistors x (lots)

The arduino mega is the brains of this particular operations. I needed to use it over an uno because of the higher clock speed. Turns out you need a fair bit of horse power to drive 2048 RGB LEDs. Who would have thunk?

RGB LED Matrix

How do you actually drive 2048 LEDs?

Basically, you can’t have all those LEDs on at the same time (because power draw), so the LED matrix only turns 2 (out of 32) rows on at a time. In order to select which row you want to light up, you use the 4 address select pins to select one of 16 pairs of rows.

Once you have a row selected, you need to fill it with colour. You can set the colour of 2 pixels at a time using 2 sets of Red, Green and Blue pins. When your desired colours are chosen you “shift” in the data, using the clock pin. This way you can fill an entire row of pixels by setting the colour, then clocking in order to set the next pixel. Finally, you toggle the latch pin, which shows the entire row of pixels. If you do this FAST enough (like 200Hz aka 1 row every 5ms), the display looks to be continuously on to us puny humans.

I tried to write my own driver for the display in C for the Propeller, but I had some serious flickering issues that I wasn’t able to fix… Given that I didn’t want to give everyone at makerfaire a headache / induce seizures I chose to use this library from Adafruit. It’s a very interesting read because they do some neat things like unrolling loops and using Binary Coded Modulation (like PWM) in order to give a greater color depth than the 3 bit color you start with.

Shift Registers

shift registers in situe

I ended up using 6 shift register for input in this project. Why? Well it means that I could use 4 pins to read 32 switches (aka 32 bits of data) all in one go. That feat is accomplished with 4 shift register daisy chained together. Each input pin on each of the shift registers has a pull down resistor in order to deal with electrical gremlins, and also to make my assembly time much much longer.

I also used 2 shift registers to read the state of the two knobs I have for selecting colours. Those knobs have 8 pins to indicate which position the knob is in currently.

Software

My code can be found below:

#include <Adafruit_GFX.h>   // Core graphics library
#include <RGBmatrixPanel.h> // Hardware-specific library

#define OE   9
#define LAT 10
#define CLK 11
#define A   A0
#define B   A1
#define C   A2
#define D   A3

RGBmatrixPanel matrix(A, B, C, D, CLK, LAT, OE, false, 64);

const int data_pin = 6; //  SER_OUT (serial data out)
const int shld_pin = 5; // SH/!LD (shift or active low load)
const int clk_pin = 3; //  CLK (the clock that times the shifting)
const int ce_pin = 4; //  !CE (clock enable, active low)

const int data_pin_2 = 48;
const int ce_pin_2 = 46;
const int shld_pin_2 = 47;
const int clk_pin_2 = 49;

byte incoming1;
byte incoming2;
byte incoming3;
byte incoming4;

byte colour1;
byte colour2;


//int start[] = { 0, 1, 0, 0, 1, 0, 1, 0,
//                0, 0, 1, 0, 1, 1, 0, 1,
//                1, 1, 1, 0, 0, 1, 1, 1,
//                0, 1, 0, 1, 1, 0, 0, 1
//              };
int start[32] = {0};

int* row1;
int* row2;

int x = 0;
int y = 0;

void setup() {

  //shift registers for buttons
  Serial.begin(9600);

  // Initialize each digital pin to either output or input
  // We are commanding the shift register with each pin with the exception of the serial
  // data we get back on the data_pin line.
  pinMode(shld_pin, OUTPUT);
  pinMode(ce_pin, OUTPUT);
  pinMode(clk_pin, OUTPUT);
  pinMode(data_pin, INPUT);
  pinMode(shld_pin_2, OUTPUT);
  pinMode(ce_pin_2, OUTPUT);
  pinMode(clk_pin_2, OUTPUT);
  pinMode(data_pin_2, INPUT);

  // Required initial states of these two pins according to the datasheet timing diagram
  digitalWrite(clk_pin, HIGH);
  digitalWrite(shld_pin, HIGH);
  digitalWrite(clk_pin_2, HIGH);
  digitalWrite(shld_pin_2, HIGH);

  read_shift_regs();
  read_color_shift_regs();
  Serial.println("colours: ");
  print_byte(colour1);
  print_byte(colour2);
  Serial.println(transform(colour1));
  Serial.println(transform(colour2));
  fill_starting_row();

  //matrix
  matrix.begin();
  matrix.fillScreen(0);
  row1 = start;
  displayRow(row1);
  row2 = (int*) malloc(sizeof(int) * 32);
}

int i = 0;
int j = 0;
void loop() {

  for (x = 0; x < 64; x++) {
    int* row;
    if (x % 2) {
      apply_rule(row2, row1);
      row = row2;
    } else {
      apply_rule(row1, row2);
      row = row1;
    }
    //    j = (j + 1) % 24;
    //    Serial.print(x);
    //    Serial.print(":  ");
    //    displayRow(row);
    //    Serial.print("row1 ");
    //    displayRow(row1);
    //    Serial.print("row2 ");
    //    displayRow(row2);
    for (y = 0; y < 32; y++) {
      if (row[y]) {
        matrix.drawPixel(x, y, transform(colour1));
      } else {
        matrix.drawPixel(x, y, transform(colour2));
      }

    }
    delay(100);
  }
  for (;;);
}

void apply_rule(int a[32], int b[32]) {
  for (int i = 0; i < 32; i++) {
    if (i == 0 || i == 31) {
      b[i] = a[i] & 1;
    } else {
      if ( (a[i - 1] && a[i] && !a[i + 1]) ||
           (a[i - 1] && !a[i] && a[i + 1]) ||
           (!a[i - 1] && a[i] && a[i + 1]) ||
           (!a[i - 1] && a[i] && !a[i + 1]) ||
           (!a[i - 1] && !a[i] && a[i + 1])
         ) {
        b[i] = 1;
      } else {
        b[i] = 0;
      }
    }

  }
  //  return b;
}


// Input a value 0 to 24 to get a color value.
// The colours are a transition r - g - b - back to r.
uint16_t Wheel(byte WheelPos) {
  if (WheelPos < 8) {
    return matrix.Color333(7 - WheelPos, WheelPos, 0);
  } else if (WheelPos < 16) {
    WheelPos -= 8;
    return matrix.Color333(0, 7 - WheelPos, WheelPos);
  } else {
    WheelPos -= 16;
    return matrix.Color333(0, WheelPos, 7 - WheelPos);
  }
}

byte read_shift_regs()
{
  byte the_shifted = 0;  // An 8 bit number to carry each bit value of A-H

  // Trigger loading the state of the A-H data lines into the shift register
  digitalWrite(shld_pin, LOW);
  delayMicroseconds(5); // Requires a delay here according to the datasheet timing diagram
  digitalWrite(shld_pin, HIGH);
  delayMicroseconds(5);

  // Required initial states of these two pins according to the datasheet timing diagram
  pinMode(clk_pin, OUTPUT);
  pinMode(data_pin, INPUT);
  digitalWrite(clk_pin, HIGH);
  digitalWrite(ce_pin, LOW); // Enable the clock

  // Get the A-H values
  //the_shifted = shiftIn(data_pin, clk_pin, MSBFIRST);
  incoming1 = shiftIn(data_pin, clk_pin, MSBFIRST);
  incoming2 = shiftIn(data_pin, clk_pin, MSBFIRST);
  incoming3 = shiftIn(data_pin, clk_pin, MSBFIRST);
  incoming4 = shiftIn(data_pin, clk_pin, MSBFIRST);
  digitalWrite(ce_pin, HIGH); // Disable the clock

  return the_shifted;

}

byte read_color_shift_regs()
{
  byte the_shifted = 0;  // An 8 bit number to carry each bit value of A-H

  // Trigger loading the state of the A-H data lines into the shift register
  digitalWrite(shld_pin_2, LOW);
  delayMicroseconds(5); // Requires a delay here according to the datasheet timing diagram
  digitalWrite(shld_pin_2, HIGH);
  delayMicroseconds(5);

  // Required initial states of these two pins according to the datasheet timing diagram
  pinMode(clk_pin_2, OUTPUT);
  pinMode(data_pin_2, INPUT);
  digitalWrite(clk_pin_2, HIGH);
  digitalWrite(ce_pin_2, LOW); // Enable the clock

  // Get the A-H values
  //the_shifted = shiftIn(data_pin, clk_pin, MSBFIRST);
  colour1 = shiftIn(data_pin_2, clk_pin_2, MSBFIRST);
  colour2 = shiftIn(data_pin_2, clk_pin_2, MSBFIRST);
  digitalWrite(ce_pin_2, HIGH); // Disable the clock

  return the_shifted;

}

// A function that prints all the 1's and 0's of a byte, so 8 bits +or- 2
void print_byte(byte val)
{
  byte i;
  for (byte i = 0; i <= 7; i++)
  {
    Serial.print(val >> i & 1, BIN); // Magic bit shift, if you care look up the <<, >>, and & operators
  }
  Serial.print("\n"); // Go to the next line, do not collect $200
}

void fill_starting_row() {
  int i = 0;
  byte data;
  for (i = 0; i < 8; i++) {
    data = incoming1 >> i & 1;
    thang(i, data);
    start[i] = data;
  }
  for (i = 8; i < 16; i++) {
    data = incoming2 >> (i - 8) & 1;
    thang(i, data);
    start[i] = data;
  }
  for (i = 16; i < 24; i++) {
    data = !(incoming3 >> (i - 16) & 1);
    thang(i, data);
    start[i] = data;
  }
  for (i = 24; i < 32; i++) {
    data = !(incoming4 >> (i - 24) & 1);
    thang(i, data);
    start[i] = data;
  }
  Serial.print("\n");
}

void thang(int i, byte thing) {
  //  Serial.print(i);
  //  Serial.print(" : ");
  Serial.print(thing, BIN);
  Serial.print(" ");
}

void displayRow(int b[32]) {
  for (int a = 0; a < 32; a++) {
    thang(a, b[a]);
  }
  Serial.print("\n");
}

uint16_t transform(byte input) {
  //    return matrix.Color333(7 - WheelPos, WheelPos, 0);
  switch (input) {
    case 1:
      return Wheel(0);
    case 2:
      return Wheel(3);
    case 4:
      return Wheel(6);
    case 8:
      return Wheel(9);
    case 16:
      return Wheel(12);
    case 32:
      return Wheel(15);
    case 64:
      return Wheel(18);
    case 128:
      return Wheel(21);
    default:
      return Wheel(24);
  }
}

It’s pretty bad because I wrote it the night before (as is my tradition for any large project)

Writing this was a fun excersise to see how well I understand arrays / pointers in C. It turns out I still don’t grok them, but with sufficient sacrifices to Malloc (all praise to It), I managed to get things working.

Math

So, what the heck is the pattern I’m using anyway?

Basically, each pixel/cell (I’m going to call them cells from now on), except for the first row of dots, is set to one of two colors based on the 3 cells above it according to the following table:

               
111 110 101 100 011 010 001 000
0 1 1 0 1 1 1 0

Where 1 is an “alive” cell and 0 is a “dead” cell. The name comes from the fact that writing out 01101110 in decimal is 110, and there are other rules like rule 30 and rule 184.

If our alive colour was red, and our dead colour was blue, then a cell who had a red pixel up and to the left, another red pixel directly above, and a blue pixel above and to the right, then it would be alive and thus red. In my project colours of the alive and dead cells are set using the 2 knobs.

Mind blowing fact: Rule 110 is TURING COMPLETE

tatooed man holding hedgehog (number 1 result on google image search for turing complete, filtered by gifs)

For those of you with minds still unblown, I am going to assume it’s because you aren’t familiar with turing completeness yet. Basically, if a problem or program can be computed it can be run on a machine that is turing complete. That means if I had enough time (and enough will) I would program a pattern into a rule 110 system that couple compute prime numbers, or play minecraft.

Reactions

There were so many different reactions to my project. Kids were immediately interested in playing with the buttons and the knobs. Adults I had to cajole and encourage, which I think is a bit sad. If this massive bank of switches wasn’t supposed to be touched, don’t you think I would have indicated that some way?

I could also tell immediately the people that wanted to figure out the pattern. The first thing they would do after reseting the display for the first time, was to change a few switches and see what changed. Then they would set all the switches to one position (up or down) and try that. Then they would change just one switch. The entire project was worth it for the looks on people’s faces when they learned something new, either than confirmed some expectation, or suprised them

Conclusions

All in all, it was totally a good idea for me to apply to makerfaire. This is one of the largest electronics projects I’ve yet completed, and I doubt I would have done it without the thought of an empty table. I guess I should start thinking about next year…