Learning Goals 5 min
Cluster H is about writing less code. The first trick: replace long if/else chains with tidy lookup tables stored in arrays. By the end of this lesson you will:
- Declare and iterate over single-dimensional
const intandconst char*arrays in Arduino C++. - Convert a sequence of
iftests into a lookup table indexed by a number or matched against a key. - Use 2D arrays for grouped data — e.g. RGB colour presets each storing 3 values.
Warm-Up 10 min
No new wiring today — we're refactoring code. Pull out an L1 or L2 sketch with lots of ifs. Good candidates:
- L01-29 Serial Light Show — different keys mapped to different LED actions.
- L01-35 Mood Lamp — different button presses change colours.
- L01-46 Traffic Light — five phases with their own timings.
The pattern we want to remove
if (cmd == '1') flash(50);
else if (cmd == '2') flash(100);
else if (cmd == '3') flash(200);
else if (cmd == '4') flash(500);
else if (cmd == '5') flash(1000);Five lines that each do the same shape of thing. Add a 6th option and you copy-paste another line, easy to miss one. Replace the whole block with a table.
New Concept · Tables instead of branches 25 min
Single-value lookup
const int DURATIONS[] = { 50, 100, 200, 500, 1000 };
if (cmd >= '1' && cmd <= '5') {
int index = cmd - '1'; // '1' -> 0, '2' -> 1, etc.
flash(DURATIONS[index]);
}One table, one index calculation, one function call. Adding a 6th option: add one value to the array, change the bound check. The action code doesn't grow.
The sizeof idiom for array length
const int NUM_DURATIONS = sizeof(DURATIONS) / sizeof(DURATIONS[0]);
// ^ total bytes ^ bytes per element = element countUse this whenever you loop through an array. Don't hard-code a length next to the array — they get out of sync.
String lookup tables
const char* DAY_NAMES[] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
void printDay(int weekday) { // 0..6
Serial.println(DAY_NAMES[weekday]);
}const char* is "pointer to const characters" — a C-style string. The array is an array of pointers. Lightweight; perfect for fixed lookup tables.
2D arrays — group multiple values per row
const int COLOUR_PRESETS[][3] = {
{ 255, 0, 0 }, // red
{ 0, 255, 0 }, // green
{ 0, 0, 255 }, // blue
{ 255, 255, 255 }, // white
{ 255, 165, 0 }, // amber
};
void setRGB(int index) {
analogWrite(R_PIN, COLOUR_PRESETS[index][0]);
analogWrite(G_PIN, COLOUR_PRESETS[index][1]);
analogWrite(B_PIN, COLOUR_PRESETS[index][2]);
}The first dimension is the preset number; the second is which channel. Add a sixth colour with one row. Cleaner than 5 distinct if blocks.
Search by key (small tables)
For matching command characters to actions, two parallel arrays work:
const char COMMANDS[] = { 'r', 'g', 'b', 'w', 'a' };
const char* COMMAND_NAMES[] = { "red", "green", "blue", "white", "amber" };
const int N_CMDS = sizeof(COMMANDS) / sizeof(COMMANDS[0]);
int findCommand(char c) {
for (int i = 0; i < N_CMDS; i++) {
if (COMMANDS[i] == c) return i;
}
return -1; // not found
}Linear search is fine for < ~20 entries. For larger tables use a switch or a hash; for sorted tables, binary search.
PROGMEM for big tables
Big tables (musical scales, font bitmaps, calibration curves) live in flash, not RAM:
const int NOTE_FREQS[] PROGMEM = {
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494
};
// Access via pgm_read_word, not direct indexing:
int freq = pgm_read_word(&NOTE_FREQS[index]);The PROGMEM keyword tells the AVR compiler to keep the table in program memory. Saves RAM but adds the pgm_read_* read step. Use only when RAM is genuinely tight (mainly on UNO; the ESP's 80–520 KB usually has plenty).
Worked Example · Refactor a serial light show 25 min
Before — the if-tree version
void loop() {
if (!Serial.available()) return;
char c = Serial.read();
if (c == '1') analogWrite(R_PIN, 255), analogWrite(G_PIN, 0), analogWrite(B_PIN, 0);
else if (c == '2') analogWrite(R_PIN, 0), analogWrite(G_PIN, 255), analogWrite(B_PIN, 0);
else if (c == '3') analogWrite(R_PIN, 0), analogWrite(G_PIN, 0), analogWrite(B_PIN, 255);
else if (c == '4') analogWrite(R_PIN, 255), analogWrite(G_PIN, 255), analogWrite(B_PIN, 255);
else if (c == '5') analogWrite(R_PIN, 255), analogWrite(G_PIN, 165), analogWrite(B_PIN, 0);
else if (c == '0') analogWrite(R_PIN, 0), analogWrite(G_PIN, 0), analogWrite(B_PIN, 0);
}15 lines of repetitive analogWrites. Adding a colour means 3 more lines plus one new else if.
After — table-driven
const int R_PIN = 9, G_PIN = 10, B_PIN = 11;
const int COLOURS[][3] = {
{ 0, 0, 0 }, // 0 -> off
{ 255, 0, 0 }, // 1 -> red
{ 0, 255, 0 }, // 2 -> green
{ 0, 0, 255 }, // 3 -> blue
{ 255, 255, 255 }, // 4 -> white
{ 255, 165, 0 }, // 5 -> amber
};
const int N_COLOURS = sizeof(COLOURS) / sizeof(COLOURS[0]);
void setColour(int i) {
if (i < 0 || i >= N_COLOURS) return;
analogWrite(R_PIN, COLOURS[i][0]);
analogWrite(G_PIN, COLOURS[i][1]);
analogWrite(B_PIN, COLOURS[i][2]);
}
void setup() {
Serial.begin(9600);
pinMode(R_PIN, OUTPUT);
pinMode(G_PIN, OUTPUT);
pinMode(B_PIN, OUTPUT);
}
void loop() {
if (!Serial.available()) return;
char c = Serial.read();
if (c >= '0' && c <= '9') setColour(c - '0');
}Same behaviour. Adding a 7th colour: append one row to COLOURS. No change to the dispatcher.
Bonus — auto-name the colours for logging
const char* COLOUR_NAMES[] = {
"off", "red", "green", "blue", "white", "amber"
};
// in setColour, after the bounds check:
Serial.print("Colour: ");
Serial.println(COLOUR_NAMES[i]);Two parallel arrays — colour values + colour names. Same index for both. Always update them together (or refactor into a struct, which we'll do tomorrow).
2D table for a state machine's transitions
The traffic-light controller from L01-46 had four phases. Express them as a table:
// Each row: { red, amber, green, durationMs }
const int PHASES[][4] = {
{ 1, 0, 0, 5000 }, // STOP
{ 1, 1, 0, 1500 }, // STOP+AMBER (about to go)
{ 0, 0, 1, 5000 }, // GO
{ 0, 1, 0, 1500 }, // AMBER (about to stop)
};
const int N_PHASES = sizeof(PHASES) / sizeof(PHASES[0]);
int phase = 0;
unsigned long phaseStart = 0;
void loop() {
digitalWrite(RED, PHASES[phase][0]);
digitalWrite(AMBER, PHASES[phase][1]);
digitalWrite(GREEN, PHASES[phase][2]);
if (millis() - phaseStart >= (unsigned long)PHASES[phase][3]) {
phase = (phase + 1) % N_PHASES;
phaseStart = millis();
}
}The entire state machine's behaviour lives in the PHASES table. Changing the timing: edit a number. Adding a phase: add a row. Compare with the L01-46 version's many if blocks — same outcome, fraction of the code.
Try It Yourself 15 min
Goal: A musical notes table. const int NOTES[] = {262, 294, 330, 349, 392, 440, 494, 523} (a C major scale, C4..C5). Loop through them, playing each note for 300 ms via tone().
Hint
const int NOTES[] = {262, 294, 330, 349, 392, 440, 494, 523};
const int N_NOTES = sizeof(NOTES) / sizeof(NOTES[0]);
void loop() {
for (int i = 0; i < N_NOTES; i++) {
tone(BUZZER, NOTES[i], 300);
delay(350);
}
delay(2000);
}Goal: Parallel arrays for keypad → action. Define const char KEYS[] = "0123456789" and an action function array void (*ACTIONS[])() = {fn0, fn1, ...}. On serial input, find the key's index and call the matching action.
Hint
Function pointers are advanced — the syntax is void (*name[])(). Each entry is a pointer to a function returning void.
void fn0() { Serial.println("zero"); }
void fn1() { Serial.println("one"); }
// ... fn2 .. fn9
void (*ACTIONS[])() = { fn0, fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8, fn9 };
void loop() {
if (!Serial.available()) return;
char c = Serial.read();
if (c >= '0' && c <= '9') {
ACTIONS[c - '0'](); // call the function
}
}Each "menu" item is just one function in the array. The dispatch is one line.
Goal: Encode the Morse code alphabet as a lookup table — 26 strings of dots and dashes. Refactor L01-45's Morse blinker to use the table.
Hint
const char* MORSE[] = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....",
"..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.",
"--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-",
"-.--", "--.."
}; // index 0 = A, 1 = B, ...
void blinkLetter(char c) {
if (c < 'A' || c > 'Z') return;
const char* code = MORSE[c - 'A'];
for (int i = 0; code[i]; i++) {
flash(code[i] == '.' ? DOT_MS : DASH_MS);
delay(GAP_MS);
}
}Whole Morse alphabet in one table. Encoding new letters is just adding rows. The blinker stays the same.
Mini-Challenge · Refactor one of your projects 10 min
Open one of your earlier sketches with an if chain. Replace it with a lookup table. Compare:
- Line count before vs after.
- Effort to add a new entry (count the lines you'd change).
- Where the actual "business logic" lives — is it harder or easier to spot?
The good kind of refactor: same behaviour, fewer lines, easier to extend. The Cluster H promise.
Recap 5 min
Arrays + indexing replace if/else chains. sizeof(arr)/sizeof(arr[0]) for the length; bounds-check before indexing. 2D arrays group related values into rows. Parallel arrays / function-pointer arrays for command dispatch. PROGMEM for big static tables. Tomorrow we replace the "parallel arrays" pattern with the cleaner alternative: struct.
- Lookup table
- An array of values indexed by a key. Replaces branching code with data.
sizeof(arr)/sizeof(arr[0])- Idiom for getting an array's element count at compile time. Works only on arrays declared in the current scope, not on pointers.
- 2D array
- An array of arrays.
data[row][col]. Useful for grouped data with consistent layout per row. - Parallel arrays
- Multiple arrays where index
iin each refers to the same logical thing. The poor person'sstruct; replace with a realstructwhen it grows. - Function pointer
- A variable that points to a function. Syntax
void (*fn)(). Lets you store actions in arrays and dispatch by index. - PROGMEM
- AVR / ESP keyword storing a constant in program memory (flash) instead of RAM. Use
pgm_read_*to read. - Bounds check
- Always verifying an index is in range before using it. Skipping this is the #1 source of memory bugs in C.
- strcmp / strncmp
- Standard C string compare functions. Use these instead of
==onconst char*values.
Homework 5 min
- Pick an old sketch with at least 5
if/elsebranches. Refactor with a lookup table. Note line count delta. - Read ahead to ARD-L03-40 (Structs for Grouped Data). Tomorrow we turn parallel arrays into a single tidy
struct. - Bring tomorrow: a sensor + the SSD1306 OLED. We'll wrap a sensor reading + display info into one struct.
Bring back next class:
- Refactored sketch (before + after).
- Sensor + OLED for L03-40.