Plasma panel on an ESP32-C3
Three sine waves drift across a 16x16 WS2812B matrix, 256 LEDs on one wire from an ESP32-C3. The whole build, an ESP32-C3 and 1 more part, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 15 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $16. On Mokxi they're free.
What you'll learn
- Addressing an LED matrix by x and y
- Adding waves together to make a pattern
- Using a lookup table instead of calculating sines
- How one wire can carry data for 256 LEDs
- Why a simulated frame can be slower than real hardware
- Difficulty: Advanced
- Time: About 15 minutes
- Board: ESP32-C3-DevKitM-1
- Parts: 3
How it works
A 16 by 16 panel has 256 LEDs, all controlled over a single data wire on pin 5. For each LED, the sketch adds three sine waves together: one across the panel, one down it and one along the diagonal. The total picks a color.
Each frame, a time value t moves forward, so the waves drift and the colors flow. The panel is drawn as x and y positions, and the setPanel call handles the zigzag layout where every second row runs backward.
The sketch shows a new frame every 100 ms, so ten frames a second. No floating point is used. Instead, a lookup table holds one cycle of a sine wave.
Parts list
3 parts, plus the jumper wires. They retail for about $16; on Mokxi they're free.
- 1 × ESP32-C3-DevKitM-1In Handheld Console Kit, Desk Buddy Kit.
- 1 × Full-size breadboard
- 1 × Addressable RGB LEDsIn Claw Machine Kit, Halloween Prop Kit, Christmas Lights Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Addressable RGB LEDs pin VSS | ESP32-C3-DevKitM-1 pin GND |
| Addressable RGB LEDs pin DIN | ESP32-C3-DevKitM-1 pin 5 |
| Addressable RGB LEDs pin VDD | ESP32-C3-DevKitM-1 pin 5V |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Plasma: a 16x16 WS2812B panel, 256 LEDs on one wire.
//
// Wiring, panel to board:
// DIN -> GPIO 5 VDD -> 5 V VSS -> GND
//
// The panel is serpentine: the first row runs left to right, the second right
// to left, and so on, which is how almost all of them are built and why the
// part has a `serpentine` property. `setPanel` does that arithmetic, so the
// sketch thinks in x and y.
//
// A plasma is three sine waves added together: one across, one down, and one
// along the diagonal, each drifting at its own rate. The sum picks a hue. There
// is no floating point in this runtime and none is wanted: a 256-entry table
// of one cycle, indexed by an 8-bit angle that wraps by itself, is both faster
// and exactly periodic.
//
// # What a frame costs, and why it is ten a second
//
// 256 LEDs is 6144 bits, and each one is a pulse the driver times with a
// counted loop, so a frame takes about 7.7 ms of the board's own time. The
// plasma arithmetic is a few thousand instructions and disappears next to it.
// Interrupts are held off for the duration: a stretched pulse is not this
// protocol any more, and the panel would drop the whole frame rather than show
// half of it.
//
// The frame rate is set by the simulator rather than by the chip. Those 6144
// bits are 12 288 edges on the wire, and the Mokxi kernel costs about two
// microseconds to carry one of them through the WS2812B's decoder, so a frame
// the board would finish in 7.7 ms takes the simulator some 30 ms of real time.
// A real ESP32-C3 would run this panel at fifty frames a second without
// noticing. Ten is what the browser can carry while the clock on the toolbar
// still reads close to 1.00x, and even then it dips: a 16x16 panel is the most
// expensive thing in the parts bin to simulate, edge for edge.
#include "mokxi_ws2812.h"
const uint8_t DIN_PIN = 5;
const uint8_t SIDE = 16;
const uint16_t LEDS = (uint16_t)SIDE * SIDE;
uint8_t pixels[LEDS * 3];
Ws2812 panel(DIN_PIN, pixels, LEDS);
/** One cycle of a sine, 0..255 in and 0..255 out. The angle wraps by itself. */
static const uint8_t SIN8[256] = {
128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 162,
165, 167, 170, 173, 176, 179, 182, 185, 188, 190, 193, 196,
198, 201, 203, 206, 208, 211, 213, 215, 218, 220, 222, 224,
226, 228, 230, 232, 234, 235, 237, 238, 240, 241, 243, 244,
245, 246, 248, 249, 250, 250, 251, 252, 253, 253, 254, 254,
254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253,
253, 252, 251, 250, 250, 249, 248, 246, 245, 244, 243, 241,
240, 238, 237, 235, 234, 232, 230, 228, 226, 224, 222, 220,
218, 215, 213, 211, 208, 206, 203, 201, 198, 196, 193, 190,
188, 185, 182, 179, 176, 173, 170, 167, 165, 162, 158, 155,
152, 149, 146, 143, 140, 137, 134, 131, 128, 124, 121, 118,
115, 112, 109, 106, 103, 100, 97, 93, 90, 88, 85, 82,
79, 76, 73, 70, 67, 65, 62, 59, 57, 54, 52, 49,
47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23,
21, 20, 18, 17, 15, 14, 12, 11, 10, 9, 7, 6,
5, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 5,
5, 6, 7, 9, 10, 11, 12, 14, 15, 17, 18, 20,
21, 23, 25, 27, 29, 31, 33, 35, 37, 40, 42, 44,
47, 49, 52, 54, 57, 59, 62, 65, 67, 70, 73, 76,
79, 82, 85, 88, 90, 93, 97, 100, 103, 106, 109, 112,
115, 118, 121, 124,
};
static inline uint8_t sin8(uint8_t angle) { return SIN8[angle]; }
uint16_t t = 0;
// Bright enough to read on the canvas, dim enough that a real panel would not
// need its own supply.
const uint8_t LEVEL = 96;
const int FRAME_MS = 100;
void setup() {
Serial.begin(115200);
Serial.println("Mokxi ESP32-C3: plasma on a 16x16 WS2812B panel");
panel.begin();
panel.clear();
panel.show();
}
void loop() {
for (uint8_t y = 0; y < SIDE; y++) {
for (uint8_t x = 0; x < SIDE; x++) {
// Three waves: across, down, and along the diagonal, each drifting.
uint16_t v = (uint16_t)sin8((uint8_t)(x * 14u + t));
v = (uint16_t)(v + sin8((uint8_t)(y * 12u - t * 2u)));
v = (uint16_t)(v + sin8((uint8_t)((x + y) * 9u + t / 2u)));
// 0..765 into the 1536 steps of the wheel, twice round the cube.
uint16_t hue = (uint16_t)((v * 2u) % 1536u);
uint8_t r;
uint8_t g;
uint8_t b;
ws2812_wheel(hue, &r, &g, &b);
panel.setPanel(x, y, SIDE, true, ws2812_scale(r, LEVEL), ws2812_scale(g, LEVEL),
ws2812_scale(b, LEVEL));
}
}
panel.show();
t = (uint16_t)(t + 3u);
delay(FRAME_MS);
}
Code walkthrough
The key functions of the sketch, and what each one does.
sin8 sketch.ino
Looks up a value in the SIN8 table. An 8-bit angle goes in and a brightness from 0 to 255 comes out. Because the angle is an 8-bit number, it wraps back to zero by itself, so the wave repeats exactly.
static inline uint8_t sin8(uint8_t angle) { return SIN8[angle]; }loop sketch.ino
For every pixel, it adds three sin8 values with different directions and speeds. It turns the total into a hue with ws2812_wheel, dims it with LEVEL, and stores it with setPanel. Then panel.show sends the frame, t moves on and it waits FRAME_MS.
void loop() {
for (uint8_t y = 0; y < SIDE; y++) {
for (uint8_t x = 0; x < SIDE; x++) {
// Three waves: across, down, and along the diagonal, each drifting.
uint16_t v = (uint16_t)sin8((uint8_t)(x * 14u + t));
v = (uint16_t)(v + sin8((uint8_t)(y * 12u - t * 2u)));
v = (uint16_t)(v + sin8((uint8_t)((x + y) * 9u + t / 2u)));
// 0..765 into the 1536 steps of the wheel, twice round the cube.
uint16_t hue = (uint16_t)((v * 2u) % 1536u);
uint8_t r;
uint8_t g;
uint8_t b;
ws2812_wheel(hue, &r, &g, &b);
panel.setPanel(x, y, SIDE, true, ws2812_scale(r, LEVEL), ws2812_scale(g, LEVEL),
ws2812_scale(b, LEVEL));
}
}
panel.show();
t = (uint16_t)(t + 3u);
delay(FRAME_MS);
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The picture looks scrambled, with every other row reversed This project
This is a serpentine panel. Thetruein thesetPanelcall tells it so. If your panel runs every row the same way, set it to false. - Nothing lights up This project
Check that DIN goes to pin 5, and that VDD has 5 V and VSS has ground. - The animation looks slower than expected This project
The sketch targets ten frames a second withFRAME_MS. A largertstep inloopmakes the waves drift faster. - The first LED flickers, shows wrong colors, or the whole strip glitches. Addressable RGB LEDs
Join the grounds of the strip and the controller. Add a 300 to 500 ohm resistor in series with the data line. Put a 1000 uF capacitor across the strip's 5 V and GND. - The strip works at low brightness but dims, turns yellow, or resets when many LEDs go white. Addressable RGB LEDs
Each LED can draw about 60 mA at full white. Use a 5 V supply sized for the count, feed power in at several points, and lower the brightness in code. - Upload fails or the board is not found by the computer. ESP32-C3-DevKitM-1
Pick the ESP32-C3 board in your tools and the right port. If it still fails, hold BOOT, tap RESET, then release BOOT to enter download mode and try again. - GPIO pins act strangely or the board resets. ESP32-C3-DevKitM-1
GPIO is 3.3 V only, so never feed it 5 V. GPIO 18 and 19 are USB, and GPIO 2, 8 and 9 are boot strapping pins. Avoid loading them at reset.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.