Universal SR driven 4x4x4 to 8x8x8 LED cube library (included 5x5x5 schematics)

Hi all!

Quite some time ago I'd made a topic about my 3x3x3 and 5x5x5 LED cubes. About a year ago I started building an 8x8x8 LED cube (single color). This project was very fun and I learned very much from it. Sadly (if I remember it correctly), I never posted anything about my 8x8x8 LED cube (for those who are interested in photo's, look down further down this post). Some weeks ago I got excited to do something with my cube again. So I started thinking what I could do with my LED cube. After some browsing on the internet something struck my mind: why aren't there any opensource arduino libraries available for LED cubes that are driven by 8 bit (or higher) shift registers? The project was born :). After some research I came up with a couple of requirements:

  • Library has to support a different range of LED cube sizes, from 4x4x4 all the way up to 8x8x8 (single color);
  • Libray should be very easy to use, even for beginners;
  • Library should contain predefined animations.

With this information I started to implement my universal LED cube library. In the upcoming paragraphs I will explain the complete library with code examples, videos, drawings, etc. I'll hope some of you will enjoy reading it :)!.

HARDWARE CONFIGURATION (important!)
Hardware may not be the first thing you would think about with implementing a software library. But for this library it is quite crucial to define a standard hardware configuration to be able to satisfy the support for different cube sizes. So earlier on I mentioned (larger) LED cubes are usually controlled by Serial In Parallel Out shift registers. With this in mind I defined the hardware configuration that must be met in order to use the library. In the image below I made an example of a 5x5x5 LED cube (single layer in the picture) how the shift registers should be wired to the LEDs. With the image the hardware configuration can be defined as follows:

  • Number of shift registers needed = (number of columns / number of output of the shift register) + 1
  • The First shift register has to be the SR that is wired to the layer (connected to FETs/transitors) selector. This one is also connected via the SPI pins to the arduino microcontroller. If you put this shift register anywhere different you have to modify the sequence how the bytes are transferred to the registers.
  • The column selector shift registers are cascaded with each other (+ cascaded with the layer selector). The shift register outputs are filled with a row of LEDs, whenever you have left over outputs on the shift registers, just wire the next part of the row of LEDs to the left over pins on the shift register. If that shift register becomes full then take the next cascaded one and continue wiring the left over LEDs of that row to that new shift register. Do this until all the rows are connected (in the image that means 5 times 5 rows wired to 4 shift registers in total.
  • The MOSI line only connects to the first shift register (the layer selector), all the other lines (SCLK, CLEAR and LATCH) are connected parallel to the microcontroller. NOTE: SCLK and MOSI lines are predefined pins on the microcontroller, CLEAR and latch can be any pin of the microcontroller.

Do note that this schematic is far from complete, this is only used as illustration how the shift registers should be connected to the LEDs.

LIBRARY IN DETAIL
In this paragraph I will try to explain the complete library. The library itself contains multiple files, the two most important ones are the LedCube.h and LedCube.cpp. The header file contains all the definitions of the members that are used in the .cpp file. Relevant members are briefly explained in the table below.

CONSTRUCTOR

LedCube::LedCube(uint8_t latchPin, uint8_t clearPin, uint8_t cubeSize, uint16_t refreshFrequency)
{
  // Set variables
  _latchPin = latchPin;
  _clearPin = clearPin;
  _cubeSize = cubeSize;
  _layerSize = VOXELMAPPING_ARR_LAYER_SIZE;
  _zPositionCounter = 0;
  _allocated = false;
  if (refreshFrequency != 0)
    _refreshFrequency = refreshFrequency;
  else
    _refreshFrequency = 1;

  // Set pins
  pinModeFast(_latchPin, OUTPUT);
  pinModeFast(_clearPin, OUTPUT);
}

When a new instance of the class is made the constructor is called. The table showed us already that the constructor accepts 4 parameters. The first parameter should contain the microcontroller its pin number where the latches of the shift register are connected on. The second parameter should again be a pin number of the microcontroller where all the output enables of the shift register are connected on. The third parameter defines the size of the LED cube, I defined a couple of literals (see header file) to use for this parameter. The fourth and last parameter is used to define the refresh frequency of the cube (in Hz). In the constructor code below we can see the use of pinModeFast() (and later on digitalWriteFast()). This function is used instead of the normal pinMode() (and digitalWriteFast()) function to improve the overall speed of the code. This pinModeFast function is defined in a separate library (not mine) which I included in my own library, so no additional download is required.

BEGIN

// Start displaying animations on the cube
void LedCube::begin(void)
{
  _objPtr = this; // Reference pointer to current instance
  allocateVoxelMapping();
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
  SPI.setClockDivider(SPI_CLOCK_DIV2); // Fastest data transfer
  SPI.begin();
  initInterrupt();
}

To start refreshing the LED cube you simply call the function begin(). This function initializes the SPI, timer and voxel mapping arrays. SPI is configured as follows:

  • Bit order: MSB first
  • Data mode: SPI_MODE0
  • Clock divider: SPI_CLOCK_DIV2

Gamma correction table generator (files).zip (13 KB)

LedCube(old V3.0).zip (19.6 KB)

LedCube.zip (60.5 KB)

For the timer I'd used the 16 bit timer1. This timer is then initialized according to the user defined refresh frequency. As seen in the code below (void initInterrupt(void)) we first calculate the period of the frequency. This period will be used as parameter for the function calcPrescaler() to calculate the smallest needed prescaler. To be able to calculate the prescaler we actually need to know the value of the timer output compare register. However this value is usually calculated after the prescaler is known. Luckily we do know the maximum value of the output compare register, because we know that timer1 is a 16 bit timer. This means the maximum possible value of the output compare register is 65535. Based on this knowledge we can calculate a prescaler that will be large enough to limit the output compare register value to a maximum of 65535. Overall, when we multiply the calculated period with the microcontroller its oscillating frequency (F_CPU) and divide that value with 65535, we get an exact number of the needed prescaler to limit the output compare timer register to its maximum possible value. Because it ain't possible to use such exact prescaler we need to round up that value to the closest and greatest available prescaler. Both the microcontroller its prescaler values and select bits are defined in an array of the struct type Prescaler. Eventually the calcPrescaler() will return a single struct item of this array. This prescaler is then further used to calculate the needed output compare register value to match the desired refresh frequency and to set the prescaler bits of the TCCR1B register as can be seen in the code below.

typedef struct
{
  uint16_t value;
  uint8_t selectBits;
} Prescaler;

const Prescaler LedCube::_prescalers[] = { {1, 0b001}, {8, 0b010}, {64, 0b011}, {256, 0b100}, {1024, 0b101} };  // Largest prescaler must be the last item.

// Initialize timer interrupt
void LedCube::initInterrupt(void)
{
  // Calculate prescaler and timer counter
  double period = 1.0 / _refreshFrequency;
  Prescaler prescaler = calcPrescaler(period);
  uint16_t timerCounter = period / ((double)prescaler.value / F_CPU);

  // Set timer parameters
  TCNT1  = 0;
  TCCR1A = 0;
  TCCR1B = prescaler.selectBits;
  OCR1A = timerCounter;
  TCCR1B |= (1 << WGM12);
  TIMSK1 |= (1 << OCIE1A);
}

// Prescaler calculation
Prescaler LedCube::calcPrescaler(double period)
{
  uint16_t pr = ceil((F_CPU * period) / 65535.0);
  for (uint8_t i = 0; i < PRESCALER_ARR_SIZE; i++)
  {
    if (pr <= _prescalers[i].value)
      return _prescalers[i];
  }
  // At this point return the largest available prescaler
  return _prescalers[PRESCALER_ARR_SIZE - 1];
}

To map all the states of the LEDs I use a single 1 dimensional array of bytes. Because the size isn't known at compile we need to allocate the memory at run time. To determine the total size of this array we need to know how many bytes are needed to store a single layer of the LED cube and multiply this with the total number of layers in the cube. For example, we have a 5x5x5 LED cube. We know that one layer of this cube contains 25 LEDs, thus 25 bits need to be stored. To store 25 bits we have to use atleast 4 bytes of data. To store all the 5 layers we then need 4 * 5 = 20 bytes of data. This memory is allocated (and initialized with zeros) with the code below. The manipulating of the bits of this array is discussed later in this paragraph.

// Allocate memory needed to map the voxels of the cube
void LedCube::allocateVoxelMapping(void)
{
  if (_allocated)
    return;

  // Allocate 1 dimensional array
  uint8_t size = _layerSize * _cubeSize; // Calculate max. needed bytes
  _voxelMapping = (uint8_t *)calloc(size, sizeof(uint8_t)); // Allocate memory and initialize it with zeros
  if (_voxelMapping == NULL)
    abort();
  _allocated = true; // At this point the memory is allocated properly
}

END

// Stop displaying animations on the cube
void LedCube::end(void)
{
  TIMSK1 &= ~(1 << OCIE1A); // Disable timer
  SPI.end();
  digitalWriteFast(_clearPin, HIGH); // Clear all data

  // Free allocated memory
  if (_allocated)
  {
    free(_voxelMapping);
    _allocated = false;
  }
}

To stop refreshing the cube and to free the SPI, timer1 and memory you can call the function end(). There's not much to explain about this code so lets go to next paragraph :).

VOXEL

// Set voxel (z,y,x) state
void LedCube::voxel(uint8_t z, uint8_t x, uint8_t y, bool state)
{
  VoxelCoordinate currVoxel;

  // Make sure given input isn't greater than maximum cube size
  if (z >= _cubeSize)
    currVoxel.z = _cubeSize - 1;
  else
    currVoxel.z = z;

  if (x >= _cubeSize)
    currVoxel.x = _cubeSize - 1;
  else
    currVoxel.x = x;

  if (y >= _cubeSize)
    currVoxel.y = _cubeSize - 1;
  else
    currVoxel.y = y;

  setVoxel(currVoxel, state); // Set actual voxel value
}

To turn a LED on or off we can call the voxel(). This function accepts 4 parameters: z, x, y and state. The position is zero index based, so if you have a 5x5x5 LED cube the values can range from 0 to 4. In the code above we make sure you do not exceed the maximum value, the actual setting or clearing of a voxel happens in the setVoxel() function. This function accepts a instance of the struct type VoxelCoordinate and a state. As previously mentioned I use a byte array to store each LED state value. This byte array can be visually represented in the image below.

Now lets take focus on one single block of bytes. As you can see each block contains 4 bytes. This is the minimum to store 25 LED states (one single layer of a 5x5x5 cube). Let's say that the columns of a block can be defined as the x positions and the rows as the y positions. Then every new block (layer 0, layer 1, layer 2, etc....) can be defined as the z position. With that we only need to know which byte and bit of that byte we need to manipulate to turn on/off a voxel. For example, we want to turn on a voxel with the coordinate z = 0, x = 2 and y = 3. That means that bit 5 of the lighter green column of layer 0 should be set to a '1'. So to manipulate this particular value we need to know which byte of the voxel mapping array it is located in. We also need to know how many positions we need to shift the '1' to the left. To do this we start looking at the led position from a single layer. The led position in any given layer will be position = y-coordinate + x-coordinate * (1 dimensional cube size) = 3 + 2 * 5 = 13. With this value we can calculate both which byte we need to address and the number of positions we need to shift the '1' bit to the left. So, array index = position / byte-size = 13 / 8 = 1.625 = 1 (integer value) and array shift = position % byte-size = 13 % 8 = 5. And this are exactly the values (zero based) we need to know to address bit 5 of the lighter green column of the image. To correct for the z coordinate we simply add an offset to the calculated array index value. This means, for a 5x5x5 cube, that each layer adds an offset of 4 bytes.

typedef struct
{
  uint8_t z;
  uint8_t x;
  uint8_t y;
} VoxelCoordinate;

inline void LedCube::setVoxel(VoxelCoordinate voxelCoordinate, bool state)
{
  // Return if memory isn't allocated
  if (!_allocated)
    return;

  uint16_t voxelPos = voxelCoordinate.y + voxelCoordinate.x * _cubeSize; // Calculate the position of the voxel from a single layer
  uint8_t arrPos = voxelPos / 8 + voxelCoordinate.z * _layerSize; // Calculate index of the voxelmapping array based on the position of the voxel
  uint8_t arrShift = voxelPos % 8; // Calculate the needed shift to set the correct bit
  if (state)
    _voxelMapping[arrPos] |= (1 << arrShift); // Set bit to '1'
  else
    _voxelMapping[arrPos] &= ~(1 << arrShift); // Set bit to '0'
}

REFRESH DATA

void LedCube::refreshData(void)
{
  digitalWriteFast(_clearPin, HIGH); // Clear all data
  for (uint8_t i = 0; i < _layerSize; i++)
    SPI.transfer(_voxelMapping[i + _zPositionCounter * _layerSize]);
  SPI.transfer((1 << _zPositionCounter)); // Current layer byte
  // Latch the data
  digitalWriteFast(_latchPin, HIGH);
  digitalWriteFast(_latchPin, LOW);
  digitalWriteFast(_clearPin, LOW);
  _zPositionCounter++; // Increment counter to the next layer
  if (_zPositionCounter >= _cubeSize)
    _zPositionCounter = 0;
}

The last thing I want to discuss about this library is how the data of voxel mapping array is transferred to the shift registers. The code to do that is shown above. This function is called by the interrupt service routine of timer1. When we enter this function we first clear the current state of the shift registers by toggling the clear pin high (this pin should be connected to all the OE's of the shift registers). We then transfer all the data of the layer which is currently active. After that we transfer a byte that selects the current layer (first shift register in the schematics). To show the data on the parallel outputs we simply pulse the latch line. At this point we toggle the clear pin low to actually turn on the outputs. At last we increment the layer counter and check if it has reached its maximum value.

I also did experiment to see what the maximum refresh rate I could possibly reach. So I'd hooked my 8x8x8 LED cube (16MHz atmega 1284P) to my oscilloscope and ran the highest possible refresh frequency (65535). I was quite happy with the results. It can easily run a refresh rate of 10KHz (1.5KHz per layer) and up. In the image you can see a refresh rate of 2.02kHz per layer is the max for my cube (16,16kHz in total).

ANIMATION FILES
To make life easier I included animation files. If you want to make use of it just simply inlcude Animation.h in your project. All the animations are cube size independent. This means it will run on cubes from 4x4x4 to 8x8x8. The animation functions accept atleast 2 parameters: time and cubeRef. The first parameter defines the time (in ms) the animation should be executed. In the second parameter you should put the instance of the LedCube class. Examples will follow in the next paragraph. Each animation also have a couple of optional parameters. These parameters are used to manipulate the effect of the animation. The default value of these parameters are chosen based on my 8x8x8 LED cube, so for other size cubes the default parameters may not be optimal.

At this moment I only have written three animations: rain, slanting lines and corner cube. To know what optional parameters an animation has, just open the Animation.cpp in notepad++ (or any text editor) and look at the comments above each function. If any of you guys made a new animation that you think should be added to this file, just shoot me a PM :).

EXAMPLE
Finally, we can take a look how you can use this library in your LED cube project. First make sure this library is located in the library folder of your arduino IDE. If you done that open Arduino and make new project.

First off you have to include the header files. Next you make an instance of the LedCube class. As discussed the constructor accepts 4 parameters, the first two defines the latch and clear pins. The third parameter is used to define the cube size. Its best to use one of the predefined literals: CUBE_SIZE_4x4x4, CUBE_SIZE_5x5x5, CUBE_SIZE_6x6x6, CUBE_SIZE_7x7x7 or CUBE_SIZE_8x8x8. The last parameter should be your desired refresh frequency. You can also make use of this formula: Refresh frequency = 200 * Number of layers. After that you also make an instance of the Animation class. In the void setup() you call the begin() function. At last you need to call animations to actually show something on the cube. You have to do that in your main loop. In this example I do also make use of the optional parameters. All in all it should look something like this:

#include<SPI.h>
#include<LedCube.h>
#include<Animation.h>

//Latch pin = 13
//Clear pin = 14,
//Cube size = CUBE_SIZE_8x8x8
//Refresh frequency = 200* 8 = 1600
LedCube cube(13, 14, CUBE_SIZE_8x8x8, 1600);
Animation animation;

void setup() 
{
  //Start the cube
  cube.begin();
}

void loop() 
{
  /* Rain parameters
   - time: 10000ms
   - cubeRef: cube
   - rainVoxels: 2
   - floorVoxels: 10
   - rainDelay: 55ms
   - invertDirection: false
   */
  animation.rain(10000, cube, 2, 10, 55, false);
  
    /* Slanting lines parameters
   - time: 10000ms
   - cubeRef: cube
   - shiftDelay: 55ms
   - invertDirection: false
   */
  animation.slantingLines(10000, cube, 55, true);
  
   /* Slanting lines parameters
   - time: 10000ms
   - cubeRef: cube
   - resizeDelay: 55ms
   - minimumCubeSize: 2
   */
  animation.cornerCube(10000, cube, 55, 2);
}

More examples of creating your own animation are included with the example sketchbook.
And here you can see the result on a 8x8x8 LED cube (will post a better video): VIDEO

FUTURE UPDATES
I already have planned some things to add to the library in the near future:

  • Total and individual LED brightness control
  • Rotational functions
  • More complex animations (sine wave and such)
  • Text animation
  • Support to communicate with a computer

If anyone got more ideas just post them in this thread and maybe I'm able to add it to the library.

IMAGES OF MY CUBE

So I hoped you guys liked reading my topic and find use in my library, as promised here are some pictures of my 8x8x8 LED cube.

I just updated my first post with a new version of the library. I made a few changes and added some animations. Both clearAllVoxels() and setAllVoxels() had a flaw inside the memset function. As size of the voxel mapping I used _cubeSize * _cubeSize, this had to be _cubeSize * _layerSize instead. Besides this I noticed some flickering at the rain animation. So I started to investigate where that came from. At first I thought I had some flaw in my PCB so I tested another one, but it still flickered occasionally. After some testing I found out that it had something to do with the clearAllVoxels() function. When I draw a frame of the rain animation I always cleared the previous frame by calling the clearAllVoxels() function. After that I started inserting the voxels per layer until I looped through all the items of the rain and floor voxel arrays. Eventually, I saw that it took quite some time to do this kind of operation (especially when you provide a large number to the floor voxel parameter). After some measurements it turned out this whole inserting of the rain and floor voxels took around 4 - 5 ms. In this time it is possible that the frame buffer is already drawn on the cube. When the insertion operation isn't completed yet, but the frame buffer is displayed, there will be some pixels that will be off even if they should be turned on. This causes that some layers will produce flickering. To prevent this kind of flickering I added two functions: voxelTemp() and copyTemp(). With the voxelTemp function you will do exactly the same as the voxel() function. However the voxel values are not directly modified in the frame buffer, but instead it is modified inside a temporary (frame) buffer. Whenever a heavy calculation of a frame is done and the temporary frame is filled, you can call the copyTemp() function to copy the temporary buffer inside the actual frame buffer itself. By using these functions when an animation requires a lot of calculation you will eliminate any possible flicker. Note: the copyTemp() function accepts one parameter (default value = true). This parameter defines if you want to clear (zero it) the temporary buffer whenever it is successfully copied to the frame buffer.

I also added three new animations. They're all different types of sine waves. The function sineWave() produces a normal sine wave that is evaluated only in the X direction. The function sineWaveTwo() produces a sine wave that is evaluated both in the X and the Y direction. The last function sineWaveThree() produces a summation of two sine waves, this causes the sine wave to differ in height when the X position is increased.

At last I added two new examples. These examples shows how you could create an animation on your own. I included both an easy (random LED generator) and advanced (sine wave) example. If it ain't clear how the examples are working please tell me in this topic or PM :). Also if anyone got feedback on my story from above I love to hear it :).

An updated video with all the new function can be found HERE.

1 Like

Hi WonderTiger

I remember that I got some advice from you some time ago when I was playing around with LED cubes. I will be interested in reading and trying to understand and possibly implement your code into the 4 x4 x4 cube that I have after I modify it to use SR's. Thanks a lot for posting this and I am surprised that I am the only one to comment, seeing how popular LED cubes have been on this forum :slight_smile:

Edit - I just had a look at your video and congrats those effects are amazing so well done :slight_smile:

Hey Pedro,

Yes I had to look back but indeed I remember you from 3x3x3 LED cube topic haha. When I'm looking back at it I see how much I learned in those 2 years already, both in the software and hardware department. Next year will be my last year at the EE study and I'm even planning to make a 16x16x16 RGB LED cube.

If you implement the hardware as shown in the schematics it should work just fine for your 4x4x4 cube (single color). I did not test it on 4x4x4 cube itself, but did test it on my 8x8x8 with a smaller defined size. There it behaved as I expected so in theory it should work. If you got it all wired up I will be glad to know if it all worked as you would expect. If you got any more questions how the hardware should be connected just message me :).

Edit: took a look at your website, saw your 4x4x4 cube (and other projects) there. Looks great as well!

Thanks for your kind offer, I just might take you up on that if I remake my cube :slight_smile:

Hi WonderTiger. I sent you a pm :smiley:

I've made the complete schematics for a 5x5x5 LED cube. It uses a standalone atmega 328P chip. The latch is connected to digital pin #8 and the clear is connected to digital pin #9.

ADDED SCHEMATICS CAN BE DOWNLOADED HERE

5x5x5 LED cube.zip (1.07 MB)

Today I finished a prototype 4x4x4 LED cube to test out the library and you know what, I found a 'bug'! I was transferring the bytes to the shift registers the wrong way. So, the data of the first shift register ended it up in the last register, to solve this I reversed the iteration of the for-loop inside the refreshData() function. After this my 4x4x4 started working properly, click HERE for a video.

I also added a draw line (and draw line temp) function. This function accepts two vector points, it then automatically draws a line between these points. Do note: the smaller the angle between the points get (valid for all axis) the worse the line will get. Currently the draw line function is pretty slow as it can only draw a line every ms or so. In the future I will look if I can improve this.

There I am again...:smiley:

Today I release the V2.0 of the LED cube library. It has some added, removed and updated features. The greatest new feature will be individual brightness control. Every LED can now be dimmed with a 4 bit brightness depth (it is possible to change this value up to 8 by modifying the MODULATION_BIT_DEPTH in LedCube.cpp on your own). This will give you a 15 step resolution from off to full brightness. The brightness is controlled via Bit Angle Modulation. To prevent flicker with the use of BAM I've added double buffering to the cube. With double buffering you have one front buffer which is used to draw pixels to the cube and one back buffer to draw graphics. Because of this the way you will turn on a LED is changed a little bit. Normally you would call the following code to draw a voxel.

cubeInstance.voxel(z,x,y,1);

Now you have to do the following to draw a voxel.

cubeInstance.voxel(z,x,y,1);
cubeInstance.drawVoxels();

To draw voxels with different brightness levels you have to do the following.

cubeInstance.voxel(z,x,y,1,15); // Maximum brightness
cubeInstance.voxel(z,x,y,1); // Also valid, maximum brightness
cubeInstance.voxel(z,x,y,1,10); // 10/15 brightness
cubeInstance.drawVoxels();

Examples with brightness control are included in the example sketchbook.

The drawVoxels() function supports (default is true) vsync. This means that the buffer swapping in is executed once a multiplex cycle has completed. This will prevent any tearing or other artifacts while drawing the graphics to the cube. Because of vsync, the drawVoxels() function can block the main loop. In worst case scenario it can take up to 1 multiplex cycle. If you don't want to use vsync just call:

cubeInstance.drawVoxels(false);

Because we use the double buffering technique, the voxelTemp() and copyTemp() function became obsolete, thus was removed from the library. However with double buffering it may become problematic when you want to add graphics to a previously drawn frame (as the front buffer has no notion to added graphics in the back buffer). To solve this you can call the function copyCurrentFrame() to retrieve the voxels of the frame buffer that is drawn on the cube. For example:

cubeInstance.voxel(0,0,0,1,10); // Add a voxel on position <0,0,0> to the current frame
cubeInstance.drawVoxels(); // Show voxel <0,0,0> on the cube. By calling this function the front and back buffers are swapped, this will mean voxel on position <0,0,0> will be lost in the now swapped back buffer
cubeInstance.copyCurrentFrame(); // Retrieve current drawn frame. With this we retrieve the voxel on position<0,0,0> and copy it in the current back buffer
cubeInstance.voxel(1,0,0,1,5); // Add another voxel above the other LED with a lower brightness
cubeInstance.drawVoxels(); // Now both voxels will be drawn in the cube!

IMPORTANT: because BAM we need a higher refresh frequency. A minimum of 1Khz (4 bit modulation depth) per layer is advised.

To increase the ISR I wrote my own digital pin to port pin conversion library. This library will use direct port manipulation to set a pin high or low. A list of supported devices (not all are tested) can be found in pinConversion.h. Common devices like the Arduino uno, mega, nano, mini, leonardo, etc. are covered.

I thank PaulRB for putting me on the right track with getting flicker free brightness control, see topic.

The old library is still included in my first post, BAM is not supported in that one

Hey WonderTiger, what is the SMD part on the back of your board?
I really like that board. ATMega 1284?

It is a FTDI USB to UART chip. The controller is indeed an atmega 1284p. I still have all the Eagle files available of the 8x8x8 board. Just PM me if you want them.

Tonight I will update the library once again. This time I've added gamma correction, more about that soon.

Greetings

Mike

SMD 1284 is a quad, 44-pin flatpack.

Have you seen my 9x9x9 LED cube controller board?
Pic & schematic attached.
Uses cd74AC164 to source current to 91 columns, and AOI514 N-channel MOSFET to sink current from each layer.

SMD 1284 is a quad, 44-pin flatpack.

Have you seen my 9x9x9 LED cube controller board?
Pic & schematic attached.
Uses cd74AC164 to source current to 91 columns, and AOI514 N-channel MOSFET to sink current from each layer.

Seems nice CrossRoads, I like the layout of your board! I'm quite interested though, on your 74HC595 layer drivers (and column drivers??) you connect the last pin of the parallel output to the serial input of the next SR. How do you shift out data to the next shift register? I myself use the overflow pin (9) but you seem to have done it differently.

UPDATE

The library now adds gamma correction the the brightness control. This gamma correction is done via a predefined (in flash memory) conversion table. This table is calculated with the formula: gamma corrected brightness = (brightness / max_brightness)^(1/gamma_value) * max_brightness. The default table has a gamma value of 1.65 and is intended to use with a 4 bit modulation depth. If you want to change this table you can use the "gamma correction table generator.exe" which I created in C#. Copy the executable to the library directory and directly run it. Once running the application will detect the chosen modulation bit depth from LedCube.cpp and sets the current gamma value to the one that is specified in gammaCorrection.h (if it exists). You then can simply change the curve with either the slider or numeric up and down box to a new value. The graph will automatically update the curves. The dashed curve is showing the exact gamma corrected brightness values, which will be inserted in the predefined gamma correction table once generated. A screen shot of the program can be found below.

To turn on gamma correction in your sketch (default is off), you call the following code.

cubeInstance.setGammaCorrection (true);

I use SPI.transfer to send out all the bytes in one burst from an array. If just one bit changes, I still send the data for all of them.
With SPI divisor set to 2, for 8 MHz operation, only takes a uS per register.
Shift registers are meant to be daisychained and support it.

Yes I'm doing that as well, however you seem to have the chained register connect to the last pin of the parallel output of the previous shift register instead of the overflow pin 9 (QH'). Am I'm missing something here :P?

I've updated the gamma correction generator with some new features. In the table on the right you can see the actual gamma corrected brightness and now you also have the ability to refresh the modulation bit depth when you changed it inside the LedCube.cpp file. Screenshot added below.

I also have a question towards the visitors of my topic. Did anybody had the chance to try out this library already? I'm looking for people that can give me feedback (negative or positive, love to hear them both) regarding the usability of this library :D.

I think you may be right about the two 74HC595, will likely cut the trace from IC1-7 to IC3-14, and add IC1-9 to AC3-14. Discovered that problem later on the 32 channel MOSFET board where four 74HC595 are daisychained. had to make 4 cuts and add 4 jumpers to use the current boards until the corrected batch comes in.
We've only got 7 layers built up so far, and would likely have seen that same issue when we got to the last layer.
Daisy chaining the cd74AC164 as shown does work correctly, I've made a couple simple shapes so far, and marched data by hand thru different sections to confirm I can control it as desired.
I use 9 SPI.transfer at 8MHz to send out the column data, and 2 more to send out the layer data. Direct port manipulation for the chip selects, so maybe 15uS total to send data out, with I think 3000uS hold for each layer. Haven't done anything since last summer, need to revisit this & get the hardware finished at least.

Well I hope you will finish it sometime soon, I'm quite interested in seeing that thing flash!