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.
Basic 5 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().
Challenge 1 5 min
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.
Challenge 2 5 min
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.
Challenge 3 · Fix the broken lookup 10 min
A classmate wrote a table-driven colour picker for an RGB LED on an UNO. They type 1 in the Serial Monitor and nothing happens at all. There are four mistakes.
const int R_PIN = 9;
const int G_PIN = 10;
const int B_PIN = 11;
const int COLOURS[][3] = {
{ 0, 0, 0 },
{ 255, 0, 0 },
{ 0, 255, 0 },
{ 0, 0, 255 },
};
const int N_COLOURS = sizeof(COLOURS);
const char* COLOUR_NAMES[] = {
"off", "red", "blue", "green"
};
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]);
Serial.println(COLOUR_NAMES[i]);
}
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();
setColour(c);
}- Work out the value of
N_COLOURSon an UNO, where anintis 2 bytes. - Work out what number reaches
setColour()when you type1. (The character'0'has code 48.) - Find all four mistakes and fix them. Upload and test keys
0to4.
It works if keys 0–3 show off, red, green and blue with the right name printed, and key 4 does nothing.
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.
Extra Mission 5 min
Part 1 — Design a table-driven gadget
A lookup table lets one small piece of code do many different things. Design a gadget whose behaviour lives in a table. Ideas: a doorbell with a choice of tunes, a mood light with eight presets, a race start light with different countdowns.
Your design must include:
- The gadget's name and its one job.
- The table written out in full: every row, with a comment saying what it is.
- How the user picks a row (Serial key, button presses or a pot).
- What happens if they pick a row that does not exist.
Part 2 — Make it
Build it on the UNO with LEDs, an RGB LED or a buzzer. All the behaviour must come from the table. Use sizeof to count the rows, and check the bounds. Then add one extra row and show that nothing else in the code had to change.
Bring back next class: your design, your uploaded sketch, and a Serial Monitor screenshot showing every row being picked. Also bring a sensor and the SSD1306 OLED for L03-40.