This project contains the code for a LED cube, using an Arduino board or the Atmega328 as a standalone with Atmel Studio. The code was developed together with the Cube 3D programming tool. Supporting video tutorials found at YouTube(coming soon).
- Difference between Arduino and Atmega328
- Arduino Framework?
- Arduino vs Atmel Studio
- Wiring the Cube
- Code
- Upload Code
- Download and Tools
- Supported Cubes
- Help and Contributing
- Previous Version
- License
- Authors
The Arduino board is constructed with the microchip Atmega328. However, to program the chip on an Arduino board, one usually programs it with the Arduino IDE using the Arduino framework. This can be limiting, because the Arduino framework is an abstraction layer that causes the Atmega328 to run slower than its potential. In a LED cube, speed is very important, as well as memory for the light patterns. These reasons led the motivation to write the code in C rather than to use Arduino's C++ framework. This gave a better and more efficient code. However, the code is fully compatible with the Arduino board and can be uploaded with the Arduino IDE. The project was based on using the bootloader inside the Arduino board to upload the code. However, it's entirely possible with little to no effort to upload the code to a standalone Atmega328, using Atmel Studio, if you don't wish to use an Arduino.
When programming Arduino, to control the Input/Output (IO) pins, one uses digitalWrite(HIGH)
and digitalWrite(LOW)
. These functions are part of the Arduino class, which is fundamentally a C++ class. While convenient, digitalWrite()
is 15 to 30 times slower than manipulating the IO pins directly in C as intended by Atmel. This is done by bitshifting e.g. PORTB0 |= (1 << PIN0)
. The second part of the Arduino framework issues has to do with interrupts. Arduino uses its own millis()
function to keep track of time. For this to work, an interrupt routine has to run in the background, even if you do not utilize delay()
or interrupts in your code. This disturbs the timing and contributes to a less efficient LED cube.
For the reasons above and because the sole purpose of a LED cube is to quickly turn IO pins on/off, the Arduino framework was not used in order to produce a more efficient and accurate LED cube.
And IDE is an Integrated Developement Environment, meaning it's essentially a code editor with programming capabilities and usually some debug capabilities.
Arduino developed their own simple to use IDE for writing code and programming the boards. For beginners and quick development, it's an ok IDE but once you start getting serious, it's very limiting. It's hard to edit code efficiently and it doesn't have as much customizability. You can use it with an external code editor and simple use the Arduino IDE as the programmer if you wish. A better choice in my opinion, is to simply use Atmel Studio if you start to get serious about embedded development.
This is Atmel's own IDE created for development on their chips, like the Atmega328. It's gonna feel closer to being in Visual Studio and it's easier to work in than the Arduino IDE. It can be set up to use the AVRDUDE programmer, so that you can upload code to the board. TODO: Link to how to set up Atmel Studio programmer.
So to summarize the differences. If you just want to upload the code and make it work, I would suggest sticking with the Arduino IDE. It's not necessary to write any code as it's already done, except for the pattern.h file to generate LED patterns. However, I highly recommend using the Cube 3D software to generate this file. If you however wish to write some code yourself and want to get more into embedded development, I advice on checking out Atmel Studio.
To generate a light show on the LED cube you only need to edit the file pattern.h
. This is simply a header file .h
with an array containing the patterns. The default file looks like below and turns all LEDs on and off at a 250ms interval.
#ifndef __PATTERN_H__
#define __PATTERN_H__
// Includes
//---------------------------------
#include <stdint.h> // Use uint_t
#include <avr/pgmspace.h> // Store patterns in program memory
// Pattern that LED cube will display
//---------------------------------
const PROGMEM uint16_t pattern_table[] = {
// Blink all LEDs every 250ms
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 250,
0x0000, 0x0000, 0x0000, 0x0000, 250,
};
#endif
The array is defined as an uint16_t
, meaning it stores 16 bit unsigned variables. This is to optimize the memory usage. Inside the array there are 5 variables on each code line. The first four of them are written in hexadecimal 0xFFFF
, these represent 16 LEDs and one plane. Hexadecimal representation is merely for compact code. The last variable 250
is represented in decimal because it represents the time in milliseconds that the particular codeline should be displayed for. This makes more sense in decimal rather than hexadecimal. When the code is compiled, it's compiled down to binary anyway, so it doesn't matter wether you write in decimal, hex or binary. To the computer it's still the same.
//plane1 plane2 plane3 plane4 display time [ms]
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 250,
If the hex value 0xFFFF
is converted to binary it would be 1111 1111 1111 1111
where each bit represents one LED with the first LED starting from the right. If the bit is a 1
that particular LED will be on and if it is 0
it will be off. And that's the basic idea of how this system works and that's why a 16 bit variable will be the most optimized way of storing the patterns while still keeping it structured in planes. The time variable will of course be a 16 bit too and this is so that it's possible to have 2^16 = 65 536ms = 65.5 seconds to display each codeline. However, most patterns will be displayed for much less than 1 second.
const PROGMEM
is used to define the array as well. What PROGMEM
does is to save the code in program memory (flash) instead of SRAM memory. The reason for this is that the flash memory is bigger than the SRAM, meaning we can store more patterns in this memory. It has to be const
because flash can only be read from once it's programmed and running.
While you can write this pattern file yourself, I have created a 3D animated tool that does this for you Cube 3D. I highly suggest using that when generating patterns as it's very time effective and it's easy to visualize what you want. Understanding what goes on behind the curtains is of course always useful. Especially if you encounter any problems and need to debug.
This section is meant to be purely informative on how the code works. It's not necessary to make the LED cube work but rather to provide insight into the code for those interested.
The fundamental concept of the LED cube is that it's divided into planes and columns. This is a multiplexing method for reducing the amount of IO pins necessary. With a 4x4x4 cube that means 20 IO pins, where 4 of them ground each plane and 16 of them supply power to the columns. The code is constructed in the way that only one plane can be on at a time. This in turn means that every plane needs to be switched on and off at a time interval, giving the illusion that the LEDs on different planes are lit.
The code concists of a main.c
and pattern.h
. In main.c
an interrupt routine is activated to switch the planes on and off at the decided time interval. After setting up all the IO pins and other necessary functionality the code enters the while()
loop that does these things:
- Get a new line from pattern table in
pattern.h
and put in pattern buffer. - Calculate the IO pin port values based on the pattern line.
- Check wether or not to get a new pattern line on the next run, depending on the time variable e.g. how long the pattern has been displayed.
- Sleep CPU until interrupt.
- Upon interrupt, switch IO pins.
- Repeat
The Atmega328 is an 8-bit microcontroller and only has 8-bit registers. Therefore it cannot switch more than 8 IO pins in one instruction. We need 20 IO pins and it's therefore necessary to set three ports which each has 8 pins accessible. The image below shows how the Arduino pins map to the ports PORTB
, PORTC
and PORTD
.
To switch the planes on and off an Interrupt Service Routine (ISR) is used. The ISR merely switches on and off the LEDs from a pre-calculated value that happens in the while loop. This is to reduce the amount of time spent in the ISR.
// Interrupt Service Routine
ISR (TIMER1_COMPA_vect)
{
// Switch on the LEDs for the current plane
PORTB = port_b;
PORTD = port_d;
PORTC = port_c; // Port C is last because it turns on the plane also
}
As shown in the code above PORTB
is set to be the value of port_b
in the ISR. PORTB
is a avr-library variable for setting the ports and port_b
is calculated in the while-loop from the pattern table. Only when a plane and a column (or several columns) are on at the same time will a LED light up. Because PORTC
contains IO pins that goes to both columns and planes, it comes last so the power is turned on in that instant all at once.
The IO pins we need from the Arduino Uno is D0-D13
and A0-A5
. These corresponds to PD0-PD7
, PB0-PB5
and PC0-PC5
as can be seen from the pinout image, which makes 20 IO pins. The code below is how the actual calculation of the port values are done. This is the value that sets the correct IO pins to high and low depending on what's in the pattern table.
// Calculate port values
port_b = (pattern_buf[current_plane] & PORT_B_MASK) >> SHIFT_PORT_B;
port_c = ((pattern_buf[current_plane] & PORT_C_MASK) >> SHIFT_PORT_C) ^ PLANE_MASK[current_plane]; // XOR to find the current PLANE to turn on
port_d = (pattern_buf[current_plane] & PORT_D_MASK); // Don't need a shift
The pattern_buf[current_plane]
is an array holding one pattern line from the pattern table. It loads upon start and when the current pattern is finished displaying. The current_plane
makes sure only one plane is selected at a time. The array will then return a hex value e.g. 0xFFFF
. The PORT_B_MASK
variable merely filters out the IO pins that we are not using. It's returning the value 0x3F00
which is binary 0011 1111 0000 0000
. This gives us 5 IO pins in the place of the 1
's which will eventually be PB0
, PB1
, PB2
, PB3
, PB4
and PB5
. Looking at the Arduino Uno Pinout that's the pins 8 to 13. Thus far, the calculation of port_b
is:
0xFFFF & 0x3F00 >> SHIFT_PORT_B = 0x3F00 >> SHIFT_PORT_B
If we now look at the value 0x3F00
it didn't change because when &'ed with 0xFFFF
it's the same value. Remember that in binary 0x3F00
is 0011 1111 0000 0000
. We want to set the first 6 IO pins, hence the reason we used the 0x3F00
mask in the first place. That's why we need to shift these values to the right into PB0
to PB5
. Thus, SHIFT_PORT_B
is simply the value 8 giving:
0x3F00 >> 8 = 0011 1111 0000 00000 >> 8 = 0000 0000 0011 1111
You can now see how the first 6 values of this number will equate to PB0
through PB5
.
For PORTC
and PORTD
the idea is exactly the same. However for PORTD
we don't need a right shift because the values are already in the right place due to the PORT_D_MASK
. This is merely because we needed those pins on the Arduino board and this is how it was mapped. Otherwise PORTD
calculation is exactly the same as PORTB
.
The calculation of PORTC
is the same with one additional parameter, it has a XOR at the end. Remember that PORTC
contains IO pins for planes and for columns? Due to this fact, we need to calculate which plane is currently going to be displayed and make that IO pin a 1
. That's the functionality of the ^
XOR operation at the end.
To ensure that the patterns run for the amount of time given by the time variable in the pattern table, the code below is necessary.
// Logic for switching correct plane and time
if (current_plane == NR_OF_PLANES - 1)
{
// Reset to sart calculating from the first plane again
current_plane = 0;
// Increment every time one pattern has finished
time_counter++;
// Logic for the amount of time for each plane
if (time_counter == (int)(pattern_buf[TIME_IDX] / MIN_PATTERN_TIME))
{
// Get new line only when the pattern has run the correct amount of times
get_new_line = true;
time_counter = 0;
}
}
else
current_plane++; // Increment to calculate ports for the next plane
The variable time_counter
is incremented every time all 4 planes have been activated, this means every fourth iteration of the while()
loop. Therefore, it counts every time one pattern line has been run once. How often the ISR activates is set with the OCR1A
register and this has to be an integer. The prescaler is set to 1024 for the most resolution. The OCR1A
is set to the value 39
, which together with a 1024 prescaler gives ~2.5ms interrupts. The cube then runs on 50Hz, quick enough to give the illusion of persistence of light. However, it means the time variable in the pattern table has to be in increments of 10 to give accurate patterns.
To use the Arduino IDE, read on.
Look at the Arduino board as a development board for the Atmega328. This board has a crystal, USB port and a programmer, among other things. So if you were going to buy a standalone Atmega328 instead, you would have to buy all these parts and connect them somehow. Luckily, you don't need that!
The Arduino board has another advantage, the Atmega328 on it comes with a bootloader. This means that there exists a tiny program on the chip that can easily upload code from your computer with a USB cable. It's not that easy with standalone chips. This is a very convenient thing that is going to be exploited in this project.
- Open the project in the Arduino folder in the Arduino IDE.
- Choose your Arduino board.
- Build and upload code.
To use Atmel Studio with or without an Arduino board, read on.
If you are using the Arduino board, the bootloader on the chip will be utilized for uploading and it works just as simply as the Arduino IDE. TODO: Add how to set up Arduino upload.
If you don't have an Arduino board, you will need a programmer like an Atmel ICE or similar. Configure Atmel Studio according to the type of programmer you have, and upload the code.
Currently this project supports a 4x4x4 LED cube.
The code can be downloaded from Releases or simply fork or download the repository.
Check out the Discord server if you need help with the code not working or if you have suggestions for improvement! The YouTube channel has video tutorials to help out as well. (YouTube videos coming soon)
The first version of the LED cube code was written entirely on Arduino. This code uses lots of nested for()
and while()
loops as well as delay()
. It's simple and it works. The code, corresponding code generator and instructions can be found at Instructables for anyone interested. Who wrote the Arduino code is not known to me, however I made the code generator application which is now deprecated to the newer Cube 3D.
This project is licensed under the MIT license and is open source. You are free to use this project as you wish as long as you credit the work. See the LICENSE file for details. I would highly appreciate if you contributed to the project that you share it so this can be a big open source project!