Projects

Octoled3 Cube: Part 3

Programmable 8 x 8 x 8 Blue LED Cube: Part 3

Johann Wyss

Issue 37, August 2020

This article includes additional downloadable resources.
Please log in to access.

Log in

In this issue, we show you how to program animations into your OCTOLED3.

In the previous two issues, we have covered how to assemble the 8x8x8 LED cube. In part one (Issue 35), we show you how to solder together the 512 LED matrix array. In part 2 (Issue 36), we built the electronics and connected the LED array, showed you how the cube works, and then to test the cube with a test program.

In this issue, we will talk you through the process of programming animations. We hope that this will allow you to create your very own custom animation sequences.

There are a number of ways that you can program animations on this cube. We have made every attempt to make the process as easy to follow and understand as possible so that makers of all skill levels can get involved. This does, however, come at a pretty significant cost with respect to the size of the program, as we will explain later.

The broad overview

To best explain the programming methods, we first need to quickly explain how the program will operate.

Our program relies on a two-dimensional 8 x 8 integer array with each element holding 8 bits / 1 byte called:

cube[8][8]

This array stores the desired state for all 512 LEDs of the matrix / array forming the physical LED cube.

const uint8_t cube[8][8]  = { //cube lines
  {B11111111, B10000001, B10000001, B10000001, B10000001, B10000001, B10000001, B11111111}, //layer 0
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 1
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 2
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 3
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 4
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 5
  {B10000001, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B10000001}, //layer 6
  {B11111111, B10000001, B10000001, B10000001, B10000001, B10000001, B10000001, B11111111}, //layer 7
};

The array is split into 8 layers each containing 8 elements. These 8 elements represent the rows. Each row consists of a one byte (8-bit) binary representation of each LED / pixel for that row. The first element in layer 0 B11111111 (B before the number tells the compiler we are using binary) represents pixels 1 – 8. A better way to show the array is to break it into layers and see the data in 3D space using a table. Altogether, this will form the cube edge test pattern shown here in plan view with a '1' representing an LED that is 'on'.

In this array, a one (1) indicates that the LED should be illuminated and a zero (0) indicates that the LED should be off.

Given that only one layer can ever be illuminated at any given time, we needed a way to send the current data held in the array out to the flip-flops, to be displayed on the physical LED array / matrix.

To do this, we used a timer interrupt to trigger an interrupt service routine (ISR). This pushes the data stored in the cube[8][8] array out to the flip-flops and decoder, producing the image on the physical LED matrix / array.

If you’re new to microcontrollers you may be asking yourself “what is a timer interrupt?”. To explain it, you first need to understand that a computer program (with the exception of programs written for special hardware such as field programable gate arrays etc) is written to follow a specific set of sequential instructions.

That is to say, the program executes the code on one line and then proceeds to the next line below it. For the most part, this is perfectly fine. However, it makes precisely timed events very difficult as the program has to execute each and every line of code, in every loop of the program.

Naturally, that makes multitasking near impossible. Imagine, you’re a computer program making toast for your program family. Your set of instructions in a world without interrupts may read something like this.

This will work perfectly fine if you only want one serving of toast, but is terribly inefficient as you need to wait for every loop of the program for the bread to toast before you can add additional slices of bread.

What if we set a timer in which we checked to see if the toast was ready? We could then check every x period if the toast in the toaster is ready. If it’s ready, we can remove it and place more bread into it before going back to the previous task of buttering the cooked toast.

This would significantly reduce the waiting time for each load of toast from the toaster, allowing us to do much more work in the same period.

The same goes for our program. For multiplexing to work, we need the cube to refresh at a rate faster than 60 frames a second (fps), when at the same time, some animations may remain a static image for a few seconds in total. To do this, we need to force the program to leave its current task and complete the display task at a frequency faster than 60 fps. After the display task is finished, the code then returns to the task it was performing before the interrupt was triggered.

To set the interrupt, we use the following code.

cli();
  TCCR1A = B00000000;
  TCCR1B = B00000000;
  TCNT1  = B00000000;
  OCR1A = 31999; // = 16000000 / (1 * 500) - 1
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (0 << CS12) | (0 << CS11) | 
(1 << CS10);
  TIMSK1 |= (1 << OCIE1A);
  sei();

The ATmega328P microcontroller has three different interrupt vectors: timer 0, timer 1, and timer 2.

Timer 0 is an 8-bit timer and is used in the Arduino environment to control the delay(), millis(0 and micros() functions.

Note: Adjusting this timer is not advised if you’re using any of these functions as it can have detrimental effects. It can also have significant effects on most libraries so in general, it’s best to steer clear until you’re a little more confident. Modifying this timer may also have an impact when using analogWrite() on pins 5 and 6.

Timer 1 is a 16-bit timer and is much less likely to have adverse issues with most libraries. However, the servo library uses this timer so caution is required when using that library. Modifying this timer may also have an impact when using analogWrite() on pins 9 and 10.

Timer 2 is another 8-bit timer and is not used a great deal in the Arduino environment. Some functions like tone() use it but it’s used much less compared to Timer 0, and thus, less critical. Modifying this timer may also have an impact when using analogWrite() on pins 3 and 11.

We set the timer using the code shown. No doubt, if you’re new to programming microcontrollers this lump of code is quite daunting. Don’t be dissuaded, it isn’t that complex.

The functions cli(); and sei(); are called to disable and enable interrupts. We first disable any interrupts that may be running using cli(); and and after setting the interrupt registers we enable them once again using sei();.

TCCR1A and TCCR1B sets the timer 1 / counter control registers A and B to zero. We will change these later when we set the prescaler.

TCNT1 clears the timer / counter register. This register holds the timer value as it increments.

OCR1A is the output compare register. This is where we set the value for our desired interrupt frequency using the following equation.

OCR1A = (clock frequency in hertz / (prescaler x desired frequency)) - 1

You may be asking yourself what is the clock frequency? The ATmega328P uses an oscillating signal called the system clock. We set this value in the hardware by using the 16MHz crystal oscillator. Thus, in our project, the frequency is 16 million or 16,000,000Hz.

The prescaler allows us to essentially divide the clock signal to increase the duration of our timer. The timer counts the number of clock cycles and compares the counted number against the desired number. When the number is reached a flag is set and the program jumps to the interrupt service routine (ISR). To do this, the current number needs to be recorded. If you remember how we said that the three timers on the ATmega328P were either an 8-bit or 16-bit timer, this is where it comes into play.

This shows that on timer one without a prescaler, the maximum value we can achieve is 65,353 clock cycles. Whereas, with a prescaler, we don’t count every clock cycle but rather every 8, 64, 256 or 1024 clock cycles.

Since we want a frequency of 500Hz, we are able to easily use a prescaler of one as it is below the maximum value of 65,535.

OCR1A = (16,000,000Hz / (1 x 500Hz)) - 1 = 31,999

We picked the 500Hz frequency as at this frequency the cube creates a uniform picture without flicker. For this project, we decided that a 60Hz refresh rate similar to a PC monitor would suffice as this should give us fluid motion. Since we have 8 layers to go through each loop of the program, we needed the interrupt to loop at a rate of 8 times the desired refresh rate. 8 x 60 = 480Hz. We tested the project at 400Hz, and we could indeed detect a slight flicker in the cube and decided to bump the timer to 500Hz.

TCCR1B - Timer/Counter1 Control Register B from ATmega328P datasheet.

TCCR1B |= (1 << WGM12); Puts the register into clear timer on compare mode.

TCCR1B |= (0 << CS12) | (0 << CS11) | (1 << CS10); sets bit 12, 11 and 10 CS12, CS11 and CS10 to 0, 0, 1. This sets the prescaler as 1 or no prescaler.

Finally, TIMSK1 |= (1 << OCIE1A); enables the timer 1 compare match interrupt by setting bit 1 to 1.

TIMSK1 - Timer/Counter1 Interrupt Mask Register from ATmega328P dataseheet

Every time the value in Timer Counter 1 (TCNT1) matches the stored value 31,999 stored in the output compare register 1 A (OCR1A), the program will jump into the interrupt service routine shown here.

ISR(TIMER1_COMPA_vect){
   //do something

For our program, we want it to display the array data stored in the 2D array cube[8][8] on the physical LED matrix / array. We will discuss this in the Interrupt service routine section a little later.

Direct Port Manipulation

You may notice that this program / sketch looks quite a bit different from the programs you usually see written in the Arduino integrated development environment (IDE). We have used direct port manipulation so that we can maximise the efficiency of the program.

The Arduino programming style that you may be used to is a heavily simplified environment designed to make programming on the respective microcontrollers as approachable as possible. This simplicity, however, comes at a cost to performance.

Take, for example, the very simple task of pulling a digital pin high or low. In the normal Arduino style of programming we can use the line:

digitalWrite(13, HIGH);
digitalWrite(13, LOW);

This is nice and easy to remember but does not resemble how microcontrollers actually work. The ATmega328P used in this project (which is identical to the microcontroller used in the Arduino UNO) would have no idea how to interpret commands such as that.

In the absence of the Arduino IDE we would need to directly change the port register for the corresponding pin.

We can see that digital pin 13 in the Arduino nomenclature is actually called PB5 in the AVR architecture. PB refers to the Port B register, which is shown in the ATmega328P datasheet as this.

PORTB - The Port B Data Register.

Writing a 1 to PORTB bit 5 will pull that pin high and a 0 will pull it low. In order to do that without changing the state of the remaining 7 bits, and thus affecting the state of the pins (digital pins 8 – 12 and the oscillator pins), we need to use bitwise operations.

The bitwise AND (&) to set pins low.

And:

The bitwise OR (|) to set pins high.

Let’s say PORTB = B00001111.

If we wanted to change the state of bit 0 and bit 1 from a 1 to a 0 without changing the state of any of the remaining bits we would use the line:

PORTB + PORTB & B11111100;

The bitwise (AND) will only produce a 1 in the resultant if the current and the mask both have a 1, as shown here.

The bitwise (OR) will change the output of a respective bit to a 1 if either of the bits (current or new) have a 1.

If we perform the OR bitwise operation to return the previous result back to its previous state, the code would be:

PORTB = PORTB | B00000011;

You may be asking yourself, why bother going to this extra effort when we can just use the digital write command to do the exact same task. The answer is, of course, efficiency. This code requires the use of timer interrupts and we want to maximise the speed at which it can operate to ensure that the cube operates smoothly. To demonstrate just how big a difference using direct port manipulation is, we will run a simple comparison code that will change the state of an output. This code will show both methods and their respective results on an oscilloscope.

Firstly, we will use the usual Arduino user friendly method of changing the state of a digital output.

void setup() {
  pinMode(13, OUTPUT);
}
void loop() {
  digitalWrite(13, HIGH);  
  digitalWrite(13, LOW);    
}
734 bytes.

We can see that this code produces a 5V pulsating DC squarewave output with a frequency of about 150KHz. This is nowhere near as fast as the ATmega328P microcontroller with 16MHz crystal used in the Arduino Uno is capable of. To prove this, we will now show the results using direct port manipulation using the code.

Digital pin 13 is PORTB bit 5, thus we can control this bit using the code.

void setup() {
DDRB = DDRB | B00100000;  // pinMode(13, OUTPUT);
}
void loop() {
PORTB = PORTB | B00100000;  
// digitalWrite(13, HIGH);
PORTB = PORTB & B11011111; 
// digitalWrite(13, LOW);
}

This tiny adjustment means that the program creates a 5V pulsating DC square wave with a frequency of nearly 2MHz.

454 bytes.

Of course, the speed is not the only benefit of using direct port manipulation. We also make a decent saving in the amount of memory the base program uses. For example, the Arduino simplified code uses 734 bytes of program storage memory and 9 bytes of dynamic memory, whereas, with the direct port manipulation we only used 454 bytes of program memory and 9 bytes of dynamic memory.

This means that our program will use less space of the microcontroller’s memory and will run as quickly as practical.

The interrupt service routine

The Interrupt Service Routine (ISR) is triggered 500 times a second. This routine illuminates one layer every time it's triggered, and increments the layer variable so that all 8 layers get illuminated at a rate of about 60 times a second, which produces a solid three-dimensional image.

ISR(TIMER1_COMPA_vect) { 
// interrupt triggered 500Hz this displays a layer of the cube[8][8]
  PORTC = B00000000;
  PORTB &= B00001111; 
  //0 means 0, 1 means unchanged sets 
A B and C LOW
  PORTB = PORTB | B00001000; 
  //digitalWrite(OE, HIGH); 
  for (int i = 0; i < 8; i++) {
    PORTD = cube[layer][i];
    PORTB = (PORTB & B11111000) | (B00000111 & (i + 1)); 
    //increments A B and C
  }
  PORTB = PORTB & B11110111; 
//digitalWrite(OE, LOW); 
  if (layer < 6) {
    PORTC = (B0000001 << layer);
  }
  else if (layer == 6) {
    PORTB = PORTB | B00010000; 
//digitalWrite(12, HIGH);
  }
  else {
    PORTB = PORTB | B00100000; 
//digitalWrite(13, HIGH);
  }
  layer++;
  if (layer >= 8) {
    layer = 0;
  }
}
PORTC = B00000000;

Turns the digital pins 0 – 7 low.

PORTB &= B00001111; 

Pulls AB and C low. These are connected to the decoder which controls which flip-flop we are addressing.

We need to increment this 3-bit counter so that each of the 8 rows is addressed correctly.

PORTB = PORTB | B00001000;

Pulls the output enable pin high which turns off the output for all 8 flip-flops.

  for (int i = 0; i < 8; i++) {
    PORTD = cube[layer][i];
    PORTB = (PORTB & B11111000) | (B00000111 &
(i + 1));
    //increments A B and C
  }

Writes the values stored in the cube array to the flip-flops.

Layer is the current layer position and will increment each time the ISR is called.

PORTB is incremented on each iteration of this for loop so that all 8 rows forming a layer have the value written to the flip-flops.

PORTB = PORTB & B11110111;

Pulls the output enable pin on the flip-flops low allowing the flip-flops to illuminate the desired LEDs for the layer.

  if (layer < 6) {
    PORTC = (B0000001 << layer);
  }
  else if (layer == 6) {
    PORTB = PORTB | B00010000; 
  }
  else {
    PORTB = PORTB | B00100000; 
  }
  layer++;
  if (layer >= 8) {
    layer = 0;
  }

This code controls the layer and ensures that it is incremented correctly. Since the layers are shared across digital and analog pins, we need the if statements for when the layer is 6 or 7 in software (layer 7 or 8 in hardware). On these software layers we need to manipulate PORTB 5 and PORTB 4 (digital pin 13 and 12) whereas the first 6 layers are in PORTC 0 – 6 (Analog pins 0 – 5) and can easily be incremented using bit shifting (<<).

With the basics of how the cube operates out of the way, we can now get into explaining how you can modify the cube to display your very own custom animations.

Creating an animation

From the very inception of this project, we wanted to create an 8x8x8 LED cube that was easy enough for anyone, no matter how basic their programming capabilities, to make custom animations. For this reason, we settled on the 2-dimensional array system. This is the easiest way we can imagine for a person to animate the cube. The user can easily create stationary three-dimensional images, which if played in sequence, will appear as an animation.

To assist in creating custom animations we created a function called cubeBeat(). In this function, we take the cube lines 2D array and animate it. We make it shrink down to a tiny 2x2x2 lit array and then expand back to the cube outline.

The cube beat animation starts with the outer lines of the cube illuminated producing the outline of a 2D cube.

This data was stored in a 2D array called cube1 which was saved to the program memory. To write this data to the cube[8][8] array we can use the following code.

void cubeLines() {
  allOff();
  cli();
  for (int l = 0; l < 8; l ++) {
    for (int m = 0; m < 8; m ++) {
      cube[l][m] = pgm_read_word(&cube1[l][m]);
    }
  }
  sei();
  delay(3000);
}

This code has been stored as a functional block which is called in the main loop with the line:

cubeLines();

The first line calls another function allOff(), which clears the cube by writing “B00000000” to all 64 elements of the 2D array.

The function cli(); briefly turns off the interrupts so that the program can't be called away while reading the array data from program memory and writing it to the cube[8][8] array. We found if we didn’t suspend interrupts the data could become corrupted and out of place when transferring.

We then use a nested for loop to copy all 64 elements from this array and store them in the cube[8][8] array to be displayed.

sei(); reenables the interrupts, allowing the new data in cube[8][8] to be written to the display.

delay(3000); causes the program to halt here for 3000ms / 3 seconds.

Note: This delay does not prevent the interrupt from triggering and displaying the image.

After this delay we need to write the next frame to the cube[8][8] array.

Since we want to give the look as if the cube is shrinking, the next image should be identical to the cubeLines array but shrunken in on all four sides by one. As such, the array will look like the code shown here.

Remember, it may be more intuitive to look at the array one layer at a time and in their respective positions as shown here. The first element of each layer of the array is row 1 and the last element is row 8. We load the data into the cube[8][8] array bottom first so from layer 0 – layer 7. Thus, starting from the top and working down the array will display this image.

const uint8_t  cube2[8][8] PROGMEM  = { //cube lines2
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 0
  {B00000000, B01111110, B01000010, B01000010, B01000010, B01000010, B01111110, B00000000}, //layer 1
  {B00000000, B01000010, B00000000, B00000000, B00000000, B00000000, B01000010, B00000000}, //layer 2
  {B00000000, B01000010, B00000000, B00000000, B00000000, B00000000, B01000010, B00000000}, //layer 3
  {B00000000, B01000010, B00000000, B00000000, B00000000, B00000000, B01000010, B00000000}, //layer 4
  {B00000000, B01000010, B00000000, B00000000, B00000000, B00000000, B01000010, B00000000}, //layer 5
  {B00000000, B01111110, B01000010, B01000010, B01000010, B01000010, B01111110, B00000000}, //layer 6
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 7
};
The cube then shrinks so that the cube outline is of a 6x6x6 cube.
The cube shrinks again displaying a 4x4x4 cube outline.
Finally, the cube is its smallest size of a 2x2x2 cube. After this, the process is reversed so the cube returns to its previous 8x8x8 outline.

The data for this array can be written to the cube[8][8] array in an identical way as cubeLines was written.

void cubeLines2() {
  allOff();
  cli();
  for (int l = 0; l < 8; l ++) {
    for (int m = 0; m < 8; m ++) {
      cube[l][m] = pgm_read_word(&cube2[l][m]);
    }
  }
  sei();
  delay(rate);
}

However, for this animation, we found that 3 seconds was too long so we dropped it down to 80ms / 0.8 seconds. At this speed, the motion for the next few frames looks fairly constant, and thus, we declared a global variable rate = 80; and use this variable to set the speed of other functions. If you change this variable, it will change the speed of the other programmed animations so feel free to experiment.

We simply need to repeat the same process of storing the static image into a 2D array, and then calling a function to write that array to the cube[8][8], which will display until the cube is a tiny 2x2x2 in the centre. From here, we can just reverse the process calling each function until the cube is back to full size.

When played, the cube shrinks and expands back to its original size and looks quite fluid. We can then call the functions in order to display the animation on the LED as we show here. This function is called in the main loop using cubeBeat();

void cubeBeat() {
  cubeLines();
  cubeLines2();
  cubeLines3();
  cubeLines4();
  cubeLines3();
  cubeLines2();
  cubeLines();
}

Now, as we stated earlier, this is not the most efficient way to program such a device. However, it is by far the most simplistic approach that we can think of. We hope that this makes it accessible to as many makers as possible.

Efficiency wise, each 2D array and subsequent function to load it into the cube[8][8] array takes about 140 bytes of the 32,256 bytes of program storage, and 2048 bytes of dynamic memory. This means, in program memory, you could store about 20 custom static images into program storage and another 10 in dynamic storage.

To store the array into dynamic storage, you can use the code:

const uint8_t  nameOfArray[8][8] = {

Rather than:

const uint8_t  cubeTest[8][8] PROGMEM  = {

As demonstrated in the cubeOff array shown here.

const uint8_t  cubeOff[8][8] = { // all off
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 0
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 1
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 2
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 3
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 4
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 5
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 6
  {B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}, //layer 7
};

To copy an array stored in dynamic memory like this, the code is slightly different. If you’re using this method to store additional static images you will need to use the following code to copy the array to the cube[8][8] array.

void clearArray() {  
  for (int l = 0; l < 8; l ++) {
    for (int m = 0; m < 8; m ++) {
      cube[l][m] = cubeOff[l][m];
    }
  }
}

We don’t need to disable interrupts when moving data stored in dynamic memory to another location in dynamic memory. We can simply use nested for loops to ensure that all 64 elements are copied over.

Note: This function is designed to clear the array, resetting all elements to zero and turning each LED off. This is a very inefficient way to do this. It takes up 140 bytes.

A much more efficient way to code such a function would be as follows.

void allOff() { // turns off all leds
  for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j ++) {
      cube[i][j] = {B00000000};
    }
  }
}

In this function, we simply write B00000000 to each of the 64 elements without needing to keep an entire array of 64 elements. Naturally, this significantly reduces the number of bytes needed for the function. For loop i controls the layer and j controls the row. You can perform many complex tasks on the array using such loops. You can use bit shifting left or right to move the active bits in either direction. You can rotate the array and even invert it.

This technique of filling the array using nested for loops whilst a little more complex to initially understand will allow you to make many more animations in the limited program space of the ATmega328P microcontroller.

The code will be available for download from our website. We have created seven basic animations to display on the cube using a mixture of both techniques shown here. In total we are using 3224 bytes (9%) of the program memory and 97 bytes (4%) of the dynamic memory. This leaves plenty of room for you to create your own custom animations to show off your cube.

Power supply

In the previous issue, we did not get to delve into the power supply requirements for the project due to space and time constraints so we will add them here to finish off the project. We designed the cube to work with power supplies that most people will commonly have in their homes, namely smartphone chargers. These power supplies normally put out a stable 5VDC voltage with current capabilities up to a few amps. Our cube will comfortably run with a 5V1A DC supply.

To confirm this, we programmed the cube to illuminate all 512 of the LEDs and attached our digital multimeter in series with the power supply. This is the absolute maximum current state for the cube, and it was drawing 850mA at 5.21V.

There is no voltage regulation or reverse polarity protection on this project, therefore, it is imperative that you do not supply the project with voltages greater than the maximum voltage of any of the components. We checked the datasheets for the 10 integrated circuits on the PCB to find the maximum and minimum working voltages for the cube.

Using the datasheets, we can conclude that the project may work with voltages down to as low as 2.7V but must not exceed 5.5V. Exceeding the upper limit may cause significant damage to the cube’s electronics.

If you’re intending on using a smartphone charger, these chargers usually have a USB output. You will need a USB to 2.1mm centre positive barrel jack cable similar to this Adafruit power cable sold by Core Electronics SKU: ADA2697

ADA2697 power cable sold by Core Electronics.

Where to from here?

Congratulations! You now have a 512 LED cube that you can program with amazing animations. Be sure to show off your custom animations on our social media platforms for others to see.

EDITOR'S NOTE: Similar to the 4x4x4 LED cube published in Issue 34, this 8x8x8 LED cube has been designed in collaboration with Jaycar. Individual parts for this 8x8x8 cube are available from most electronics retailers, however, you should also be able to find kits for this project in Jaycar stores shortly after this issue hits the streets.

Part 1

Part 2

Johann Wyss

Johann Wyss

Staff Technical Writer.