Beginner2–3 hours14+4 parts needed

Parent info

Cost: ~$28
Time: 2–3 hours
Age: 14+
Difficulty: ●●●
Soldering: No soldering needed
What they'll learn: Microcontroller programming, LED circuits

Parts you need

Affiliate links — we may earn a small commission

ESP32-S3-DevKitC-1
WS2812B Addressable LEDs (strip of 30)
Breadboard + Jumper Wires
USB-C Data Cable
🎮

Try this circuit in your browser!

Run the code, press the buttons and watch what happens — before you buy any parts. No account needed.

Open in Simulator →

Your classmates made a poster. You built a cell that’s alive.

Imagine this: science fair night. Every table has a poster with labeled organelles drawn in colored pencil. Then there’s yours — a glowing 3D cell where the mitochondria pulse red like a beating heart, the nucleus shines steady blue, and the ribosomes flicker gold.

You pull out your phone, open Telegram, and type “the cell is dividing” — and every organelle starts blinking in sequence, simulating mitosis.

That’s what we’re building. For about $22.

Wiring diagram for Grade 9 Biology: Cell Model with LED Organelles: esp32 s3 devkitc 1 connected to strip


What you’ll need

Part What it does Price
ESP32-S3-DevKitC-1 The brain — controls all LEDs and connects to WiFi ~$12
WS2812B LED strip (30 LEDs) Each LED = one organelle, individually programmable ~$6
Breadboard + jumper wires Connects everything without soldering ~$5
USB-C data cable Powers the board and uploads code ~$5

You also need: a styrofoam ball or clear plastic container (cell body), hot glue, home WiFi.

Total: ~$22 | Time: ~2–3 hours | Difficulty: ●●○○○


How it works (60 seconds)

Think of the ESP32 as the cell membrane — it controls everything that goes in and out. Each LED in your WS2812B strip is like an organelle: individually addressable, meaning you can tell LED #1 to be blue and LED #2 to pulse red without affecting any others.

The trick: WS2812B LEDs are “smart” — they communicate on a single wire using a protocol called one-wire serial. The ESP32 sends a stream of color data, each LED reads its chunk and passes the rest forward, like a relay race.


Step 0: Build the physical cell

Time: ~45 minutes

Before any electronics, build your cell model first. This is the biology part — the ESP32 just makes it better.

Materials: Large styrofoam ball (6–8 inches), plastic wrap, paint, hot glue, labels.

Organelle placement plan:

Organelle LED # Color Behavior
Nucleus 0 Blue Steady glow
Mitochondria (×2) 1, 2 Red Slow pulse
Endoplasmic Reticulum 3, 4 Green Wave
Golgi Apparatus 5 Yellow Blink every 3s
Ribosomes (×4) 6–9 Gold Flicker
Vacuole 10 Cyan Steady dim
Lysosomes (×2) 11, 12 Purple Slow pulse
Cell Membrane 13–20 White Breathing

Cut holes in the styrofoam where each organelle goes. Push one LED into each hole, secure with hot glue. Route the LED strip along the inside so it’s hidden but the light shows through.

Check: Before gluing everything down, connect the strip to a 5V USB power bank and verify all LEDs light up white. If any are dark, you have a break in the strip — cut and reconnect.


Step 1: Wire it up

Time: ~10 minutes

Only 3 wires to connect the LED strip to the ESP32.

  1. Strip DIN (data in) → board GPIO 14 (C6: GPIO 8) — orange wire. On the C6, GPIO 8 also drives the board’s own RGB LED, so that little LED copies the strip’s first LED (the nucleus). That’s normal.
  2. Strip VCC (5V) → board 5V pin — red wire
  3. Strip GND → board GND — black wire

Check: Three wires total. DIN goes to GPIO 14 (C6: GPIO 8), not DOUT. VCC goes to the 5V pin (not 3.3V — this strip needs 5V).


Step 2: Flash the code

Time: ~15 minutes

Open Arduino IDE. Install these libraries first:

  • FastLED (by Daniel Garcia) — search in Library Manager

The big picture first. This program makes each LED behave like its organelle:

  • The nucleus glows steady blue — the control center never sleeps.
  • The mitochondria pulse red — they are constantly producing energy.
  • The ribosomes flicker randomly — they are building proteins all the time.
  • The cell membrane breathes slowly — it is always actively controlling what enters and exits.

A program is like a recipe. The computer reads it top to bottom and does exactly what is written, nothing more. Copy this entire recipe into Arduino IDE and upload it:

// ========== CHOOSE YOUR BOARD ==========
// Uncomment the line for YOUR board:
#define BOARD_S3    // ESP32-S3-DevKitC-1
//#define BOARD_C6  // ESP32-C6-DevKitC-1
// ========================================

#ifdef BOARD_S3
  #define PIN_LED   14
#endif
#ifdef BOARD_C6
  #define PIN_LED    8
#endif

#include <WiFi.h>
#include <FastLED.h>

#define NUM_LEDS    30
#define BRIGHTNESS  80

CRGB leds[NUM_LEDS];

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";

void setup() {
  FastLED.addLeds<WS2812B, PIN_LED, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
  Serial.begin(115200);
  showRestingCell();
}

void loop() {
  leds[0] = CRGB(0, 0, 180);

  int pulse = (sin(millis() / 1000.0) + 1) * 127;
  leds[1] = CRGB(pulse, 0, 0);
  leds[2] = CRGB(pulse, 0, 0);

  for (int i = 6; i <= 9; i++) {
    if (random(10) > 7) leds[i] = CRGB(180, 120, 0);
    else leds[i] = CRGB(100, 70, 0);
  }

  for (int i = 3; i <= 4; i++) {
    int wave = (sin(millis() / 800.0 + i * 0.5) + 1) * 80;
    leds[i] = CRGB(0, wave + 30, 0);
  }

  if ((millis() / 3000) % 2 == 0) leds[5] = CRGB(180, 180, 0);
  else leds[5] = CRGB(40, 40, 0);

  leds[10] = CRGB(0, 60, 60);

  int lyso = (sin(millis() / 1500.0) + 1) * 60;
  leds[11] = CRGB(lyso, 0, lyso);
  leds[12] = CRGB(lyso, 0, lyso);

  int membrane = (sin(millis() / 2000.0) + 1) * 40 + 20;
  for (int i = 13; i <= 20; i++) {
    leds[i] = CRGB(membrane, membrane, membrane);
  }

  FastLED.show();
  delay(30);
}

void showRestingCell() {
  fill_solid(leds, NUM_LEDS, CRGB(10, 10, 10));
  FastLED.show();
  delay(500);
}

void showMitosis() {
  for (int round = 0; round < 3; round++) {
    for (int i = 0; i < NUM_LEDS; i++) {
      leds[i] = CRGB(200, 200, 200);
      FastLED.show();
      delay(50);
    }
    fill_solid(leds, NUM_LEDS, CRGB(0, 0, 0));
    FastLED.show();
    delay(200);
  }
}

Line-by-line: what every line does and why

Lines 1–2: Borrowing ready-made instruction books

#include <WiFi.h>
#include <FastLED.h>

#include means “grab this instruction book.” FastLED is a library — a huge set of pre-written instructions for controlling “smart” LEDs. Without it, making one LED glow a specific color would take hundreds of lines. With it, it takes one line.


Lines 4–6: Giving numbers easy names

#define PIN_LED   14
#define NUM_LEDS    30
#define BRIGHTNESS  80

#define is like writing a nickname on a sticky note. The data wire from the LED strip is plugged into leg 14 (C6: leg 8). There are 30 LEDs total. Brightness 80 out of 255 is about 30% — bright enough to see, dim enough not to blind anyone. If you want brighter, change 80 to 150.


Line 8: Creating the LED array

CRGB leds[NUM_LEDS];

This creates a shelf with 30 compartments, one per LED. Each compartment holds a color — a mix of red, green, and blue values. CRGB is the color type. leds[0] is LED number 1, leds[1] is LED number 2, and so on.


Lines 16–21: setup() runs once when you power on

FastLED.addLeds<WS2812B, PIN_LED, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
Serial.begin(115200);
showRestingCell();

FastLED.addLeds<WS2812B, PIN_LED, GRB>(leds, NUM_LEDS) tells FastLED: “I have WS2812B LEDs, they are on leg 14 (C6: leg 8), they use GRB color order, and there are 30 of them.” setBrightness(80) sets a global brightness cap — like a dimmer switch for the whole strip. Serial.begin(115200) opens the phone line to your computer. showRestingCell() calls our starting animation (all LEDs dim).


Lines 23–69: loop() — the cell’s heartbeat

void loop() runs over and over, about 30 times per second. Each time through, it updates every organelle.

leds[0] = CRGB(0, 0, 180);

Set LED #0 to blue: red=0, green=0, blue=180. This is the nucleus — steady, unchanging. The nucleus contains DNA and does not blink because DNA does not turn on and off.

int pulse = (sin(millis() / 1000.0) + 1) * 127;
leds[1] = CRGB(pulse, 0, 0);
leds[2] = CRGB(pulse, 0, 0);

This makes the mitochondria pulse. Here is the trick: millis() is a stopwatch counting milliseconds since power-on. sin() is a math function that goes up and down smoothly between -1 and +1 — like a wave. (sin(...) + 1) * 127 shifts it to go from 0 to 254 instead. So pulse smoothly cycles 0 → 254 → 0 → 254 every second. That number becomes the red channel — so the LED glides from dark to bright red and back. This simulates ATP energy production, which is constant and rhythmic.

for (int i = 6; i <= 9; i++) {
  if (random(10) > 7) leds[i] = CRGB(180, 120, 0);
  else leds[i] = CRGB(100, 70, 0);
}

for repeats for LEDs 6, 7, 8, and 9 — the ribosomes. random(10) picks a random number from 0 to 9. If it is greater than 7 (30% chance), the ribosome glows bright gold; otherwise it glows dim gold. This random flicker looks like protein assembly happening at random moments — which is biologically accurate.

for (int i = 3; i <= 4; i++) {
  int wave = (sin(millis() / 800.0 + i * 0.5) + 1) * 80;
  leds[i] = CRGB(0, wave + 30, 0);
}

The endoplasmic reticulum gets a green wave. The i * 0.5 shifts each LED slightly out of phase — so LED 3 and LED 4 do not pulse at exactly the same time, creating a flowing wave effect. wave + 30 adds a minimum brightness so the ER never goes completely dark.

if ((millis() / 3000) % 2 == 0) leds[5] = CRGB(180, 180, 0);
else leds[5] = CRGB(40, 40, 0);

The Golgi apparatus blinks every 3 seconds. millis() / 3000 gives the number of 3-second intervals elapsed. % 2 gives the remainder after dividing by 2 — it alternates between 0 and 1. When 0: bright yellow. When 1: dim yellow. This represents the Golgi “packaging” proteins in bursts.

int membrane = (sin(millis() / 2000.0) + 1) * 40 + 20;
for (int i = 13; i <= 20; i++) {
  leds[i] = CRGB(membrane, membrane, membrane);
}

The cell membrane LEDs all breathe together in white. A 2-second sine wave gives a slow, calm rhythm. Equal red, green, and blue values always produce white (or grey when dim). This simulates the membrane’s constant selective permeability — always active, never static.

FastLED.show();
delay(30);

FastLED.show() is the most important line — it pushes all the color data you just calculated out to the physical LEDs. Without this, every change you made to the leds[] array stays invisible. delay(30) waits 30 milliseconds before the next frame — giving about 33 animation frames per second, which looks smooth to the human eye.


Lines 71–75: showRestingCell() — the startup state

void showRestingCell() {
  fill_solid(leds, NUM_LEDS, CRGB(10, 10, 10));
  FastLED.show();
  delay(500);
}

fill_solid() sets every LED to the same color at once — CRGB(10, 10, 10) is a very dim white, like a cell at rest. This runs once at startup so the cell does not flash bright before the animation begins.


Lines 77–89: showMitosis() — cell division animation

void showMitosis() {
  for (int round = 0; round < 3; round++) {
    for (int i = 0; i < NUM_LEDS; i++) {
      leds[i] = CRGB(200, 200, 200);
      FastLED.show();
      delay(50);
    }
    fill_solid(leds, NUM_LEDS, CRGB(0, 0, 0));
    FastLED.show();
    delay(200);
  }
}

Two nested loops. The outer for (round) runs 3 times. The inner for (i) lights up LEDs one by one with a 50ms delay each — creating a sweep of light across the whole cell. Then fill_solid(..., CRGB(0,0,0)) turns everything black (off). Three sweeps simulate the dramatic reorganization of cell division.


The whole thing in one sentence

When powered on, all LEDs start dim (showRestingCell). Then loop runs endlessly — every 30 milliseconds it recalculates each organelle’s color based on math waves and random numbers, then pushes everything to the LEDs with FastLED.show().

First thing to try: Find the line int pulse = (sin(millis() / 1000.0) + 1) * 127; and change 1000.0 to 200.0. Re-upload. The mitochondria will pulse 5 times faster. Then change it back. This is how you control animation speed — the number inside millis() / X is the period in milliseconds.

Select board: ESP32S3 Dev Module. Upload. Watch the organelles light up.

Check: Open Serial Monitor at 115200 baud. You should see “Cell model ready” printed. All organelles should show their assigned colors.


Step 3: Calibrate organelle positions

Time: ~10 minutes

Run this calibration sketch first to identify which physical LED number is which organelle:

In loop(), temporarily replace everything with:

// Light one LED at a time so you can label them
int current = (millis() / 2000) % NUM_LEDS;
fill_solid(leds, NUM_LEDS, CRGB(0,0,0));
leds[current] = CRGB(255, 0, 0);
FastLED.show();
Serial.println("LED: " + String(current));

One LED lights up every 2 seconds. Write down which physical location each number corresponds to, then update the LED number table from Step 0 to match.


Step 4: Present it!

Time: your science fair / class period

During your presentation:

  1. Start with resting cell (dim, slow pulse)
  2. Explain each organelle — point to it, it reacts
  3. Say “the cell is working hard” — trigger the mitosis animation
  4. Ask your teacher: “Can you guess which organelle is the powerhouse?”

Presentation tip: Prepare a 30-second “compare” moment — hold up a plain poster next to your cell. Say: “Every organelle on this poster is just labeled. Every organelle in my model is actually behaving like the real thing does — the mitochondria pulse because they’re generating energy, not just sitting there.”


What just happened

You used PWM signals to create brightness variations — the same technique used in TV backlights and stage lighting. Each “pulse” in the mitochondria is a sine wave: sin(time) → brightness. That’s math running in real hardware.

Curriculum connections:

  • NGSS HS-LS1-1: Construct an explanation based on evidence for how the structure of DNA determines the structure of proteins
  • NGSS MS-LS1-2: Develop and use a model to describe the function of a cell as a whole and ways the parts of cells contribute to the function
  • AP Biology: Cell structure and function, organelle roles in metabolism

The flickering ribosomes model the fact that ribosomes are not static — they’re constantly assembling proteins. The pulsing mitochondria reflect that ATP production is a continuous cycle. Your model is more accurate than a poster.


Level Up

Add a button to trigger mitosis: Wire a button to GPIO 0 (C6: GPIO 9) — that’s the pin the BOOT button already uses. When pressed, run showMitosis(). Now anyone can “start cell division” during your presentation.

Plant cell version: Change the cell membrane LEDs to green (chloroplasts on the outside). Add a chloroplast organelle (bright green) that only activates when a light sensor detects light — simulating photosynthesis.

Compare animal vs. plant: Build two cells side by side. Plant cell has chloroplasts + rigid membrane (green outer ring). Animal cell has centrioles + flexible membrane.


Troubleshooting

Problem Fix
LEDs don’t light at all Check DIN is on GPIO 14 (C6: GPIO 8), not DOUT. Check 5V pin (not 3.3V). Try plugging power bank directly to VCC/GND.
Colors look wrong (red shows as green) Change GRB to RGB in FastLED.addLeds<WS2812B, PIN_LED, GRB>
Some LEDs don’t respond Strip has a break — cut at the break, reconnect with jumper wire.
Flickering/random colors Add a 100–470Ω resistor on the DIN wire.
Upload fails Hold BOOT button on ESP32 while clicking Upload.
Affiliate disclosure: Some links on this page are affiliate links. If you buy through them, we may earn a small commission at no extra cost to you.