4×4×4 LED Cube with Arduino Mega2560: Sound- and Motion-Reactive Multiplexing

🌏 繁體中文版:[DIY專案] 4x4x4 LED 立方體:Arduino Mega2560 結合聲音感測器與 PIR 人體感測器

This DIY project is an interactive 4×4×4 cube of 64 hand-soldered LEDs, driven by an Arduino Mega2560 with a sound sensor and a PIR motion sensor attached, so the light patterns respond to noise and to people moving nearby.

The finished cube running an animation (May 2024)
The finished cube running an animation (May 2024)

Building the cube: a laser-cut soldering jig

The hardest part of an LED cube isn't the electronics — it's getting 64 LEDs into a straight, evenly spaced grid. Soldering them freehand ends up crooked, so I laser-cut a simple jig from MDF: a plate with a 4×4 grid of holes at the LED spacing. Each layer of 16 LEDs is placed into the holes upside down, their legs are bent and soldered together, and the finished layer lifts out perfectly square. Four identical layers are then stacked and joined.

The laser-cut MDF jig: a 4×4 grid of holes at LED spacing
The laser-cut MDF jig: a 4×4 grid of holes at LED spacing
The jig next to the shield the cube plugs into
The jig next to the shield the cube plugs into

The electronics

  • Controller: Arduino Mega2560 with a plug-in shield (RobotWolf Group) that the cube mounts onto
  • LEDs: 64 × blue LEDs in a 4×4×4 grid
  • Sensors: a sound sensor module and a PIR motion sensor
The cube on the Mega2560, with the PIR sensor (white dome) and the sound sensor module
The cube on the Mega2560, with the PIR sensor (white dome) and the sound sensor module
Lit up in the dark
Lit up in the dark

Why multiplex instead of using 64 pins

Giving every LED its own I/O pin would take 64 pins. The standard trick for LED cubes is multiplexing: in each vertical column, the 4 LEDs share their anodes (16 columns), and in each horizontal layer, the 16 LEDs share their cathodes (4 layers). Only one layer is switched on at any instant, and the 16 column lines decide which LEDs in that layer light up. Cycle through the 4 layers hundreds of times per second and persistence of vision makes it look like any pattern of all 64 LEDs is lit at once — the same idea a display uses when it refreshes line by line.

That brings the pin count down from 64 to 16 + 4 = 20, which the Mega2560 handles with plenty of pins to spare for sensors. Two details matter in practice:

  • Current: a layer can carry the current of up to 16 LEDs at once, which is more than a single I/O pin should sink. The usual fix is a transistor per layer (or a ULN2003 driver array) between the layer and ground.
  • Brightness: each layer is lit only a quarter of the time, so a multiplexed cube is dimmer than directly driven LEDs. Slightly smaller current-limiting resistors on the columns compensate.

Example sketch: layer scanning with sound and motion

Below is an example sketch that shows the technique — layer scanning plus the two sensors — for anyone building a similar cube. Pin numbers are placeholders: adapt them to your own wiring.

/*
 * 4x4x4 LED cube: layer multiplexing + sound sensor + PIR (example)
 *   16 columns (anodes)  -> COL_PIN[], each through a current-limiting resistor
 *   4 layers (cathodes)  -> transistor / ULN2003, driven by LAYER_PIN[]
 *   Sound sensor analog out -> A0
 *   PIR sensor output       -> D2 (HIGH while motion is detected)
 */
const uint8_t COL_PIN[16]  = {22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37};
const uint8_t LAYER_PIN[4] = {38,39,40,41};   // HIGH = layer switched on
const uint8_t PIR_PIN = 2;

bool cube[4][16];          // cube[layer][column]
uint8_t layer = 0;

void setup() {
  for (uint8_t c = 0; c < 16; c++) pinMode(COL_PIN[c], OUTPUT);
  for (uint8_t z = 0; z < 4; z++)  pinMode(LAYER_PIN[z], OUTPUT);
  pinMode(PIR_PIN, INPUT);
}

void updatePattern() {
  static uint32_t last = 0;
  uint32_t now = millis();
  if (now - last < 30) return;         // update the pattern every 30 ms
  last = now;

  int sound = analogRead(A0);
  uint8_t level = constrain(map(sound, 300, 700, 0, 4), 0, 4);  // calibrate to your module
  bool motion = digitalRead(PIR_PIN);
  uint32_t t = now / 150;

  for (uint8_t z = 0; z < 4; z++)
    for (uint8_t c = 0; c < 16; c++) {
      if (motion)  cube[z][c] = ((c + z + t) % 4 == 0);   // someone nearby: a moving ripple
      else         cube[z][c] = (z < level);              // otherwise: a sound-level meter
    }
}

void loop() {
  updatePattern();

  // scan: switch off the old layer, set up the columns, switch on the next layer
  digitalWrite(LAYER_PIN[layer], LOW);
  layer = (layer + 1) & 3;
  for (uint8_t c = 0; c < 16; c++) digitalWrite(COL_PIN[c], cube[layer][c] ? HIGH : LOW);
  digitalWrite(LAYER_PIN[layer], HIGH);
  delayMicroseconds(500);              // 4 layers x 0.5 ms = 2 ms per frame (~500 fps)
}

Two habits from this sketch are worth keeping. The layer is switched off before the columns change, which prevents "ghosting" (faint copies of the previous layer's pattern). And sensing runs on a timer instead of every pass through loop(), so slow analogRead calls never stretch one layer's on-time and cause flicker.

Troubleshooting tips

  • Random flickering LEDs: almost always a missing common ground between the cube, the driver transistors, and the sensors.
  • Visible flicker: a full 4-layer pass must finish well under 20 ms (above 50 Hz). Keep any slow work out of the scan path.
  • A whole column or layer dead: check the solder joints along that column or layer — with 64 LEDs, one cold joint is the usual culprit.
  • PIR "stuck on": most PIR modules have on-board potentiometers for sensitivity and hold time; a long hold time makes the cube look unresponsive.

Wrapping up

An LED cube packs three core electronics ideas into one small build: multiplexing, current, and timing. The 64 LEDs work because of scan speed, not pin count — your eye can't separate hundreds of layer scans per second, so it sees one solid 3D pattern. And the laser-cut jig turned out to be the most valuable part of the build: without it, the grid would never have come out straight.

Related tools: LED Resistor Calculator


This post is adapted from the Traditional Chinese original on Vic's Iteration Notes.

Comments

Popular posts from this blog

Gear Ratio Calculator: Multi-Stage Ratio, Output Speed, Torque and Direction

Mecanum-Wheel Pick-and-Place Robot: From Arduino UNO to Mega2560 + ESP32-CAM

Voltage Divider Calculator: Output Voltage, or the Best E24 Resistor Pair for a Target Voltage