← all posts
Posted on Feb 1, 2016

Building a 4x4x4 LED Cube. Part II; The software

It’s been awhile since I wrote about the LED cube I’ve been building and I figured it was about time I did the long planned followup where we take a closer look at the software that drives the cube.

If you’ve followed along with my earlier writings you’ll know that we’ve been building up to this point by first driving 16 LEDs using only three pins of an Arduino and then learned how to apply fading effects to the LEDs by taking a deep dive in Bit Angle Modulation (BAM). In my previous post we’ve talked about the hardware of the cube and in this post we’ll combine the pieces and learn how we can drive the individual LEDs in the cube.

We’re just getting started and we’re already almost done

Ok, in the previous examples we’ve kept is relatively simple. Driving 16 LEDs using 595-shift registers.. Done. Applying fading effects using Bit Angle Modulation.. Done. But now we’re dealing with 64 individual LEDs. thats hard! Or is it?

Not really; if you think about it a 64 LED cube are just four layers of 16 LEDs, and we’ve already nailed that part. Lets have a look at a couple of changes we have to make to our existing codebase.

Selecting a layer

The main difference between the cube setup and simply driving 16 LEDs is that we have to deal with multiple layers stacked on top of each other, so we need to find a way to select a specific layer to control.

Looking at my previous article about the hardware of this build you’ll see that we’ve added one extra shift register so we now have three instead of the two we used to drive the 16 LED example. This shift register is hooked up to four transistors which will complete the circuit once pulled high.

 1// turn on correct LED using 595's
 2void turnOnLed(int ledNr, int layer) {
 3  digitalWrite(latchPin, LOW);
 4  SPI.transfer(1<<layer);
 5
 6  if (ledNr >= 8) {
 7    SPI.transfer(1<<ledNr-8);
 8    SPI.transfer(0);
 9  }
10  else {
11    SPI.transfer(0);
12    SPI.transfer(1<<ledNr);
13  }
14
15  digitalWrite(latchPin, HIGH);
16}

As you can see in the example included above we’ve extended our turnOnLed() function we’ve created in the BAM example to also take an integer called layer. We’re shifting 1 << layer out first, which makes sure that a value between 1000 and 0001 (binary) will be pushed in first.

Followed by the actual LED information that will be pushed to the second and first shift register, this part is actually not changed when comparing to the BAM example.

Note that we’re shifting MSBFIRST (Most Significant Bit First) here, so the layer information we’ve shifted out first will be pushed through to the last shift register when we’re shifting out the actual LED information.

Keeping track of our four layers

Ok, so we can select a layer to control now. The next challenge is keeping track of the value of each LED. We’ve already dealt with BAM for 16 LEDs so we can basically just copy and paste the brightnessMask array from the BAM example three more times.

1bool brightnessMask_layer1[NUMBER_OF_CONNECTED_LEDS*4];
2bool brightnessMask_layer2[NUMBER_OF_CONNECTED_LEDS*4];
3bool brightnessMask_layer3[NUMBER_OF_CONNECTED_LEDS*4];
4bool brightnessMask_layer4[NUMBER_OF_CONNECTED_LEDS*4];

Note that this is the solution I came up with since the language didn’t (and doesn’t?) allow me to define a multi-dimentional array here.

Writing values for each layer

So we have our layer select part done, we’re keeping track of the value of each LED in each of our four layers. So what’s next? We need a way to write the correct value to the correct position of the brightnessMask array before we can shift it all out. Therefore I’ve changed up the led() function a bit.

 1void led(int x, int y, int z, int brightness){
 2   // ensure 4-bit limited brightness
 3  brightness = constrain(brightness, 0, 15);
 4
 5  int ledNr = y*4+x;
 6
 7  // turn 4-bit brightness into brightness mask
 8  for (int i = 3; i >= 0; i--) {
 9    if (brightness - (1 << i) >= 0) {
10      brightness -= (1 << i);
11      setBrightnessForLayerAddressValue(z, (ledNr*4)+i, true);
12    }
13    else{
14      setBrightnessForLayerAddressValue(z, (ledNr*4)+i, false);
15    }
16  }
17
18}
19
20void setBrightnessForLayerAddressValue(int layer, int address, bool value){
21  if(layer == 0){
22    brightnessMask_layer1[address] = value;
23  }
24  else if(layer == 1){
25    brightnessMask_layer2[address] = value;
26  }
27  else if(layer == 2){
28    brightnessMask_layer3[address] = value;
29  }
30  else if(layer == 3){
31    brightnessMask_layer4[address] = value;
32  }
33}

As you can see in the code snippet included above the led() function now accepts an x, y and z value along with its brightness value. Where the x and the y values obviously map to their corresponding LED on a given layer which is determined by the z parameter.

We’re determining the overall LED address (integer) by multiplying the y value by four and adding the x axis on top of that. Calculating the 4-bits based on the 0–15 brightness value is still the same as in the BAM example, but we’re delegating the outcome together with the layer information to setBrightnessForLayerAddressValue function which selects the correct brightnessMask array to write to.

Shifting it all out

Okay, so now we’re able to select a layer, keep track of brightness information of each layer and writing brightness information to the brightnessMask. These were in fact the biggest changes, pushing this information to the cube is basically more of the same we already had.

 1void refresh(){
 2
 3 // Loop over each LED
 4 for (int cycle = 0; cycle < 16; cycle++) {
 5
 6   for (int currentLed = 0; currentLed < NUMBER_OF_CONNECTED_LEDS; currentLed++) {
 7     int maskPosition = currentLed * 4;
 8     if (cycle == 1 && brightnessMask_layer1[maskPosition]) {
 9       turnOnLed(currentLed, 0);
10     }
11     else if ((cycle == 2 || cycle == 3) && brightnessMask_layer1[maskPosition+1]) {
12       turnOnLed(currentLed, 0);
13     }
14     else if (cycle >= 4 && cycle <= 7 && brightnessMask_layer1[maskPosition+2]) {
15       turnOnLed(currentLed, 0);
16     }
17     else if (cycle >= 8 && cycle <= 15 && brightnessMask_layer1[maskPosition+3]) {
18       turnOnLed(currentLed, 0);
19     }
20   }
21
22   for (int currentLed = 0; currentLed < NUMBER_OF_CONNECTED_LEDS; currentLed++) {
23     int maskPosition = currentLed * 4;
24     if (cycle == 1 && brightnessMask_layer2[maskPosition]) {
25       turnOnLed(currentLed, 1);
26     }
27     else if ((cycle == 2 || cycle == 3) && brightnessMask_layer2[maskPosition+1]) {
28       turnOnLed(currentLed, 1);
29     }
30     else if (cycle >= 4 && cycle <= 7 && brightnessMask_layer2[maskPosition+2]) {
31       turnOnLed(currentLed, 1);
32     }
33     else if (cycle >= 8 && cycle <= 15 && brightnessMask_layer2[maskPosition+3]) {
34       turnOnLed(currentLed, 1);
35     }
36   }
37
38   for (int currentLed = 0; currentLed < NUMBER_OF_CONNECTED_LEDS; currentLed++) {
39     int maskPosition = currentLed * 4;
40     if (cycle == 1 && brightnessMask_layer3[maskPosition]) {
41       turnOnLed(currentLed, 2);
42     }
43     else if ((cycle == 2 || cycle == 3) && brightnessMask_layer3[maskPosition+1]) {
44       turnOnLed(currentLed, 2);
45     }
46     else if (cycle >= 4 && cycle <= 7 && brightnessMask_layer3[maskPosition+2]) {
47       turnOnLed(currentLed, 2);
48     }
49     else if (cycle >= 8 && cycle <= 15 && brightnessMask_layer3[maskPosition+3]) {
50       turnOnLed(currentLed, 2);
51     }
52   }
53
54   for (int currentLed = 0; currentLed < NUMBER_OF_CONNECTED_LEDS; currentLed++) {
55     int maskPosition = currentLed * 4;
56     if (cycle == 1 && brightnessMask_layer4[maskPosition]) {
57       turnOnLed(currentLed, 3);
58     }
59     else if ((cycle == 2 || cycle == 3) && brightnessMask_layer4[maskPosition+1]) {
60       turnOnLed(currentLed, 3);
61     }
62     else if (cycle >= 4 && cycle <= 7 && brightnessMask_layer4[maskPosition+2]) {
63       turnOnLed(currentLed, 3);
64     }
65     else if (cycle >= 8 && cycle <= 15 && brightnessMask_layer4[maskPosition+3]) {
66       turnOnLed(currentLed, 3);
67     }
68   }
69   clearLeds();
70 }
71
72}

While this looks like a lot of code, it comes all down to this: Loop 16 times for one cycle, loop over the leds in and determine wether the LED should be turned on or off for the current cycle. Each cycle will call the turnOnLed() function we’ve already described above. Repeat this step for all four layers and we’re done!

It’s a wrap

These where in fact the biggest changes. For the full source code have a look at the gist. In a future article we’ll be looking at how we can take these building blocks and create a neat little animation with it. Of course we’ll also be looking at how we can refactor our code since it gained quite some complexity to the point where it isn’t exactly DRY anymore ;)