Learning Goals 5 min
L2 ended with a 16×2 character LCD. Today we upgrade to a 128×64 graphical display — a tiny black OLED panel that costs less than a coffee and shows text, lines, rectangles, even animations. Two wires (plus power) carry the whole bitmap. By the end of this lesson you will be able to:
- Install the Adafruit_SSD1306 and Adafruit_GFX libraries via Library Manager and connect the OLED to A4 / A5.
- Initialise the display, clear it, print text at multiple sizes, and draw lines, rectangles and circles using
Adafruit_GFXmethods. - Build a small "sensor dashboard": a header label plus a live value, refreshed several times per second without flicker, using
display.clearDisplay()followed bydisplay.display().
Warm-Up 10 min
Pull out the OLED — typically a small bare board with a 0.96" black panel labelled "OLED". Four pins on the back: VCC, GND, SCL, SDA (order varies by board).
Wire it
| OLED pin | UNO pin |
|---|---|
| VCC | 5V (most modules accept 5V — verify on yours; some are 3.3V-only) |
| GND | GND |
| SCL | A5 |
| SDA | A4 |
Sanity-check with yesterday's scanner
Upload i2c-scanner.ino from yesterday. The OLED should show up at 0x3C (or 0x3D if the solder jumper on the back has been moved). If the scanner shows nothing, the OLED isn't wired right — fix it before continuing.
Install the libraries
Sketch → Include Library → Manage Libraries → search for "Adafruit SSD1306". Install. The Library Manager will also offer "Adafruit GFX Library" as a dependency — install both. You only need to do this once per machine.
New Concept · GFX + SSD1306 layered libraries 25 min
Two libraries, one display
Adafruit_SSD1306 knows how to talk to this specific chip: init sequence, framebuffer layout, "tell the OLED to refresh". Adafruit_GFX knows about graphics: text, lines, rectangles, circles. It calls into the chip-specific library through a small set of low-level "set this pixel" primitives. Same GFX library powers the SH1106, ILI9341, ST7735 — change the chip-specific library and your drawing code is unchanged.
The boilerplate
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SCREEN_WIDTH = 128;
const int SCREEN_HEIGHT = 64;
const int OLED_RESET = -1; // no reset pin on most modules
const uint8_t OLED_ADDR = 0x3C;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("# OLED begin failed");
while (true) ;
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
}Three things to highlight:
SSD1306_SWITCHCAPVCCtells the library to enable the OLED's built-in charge pump (generates the high voltage the OLED pixels need from your 5 V supply). Pretty much always the right value.display.begin()returnsfalseif it can't talk to the OLED — usually means the wrong address or a wiring fault. Check the return.SSD1306_WHITEis the "on" colour (it's really just "pixel on"; the panel is white-on-black or yellow-on-black depending on model).
The five GFX calls you'll use 90% of the time
| Call | What it does |
|---|---|
display.clearDisplay() | Empties the framebuffer (memory only — nothing visible yet). |
display.setCursor(x, y) | Sets where the next print / println will draw text. |
display.setTextSize(n) | 1× = 6×8 px font; 2× = 12×16; etc. Just scaling — no smoother glyphs. |
display.print(...) / display.println(...) | Same API as Serial, draws to the framebuffer. |
display.display() | Pushes the framebuffer to the OLED. Nothing changes on screen until you call this. |
The framebuffer pattern
Every change to what's shown follows the same three steps:
- Optionally
clearDisplay()to start from blank. - Set up your text / drawing calls.
- Call
display()to push everything to the screen.
This means you can write 50 calls and they all hit the screen at once on the display() — no flicker. The opposite (one drawing call → immediate update) is what a character LCD does, and is why LCD output sometimes flickers when updating.
Coordinates
Origin (0, 0) is the top-left. X increases right; Y increases down. For a 128×64 display, valid coordinates are X = 0..127, Y = 0..63.
Worked Example · Hello world to live sensor dashboard 25 min
Step 1 — "Hello, world!"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("# OLED begin failed");
while (true) ;
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(0, 0);
display.println("Hello,");
display.println("world!");
display.display();
}
void loop() { }Upload. The OLED lights up with two lines of double-size text. If you see nothing, check yesterday's scanner: was the address really 0x3C?
Step 2 — draw some shapes
display.clearDisplay();
display.drawRect(0, 0, 128, 64, SSD1306_WHITE); // frame around the screen
display.drawLine(0, 0, 127, 63, SSD1306_WHITE); // diagonal
display.drawCircle(96, 32, 20, SSD1306_WHITE); // circle near the right
display.fillRect(8, 50, 30, 10, SSD1306_WHITE); // solid rectangle
display.setCursor(8, 8);
display.setTextSize(1);
display.print("Shapes!");
display.display();Replace the body of setup() with the above. You should see a frame, a diagonal line, a circle, a filled rectangle, and the word "Shapes!" — all on the same screen. Try changing the coordinates and see what moves.
Step 3 — a live dashboard with a pot-driven number
// L03-19 · Sensor dashboard — header + live value from A0
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("# OLED begin failed");
while (true) ;
}
display.setTextColor(SSD1306_WHITE);
}
void loop() {
int value = analogRead(A0);
display.clearDisplay();
// Header
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Pot reading");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
// Big number
display.setTextSize(3);
display.setCursor(16, 20);
display.print(value);
// Bar across the bottom showing the value 0..1023 -> 0..120 px
int barW = map(value, 0, 1023, 0, 120);
display.drawRect(4, 56, 122, 6, SSD1306_WHITE);
display.fillRect(5, 57, barW, 4, SSD1306_WHITE);
display.display();
delay(50); // ~20 fps update rate
}Upload, turn the pot. You should see the number change live (0..1023) and the bar fill / empty in time. No flicker because every refresh wipes the framebuffer and rebuilds, then commits once with display().
Step 4 — try too-fast a refresh
Drop delay(50) to delay(5). You're now refreshing 200× per second. The pot reading is still smooth; the OLED might flicker slightly. Push too hard and the screen looks "dim" because the pixels never have time to settle. 30–60 fps is the sweet spot.
Step 5 — partial updates without clear
Sometimes you don't want to redraw everything. For ticker-tape-style updates, skip clearDisplay() and just overwrite the changing region — but you'll need to draw a black rectangle over the old text first:
display.fillRect(16, 20, 96, 24, SSD1306_BLACK); // wipe the number area
display.setCursor(16, 20);
display.setTextSize(3);
display.print(value);
display.display();For a 3-digit number that fits in a known area, this avoids redrawing the header on every update — saves a couple of milliseconds per frame.
Try It Yourself 15 min
Goal: Replace the "Pot reading" header with "Light" and the pot input with an LDR voltage divider on A0 (re-use L01-36 wiring).
Hint
Wiring change only — sketch is the same. The LDR's 0..1023 raw reading still maps to 0..120 pixels for the bar.
Goal: Two readings on one screen: pot value on top half, an HC-SR04 distance reading on the bottom half. Use the L02-23 distance helper for the second value.
Hint
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("POT ");
display.setTextSize(2);
display.setCursor(0, 12);
display.print(pot);
display.drawLine(0, 30, 127, 30, SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 36);
display.print("DIST ");
display.setTextSize(2);
display.setCursor(0, 48);
display.print(distCm, 1);
display.print(" cm");
display.display();Use display.print(distCm, 1) to print a float with 1 decimal place. The library's API matches Serial here.
Goal: A scrolling line graph: each loop iteration appends a new pixel for the current sensor value at the right edge, then shifts the whole graph one pixel left. Looks like an oscilloscope.
Hint
Maintain an array of last-N values; each loop, shift them left, drop the oldest, append the new one; redraw all N as a polyline. Or for a memory-cheaper version, shift the screen buffer left one pixel — Adafruit_GFX doesn't expose that directly, so the array approach is cleaner.
const int W = 120;
int history[W];
// each iteration:
for (int i = 0; i < W - 1; i++) history[i] = history[i + 1];
history[W - 1] = map(analogRead(A0), 0, 1023, 60, 12); // y coord
display.clearDisplay();
for (int i = 0; i < W - 1; i++) {
display.drawLine(4 + i, history[i], 4 + i + 1, history[i + 1], SSD1306_WHITE);
}
display.display();Mini-Challenge · Ship a dashboard 10 min
- Wire ONE real sensor (temperature, distance, light, sound — your pick).
- Display it on the OLED in three sizes simultaneously: a small label, a big numeric readout, and a graphical bar/gauge.
- Add a startup splash: 1.5 seconds of "[your name]'s sensor" centred on the screen, before the live display starts.
- Photograph the working dashboard.
Ship-ready test: can a classmate read off the current sensor value from across the room? Big text + a high-contrast layout makes this possible. If they have to lean in, your numbers are too small or your bar is too short.
This is the template for the L04 IoT Room Monitor — same OLED layout, with the data pulled from a cloud sync rather than a local sensor.
Recap 5 min
The SSD1306 is the classroom OLED standard — 128×64 monochrome, I²C, <$5. Adafruit_SSD1306 talks to the chip; Adafruit_GFX provides the drawing primitives. Boilerplate is 10 lines; everything else is GFX calls. The framebuffer pattern (clearDisplay → draw → display) prevents flicker by committing all changes in one push. RAM cost: 1 KB out of the UNO's 2 KB total — watch the "Global variables" report on compile. Tomorrow we leave I²C behind and meet its faster, simpler cousin: SPI.
- SSD1306
- The OLED controller chip on most cheap monochrome OLED breakouts. 128×64 is the common size; 128×32 also exists.
- OLED (organic LED)
- A display technology where each pixel is its own light source. No backlight needed, deep blacks, low power for sparse images.
- Adafruit_GFX
- A generic graphics primitive library: text, lines, rectangles, circles, bitmaps. Used by all Adafruit display libraries — write once, switch displays freely.
- Adafruit_SSD1306
- The SSD1306-specific layer that handles the chip's init sequence and framebuffer push.
- Framebuffer
- An in-memory pixel array that mirrors the screen. You draw into the framebuffer;
display()sends it to the panel in one transaction. display.display()- The function that actually pushes the framebuffer to the OLED. Without it, nothing visible changes.
- Charge pump (
SSD1306_SWITCHCAPVCC) - The OLED's built-in voltage booster that generates the high voltage the panel needs from your low-voltage supply. Enable in
begin()on all modules without a separate high-voltage rail. - Frame rate
- Updates per second. 30–60 fps is smooth; 5 fps feels laggy; 200 fps wastes CPU and may dim the panel.
Homework 5 min
- Save the sensor dashboard sketch as
oled-dashboard.ino. - Take a photo of your dashboard displaying a live reading.
- Read ahead to ARD-L03-20 (SPI Protocol Concepts). Tomorrow we meet a different — faster, four-wire — chip-to-chip bus.
- Bring tomorrow: a 74HC595 shift register (small 16-pin DIP IC, in most kits) and 8 LEDs. We'll wire SPI in L03-21.
Bring back next class:
- Working dashboard photo.
- 74HC595 + 8 LEDs for the L03-21 build.