Learning Goals 5 min
By the end of this lesson you will be able to:
- Wire five LEDs in a row, each with its own 220 Ω resistor, all sharing one GND through the breadboard's − rail — and address them as
leds[0]throughleds[4]by storing their pin numbers in an array. - Store an animation as a 2D array of frames: each row of the array is one snapshot of all five LEDs, and each row gets shown in turn.
- Write a single nested
for-loop "player" that walks the animation — outer loop over frames, inner loop over LEDs — so that adding a new pattern is a data change, not a code change.
Warm-Up 10 min
L01-33 took a tune and stored it as data. Today the same trick goes from sound to light. Five LEDs in a row are the visual analog of a tiny screen — and just like a screen, you tell them what to show by giving them a sequence of frames.
Quick-fire puzzle
You want to drive five LEDs through a "Knight Rider" sweep — the light moves left to right across all five, then back, then left to right again, forever. Without arrays, every frame is a fresh set of digitalWrite calls.
- How many
digitalWritecalls would one frame need (one snapshot of all five LEDs)? - If the sweep has 10 frames (left to right, then back), how many
digitalWritecalls in total — typed out as separate lines — would the whole animation take? - If you wanted to change the pattern later, how many of those lines would you need to find and edit?
Reveal the answer
- Five — one per LED, every frame.
- 50 calls, plus 10
delays. Possibly more if you have tosetLeds(LOW, …)the rest each frame. - Whichever lines describe the "wrong" behaviour. With 50 scattered calls, finding the right ones is a chore — and an easy place to introduce typos.
Arrays compress all of this dramatically. The animation becomes one rectangular block of 1s and 0s that you can read like a picture in the source code: each row is one moment in time, each column is one LED. A single nested loop walks the whole thing. Editing the pattern means changing the 1s and 0s — never the surrounding code.
New Concept — three arrays you'll meet today 15 min
The big idea — pins, frames, and a stack of frames
Three array shapes show up in every multi-LED sketch from here on. Learn the shapes; the rest is bookkeeping.
Array #1 — pin numbers
You have five LEDs, each on its own Arduino pin. Don't write the pin numbers all over the place; collect them once at the top:
const int NUM_LEDS = 5;
int leds[NUM_LEDS] = { 7, 8, 9, 10, 11 };Now leds[0] is pin 7, leds[1] is pin 8, and so on. To set the leftmost LED, write digitalWrite(leds[0], HIGH). To set the rightmost, digitalWrite(leds[4], HIGH). The benefit: position (0–4 from left to right) is now separate from which Arduino pin. Rewiring is a one-line change.
Array #2 — one frame (a snapshot of all five)
A frame is one moment of the animation — what each LED is doing right now. With five LEDs and only HIGH/LOW values, a frame is five 1s and 0s:
int frame[NUM_LEDS] = { 1, 0, 0, 0, 0 }; // only leftmost onframe[i] is the desired state of LED i. (Arduino accepts the literals 1 and 0 wherever it expects HIGH and LOW — they're literally the same numeric values. Using 1/0 here lets the animation array look like a picture.) A small helper that takes a frame and applies it:
void showFrame(int frame[]) {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], frame[i]);
}
}One call to showFrame pushes all five values to the LEDs in a single line of your top-level code. That's the L01-33 idea in pure form — data goes in, a loop does the boring work.
Array #3 — a 2D array of frames (the whole animation)
One frame is a snapshot. An animation is a sequence of snapshots. Stack them as a 2D array: rows = frames, columns = LEDs.
const int NUM_FRAMES = 5;
int chase[NUM_FRAMES][NUM_LEDS] = {
{ 1, 0, 0, 0, 0 }, // frame 0: leftmost
{ 0, 1, 0, 0, 0 }, // frame 1: one right
{ 0, 0, 1, 0, 0 }, // frame 2: middle
{ 0, 0, 0, 1, 0 }, // frame 3: one more right
{ 0, 0, 0, 0, 1 } // frame 4: rightmost
};Two pairs of square brackets in the declaration. First number is the number of frames; second is LEDs per frame. The rectangle of 1s and 0s in the curly braces is the animation — you can read it like a picture. The diagonal line of 1s shows the LED moving left to right across the row.
To get the value of LED i in frame f, write chase[f][i]. The first index picks the row; the second picks the column.
The nested-loop player — outer for frames, inner for LEDs
One for walks frames; inside it, another walks LEDs. The whole player is six lines:
for (int f = 0; f < NUM_FRAMES; f = f + 1) {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], chase[f][i]);
}
delay(150);
}That's it. The outer loop counts frames 0, 1, 2, 3, 4. For each, the inner loop pushes five LED values, then a 150 ms pause holds the frame on screen. Six lines and you've played a whole animation. Change the pattern? Edit the rectangle. Change the speed? Edit the delay. Change the number of LEDs? Edit two constants.
Why it matters
The "frames stacked as rows in a 2D array" shape is exactly how every screen and image format on earth stores a picture or video — including this web page, your phone's camera roll, and every game you've ever played. Today's array is 5 wide × N tall and made of 1s and 0s; a real screen's is 1920 wide × 1080 tall and made of RGB tuples; but the structure is the same. Master it on five LEDs and you've got the foundation of every pixel-based device.
Worked Example — five LEDs, three patterns 20 min
Step 1 — Wire the row
Five LEDs in a row across the top of the breadboard. Each LED's anode in row B, cathode in row A. Each anode connects through a 220 Ω resistor in row D to a yellow signal wire heading down to D7, D8, D9, D10, D11. The cathodes all drop into the − rail, which carries one shared GND wire back to the Arduino's GND pin. This is the L01-15 style of "one GND bus" wiring, extended from three LEDs to five.
leds array is the dictionary that translates between the two.Step 2 — The full sketch with three named patterns
// Multi-LED patterns — five-frame chase, ten-frame bounce, eight-frame wave
const int NUM_LEDS = 5;
int leds[NUM_LEDS] = { 7, 8, 9, 10, 11 };
const int CHASE_FRAMES = 5;
int chase[CHASE_FRAMES][NUM_LEDS] = {
{ 1, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 0 },
{ 0, 0, 0, 0, 1 }
};
const int BOUNCE_FRAMES = 8;
int bounce[BOUNCE_FRAMES][NUM_LEDS] = {
{ 1, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 0 },
{ 0, 0, 0, 0, 1 },
{ 0, 0, 0, 1, 0 },
{ 0, 0, 1, 0, 0 },
{ 0, 1, 0, 0, 0 }
};
const int WAVE_FRAMES = 8;
int wave[WAVE_FRAMES][NUM_LEDS] = {
{ 1, 0, 0, 0, 0 },
{ 1, 1, 0, 0, 0 },
{ 1, 1, 1, 0, 0 },
{ 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1 },
{ 0, 0, 1, 1, 1 },
{ 0, 0, 0, 1, 1 }
};
void showFrame(int frame[]) {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], frame[i]);
}
}
void playPattern(int pattern[][NUM_LEDS], int numFrames, int stepMs) {
for (int f = 0; f < numFrames; f = f + 1) {
showFrame(pattern[f]);
delay(stepMs);
}
}
void setup() {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
pinMode(leds[i], OUTPUT);
}
}
void loop() {
playPattern(chase, CHASE_FRAMES, 120);
playPattern(bounce, BOUNCE_FRAMES, 120);
playPattern(wave, WAVE_FRAMES, 180);
}Step 3 — Upload and watch
- Upload. You should see the chase sweep left-to-right once (5 frames × 120 ms = 600 ms total).
- Then the bounce: same sweep right, then back left, taking about a second.
- Then the wave: a growing band fills the row, then a gap eats it from the left. This is the prettiest pattern — eight frames spelt out as a tiny picture in code.
- Then everything starts again from the top of
loop().
Notice setup() uses a loop too — five pinMode calls compressed into one. Any time you have "the same thing for every entry in leds", a small for loop is the right move.
Reading the arrays as pictures
Tilt your head and look at the wave array — the 1s form a parallelogram sweeping from upper-left to lower-right. That's exactly what the LEDs do on the breadboard. You can design patterns by drawing 1s and 0s on paper first, then typing them in. This is the same workflow a sprite-sheet artist uses for 2D games: paint pixels in a grid, paste the grid into code.
Trace one frame on paper
The player calls showFrame(bounce[5]) on frame index 5 of the bounce array. Fill in what each LED is set to.
i | leds[i] | bounce[5][i] | LED state |
|---|---|---|---|
| 0 | 7 | 0 | off |
| 1 | 8 | ____ | ____ |
| 2 | 9 | ____ | ____ |
| 3 | 10 | ____ | ____ |
| 4 | 11 | ____ | ____ |
If you can fill in this table without uploading, you've internalised both the 1D pin lookup and the 2D frame indexing in one go.
Try It Yourself — three more patterns 20 min
Goal: A countdown pattern that goes 5 → 4 → 3 → 2 → 1 → 0 LEDs lit, then resets. Use the worked example's wiring and player; just add a new 6-frame 2D array.
Plan: each frame has one fewer 1 than the last, eaten from the right.
const int COUNTDOWN_FRAMES = 6;
int countdown[COUNTDOWN_FRAMES][NUM_LEDS] = {
{ 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 0 },
{ 1, 1, 1, 0, 0 },
{ 1, 1, 0, 0, 0 },
{ 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
};Then add playPattern(countdown, COUNTDOWN_FRAMES, 1000); to loop() with a 1-second hold per frame so the countdown feels paced like a real one.
Questions:
- Squint at the 1s in your array — what shape do they make? Why does that shape look like the animation? ____
- What change makes the countdown go the other direction (1 LED grows to 5)? ____
- If you wanted a buzz at the very end (final frame = all off + a buzzer beep), where in your code would the new
tone(BUZZER_PIN, ...)call go? ____
Goal: A generated sparkle. Instead of storing a fixed pattern, each frame randomly picks two of the five LEDs to be on. Run for 50 frames at 80 ms each. No array of frames needed.
Plan: in each frame, first turn every LED off; then pick two random positions with random(0, NUM_LEDS) and set them on. (The two random picks might land on the same LED — that's fine, you'll just get one on.) Then delay and repeat.
void sparkle(int numFrames, int stepMs) {
for (int f = 0; f < numFrames; f = f + 1) {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], LOW);
}
digitalWrite(leds[random(0, NUM_LEDS)], HIGH);
digitalWrite(leds[random(0, NUM_LEDS)], HIGH);
delay(stepMs);
}
}Questions:
- This sketch has no 2D array. Why is that allowed — and what makes sparkle different from the chase or bounce patterns? ____ (Hint: sparkle isn't a fixed sequence — every run is different.)
- Each random call returns 0–4. About what percentage of frames will both picks land on the same LED, giving you only one lit instead of two? ____ (Hint: 1 in 5.)
- How would you change the sketch to sparkle three LEDs per frame instead of two? ____
Goal: A binary counter. The five LEDs show the value of a number that counts from 0 to 31 in binary, with one frame per number. LED 0 is the 1s place, LED 1 is the 2s, LED 2 is the 4s, LED 3 is the 8s, LED 4 is the 16s. No 2D array — derive the LED states from the counter value each frame.
This is the first time you've seen computed animation: the pattern is mathematics, not data. The hint: for each LED i at counter value n, the LED is on if the bit at position i is set, which is (n >> i) & 1.
void binaryCount() {
for (int n = 0; n <= 31; n = n + 1) {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], (n >> i) & 1);
}
delay(400);
}
}(The >> operator shifts a number right by i bits; & 1 keeps only the lowest bit of the result. Together they say "is bit i set in n?" You'll meet these operators properly in Level 2 — for now, treat them as the formula that makes binary work.)
Questions:
- How long does the full count from 0 to 31 take with a 400 ms per-frame delay? ____
- Compare this approach to writing 32 frames as a 2D array. Which is shorter? Which is easier to understand at a glance? ____
- If you had 8 LEDs and the same counter, what's the largest number it could display? ____ (Hint:
2⁸ − 1.)
Mini-Challenge — design your own pattern 10 min
"Sketch it on paper first, then type the 1s and 0s"
Design a brand-new animation for five LEDs. The constraint: it must be a fixed sequence stored as a 2D array (not generated, not random) and at least 10 frames long. Some seeds:
- Eyes: the two outer LEDs blink together while the middle three stay off — alternate with all three middle on while the outer pair blinks off.
- Heartbeat: two quick all-on flashes, then a long all-off pause, repeat. ASCII it:
11111 / 00000 / 11111 / 00000 / 00000 / 00000 / 00000. - Crossing trains: a "1" moving from left to right meeting a "1" moving from right to left, crossing in the middle, continuing to opposite ends.
- Tetris drop: a single 1 falls down… wait, it's a row, not a column. Try a single 1 starting at LED 4 and sliding left to LED 0, then a new 1 appearing at LED 4 to repeat.
Your task:
- On a piece of paper, draw a grid 5 wide × however-many-frames tall. Pencil in 1s where you want LEDs lit, 0s elsewhere.
- Type the grid into Arduino as a 2D array exactly the way you drew it — one curly-braced row per frame.
- Add a matching
const int MY_FRAMES = …;constant. - Call
playPattern(myPattern, MY_FRAMES, 150);fromloop().
It works if:
- What you see on the LEDs matches what you drew on paper.
- You can squint at the array in code and "see" the animation as a tiny picture.
- Editing the pattern means editing 1s and 0s, never the surrounding code.
Reveal a sample heartbeat pattern
// Heartbeat — two quick flashes then a longer rest
const int HEART_FRAMES = 10;
int heartbeat[HEART_FRAMES][NUM_LEDS] = {
{ 1, 1, 1, 1, 1 }, // thump
{ 0, 0, 0, 0, 0 },
{ 1, 1, 1, 1, 1 }, // THUMP
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
};
// In loop():
playPattern(heartbeat, HEART_FRAMES, 120);Reading the array top to bottom you can "see" the heartbeat shape — two bursts of 1s near the top, then a long quiet of 0s, then the cycle repeats. That's the central trick of 2D animation arrays: the code looks like what you're going to see. The same trick scales up to sprite sheets in video games, where a character's walking animation is a grid of pixels organised as a 2D (or higher) array.
Recap 5 min
Three array shapes do all the heavy lifting today: a 1D array of pin numbers (leds) so position is separate from pin; a 1D array of HIGH/LOW values (frame) so one snapshot of the LEDs is one tidy unit; and a 2D array of frames (pattern) so an animation is a rectangle of 1s and 0s that looks like what you'll see on the LEDs. A single nested for loop walks the rectangle — outer over frames, inner over LEDs — and one helper function plays any animation. Adding new patterns means adding data; the player code never changes.
- Frame
- One snapshot of an animation. With five LEDs, a frame is five values — one per LED — describing what each one is doing right now.
- 2D array
- An array of arrays. Declared with two pairs of brackets —
int pattern[ROWS][COLS]— and indexed the same way:pattern[r][c]. The rows-and-columns shape mirrors the rectangle you draw on paper. - Nested loop
- A
forloop inside anotherforloop. The standard tool for walking a 2D array: outer loop picks the row, inner loop picks the column. - Player function
- A small function that takes a pattern (and its size and timing) and runs it on the LEDs. Once written, every new pattern is a data change, not a code change.
- Data-driven animation
- The technique of describing an animation as a block of values (1s and 0s here, RGB tuples in a game) and using one generic loop to play it back. The opposite of writing each frame as separate code.
- Generated animation
- An animation that's computed each frame rather than stored — like the sparkle (random) or binary counter (mathematics). Both styles coexist; pick whichever fits the effect.
1and0vsHIGHandLOW- Inside C++,
HIGHandLOWare literally the numbers 1 and 0. Using the digits in a 2D-array initialiser makes the data look like a tiny picture on the page — that's the only reason to prefer them over the keyword names.
Homework 5 min
The progress bar. Build a sketch that imitates a download progress bar:
- Start with all 5 LEDs off (0% loaded).
- Every 2 seconds, light one more LED from left to right until all 5 are on (100% loaded).
- When the bar fills, all 5 LEDs blink together rapidly three times (the "done!" celebration).
- Then everything goes off and stays off until the Arduino is reset.
Use a 2D array for the fill phase (six frames: 0 → 1 → 2 → 3 → 4 → 5 LEDs lit) and a separate 2D array (or just a small loop) for the "done" celebration.
Also: a design reflection on paper.
- Imagine you wanted to design a longer progress bar — 10 LEDs in a row instead of 5. How many constants would you need to change in your sketch? How many array entries? ____
- Some real progress bars don't fill linearly — they "stall" at 99% for a while because the last step is the slowest. How could you imitate that with today's tools? ____ (Hint: not every frame needs the same
delay.) - Pick one app on your phone or computer that uses a progress bar. Describe what it tracks — bytes downloaded? files copied? something else? Why does the bar feel different from a simple linear fill? ____
- The lesson said "data, not code". For your progress-bar sketch, what's data and what's code? List them in two columns. ____
Bring back next class:
- The saved
.inofile (call itfive-led-progress-bar). - A 15-second phone video showing one full progress sequence, fill plus celebration.
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-35 "Mood Lamp + Theme Tune" is the Cluster E project — it combines today's array-of-frames animation with last lesson's array-of-notes melody to build a tiny device that lights up and plays a personalised jingle at power-on. The data/code split from L01-33 and L01-34 pays off twice over.