Guides · 19 min read

Arduino LED Cube: How to Build a 3x3x3 and 4x4x4 Cube with Code

Build an Arduino LED cube: soldering jig, 3x3x3 and 4x4x4 assembly, transistor layer drivers, resistor maths, 74HC595 expansion and array animation code.

An LED cube is one of the most eye-catching light projects you can build with Arduino: LEDs arranged in a three-dimensional grid light up layer by layer to draw spinning planes, falling raindrops and cubes that grow and shrink in mid-air. The best way to start an Arduino LED cube is with a 3x3x3 (27 LEDs) or 4x4x4 (64 LEDs) size; at these sizes the pins of an Arduino Uno, a few transistors and some patient soldering are all you need. The trick is not to give every LED its own pin but to use a method called layer-column multiplexing: you control 27 LEDs with 12 pins, and 64 LEDs with just 7 pins thanks to two 74HC595 chips.

In this guide you'll learn how an LED cube works, how to make a soldering jig, how to join the layers step by step, why the layers must be driven by transistors and how the resistor values are calculated. After that you'll find Arduino code that plays array-based animations on a 3x3x3 cube, how to scale up to a 4x4x4 cube with the 74HC595, ESP32 and Raspberry Pi Pico (MicroPython) versions, a WS2812B alternative for a colour cube, and fixes for the most common assembly mistakes such as a reversed LED or a cold solder joint.

5 mm LED for an LED cube
The building block of an LED cube is the 5 mm LED: the long leg is the anode (+), the short leg and the flat edge on the body mark the cathode (−).

What is an LED cube and how does it work?

An LED cube is made by stacking horizontal layers of evenly spaced LEDs. In each layer the cathode (−) legs of all the LEDs are soldered together, so every layer has a single common cathode line. The anode (+) legs of LEDs sitting above one another are soldered together vertically and form the columns. A 3x3x3 cube has 3 layers and 9 columns, and a 4x4x4 cube has 4 layers and 16 columns. Every LED sits exactly at the crossing of one layer and one column: to light an LED you put a positive voltage on its column and connect its layer to GND.

You can describe every LED in the cube with three coordinates: x (left to right), y (front to back) and z (bottom to top). In the code in this article the column number is calculated as c = y × 3 + x (c = y × 4 + x on a 4x4x4), and z is the layer number. Just like a pixel on a screen, a single point of light in the cube is called a voxel (volume pixel). Making an animation really means deciding which voxels are lit at which moment.

How does layer-column multiplexing work?

If you switch on more than one layer at a time, the positive voltage you put on a column lights an LED in every active layer of that column, so you can't draw the pattern you want. That's why the cube is scanned layer by layer: the Arduino first switches off all layers, writes the column pattern of the first layer to the pins, turns on only that layer for a few milliseconds, then switches it off and moves on to the second layer. When this loop repeats more than a hundred times per second, your eye holds the light for a moment (persistence of vision), so you see all the layers lit at the same time.

Working out the scan rate is easy: refresh rate = 1 / (number of layers × layer time). If you keep each layer of a 3x3x3 cube on for 3 ms, one pass takes 9 ms and the cube refreshes about 110 times per second. On a 4x4x4 with 2 ms per layer, a pass takes 8 ms and the rate is 125 Hz. Below 50–60 Hz the eye notices flicker; above 100 Hz it looks completely steady to the eye and shows less banding on a phone camera. The price is brightness: because each layer is lit only 1/3 of the time (1/4 on a 4x4x4), an LED's average current drops to that fraction of its instantaneous current.

CubeLEDsColumns + layersPins, direct drivePins with 74HC595Pins without multiplexing
3x3x3279 + 3123 + 3 = 627
4x4x46416 + 4203 + 4 = 764
5x5x512525 + 5303 + 5 = 8125
8x8x851264 + 8723 + 8 = 11512

Parts list

Part3x3x3 cube4x4x4 cube (with 74HC595)
5 mm LED (single colour, diffused if possible)27 + 3 spares64 + 6 spares
Column resistor9 × 220 Ω16 × 330 Ω (blue, white) or 470 Ω (red, yellow)
NPN transistor (layer)3 × 2N2222A or BC3374 × 2N2222A or BC337
Base resistor3 × 470 Ω4 × 470 Ω
Shift registerNot needed2 × 74HC595 (74HCT595 for ESP32 and Pico) + 100 nF each
Controller boardArduino Uno or NanoArduino Uno, Nano, ESP32 or Pico
AssemblyPerfboard, thin solid-core wirePerfboard, 0.5–0.8 mm tinned copper wire
ToolsSoldering iron, solder, side cutters, pliers, 5 mm drill bitSame tools + spacers of equal height
Arduino Uno
Arduino Uno
220 Ω
220 Ω
Breadboard
Breadboard
Jumper
Jumper

Making a soldering jig

The secret of a neat cube is the jig. If you solder the LEDs while holding them by hand, the spacing drifts, the layers end up crooked and the cube looks skewed. A jig is a piece of wood with evenly spaced holes in which the LEDs sit upside down, dome facing down. The spacing is set by the length of the cathode leg: on most 5 mm LEDs the short leg is around 25 mm, but measure your own LEDs with a ruler. When you bend the leg 90° about 2–3 mm from the body, the remaining horizontal part should overlap the neighbouring LED's leg by 2–3 mm, which is why a spacing of 20 mm is a good starting point.

  1. Draw a 3x3 (or 4x4) grid with a ruler on plywood, MDF or thick cardboard, with 20 mm between points and at least 2 cm of margin from the edges.
  2. Mark each point with a nail first, then drill it straight with a 5 mm bit. The LED should sit firmly in the hole but come out without force; if it's too tight, widen the hole slightly.
  3. Draw an arrow on one edge of the jig to show the cathode direction; the short leg of every LED will point the same way.
  4. Prepare four spacers of equal height that suit your layer spacing, to carry the upper layer while you join the layers; scraps of wooden strip or folded cardboard do the job.
Test every LED one by one before you start soldering: just touch the long leg to the plus side and the short leg to the minus side of a 3 V CR2032 coin cell. The diode setting of a multimeter also lights an LED faintly. Removing a dead LED from the middle of a cube is very hard. For the looks, choose diffused LEDs; clear LEDs focus light in one direction, so the cube glows unevenly when you look at it from the side. You can frost the dome of clear LEDs with fine sandpaper.

Step-by-step assembly: layers and columns

  1. Bend the legs: Bend the short (cathode) leg of each LED horizontally by 90°, 2–3 mm from the body, towards the arrow on the jig. The long (anode) leg stays upright.
  2. Lay out the first layer: Place the LEDs in the holes with their domes facing down. Every cathode leg in a row should touch the cathode leg of the next LED; the leg of the last LED in the row can stay free.
  3. Solder the cathodes: Solder each contact point within 2–3 seconds with the iron at 320–350 °C. Then solder a straight wire (a clipped resistor leg or tinned copper wire) across the ends of the rows to join them. The layer becomes both a single electrical line and a sturdy frame; on a 4x4x4, adding a wire at both ends makes the layer even stronger.
  4. Test the layer: Without taking the layer out of the jig, connect the cathode line to GND and touch 5 V through a 220 Ω resistor to each anode leg. If an LED doesn't light, replace it now.
  5. Make the other layers: Repeat the process until you have 3 (or 4) layers. Keep the cathode legs pointing the same way in every layer so that the columns line up exactly.
  6. Join the layers: Leave one layer in the jig and line up the next layer exactly above it on the spacers. Put a small 2 mm kink at the tip of each upward-pointing anode leg of the lower LEDs, so the leg passes beside the body of the LED above and rests against its anode leg. Solder the four corners first, check the alignment, then finish the remaining columns.
  7. Bring out the layer wires: Solder a wire to the cathode line of each layer and run it down one side of the cube. These wires must not touch each other or the columns; leave a few millimetres between them.
  8. Mount it on the base: Turn the cube over; the free anode legs of the last layer you added now point down, and that layer becomes the one closest to the base, the bottom layer (z = 0). Push the legs and layer wires through a perfboard, solder them and place the column resistors next to each column.
The most expensive assembly mistake is a single reversed LED. A reversed LED doesn't just stay dark itself; when a column pin is LOW it opens a reverse current path from the layer line into that column, and ghost LEDs may light up on layers that should be off. Check that the short leg (the flat edge of the body) points at the arrow every time you place an LED in the jig, and always test each layer before joining it.

Driving the layers with transistors

NPN transistor
The layer current is carried by an NPN transistor, not by an Arduino pin: the 2N2222A (E-B-C) or BC337 (C-B-E) are good choices.

A column pin supplies current to a single LED, but when a layer is switched on, the current of every LED in that layer returns to GND through the same cathode line. On a 3x3x3 cube that's 9 LEDs × 12.7 mA ≈ 115 mA, and on a 4x4x4 cube with 74HC595s it's 16 LEDs × 5 mA ≈ 80 mA. The recommended limit for an Arduino Uno pin is 20 mA and the absolute maximum is 40 mA, so you can't connect a layer line directly to a pin. The solution is an NPN transistor under every layer: the collector goes to the layer line, the emitter to GND, and the base to an Arduino pin through a resistor. When the pin goes HIGH, the transistor saturates and pulls the layer to GND. To switch the transistor fully on, choose a base current of about 1/10 to 1/20 of the collector current: 115 mA needs 6–12 mA, and (5 V − 0.7 V) / 470 Ω ≈ 9 mA falls right in that range. For the details see What Is a Transistor?, and for pinouts the transistor tool.

TransistorContinuous ICPin order (flat face towards you)Use in an LED cube
BC547100 mAC-B-EOnly for low-current cubes; the 115 mA of a 3x3x3 exceeds its limit
2N2222A600 mAE-B-CIdeal for 3x3x3 and 4x4x4
BC337800 mAC-B-EIdeal for 3x3x3 and 4x4x4
IRLZ44N (logic-level MOSFET)Tens of ampsG-D-SBig cubes such as 8x8x8; 220 Ω to the gate, 10 kΩ from gate to GND

Resistor maths: column and base resistors

The column resistor is the only part that limits the LED current, and there's one in each column; you don't need a resistor per LED, because only one LED in a column is lit at any moment. The formula comes from Ohm's law: R = (Vsupply − VLED − VCE(sat)) / ILED. With a 5 V supply, a red LED with a 2.0 V forward voltage and a transistor dropping about 0.2 V when saturated, aiming for 12 mA gives R = (5 − 2.0 − 0.2) / 0.012 ≈ 233 Ω. The nearest standard value, 220 Ω, lets about 12.7 mA through the LED. With a blue LED at 3.1 V, the same 220 Ω gives about 7.7 mA, which is still comfortably visible under multiplexing.

LED colourForward voltage (VF)3x3x3, direct from Arduino (5 V)4x4x4, 74HC595 (5 V)
Red, yellow1.8–2.2 V220 Ω (≈12–13 mA)470 Ω (≈5–6 mA)
True green, blue2.9–3.3 V180–220 Ω (≈8–9 mA)330 Ω (≈4–5 mA)
White2.9–3.4 V180–220 Ω (≈7–9 mA)330 Ω (≈4–5 mA)

On the 74HC595 cube we deliberately choose larger resistors: at most 70 mA in total may flow through this chip's VCC or GND pin, and with a layer on, all eight outputs can be lit at once. 8 × 5.5 mA ≈ 44 mA leaves a safe margin. The average LED current, which sets the perceived brightness, is the instantaneous current divided by the number of layers: 12.7 / 3 ≈ 4.2 mA on a 3x3x3 and 5.5 / 4 ≈ 1.4 mA on a 4x4x4. Today's efficient LEDs are clearly visible indoors at these currents, but in a very bright room a 4x4x4 cube can look a little dim. To calculate with your own LED's values, use the LED resistor and LED voltages tools.

3x3x3 LED cube wiring

ConnectionArduino Uno / NanoNote
Columns 0–8 (anodes)D2–D10220 Ω series resistor on each column
Bottom layer (z = 0)A0To the base of transistor 1 via 470 Ω
Middle layer (z = 1)A1To the base of transistor 2 via 470 Ω
Top layer (z = 2)A2To the base of transistor 3 via 470 Ω
Transistor collectors—Each to the cathode wire of its own layer
Transistor emittersGNDAll three to a common GND
Top viewx = 0x = 1x = 2
y = 2 (back)Column 6 → D8Column 7 → D9Column 8 → D10
y = 1 (middle)Column 3 → D5Column 4 → D6Column 5 → D7
y = 0 (front)Column 0 → D2Column 1 → D3Column 2 → D4
Don't use D0 and D1 for columns: these pins are shared with uploading over USB and with the Serial Monitor, and LEDs on them can break uploads. That's why the 3x3x3 cube uses D2–D10 and A0–A2; the A0–A5 pins work as ordinary digital outputs with digitalWrite(). The total current stays within limits too: when all nine columns draw 12.7 mA, about 115 mA comes out of the Arduino, below the ATmega328P's 200 mA total limit. You can see where the pins are on the board in the pinout tool.

3x3x3 LED cube Arduino code: animations with arrays

Each frame of the animation is stored as one number per layer. A 3x3x3 layer has 9 LEDs, so a 9-bit value is enough; we store it as a uint16_t, and bit c stands for column c. The frames are written to flash memory with PROGMEM, so even animations with hundreds of frames don't fill the Uno's 2 KB of RAM. The code below needs no extra library.

C++ (Arduino IDE)
1// 3x3x3 LED küp: katman çoklama ve dizilerle animasyon / 3x3x3 LED cube: layer multiplexing and array animations
2const byte colPins[9]   = {2, 3, 4, 5, 6, 7, 8, 9, 10};   // sütun anotları, 220 ohm ile / column anodes via 220 ohm
3const byte layerPins[3] = {A0, A1, A2};                     // alt, orta, üst katman transistörü / bottom, middle, top layer transistor
4
5// Her kare 3 katman, her katman 9 bit: bit c = sütun c (c = y*3 + x)
6// Each frame has 3 layers of 9 bits: bit c = column c (c = y*3 + x)
7const uint16_t frames[][3] PROGMEM = {
8  {0b111111111, 0b000000000, 0b000000000},  // 0: alt katman / bottom layer
9  {0b000000000, 0b111111111, 0b000000000},  // 1: orta katman / middle layer
10  {0b000000000, 0b000000000, 0b111111111},  // 2: üst katman / top layer
11  {0b000000000, 0b000010000, 0b000000000},  // 3: yalnız merkez / centre only
12  {0b111101111, 0b101000101, 0b111101111},  // 4: tel kafes, 12 kenar / wireframe, 12 edges
13  {0b010010010, 0b010010010, 0b010010010},  // 5: dönen düzlem, 1. konum / rotating plane, step 1
14  {0b100010001, 0b100010001, 0b100010001},  // 6: dönen düzlem, 2. konum / rotating plane, step 2
15  {0b000111000, 0b000111000, 0b000111000},  // 7: dönen düzlem, 3. konum / rotating plane, step 3
16  {0b001010100, 0b001010100, 0b001010100},  // 8: dönen düzlem, 4. konum / rotating plane, step 4
17  {0b111111111, 0b111111111, 0b111111111}   // 9: hepsi açık / all on
18};
19
20void setup() {
21  for (byte c = 0; c < 9; c++) pinMode(colPins[c], OUTPUT);
22  for (byte z = 0; z < 3; z++) pinMode(layerPins[z], OUTPUT);  // LOW: katmanlar kapalı / LOW: layers off
23}
24
25// Bir kareyi verilen süre boyunca katman katman tarar / scans one frame layer by layer for the given time
26void showFrame(byte f, unsigned int ms) {
27  unsigned long start = millis();
28  while (millis() - start < ms) {
29    for (byte z = 0; z < 3; z++) {
30      uint16_t bits = pgm_read_word(&frames[f][z]);         // katman verisini oku / read the layer data
31      for (byte c = 0; c < 9; c++) digitalWrite(colPins[c], bitRead(bits, c));
32      digitalWrite(layerPins[z], HIGH);                      // katmanı aç / layer on
33      delay(3);                                              // 3 ms göster / show for 3 ms
34      digitalWrite(layerPins[z], LOW);                       // sütunlar değişmeden önce kapat / off before the columns change
35    }
36  }
37}
38
39void loop() {
40  for (byte f = 0; f <= 4; f++) showFrame(f, 500);           // katmanlar, merkez, kafes / layers, centre, wireframe
41  for (byte turn = 0; turn < 4; turn++)                      // düzlemi 4 tur döndür / spin the plane 4 times
42    for (byte f = 5; f <= 8; f++) showFrame(f, 120);
43  showFrame(9, 800);                                         // hepsi açık / all on
44}

The heart of the code is the showFrame() function. For each layer it first writes the column pattern to the pins, then turns on only that layer's transistor for 3 ms and switches the layer off before the columns change. This order matters: if you change the columns while a layer is on, the previous layer's pattern shows up on the new layer for a brief moment and causes ghosting. bitRead(bits, c) returns bit c of the number as 0 or 1, and pgm_read_word() reads the 16-bit value from flash. loop() first shows the layers one by one, then the centre and the 12 edges of the cube, and then spins a vertical plane through four positions to create a pinwheel effect.

Designing your own animations

To draw a new frame, look down at the cube from above and think of each layer as a 3x3 sheet of squared paper. Mark the columns you want lit and write the binary number after 0b from left to right, from bit 8 down to bit 0. The four corners, for example, are columns 0, 2, 6 and 8, which gives 0b101000101. Write the same value to all three layers and you get vertical bars at the four corners; write it only to the middle layer and you get four points floating in the air. If binary numbers are new to you, Number Systems and Bitwise Operations explains the idea in detail. A few techniques that come in handy when you write the code:

  • Bit shifting: Shift a layer's value left by 3 bits on a 3x3x3 or by 4 bits on a 4x4x4 and the pattern moves one row back; mask off the overflowing bits with & to make wave and scrolling-text effects.
  • Copying layers: Copying the layer array one step down or up gives rain, rising-bubble and lift effects.
  • Randomness: Voxels picked with random() that flash briefly create a starfield or sparkle effect.
  • Inverting: The ~ operator turns a frame into its negative, and switching quickly between the two gives a flashing highlight.

4x4x4 LED cube: scaling up with the 74HC595

A 4x4x4 cube has 16 columns and 4 layers; driving it directly takes 20 pins, which means every pin of the Uno, D0 and D1 included. If you drive the columns with two daisy-chained 74HC595 shift registers instead, the 16 columns cost only 3 pins (data, clock and latch); add 4 pins for the layers and you're at 7 pins in total. The Q7' output (pin 9) of the first chip goes to the DS input of the second chip, while the clock and latch lines are shared by both chips. Columns 0–7 (the front two rows) connect to the Q0–Q7 outputs of the first chip, and columns 8–15 (the back two rows) to the second chip. The same method scales to 4 chips for a 5x5x5 cube and 8 chips for an 8x8x8 cube.

ConnectionArduino UnoESP32Raspberry Pi Pico
DS (14), first chipD11GPIO 23GP19
SH_CP (11), shared by both chipsD13GPIO 18GP18
ST_CP (12), shared by both chipsD10GPIO 5GP17
OE (13) / MR (10)GND / 5VGND / 5VGND / VBUS
VCC (16) / GND (8)5V / GND5V / GNDVBUS / GND
Q7' (9) → DS (14) of the second chipChainChainChain
Layer 0–3 transistors (via 470 Ω)D2, D3, D4, D5GPIO 25, 26, 27, 32GP10, GP11, GP12, GP13
Chip type74HC59574HCT59574HCT595
At most 70 mA in total may flow through the 74HC595's VCC or GND pin. With small resistors such as 220 Ω, a fully lit layer draws 80–100 mA per chip; the chip heats up, its output voltage sags and it eventually fails. Use the 330–470 Ω values from the resistor table and put a 100 nF capacitor between the VCC and GND pins of each chip, as close to the pins as possible.
C++ (Arduino IDE)
1// 4x4x4 LED küp: 2 adet 74HC595 (16 sütun) + 4 transistörlü katman / 4x4x4 LED cube: two 74HC595s (16 columns) + 4 transistor layers
2const byte DATA_PIN  = 11;                 // 74HC595 DS (14)
3const byte CLOCK_PIN = 13;                 // 74HC595 SH_CP (11)
4const byte LATCH_PIN = 10;                 // 74HC595 ST_CP (12)
5const byte layerPins[4] = {2, 3, 4, 5};    // alttan üste katman transistörleri, 470 ohm ile / layer transistors bottom to top, via 470 ohm
6
7uint16_t cube[4];                          // ekrandaki görüntü: katman başına 16 bit / current image: 16 bits per layer
8
9// Hazır kareler: bit c = sütun c (c = y*4 + x) / ready-made frames: bit c = column c (c = y*4 + x)
10const uint16_t frames[][4] PROGMEM = {
11  {0xFFFF, 0x0000, 0x0000, 0x0000},        // alt katman / bottom layer
12  {0x0000, 0xFFFF, 0x0000, 0x0000},
13  {0x0000, 0x0000, 0xFFFF, 0x0000},
14  {0x0000, 0x0000, 0x0000, 0xFFFF},        // üst katman / top layer
15  {0x9009, 0x0000, 0x0000, 0x9009},        // 8 köşe / 8 corners
16  {0xF99F, 0x9009, 0x9009, 0xF99F},        // tel kafes / wireframe
17  {0x0000, 0x0660, 0x0660, 0x0000}         // içteki 2x2x2 küp / inner 2x2x2 cube
18};
19const byte FRAME_COUNT = sizeof(frames) / sizeof(frames[0]);
20
21void write16(uint16_t v) {                 // 16 biti iki entegreye gönder / send 16 bits to both chips
22  digitalWrite(LATCH_PIN, LOW);
23  shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, highByte(v));  // önce uzaktaki entegre / far chip first
24  shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, lowByte(v));
25  digitalWrite(LATCH_PIN, HIGH);           // çıkışlara aktar / copy to the outputs
26}
27
28void refresh() {                           // her 2 ms'de sıradaki katmana geç / move to the next layer every 2 ms
29  static byte z = 0;
30  static unsigned long last = 0;
31  if (micros() - last < 2000) return;
32  last = micros();
33  digitalWrite(layerPins[z], LOW);         // eski katmanı kapat / old layer off
34  z = (z + 1) % 4;
35  write16(cube[z]);                        // yeni katmanın sütunları / columns of the new layer
36  digitalWrite(layerPins[z], HIGH);        // yeni katmanı aç / new layer on
37}
38
39void holdFor(unsigned int ms) {            // beklerken küpü yenilemeye devam et / keep refreshing while waiting
40  unsigned long start = millis();
41  while (millis() - start < ms) refresh();
42}
43
44void clearCube() {
45  for (byte z = 0; z < 4; z++) cube[z] = 0;
46}
47
48void setVoxel(byte x, byte y, byte z) {    // tek LED yak, x/y/z = 0..3 / light one LED, x/y/z = 0..3
49  bitSet(cube[z], y * 4 + x);
50}
51
52void loadFrame(byte f) {                   // PROGMEM'deki kareyi yükle / load a frame from PROGMEM
53  for (byte z = 0; z < 4; z++) cube[z] = pgm_read_word(&frames[f][z]);
54}
55
56void sweep() {                             // dikey düzlemi x ekseninde kaydır / sweep a vertical plane along x
57  for (byte x = 0; x < 4; x++) {
58    clearCube();
59    for (byte y = 0; y < 4; y++)
60      for (byte z = 0; z < 4; z++) setVoxel(x, y, z);
61    holdFor(150);
62  }
63}
64
65void rain(byte steps) {                    // yağmur: damlalar yukarıdan düşer / rain: drops fall from the top
66  clearCube();
67  for (byte s = 0; s < steps; s++) {
68    for (byte z = 0; z < 3; z++) cube[z] = cube[z + 1];   // her şeyi bir kat indir / move everything down a layer
69    cube[3] = bit(random(16)) | bit(random(16));         // üstte 1-2 yeni damla / 1-2 new drops on top
70    holdFor(120);
71  }
72}
73
74void setup() {
75  pinMode(DATA_PIN, OUTPUT);
76  pinMode(CLOCK_PIN, OUTPUT);
77  pinMode(LATCH_PIN, OUTPUT);
78  for (byte z = 0; z < 4; z++) pinMode(layerPins[z], OUTPUT);
79  randomSeed(analogRead(A0));              // boştaki A0'dan rastgele tohum / random seed from the unconnected A0
80}
81
82void loop() {
83  for (byte f = 0; f < FRAME_COUNT; f++) {
84    loadFrame(f);
85    holdFor(500);
86  }
87  for (byte i = 0; i < 3; i++) sweep();
88  rain(80);
89}

In this version the scanning happens in the refresh() function with a non-blocking design: every time it's called it uses micros() to check whether 2 ms have passed; if so, it switches the old layer off, sends the new layer's 16 bits to the two chips and switches the new layer on. That way the animation code only changes the cube[] array and never deals with refreshing the display. holdFor() keeps calling refresh() while it waits, so you shouldn't use a long delay() anywhere in the program. setVoxel(x, y, z) lets you light a single LED by its coordinates; sweep() uses it to move a vertical plane through the cube, and rain() copies the layers one step down and adds random drops at the top. You'll find the difference between delay() and millis() in millis() vs delay(), and how to move the scanning into a timer interrupt in Arduino Interrupts.

LED cube with the ESP32

The ESP32's pins output 3.3 V. That isn't enough to drive blue and white LEDs, whose forward voltage is around 3 V, directly; after the drop across the resistor and the transistor almost nothing is left. That's why the cleanest solution on the ESP32 is to drive the columns through a 74HCT595 powered from 5 V: the HCT family treats even 2 V as HIGH, so it reads 3.3 V signals without trouble, while the LEDs are fed from 5 V. Keep 470 Ω on the bases of the layer transistors; (3.3 − 0.7) / 470 ≈ 5.5 mA is enough for an 80 mA layer current. The 4x4x4 code above compiles on the ESP32 as it is; you only need to change the pin constants to the values in the table (23, 18, 5 and 25, 26, 27, 32). If you like, you can also use the ESP32's Wi-Fi to pick animations from a web page on your phone; to get started, see What Is the ESP32?.

LED cube with the Raspberry Pi Pico and MicroPython

The Pico is a 3.3 V board too, and with the default setting its pins supply only a few milliamps, so you use the same 74HCT595 circuit. Power the chips and LEDs from VBUS (pin 40), which carries the 5 V from USB. In MicroPython the 16 bits are sent with hardware SPI: SPI0's GP18 (SCK) and GP19 (MOSI) pins drive the clock and data lines, and GP17 drives the latch. The code follows the same logic: show() scans a frame layer by layer for the given time, and in the rain effect list slicing (cube[1:]) moves every layer one step down. For setup and first steps, see What Is the Raspberry Pi Pico?.

MicroPython
1# Pico + 2 adet 74HCT595 ile 4x4x4 LED küp / 4x4x4 LED cube with a Pico + two 74HCT595s
2from machine import Pin, SPI
3import time
4import random
5
6# GP18 = SCK -> SH_CP, GP19 = MOSI -> DS; GP16 (MISO) boş kalır / GP16 (MISO) stays unconnected
7spi = SPI(0, baudrate=1000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19))
8latch = Pin(17, Pin.OUT, value=0)                                # ST_CP
9layers = [Pin(n, Pin.OUT, value=0) for n in (10, 11, 12, 13)]    # alttan üste, 470 ohm ile / bottom to top, via 470 ohm
10
11def write16(v):
12    latch.value(0)
13    spi.write(bytes((v >> 8, v & 0xFF)))    # önce uzaktaki entegre / far chip first
14    latch.value(1)                          # çıkışlara aktar / copy to the outputs
15
16def show(frame, ms):
17    # kareyi ms boyunca katman katman tara / scan the frame layer by layer for ms
18    end = time.ticks_add(time.ticks_ms(), ms)
19    while time.ticks_diff(end, time.ticks_ms()) > 0:
20        for z in range(4):
21            write16(frame[z])
22            layers[z].value(1)              # katmanı aç / layer on
23            time.sleep_us(2000)
24            layers[z].value(0)              # sütunlar değişmeden kapat / off before the columns change
25
26FRAMES = [
27    [0x9009, 0x0000, 0x0000, 0x9009],       # 8 köşe / 8 corners
28    [0xF99F, 0x9009, 0x9009, 0xF99F],       # tel kafes / wireframe
29    [0x0000, 0x0660, 0x0660, 0x0000],       # içteki küp / inner cube
30]
31
32while True:
33    for f in FRAMES:
34        show(f, 600)
35    cube = [0, 0, 0, 0]
36    for _ in range(60):                     # yağmur: her şey bir kat iner / rain: everything drops a layer
37        drop = (1 << random.getrandbits(4)) | (1 << random.getrandbits(4))
38        cube = cube[1:] + [drop]
39        show(cube, 120)

Bigger cubes: 5x5x5 and 8x8x8

As the cube grows, two problems grow with it. The first is current: an 8x8x8 cube has 64 LEDs per layer, and at 10 mA per LED the layer current is 640 mA; that's no longer a job for a small TO-92 transistor but for a logic-level MOSFET. The second is scan time: to keep 100 Hz with 8 layers, each layer gets 1.25 ms and 64 bits (8 bytes) must be sent per layer; shiftOut() is too slow for this, so hardware SPI is a must. On top of that, each LED is lit only 1/8 of the time, so brightness drops; big cubes need stronger column drivers and an external 5 V adapter instead of USB (see Powering an Arduino). That's why your first cube should be a 3x3x3 or 4x4x4; you'll master the technique before soldering 512 LEDs.

Alternative: a colour LED cube with WS2812B

WS2812B addressable RGB LED
Every WS2812B LED has its own driver chip; a 64-LED cube is controlled from a single data pin.

If you'd rather pick the colour of every LED individually instead of building a single-colour cube, you can use addressable LEDs such as the WS2812B. Each of these LEDs has its own driver chip, so there's no need for multiplexing, transistors or column resistors: the 64 LEDs are daisy-chained with 5 V, GND and a data line and driven from a single pin. For a cube you use 4-leg 5 mm addressable LEDs (such as the PL9823 or APA106) or pieces cut from a WS2812B strip. The price is current: about 60 mA per LED at full white, which is 3.8 A for 64 LEDs. So use a 5 V 4 A adapter, add a 1000 µF capacitor on the supply and a 330 Ω resistor on the data line, and limit the brightness in software. The code uses the Adafruit NeoPixel library.

C++ (Arduino IDE)
1// WS2812B ile 4x4x4 RGB LED küp / 4x4x4 RGB LED cube with WS2812B
2#include <Adafruit_NeoPixel.h>
3
4const byte DATA_PIN = 6;                   // 330 ohm ile ilk LED'in DIN ucuna / to the first LED's DIN via 330 ohm
5const byte N = 4;
6Adafruit_NeoPixel cube(N * N * N, DATA_PIN, NEO_GRB + NEO_KHZ800);
7
8uint16_t idx(byte x, byte y, byte z) {     // koordinattan zincirdeki sıraya / coordinate to position in the chain
9  return z * 16 + y * 4 + x;               // düz (zikzaksız) kablolama varsayılır / assumes straight, non-zigzag wiring
10}
11
12void setup() {
13  cube.begin();
14  cube.setBrightness(40);                  // parlaklığı ve akımı sınırla (0-255) / limit brightness and current (0-255)
15}
16
17void loop() {
18  static uint16_t hue = 0;
19  for (byte z = 0; z < N; z++)
20    for (byte y = 0; y < N; y++)
21      for (byte x = 0; x < N; x++)         // her katman farklı tonda / each layer gets a different hue
22        cube.setPixelColor(idx(x, y, z), cube.gamma32(cube.ColorHSV(hue + z * 8192U)));
23  cube.show();
24  hue += 256;                              // gökkuşağını kaydır / shift the rainbow
25  delay(20);
26}

The idx() function in the code converts an x, y, z coordinate into the position in the chain and assumes you wired every row in the same direction. If you ran the wiring in a zigzag, you'll need to use 3 − x instead of x on odd rows. Some 5 mm addressable LEDs expect colours in RGB order; if you see green instead of red, change NEO_GRB to NEO_RGB. setBrightness(40) reduces the brightness to about 16%, so even in the worst case the current stays around 0.6 A.

FeatureSingle-colour LED cubeWS2812B LED cube
ColourOne colour (the LED's own)16 million colours, set per LED
Pins used (4x4x4)7 (with 74HC595)1
Extra partsTransistors, resistors, 74HC595Capacitor, data resistor, strong adapter
Soldering work2 legs per LED4 legs per LED, more wires
Brightness controlOn/off (per-LED PWM is hard)256 levels for every LED
Current (4x4x4)Around 100 mA, USB is enoughUp to ≈3.8 A, external supply required
What it teachesMultiplexing, transistors, bit operationsAddressing, hue, power management

LED cube project ideas

  • Music-reactive cube: Raise the number of lit layers according to the sound level measured with a sound sensor to make a three-dimensional equaliser.
  • 3D snake game: Steer the snake along three axes on a 4x4x4 cube with a joystick and a button; for more game ideas, see Arduino game projects.
  • Dice cube: When you press a button, the voxels shuffle quickly and the cube shows a number from 1 to 6 as a dot pattern.
  • Science fair project: Explain persistence of vision by stretching the layer time with a potentiometer and showing the moment flicker starts; for more ideas, see science fair project ideas.
  • Choosing animations from your phone: Let every letter you send with an HC-05 Bluetooth module start a different animation.
  • Colourful night light: Set up slow colour fades on a WS2812B cube; if you're using an ESP32, control the effects from your phone with WLED.

Common mistakes and how to fix them

SymptomLikely causeFix
A single LED never lightsThe LED is reversed or dead, or a leg joint is brokenCheck the cathode direction, test the LED with a coin cell, replace it if needed
LEDs that should be off glow faintly (ghosting)The columns change while a layer is on, or the cube has a reversed LEDSwitch the layer off first, then write the columns; if the code is right, find the reversed LED
LEDs flicker or go out when you touch the cubeCold solder joint: a dull, grainy, ball-shaped jointReheat the joint with a clean iron tip and add a little fresh solder
A whole layer stays darkThe transistor legs are reversed (E-B-C mixed up with C-B-E) or the base resistor is disconnectedCheck the pin order in the datasheet and measure the base connection
A whole column stays darkThe column isn't soldered to the base, or the resistor or pin is wrongMeasure the column end to end with a multimeter and check the pin array in the code
The cube flickersThe layer time is too long, or there's a Serial.print() or delay() in the scan loopSet the layer time to 2–3 ms and remove code that blocks the scan
The cube is very dimThe resistors are too large, blue LEDs are driven from 3.3 V, or there are too many layersRecalculate the resistor; on 3.3 V boards power a 74HCT595 from 5 V
The sketch won't uploadAn LED is connected to D0 or D1Start the columns at D2, or disconnect these wires while uploading
The best way to troubleshoot is to scan the cube with a test sketch: write a small loop that lights each LED in turn for 200 ms. An LED that stays dark or lights in the wrong place immediately points to a broken joint or a mixed-up column. A cold solder joint looks dull and grainy rather than shiny; a good joint looks like a shiny, slightly concave cone hugging the leg. To brush up your technique, have a look at How to Solder.

The LED cube is one of the rare projects that teaches soldering, transistors, resistor maths, multiplexing and bit operations all at once. Start with a 3x3x3, and once the code and the animation logic click, move on to a 4x4x4 with the 74HC595, or to WS2812B if you want colour. If you'd like to try the idea of lighting LEDs in sequence on a straight row first, Arduino Knight Rider and running LEDs is a good warm-up. In the BlueGrays app you'll find running LED, Knight Rider, WS2812B animation and LED matrix projects with step-by-step circuit diagrams and code.

Frequently asked questions

How many LEDs and pins does an LED cube need?

A 3x3x3 cube needs 27 LEDs and a 4x4x4 cube needs 64; it's worth buying a few spares. With layer-column multiplexing, a 3x3x3 cube needs 9 columns plus 3 layers, so 12 pins. A 4x4x4 cube takes 20 pins when driven directly, but if you drive the columns with two 74HC595s the total drops to 7 pins.

What resistor should I use in an LED cube?

There's one resistor per column; for a 3x3x3 cube driven directly by an Arduino at 5 V, 220 Ω is a common and safe value. On a 74HC595 cube, use 330 Ω for blue and white LEDs and 470 Ω for red and yellow LEDs so you stay under the chip's 70 mA total limit. For your own value, apply R = (Vsupply − VLED − 0.2 V) / ILED.

Why are the layers of an LED cube driven by transistors?

When a layer is switched on, the current of every LED in that layer returns to GND through a single line; on a 3x3x3 cube that's more than 100 mA. Because the recommended limit of an Arduino pin is 20 mA, the layer line is pulled to GND by an NPN transistor such as a 2N2222A or BC337. A 470 Ω base resistor is enough.

Why is my LED cube flickering?

Flicker appears when the scan rate is too low. The refresh rate is 1 / (number of layers × layer time) and should be above 100 Hz, so keep the layer time at 2–3 ms and don't put a long delay() or Serial.print() in the scan loop. If the cube only flickers when you touch it, the problem is most likely a cold solder joint.

Why do LEDs light up that should be off?

This is called ghosting, and it has two main causes. Changing the column values while a layer is on carries the previous pattern onto the new layer for a moment; switch the layer off first, then write the columns. If the code is right, a reversed LED in the cube may be opening a current path into other layers.

Can you build an 8x8x8 LED cube with an Arduino Uno?

Yes, but the pins aren't enough on their own: you need a 74HC595 chain or similar drivers for the 64 columns, logic-level MOSFETs for the 8 layers and hardware SPI for fast scanning. Soldering 512 LEDs also takes days. Mastering the technique on a 3x3x3 or 4x4x4 cube first is the best way to go.

See all projects
LED Chaser
#5Intermediate

LED Chaser

Lights that switch on one after another, as if they were running. The Arduino lights six LEDs one at a time; each glows for a brief moment and goes out, so the light walks from start to end and then s

4.6(85)
207
Knight Rider LED Effect
#6Intermediate

Knight Rider LED Effect

The classic scanner light: a dot of light slides from one end of six LEDs to the other and then comes back. The Arduino lights each LED for a tenth of a second before moving on, turns around at each e

4.8(106)
202
Knight Rider LED Effect with Adjustable Speed
#8Intermediate

Knight Rider LED Effect with Adjustable Speed

A light runs from one end of six LEDs to the other, turns around and runs back again. Turn the potentiometer knob and you change the speed of that run instantly: with the knob at one end the light alm

4.5(60)
120
Using a WS2812B LED Strip
#69Intermediate
Premium

Using a WS2812B LED Strip

Meet a smart LED strip where you can colour every single light on its own. The Arduino first lights the first five LEDs red, a second later turns the other five blue, then switches them all off and st

4.8(54)
70
WS2812B Animation 1
#75Intermediate
Premium

WS2812B Animation 1

On a strip of 56 LEDs, all the colours of the rainbow line up side by side and flow without ever stopping, as if a river of light were gliding along the strip. Each LED glows in a slightly different c

4.9(69)
89
WS2812B Animation 2
#76Intermediate
Premium

WS2812B Animation 2

Set up a light show in your room that changes colors all by itself. The Arduino keeps colors flowing along a 30-LED strip and switches to a new color set every few seconds: rainbow, purple and green s

4.8(65)
84
Pixel Snake 8x8 Matrix
#88Advanced
Premium

Pixel Snake 8x8 Matrix

Build the classic snake game with your own hands and play it. On a colorful 8x8 light matrix, you steer a green snake with a joystick and eat red apples; with every apple the snake grows, speeds up a

4.7(39)
57
4x 32x8 LED Matrix - 6-Animation Pack
#91Intermediate
Premium

4x 32x8 LED Matrix - 6-Animation Pack

Four 32x8 LED panels stacked on top of each other form a square screen where six animations play in turn: falling digital rain, a growing spiral, cells that are born and die all by themselves, ripplin

4.5(35)
48