Learning Goals 5 min
Parallel arrays — one for names, one for values, one for thresholds — drift out of sync the moment your project grows. A struct bundles related values into one named record. By the end of this lesson you will:
- Declare a
structwith named fields and create instances of it. - Access fields with the dot operator and pass structs to functions (by value or by reference).
- Store an array of structs to model a list of sensors, motors, or other repeating entities — the bridge to classes (tomorrow).
Warm-Up 10 min
You've already used a few structs without knowing: Motor in motor.h from L03-08 / L03-09, the LampState in the smart-lamp project, and ShiftReg in shift595.h. Today we formalise what they are and use them more deliberately.
The problem they solve
Imagine you're tracking three sensors: name, pin, last reading, threshold, alarm-fired flag. With parallel arrays:
const char* NAMES[] = { "Light", "Temp", "Moisture" };
const int PINS[] = { A0, A1, A2 };
int readings[] = { 0, 0, 0 };
int thresholds[] = { 300, 500, 200 };
bool firedFlags[] = { false, false, false };Five arrays. Add a new field → add a 6th array. Add a new sensor → add a row to all 5 arrays. Easy to miss one. Easy to swap indices accidentally.
With a struct:
struct Sensor {
const char* name;
int pin;
int reading;
int threshold;
bool fired;
};
Sensor sensors[] = {
{ "Light", A0, 0, 300, false },
{ "Temp", A1, 0, 500, false },
{ "Moisture", A2, 0, 200, false },
};One source of truth per sensor. Add a sensor: one new line. Add a field: one new field in the struct + initialise it in each row. The dispatch / logging code stays the same.
New Concept · Declaring and using structs 25 min
Declaration
struct Point {
int x;
int y;
};Defines a new type called Point with two named fields, both int. The semicolon at the end matters in C++.
Creating an instance
Point a; // uninitialised
Point b = { 10, 20 }; // both fields initialised by position
Point c = { .x = 5 }; // designated initializer (C99 / C++20)
a.x = 3; // assign after creation
a.y = 4;Accessing fields
Serial.println(b.x); // 10
Serial.println(b.y); // 20
b.x += 1; // mutate
Serial.println(b.x); // 11Passing to functions
// by value -- the function gets a copy, original is unchanged
void describe(Point p) {
Serial.print("Point at ");
Serial.print(p.x);
Serial.print(",");
Serial.println(p.y);
}
// by reference -- the function can modify the caller's struct
void translate(Point& p, int dx, int dy) {
p.x += dx;
p.y += dy;
}
// const reference -- read-only access, no copy (best for big structs)
void log(const Point& p) {
Serial.println(p.x);
}For tiny structs (2–3 fields) by value is fine. For bigger ones use const Point& to avoid copying.
Arrays of structs
struct LED {
int pin;
int brightness;
};
LED leds[] = {
{ 9, 0 },
{ 10, 50 },
{ 11, 100 },
};
const int N_LEDS = sizeof(leds) / sizeof(leds[0]);
void setup() {
for (int i = 0; i < N_LEDS; i++) {
pinMode(leds[i].pin, OUTPUT);
analogWrite(leds[i].pin, leds[i].brightness);
}
}One loop handles all LEDs, regardless of how many you add. Each struct carries its own data; the iteration code is generic.
Structs containing structs
struct Range {
int lo;
int hi;
};
struct Sensor {
const char* name;
int pin;
Range okBand;
Range warningBand;
};
Sensor s = { "Light", A0, {200, 800}, {100, 900} };
Serial.println(s.okBand.lo); // 200Composition. The Range idea is reusable across sensors.
Returning structs from functions
struct Reading {
int raw;
float volts;
float celsius;
};
Reading readTMP36(int pin) {
Reading r;
r.raw = analogRead(pin);
r.volts = r.raw * (5.0 / 1023.0);
r.celsius = (r.volts - 0.5) * 100.0;
return r;
}
// caller:
Reading r = readTMP36(A0);
Serial.print(r.celsius);
Serial.println(" C");A function that "returns three numbers" without going through global variables or out-parameters. The reading + its derived values travel as one record.
Worked Example · Multi-sensor dashboard 25 min
Step 1 — wire
Three analog inputs. If you only have a UNO/ESP8266 with one ADC, use the pot on A0 and pretend it's three different sensors over time. For a richer demo, use an ESP32 with multiple ADC pins.
Step 2 — the struct-driven sketch
// L03-40 · Multi-sensor dashboard with structs
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
struct Sensor {
const char* name;
int pin;
int raw;
int threshold; // alarm above this
bool alarmActive;
};
Sensor sensors[] = {
{ "Light", A0, 0, 800, false },
{ "Temp", A1, 0, 600, false },
{ "Loud", A2, 0, 900, false },
};
const int N_SENSORS = sizeof(sensors) / sizeof(sensors[0]);
void readAll() {
for (int i = 0; i < N_SENSORS; i++) {
sensors[i].raw = analogRead(sensors[i].pin);
sensors[i].alarmActive = sensors[i].raw > sensors[i].threshold;
}
}
void drawAll() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
for (int i = 0; i < N_SENSORS; i++) {
int y = i * 20;
display.setCursor(0, y);
display.print(sensors[i].name);
display.print(": ");
display.print(sensors[i].raw);
if (sensors[i].alarmActive) display.print(" !");
// small bar
int barW = map(sensors[i].raw, 0, 1023, 0, 100);
display.drawRect(0, y + 10, 100, 6, SSD1306_WHITE);
display.fillRect(1, y + 11, barW, 4, SSD1306_WHITE);
}
display.display();
}
void setup() {
Serial.begin(9600);
for (int i = 0; i < N_SENSORS; i++) {
pinMode(sensors[i].pin, INPUT);
}
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
}
void loop() {
readAll();
drawAll();
delay(100);
}Step 3 — observe
Three rows on the OLED. Each row: name, raw value, alarm indicator, mini bar gauge. The readAll() and drawAll() functions don't know how many sensors there are — they loop through the array. Add a 4th sensor by adding one row to the initialiser and a 4th pin.
Step 4 — add a helper that takes a struct ref
void describe(const Sensor& s) {
Serial.print(s.name);
Serial.print(" = ");
Serial.print(s.raw);
if (s.alarmActive) Serial.print(" [ALARM]");
Serial.println();
}
// in loop:
for (int i = 0; i < N_SENSORS; i++) describe(sensors[i]);The helper receives one sensor at a time and prints it. No global access; works with any Sensor. Composable.
Step 5 — return a struct from a sensor reader
struct Stats { int min;
int max;
float avg;
};
Stats summarise() {
Stats s = { 1023, 0, 0.0 };
long sum = 0;
for (int i = 0; i < N_SENSORS; i++) {
int r = sensors[i].raw;
if (r < s.min) s.min = r;
if (r > s.max) s.max = r;
sum += r;
}
s.avg = (float)sum / N_SENSORS;
return s;
}
// in loop:
Stats st = summarise();
Serial.print("min=");
Serial.print(st.min);
Serial.print(" max=");
Serial.print(st.max);
Serial.print(" avg=");
Serial.println(st.avg);One function, three return values. Clean.
Basic 5 min
Goal: Add a units field to Sensor (e.g. "lux", "°C", "dB"). Display it on the OLED next to the value.
Challenge 1 5 min
Goal: Add an "average over last 10 readings" field. Each sensor maintains its own running window — the running average from L02-08 lives inside the struct.
Challenge 2 5 min
Goal: Add a void (*onAlarm)(const Sensor&) function pointer field. Each sensor specifies what to do when its alarm fires. Light sensor fires a buzzer; temp sensor sends an HTTP POST; moisture sensor lights a particular LED.
Challenge 3 · Parallel arrays to a struct 10 min
This sketch runs three button-and-LED stations. Each press toggles that station's LED and prints a count. It works, but each station is spread across five arrays.
const char* NAMES[] = {"Red", "Green", "Blue"};
const int BUTTON_PINS[] = {2, 3, 4};
const int LED_PINS[] = {8, 9, 10};
int presses[] = {0, 0, 0};
int lastState[] = {HIGH, HIGH, HIGH};
const int N = 3;
void setup() {
Serial.begin(9600);
for (int i = 0; i < N; i++) {
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
pinMode(LED_PINS[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < N; i++) {
int now = digitalRead(BUTTON_PINS[i]);
if (now == LOW && lastState[i] == HIGH) {
presses[i]++;
digitalWrite(LED_PINS[i], presses[i] % 2);
Serial.print(NAMES[i]);
Serial.print(" pressed ");
Serial.print(presses[i]);
Serial.println(" times");
}
lastState[i] = now;
}
delay(10);
}- Write a
struct Stationthat holds all five pieces of data for one station. - Replace the five arrays with one array of
Station. Count it withsizeof. - Move the press handling into
void update(Station& s). - Add a fourth station, "Yellow", with its button on D5 and its LED on D11.
It works if the Serial output looks the same as before, and adding Yellow took exactly one new line.
Recap 5 min
A struct is a named bundle of fields — the natural shape of "a sensor", "a motor", "a wifi config". Replace parallel arrays with arrays of structs the moment the data has > 2 fields. Pass by const reference for big structs. Function pointer fields give you data-driven dispatch. Tomorrow we add functions inside the struct definition — and a struct with functions is just a class.
- struct
- A C / C++ keyword that bundles related fields into one named type. Same as a C++
classwith all members public. - Field / member
- A named value inside a struct. Accessed with the dot operator.
- Instance
- A specific variable of a struct type.
Sensor s;creates an instance. - Designated initializer
- Initialising fields by name:
{ .x = 5, .y = 10 }. C99 / C++20. - Pass by value / reference
- By value = a copy. By reference (
T&) = caller's storage.const T&= read-only reference (no copy, no mutation). - Array of structs
- The cleanest way to represent "a list of similar entities" each with its own data.
- Function pointer field
- A struct field that holds a function pointer. Each instance can have its own action. The bridge to virtual methods.
- Composition
- One struct containing another. Reusable parts (Range, RGB, Timer) used inside richer structs (Sensor, Motor, Effect).
Extra Mission 5 min
Part 1 — Design a gadget with many similar parts
Structs shine when a device has several parts that are the same shape. Design one. Ideas: a plant station with three pots, a quiz buzzer box for four players, a car park with four bays that each show free or taken.
Your design must include:
- The gadget's name and its one job.
- The
structwritten out, with every field, its type and what it means. - The array of structs filled in for every part, with real pin numbers.
- One helper function that takes a
constreference to your struct, and what it does.
Part 2 — Make it
Build your design on the UNO. Use one struct array, loops that do not know how many parts there are, and your helper function. Show the readings or states on the OLED or in the Serial Monitor.
Bring back next class: your design, your uploaded sketch, and a photo of the circuit with the display showing every part. Keep the multi-sensor wiring up for L03-41.