Build a 32×32 LED Matrix
with Arduino Mega
Chain four MAX7219 modules with just 3 SPI wires, write a framebuffer, and animate smooth circular wave patterns across 1024 LEDs — all in your browser.
Project Overview
This project builds a 32×32 LED matrix display using an Arduino Mega 2560 and four chained MAX7219 8×8 LED matrix modules. Instead of managing 64 individual row/column wires, the MAX7219 driver handles all multiplexing internally — requiring just 3 control wires (DIN, CLK, CS) via SPI.
SPI Communication
Control 16 individual 8×8 modules using just 3 daisy-chained wires
Framebuffer
Store the full 32×32 pixel state in memory before rendering to hardware
Wave Animation
Integer square root mathematics drive smooth circular wave patterns
Daisy Chaining
4 matrix rows × 4 modules each = 16 MAX7219 chips on a single SPI bus
Understanding the Hardware
Why Arduino Mega?
The Arduino Mega 2560 is ideal for large chained display projects:
An Arduino Uno can drive MAX7219 modules, but the Mega is the better choice for 32×32 due to its extra memory and stable power delivery to 16 daisy-chained chips.
Matrix Layout
The 32×32 display is assembled from 16 individual 8×8 LED sections arranged in a 4×4 boustrophedon (zig-zag) layout. Each section contains one MAX7219 driver that handles its own row multiplexing automatically.
Boustrophedon (zig-zag) layout: even rows are sent MSB-first, odd rows are reversed with LSB-first to correct physical orientation.
SPI Wiring & Pin Mapping
Only 3 signal wires connect the Arduino Mega to the first MAX7219 module. All subsequent modules are daisy-chained.
Arduino Mega → First MAX7219
| Arduino Mega Pin | MAX7219 Pin | Wire Colour | Function |
|---|---|---|---|
| Pin 11 | DIN | 🔵 Blue | SPI Data In |
| Pin 13 | CLK | 🟠 Orange | SPI Clock |
| Pin 10 | CS | 🟢 Green | Chip Select |
| 5V | V+ | 🔴 Red | Power |
| GND | GND | ⚫ Black | Ground |
Daisy-Chain Between Modules
| From Module | Connection | To Next Module |
|---|---|---|
| DOUT | → | DIN |
| CLK | → | CLK |
| CS | → | CS |
| V+ (pass-through) | → | V+ |
| GND (pass-through) | → | GND |
Only the first module connects directly to the Arduino Mega. All others receive power and signals from the previous module in the chain. The SPI data propagates through all 16 chips in sequence.
How the MAX7219 Works
The MAX7219 is a serial-input, parallel-output display driver. Unlike direct LED matrix wiring, it takes care of everything internally:
Internal Multiplexing
Automatically scans all 8 rows at high speed — no CPU intervention needed.
SPI Row Data
You send row address + pixel data over SPI; the chip handles the rest.
Brightness Control
An intensity register (0x0A) controls LED brightness from 1/32 to 31/32.
No-Decode Mode
Register 0x09 = 0x00 enables raw bitmap mode — each bit maps to one LED.
Initialization Register Sequence
| Register | Address | Value | Effect |
|---|---|---|---|
| Display Test | 0x0F | 0x00 | Test mode OFF |
| Scan Limit | 0x0B | 0x07 | Display all 8 rows |
| Shutdown | 0x0C | 0x01 | Normal operation |
| Intensity | 0x0A | 0x0F | Maximum brightness |
| Decode Mode | 0x09 | 0x00 | No BCD decode (raw bitmap) |
Key Code Concepts
Framebuffer Array
byte fb[8 * NUM_SEGMENTS] — A 128-byte array storing the entire 32×32 display. Each bit = one LED. Modified in RAM, then pushed to hardware via show().
shiftAll() Function
Broadcasts the same register+value to all 16 chained MAX7219s simultaneously. Pulls CS LOW, loops through all segments with shiftOut(), then releases CS.
Circular Wave Math
A integer fixed-point CORDIC-style circle oscillator updates centre offset each frame. The radial distance to each pixel is computed with incremental square root for zero floating-point cost.
show() & Boustrophedon
Sends framebuffer rows to all daisy-chained modules. Even segment rows use MSBFIRST byte order; odd rows flip to LSBFIRST to correct the physical zig-zag orientation.
diagram.json
Paste this into the diagram.json tab in Wokwi. It places the Arduino Mega, four MAX7219 matrix strips, and all daisy-chain connections automatically.
{
"version": 1,
"author": "sutaburosu",
"editor": "wokwi",
"parts": [
{
"type": "wokwi-arduino-mega",
"id": "mega",
"top": -177.58,
"left": 252.33,
"rotate": 90,
"attrs": {}
},
{
"type": "wokwi-max7219-matrix",
"id": "matrix1",
"top": -126.7, "left": -79.5,
"attrs": { "chain": "4", "layout": "fc16" }
},
{
"type": "wokwi-max7219-matrix",
"id": "matrix2",
"top": -52.5, "left": -79.5,
"rotate": 180,
"attrs": { "chain": "4", "layout": "fc16" }
},
{
"type": "wokwi-max7219-matrix",
"id": "matrix3",
"top": 22.46, "left": -78.96,
"attrs": { "chain": "4", "layout": "fc16" }
},
{
"type": "wokwi-max7219-matrix",
"id": "matrix4",
"top": 96.8, "left": -79.5,
"rotate": 180,
"attrs": { "chain": "4", "layout": "fc16" }
}
],
"connections": [
// ── Daisy-chain: matrix1 → matrix2 ──
[ "matrix1:GND.2", "matrix2:GND", "black", [ "h-47.8", "v89.07" ] ],
[ "matrix1:DOUT", "matrix2:DIN", "blue", [ "h-38.2", "v69.87" ] ],
[ "matrix1:CS.2", "matrix2:CS", "green", [ "h-28.6", "v50.67" ] ],
[ "matrix1:CLK.2", "matrix2:CLK", "orange", [ "h-19", "v31.47" ] ],
// ── Daisy-chain: matrix2 → matrix3 ──
[ "matrix2:CLK.2", "matrix3:CLK", "orange", [ "h58.3", "v110.3" ] ],
[ "matrix2:CS.2", "matrix3:CS", "green", [ "h48.7", "v91.1" ] ],
[ "matrix2:DOUT", "matrix3:DIN", "blue", [ "h39.1", "v71.9" ] ],
[ "matrix2:GND.2", "matrix3:GND", "black", [ "h29.5", "v52.7" ] ],
// ── Power pass-through ──
[ "matrix3:V+", "matrix2:V+.2", "red", [ "h19.2", "v-31.46" ] ],
[ "matrix2:V+", "matrix1:V+.2", "red", [ "h-56.74", "v-110.5" ] ],
[ "matrix4:V+", "matrix3:V+.2", "red", [ "h-56.74", "v-115.8" ] ],
// ── Daisy-chain: matrix3 → matrix4 ──
[ "matrix3:GND.2", "matrix4:GND", "black", [ "h-47.84", "v93.34" ] ],
[ "matrix4:DIN", "matrix3:DOUT", "blue", [ "h-37.54", "v-67.8" ] ],
[ "matrix4:CS", "matrix3:CS.2", "green", [ "h-27.94", "v-58.2" ] ],
[ "matrix4:CLK", "matrix3:CLK.2","orange", [ "h-18.34", "v-39" ] ],
// ── Arduino Mega → matrix1 ──
[ "mega:5V", "matrix1:V+", "red", [ "h-33.66", "v-77.39" ] ],
[ "mega:GND.2", "matrix1:GND", "black", [ "h-42.41", "v-79.48" ] ],
[ "mega:11", "matrix1:DIN", "blue", [ "h20.97", "v249.88", "h-211.2", "v-211.2" ] ],
[ "mega:10", "matrix1:CS", "green", [ "h30.57", "v249.98", "h-230.4", "v-220.8" ] ],
[ "mega:13", "matrix1:CLK", "orange", [ "h40.17", "v288.08", "h-249.6", "v-220.8" ] ]
]
}
Full Arduino Code
This sketch uses bit-banged SPI to drive all 16 MAX7219 modules. A framebuffer stores the full 32×32 state, and a circular wave animation updates every frame.
If you see tearing (jagged circle edges), try the hardware SPI version at wokwi.com/arduino/projects/318868939929027156.
// Bit-banged SPI version. For hardware SPI (less tearing) see: // https://wokwi.com/arduino/projects/318868939929027156 #define CLK 13 #define DIN 11 #define CS 10 #define X_SEGMENTS 4 #define Y_SEGMENTS 4 #define NUM_SEGMENTS (X_SEGMENTS * Y_SEGMENTS) // Framebuffer: entire 32×32 display in raster order, (0,0) = top-left byte fb[8 * NUM_SEGMENTS]; // Broadcast register + value to ALL chained MAX7219s void shiftAll(byte send_to_address, byte send_this_data) { digitalWrite(CS, LOW); for (int i = 0; i < NUM_SEGMENTS; i++) { shiftOut(DIN, CLK, MSBFIRST, send_to_address); shiftOut(DIN, CLK, MSBFIRST, send_this_data); } digitalWrite(CS, HIGH); } void setup() { Serial.begin(115200); pinMode(CLK, OUTPUT); pinMode(DIN, OUTPUT); pinMode(CS, OUTPUT); // Initialise all MAX7219s shiftAll(0x0f, 0x00); // Display test OFF shiftAll(0x0b, 0x07); // Scan limit: rows 0–7 shiftAll(0x0c, 0x01); // Shutdown: normal operation shiftAll(0x0a, 0x0f); // Intensity: maximum shiftAll(0x09, 0x00); // Decode mode: raw bitmap } // ── Animation loop ────────────────────────────────────────────────── // Two CORDIC-style integer circle oscillators control the wave centre. // Incremental integer square root computes radial distance per pixel. void loop() { static int16_t sx1 = 15 << 8, sx2 = sx1, sy1, sy2; sx1 = sx1 - (sy1 >> 6); sy1 = sy1 + (sx1 >> 6); sx2 = sx2 - (sy2 >> 5); sy2 = sy2 + (sx2 >> 5); static byte travel = 0; travel--; byte *dst = fb; byte output = 0; int8_t x_offset = (sx1 >> 8) - X_SEGMENTS * 4; int8_t y_offset = (sx2 >> 8) - Y_SEGMENTS * 4; uint8_t screenx, screeny, xroot, yroot; uint16_t xsumsquares, ysumsquares, xnextsquare, ynextsquare; int8_t x, y; x = x_offset; y = y_offset; ysumsquares = x_offset * x_offset + y * y; yroot = int(sqrtf(ysumsquares)); ynextsquare = yroot * yroot; // Quadrant II – top left screeny = Y_SEGMENTS * 8; while (y < 0 && screeny) { x = x_offset; screenx = X_SEGMENTS * 8; xsumsquares = ysumsquares; xroot = yroot; if (x < 0) { xnextsquare = xroot * xroot; while (x < 0 && screenx) { screenx--; output <<= 1; output |= ((xroot + travel) & 8) >> 3; if (!(screenx & 7)) *dst++ = output; xsumsquares += 2 * x++ + 1; if (xsumsquares < xnextsquare) xnextsquare -= 2 * xroot-- - 1; } } // Quadrant I – top right if (screenx) { xnextsquare = (xroot + 1) * (xroot + 1); while (screenx) { screenx--; output <<= 1; output |= ((xroot + travel) & 8) >> 3; if (!(screenx & 7)) *dst++ = output; xsumsquares += 2 * x++ + 1; if (xsumsquares >= xnextsquare) xnextsquare += 2 * ++xroot + 1; } } ysumsquares += 2 * y++ + 1; if (ysumsquares < ynextsquare) ynextsquare -= 2 * yroot-- - 1; screeny--; } // Quadrants III & IV – bottom half ynextsquare = (yroot + 1) * (yroot + 1); while (screeny) { x = x_offset; screenx = X_SEGMENTS * 8; xsumsquares = ysumsquares; xroot = yroot; if (x < 0) { xnextsquare = xroot * xroot; while (x < 0 && screenx) { screenx--; output <<= 1; output |= ((xroot + travel) & 8) >> 3; if (!(screenx & 7)) *dst++ = output; xsumsquares += 2 * x++ + 1; if (xsumsquares < xnextsquare) xnextsquare -= 2 * xroot-- - 1; } } if (screenx) { xnextsquare = (xroot + 1) * (xroot + 1); while (screenx--) { output <<= 1; output |= ((xroot + travel) & 8) >> 3; if (!(screenx & 7)) *dst++ = output; xsumsquares += 2 * x++ + 1; if (xsumsquares >= xnextsquare) xnextsquare += 2 * ++xroot + 1; } } ysumsquares += 2 * y++ + 1; if (ysumsquares >= ynextsquare) ynextsquare += 2 * ++yroot + 1; screeny--; } show(); } // ── Pixel helpers ───────────────────────────────────────────────── void set_pixel(uint8_t x, uint8_t y, uint8_t mode) { byte *addr = &fb[x / 8 + y * X_SEGMENTS]; byte mask = 128 >> (x % 8); switch (mode) { case 0: *addr &= ~mask; break; // clear case 1: *addr |= mask; break; // plot case 2: *addr ^= mask; break; // XOR } } void safe_pixel(uint8_t x, uint8_t y, uint8_t mode) { if ((x >= X_SEGMENTS * 8) || (y >= Y_SEGMENTS * 8)) return; set_pixel(x, y, mode); } void clear() { byte *addr = fb; for (byte i = 0; i < 8 * NUM_SEGMENTS; i++) *addr++ = 0; } // ── show() — push framebuffer to all chained MAX7219s ────────────── // Handles boustrophedon (zig-zag) layout: // even segment rows → MSBFIRST (normal orientation) // odd segment rows → LSBFIRST (flipped orientation) void show() { for (byte row = 0; row < 8; row++) { digitalWrite(CS, LOW); byte segment = NUM_SEGMENTS; while (segment--) { byte x = segment % X_SEGMENTS; byte y = segment / X_SEGMENTS * 8; byte addr = (row + y) * X_SEGMENTS; if (segment & X_SEGMENTS) { // odd rows of segments shiftOut(DIN, CLK, MSBFIRST, 8 - row); shiftOut(DIN, CLK, LSBFIRST, fb[addr + x]); } else { // even rows of segments shiftOut(DIN, CLK, MSBFIRST, 1 + row); shiftOut(DIN, CLK, MSBFIRST, fb[addr - x + X_SEGMENTS - 1]); } } digitalWrite(CS, HIGH); } }
Step-by-Step Instructions
Open Wokwi
Go to wokwi.com, create a new project and select Arduino Mega 2560 as the board.
Paste diagram.json
Click the diagram.json tab and replace its contents with the JSON from Section 6. This places all 4 matrix strips and wires them automatically.
Paste the Sketch
In the main sketch.ino file, paste the full C++ code from Section 7. No external libraries are required — just built-in Arduino functions.
Verify Wiring
Confirm in the diagram that Pin 11 → DIN (blue), Pin 13 → CLK (orange), Pin 10 → CS (green). Matrix strips should be chained DOUT→DIN top to bottom.
Click ▶ Start Simulation
Hit the green play button. The display will initialise and begin rendering an animated circular wave pattern across all 1024 LEDs.
Experiment
Try changing 0x0a brightness value (0x00–0x0F) in shiftAll to dim the display, or modify the oscillator shift values (>> 6, >> 5) to change animation speed.
Comments
Post a Comment