Learning Goals 5 min
Cluster D ends with both protocols on the same board: an OLED on I²C showing numbers and labels, a 74HC595 + LED bar on SPI showing a quick at-a-glance level. Two protocols, one project, no pin conflicts. By the end of this lesson you will:
- Wire an SSD1306 OLED (I²C on A4/A5) and a 74HC595 + 8 LED bar (SPI on D11/D13 + D10 latch) to the same UNO, sharing nothing but power and ground.
- Build a finished "dashboard" sketch where one sensor (your choice — pot, LDR, temperature) drives both displays simultaneously: precise number on the OLED, coarse bar on the LEDs.
- Add a second sensor / second visualisation on the same dashboard, demonstrating that you can run multiple peripherals on multiple buses without them interfering with each other.
Warm-Up 10 min
Time to combine the last two lessons' rigs on a single breadboard. Lay out:
- OLED — A4 (SDA), A5 (SCL), 5V, GND.
- 74HC595 — D11 (data), D13 (clock), D10 (latch), 5V, GND, plus 8 LEDs with 220 Ω resistors.
- One sensor on A0 (a pot to start; swap for an LDR or TMP36 later).
Pin map sanity check
Three separate "territories" on the Arduino:
| Pins | Purpose |
|---|---|
| A4, A5 | I²C bus → OLED |
| D11, D12, D13 | SPI bus → 74HC595 (we don't use CIPO/D12 here, but it's reserved by the SPI peripheral) |
| D10 | 74HC595 latch (acts as "CS" even though the chip doesn't have a real CS pin) |
| A0 | Sensor input |
Nothing overlaps. Once you've wired the build, run yesterday's I²C scanner — confirm the OLED still appears at 0x3C. Then load yesterday's shift-register counter and confirm the LEDs still cycle. Both must work in isolation before you combine the code.
New Concept · Two buses, one loop 20 min
Why this works — the two protocols don't share resources
I²C lives on A4 / A5 with the Wire peripheral. SPI lives on D11–D13 with the SPI peripheral. They're completely separate hardware blocks inside the AVR. You can "SPI.transfer()" and "Wire.write()" in the same loop without any worry about collisions.
The dashboard pattern
One reading drives multiple outputs. We'll structure the sketch as:
- Read the sensor (or sensors).
- Compute derived values (smoothed value, bar level, threshold flags).
- Drive each output in turn: OLED redraw, then LED bar update.
- Sleep for the frame interval (~50 ms = 20 fps is smooth and gentle).
What goes on each display
| Output | What it shows | Why it's the right tool |
|---|---|---|
| OLED | Exact number, units, header label, second-line trend | Precision, text. Slow to read but rich. |
| LED bar | Coarse level (0–8 LEDs lit) | Glanceable, visible across a room. No detail. |
This is exactly how dashboards in cars, aeroplanes and industrial control rooms are designed: precise gauges for the operator to read; warning lights and bars for "at-a-glance". We're building a tiny version of the same idea.
Avoiding flicker
The OLED is the slower output — its display() push takes ~20 ms. The LED bar is essentially instant. Update the OLED last so the flicker (if any) is at the slow output, not on the LEDs. In practice with a 50 ms loop, neither flickers visibly.
Worked Example · The full dashboard 30 min
Step 1 — wire everything as in §1
OLED on I²C (A4/A5), 74HC595 + 8 LEDs on SPI (D10/D11/D13), pot wiper on A0. Common 5V and GND between all devices.
Step 2 — the sketch
// L03-22 · Multi-display dashboard
// Sensor on A0 (pot/LDR/TMP36) -> OLED (precise number) + 8-LED bar (coarse level)
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SENSOR_PIN = A0;
const int LATCH_PIN = 10;
const int SCREEN_W = 128;
const int SCREEN_H = 64;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);
// --- smoothing ---
const int WINDOW = 8;
int buf[WINDOW];
int bufIndex = 0;
long bufSum = 0;
int smoothed(int newValue) {
bufSum -= buf[bufIndex];
buf[bufIndex] = newValue;
bufSum += newValue;
bufIndex = (bufIndex + 1) % WINDOW;
return bufSum / WINDOW;
}
void writeBar(byte pattern) {
digitalWrite(LATCH_PIN, LOW);
SPI.transfer(pattern);
digitalWrite(LATCH_PIN, HIGH);
}
byte barPattern(int lit) {
byte p = 0;
for (int i = 0; i < lit; i++) p |= (1 << i);
return p;
}
void setup() {
Serial.begin(9600);
pinMode(LATCH_PIN, OUTPUT);
SPI.begin();
Wire.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("# OLED begin failed");
while (true) ;
}
display.setTextColor(SSD1306_WHITE);
// pre-fill smoothing buffer
int first = analogRead(SENSOR_PIN);
for (int i = 0; i < WINDOW; i++) buf[i] = first;
bufSum = (long)first * WINDOW;
writeBar(0);
}
void loop() {
int raw = analogRead(SENSOR_PIN);
int value = smoothed(raw);
int leds = map(value, 0, 1023, 0, 8); // 0..8 LEDs lit
int pct = map(value, 0, 1023, 0, 100); // 0..100%
// 1. LED bar (instant)
writeBar(barPattern(leds));
// 2. OLED (~20 ms)
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("LEVEL");
display.setTextSize(3);
display.setCursor(0, 14);
display.print(pct);
display.print("%");
display.drawRect(0, 50, 128, 12, SSD1306_WHITE);
display.fillRect(2, 52, map(value, 0, 1023, 0, 124), 8, SSD1306_WHITE);
display.display();
delay(50);
}Step 3 — upload and test
Turn the pot. The OLED's number changes smoothly from 0% to 100%, the bottom bar fills up, and the LEDs light up 0–8 segments in step. All three respond to the same sensor reading. No flicker, no lag.
Step 4 — add a second sensor
Wire a second pot (or an LDR voltage divider) to A1. Read it as a "trend" reading and display the comparison:
int trendRaw = analogRead(A1);
int trend = map(trendRaw, 0, 1023, 0, 100);
int diff = pct - trend;
const char* dir = diff > 5 ? "^" : diff < -5 ? "v" : "-";
// Add to the OLED block:
display.setTextSize(1);
display.setCursor(0, 42);
display.print("trend ");
display.print(trend);
display.print("% ");
display.print(dir);Now you have a two-sensor dashboard: the LEDs show one reading at a glance, the OLED shows both with a trend arrow comparing them.
Step 5 — add a danger flag
Light a 9th LED (Q7 of the shift register, already wired) only when the value is above a threshold:
byte pattern = barPattern(leds);
if (pct > 80) pattern |= 0b10000000; // also light bit 7 (top LED) for danger
writeBar(pattern);That dedicates one LED as the "warning light" — visually distinct from the level bar even though they share the same chip. Standard dashboard idiom.
Step 6 — speed comparison
Time one full loop iteration:
unsigned long t0 = micros();
// ... entire loop body ...
unsigned long elapsed = micros() - t0;
Serial.print("loop took "); Serial.print(elapsed); Serial.println(" us");Typical: ~20–30 ms per iteration, dominated by the OLED's display() push. The SPI bar write takes microseconds. This is why you spread work across the loop — OLED can't refresh faster than ~50 fps, but the LEDs can refresh thousands of times per second.
Try It Yourself 15 min
Goal: Replace the pot with a TMP36 temperature sensor (L02-13 wiring). Change the OLED label to "TEMP" and the unit to "°C". Adjust the map ranges for sensible thresholds (say 15 °C to 35 °C → 0 to 100% of dashboard scale).
Hint
int raw = analogRead(SENSOR_PIN);
float volts = raw * (5.0 / 1023.0);
float tempC = (volts - 0.5) * 100.0; // TMP36 formula
int pct = map((int)tempC, 15, 35, 0, 100);
pct = constrain(pct, 0, 100);
int leds = map(pct, 0, 100, 0, 8);Note the constrain after the map — without it, a temperature outside 15..35 °C produces out-of-range pct values that confuse the displays.
Goal: Add a button on D2 that switches between two display modes: "LEVEL" (current value, current bar) and "HISTORY" (last 30 seconds' min and max printed on the OLED, bar still shows current).
Hint
State: track minSeen and maxSeen as you go. Reset them when the user enters HISTORY mode (or every 30 s of HISTORY mode). The button toggles the mode; redraw the OLED based on the active mode.
Goal: Cascade a second 74HC595 (16 LEDs total) and display the value with twice the resolution on the bar (0–16 LEDs instead of 0–8). OLED unchanged.
Hint
Wire chip A's Q7' (pin 9) → chip B's DS (pin 14). Share latch/clock between both chips. Use writeShift16 from L03-21. Modify the bar pattern generation to produce a 16-bit value: uint16_t pattern = 0; for (int i = 0; i < leds; i++) pattern |= (1 << i); and send two bytes per transaction.
Mini-Challenge · Ship the dashboard 10 min
- Mount everything on a piece of cardboard or in a small box. OLED visible at the top, LED bar visible at the bottom, sensor probe sticking out.
- Label the LEDs with a printed scale "0 — 25 — 50 — 75 — 100".
- Add a one-time startup splash on the OLED: project name + "by [your name]" for 2 seconds, then the live display.
- Take photos of the finished build at low, mid, and high sensor readings.
Ship-ready test:
- Can a classmate read the value from across the room (LEDs) AND read the precise number up close (OLED)?
- Does the warning LED catch their eye when the threshold is crossed?
- Is the wiring tidy enough that they don't see "a breadboard mess"?
Cluster D is done. You can now talk to almost any chip — UART for radios, I²C for sensors and small displays, SPI for big displays and high-speed peripherals. Cluster E (Bluetooth Control) starts tomorrow with the HC-05 module — and you'll be using SoftwareSerial from L03-16 to talk to it.
Recap 5 min
Multi-protocol projects work because I²C and SPI live on entirely separate hardware peripherals — same loop, same sketch, no conflict. The dashboard pattern: one sensor reading, multiple display channels (precise OLED + glanceable LEDs). Update the slow display last so any flicker hides on the slow channel. Cluster D recap: UART for slow point-to-point, I²C for many slow devices, SPI for a few fast devices, shift registers for cheap pin expansion. Next cluster: putting wires away and going wireless with Bluetooth.
- Multi-bus project
- A project that uses two or more communication protocols on the same microcontroller. Common combination: I²C for sensors + SPI for displays + UART for a radio.
- Dashboard pattern
- One input feeding multiple outputs designed for different reading distances and reading speeds. Big-picture (LEDs) + detail (numbers) + warning (single bright indicator).
- At-a-glance vs precise
- The two reading modes a dashboard supports. At-a-glance = coarse, fast, no thinking; precise = exact value, slower, needs focus. Good dashboards have both.
- Warning light
- A dedicated indicator that lights only when a threshold is crossed. Stays off most of the time, draws the eye when active. Universal idiom.
- Splash screen
- A short branded display shown at power-up before the live UI. Confirms the device booted and gives a moment for hardware to stabilise. Standard polish for a "ship-ready" product.
- Slow output / fast output
- Different output channels run at different update rates. OLED at 20 fps; LEDs at 200+ fps. Design the loop so the slow output happens last and doesn't block the fast one.
- Sensor smoothing
- Running average (or low-pass filter) applied to raw analog readings so the display isn't twitchy. Same pattern from L02-08 and L03-04 — wins again here.
Homework 5 min
- Finish the dashboard. Photo + 30-second video of three different sensor levels.
- Save the sketch as
dashboard.ino. - Bring tomorrow: HC-05 Bluetooth module (the small blue PCB with a black square chip and 4 or 6 pins) + your phone with a Bluetooth-serial app installed (Android: "Serial Bluetooth Terminal", iOS: "BlueGate" or similar — iOS support for HC-05 is limited; Android is easier).
- Read ahead to ARD-L03-23 (HC-05 Bluetooth Classic).
Bring back next class:
- Finished dashboard.
- HC-05 module.
- Phone with Bluetooth-serial app installed.