Project 4.2: "0.91" OLED Display
What you’ll learn
- Goal 1: Initialize a 0.91″ 128×32 OLED over I2C and draw your first pixels.
- Goal 2: Render static and dynamic text, keeping it readable and within bounds.
- Goal 3: Draw basic graphics (lines, rectangles) and understand screen coordinates.
- Goal 4: Show live sensor data (ADC as a demo) with clean formatting.
- Goal 5: Build a simple, friendly user interface with pages and navigation.
Key ideas
- I2C OLED: Uses the SSD1306 driver: width 128, height 32 pixels.
- Coordinates: Origin at top‑left (0,0); x increases right, y increases down.
- Text cells: Default font is 8×8; plan positions to avoid clipping.
- Refresh: Call show() after drawing; clear with fill(0) before new frames.
Blocks glossary
- SoftI2C: Software I2C bus for OLED (SCL, SDA, freq).
- SSD1306_I2C: Official driver class for 128×32 OLED displays.
- fill(0/1): Clear screen (0) or fill white (1).
- pixel(x, y, c): Draw a single pixel at x,y with color c (0/1).
- text(str, x, y): Draw 8×8 characters with top‑left at x,y.
- line(x1, y1, x2, y2, c): Draw a straight line.
- rect(x, y, w, h, c): Outline rectangle; fill_rect(…) for solid.
- show(): Push the buffer to the display (must call to see changes).
- ADC: Analog read for live sensor demo values.
What you need
| Part | How many? | Pin connection (R32) |
|---|---|---|
| D1 R32 | 1 | USB cable |
| OLED 0.91″ 128×32 (SSD1306 I2C) | 1 | SCL → Pin 26, SDA → Pin 5, VCC 3.3–5V, GND |
| Wires | 4 | Match pins carefully |
| Optional sensor (ADC demo) | 1 | Joystick X → Pin 2 |
- Ensure the OLED address is 0x3C (most common). If not, check module docs.
- Share GND between the R32 and OLED.
Before you start
- Connect the OLED to SCL 26 and SDA 5, power it, and open the serial monitor.
- Quick test:
print("Ready!") # Confirm serial is working
Microprojects 1–5
Microproject 4.2.1 – OLED initialization
# Microproject 4.2.1 – Initialize OLED 0.91" (128×32) and draw a pixel
import machine # Load hardware pin/I2C/ADC library
import ssd1306 # Load SSD1306 OLED driver
import time # Load time library for delays
i2c = machine.SoftI2C( # Create a software I2C bus
scl = machine.Pin(26), # SCL (clock) on pin 26
sda = machine.Pin(5), # SDA (data) on pin 5
freq = 400000 # I2C frequency 400 kHz for snappy updates
) # End I2C setup
oled = ssd1306.SSD1306_I2C( # Create an OLED display object
128, # Width in pixels (128)
32, # Height in pixels (32)
i2c, # The I2C bus object
addr = 0x3C # Typical I2C address for SSD1306 modules
) # End OLED object creation
oled.fill(0) # Clear the screen (fill with black)
oled.pixel(10, 10, 1) # Draw one white pixel at (10,10)
oled.show() # Push the buffer to the display to see the pixel
print("[OLED] Init OK, single pixel at (10,10)") # Serial: confirm initialization
time.sleep_ms(800) # Small delay so students can observe
Reflection: You brought the screen to life and confirmed you can draw.
Challenge: Draw three pixels in a diagonal (e.g., (5,5), (6,6), (7,7)).
Microproject 4.2.2 – Static and dynamic text
# Microproject 4.2.2 – Static and dynamic text with 8×8 font
import machine # Load hardware library
import ssd1306 # Load OLED driver
import time # Load time library
i2c = machine.SoftI2C( # Create I2C bus for OLED
scl = machine.Pin(26), # SCL pin
sda = machine.Pin(5), # SDA pin
freq = 400000 # I2C frequency
) # End I2C setup
oled = ssd1306.SSD1306_I2C( # Create OLED display object
128, # Display width
32, # Display height
i2c, # I2C bus
addr = 0x3C # OLED address
) # End OLED object
counter = 0 # Initialize a simple dynamic counter
def frame_static(): # Helper: draw a static header
oled.fill(0) # Clear the screen
oled.text("Clu-Bots", 0, 0) # Draw text at x=0, y=0 (top-left)
oled.text("OLED Demo", 0, 8) # Draw second line at y=8
oled.show() # Refresh display with current buffer
print("[OLED] Static frame drawn") # Serial: log static frame
def frame_dynamic(n): # Helper: draw a dynamic value
oled.fill(0) # Clear the screen
oled.text("Counter:", 0, 0) # Label for the dynamic value
oled.text(str(n), 0, 16) # Show the counter at line y=16
oled.show() # Refresh to show the new value
print("[OLED] Dynamic frame, n=", n) # Serial: log dynamic update
frame_static() # Show the static frame once
while True: # Loop: update dynamic text
frame_dynamic(counter) # Draw the current counter value
counter = counter + 1 # Increment the counter by 1
time.sleep_ms(500) # Wait half a second between updates
Reflection: You rendered readable text and updated values without clutter.
Challenge: Add a third line “Ticks=” showing the last 4 digits of time.ticks_ms().
Microproject 4.2.3 – Basic graphics (lines and rectangles)
# Microproject 4.2.3 – Lines and rectangles to build simple UI frames
import machine # Load hardware library
import ssd1306 # Load OLED driver
import time # Load time library
i2c = machine.SoftI2C( # Create I2C bus for OLED
scl = machine.Pin(26), # SCL pin
sda = machine.Pin(5), # SDA pin
freq = 400000 # I2C speed
) # End I2C
oled = ssd1306.SSD1306_I2C( # Create OLED display object
128, # Width
32, # Height
i2c, # I2C bus
addr = 0x3C # Address
) # End OLED object
def draw_frame(): # Helper: draw a top bar and box
oled.fill(0) # Clear screen before drawing
oled.rect(0, 0, 128, 32, 1) # Outline the full display area
oled.fill_rect(0, 0, 128, 10, 1) # Fill a top bar (height 10)
oled.text("Status", 2, 1) # Inverted text area trick (still drawn white)
oled.show() # Push buffer to display
print("[OLED] Frame with top bar") # Serial: log drawing
def draw_lines(): # Helper: draw diagonal and guide
oled.fill(0) # Clear screen
oled.line(0, 31, 127, 0, 1) # Diagonal line from bottom-left to top-right
oled.line(0, 16, 127, 16, 1) # Horizontal midline across the display
oled.show() # Refresh the display
print("[OLED] Lines drawn") # Serial: log lines
draw_frame() # Show the UI frame
time.sleep_ms(800) # Wait a moment to observe
draw_lines() # Show line demo
time.sleep_ms(800) # Wait to observe
Reflection: Simple shapes help structure the screen and guide the eye.
Challenge: Add a small 20×10 rectangle at x=104,y=2 to act as a status icon area.
Microproject 4.2.4 – Display sensor data (ADC demo)
# Microproject 4.2.4 – Read ADC and display raw + percent like a tiny gauge
import machine # Load hardware (ADC/I2C) library
import ssd1306 # Load OLED driver
import time # Load time library
i2c = machine.SoftI2C( # Create I2C bus
scl = machine.Pin(26), # SCL pin
sda = machine.Pin(5), # SDA pin
freq = 400000 # I2C frequency
) # End I2C
oled = ssd1306.SSD1306_I2C( # Create OLED display
128, # Width
32, # Height
i2c, # I2C bus
addr = 0x3C # Address
) # End OLED object
adc2 = machine.ADC(machine.Pin(2)) # ADC on pin 2 (e.g., joystick X)
adc2.atten(machine.ADC.ATTN_11DB) # Full voltage range up to 3.3V
adc2.width(machine.ADC.WIDTH_12BIT) # 12-bit resolution (0–4095)
print("[ADC] Ready on Pin 2") # Serial: confirm ADC setup
def show_adc(raw): # Helper: draw raw and percent + bar
oled.fill(0) # Clear display for new frame
pct = int((raw * 100) / 4095) # Convert raw to percent 0–100
oled.text("ADC2:", 0, 0) # Label on top row
oled.text(str(raw), 48, 0) # Raw value next to label
oled.text("Pct:", 0, 16) # Percent label on second row
oled.text(str(pct) + "%", 40, 16) # Percent value
bar_w = int((pct * 120) / 100) # Map percent to bar width (max ~120 px)
oled.fill_rect(4, 26, bar_w, 4, 1) # Simple horizontal bar at bottom
oled.rect(4, 26, 120, 4, 1) # Bar outline for reference
oled.show() # Refresh to show results
print("[OLED] ADC raw=", raw, "pct=", pct) # Serial: log values
while True: # Live sensor loop
val = adc2.read() # Read raw ADC value
show_adc(val) # Draw the ADC frame
time.sleep_ms(300) # Refresh cadence ~3 frames per second
Reflection: Translating raw numbers into percent and a bar makes data easy to understand.
Challenge: Show volts as “V=0.00” using volts = raw*3.3/4095, placed at x=88,y=0.
Microproject 4.2.5 – Simple user interface (pages + navigation)
# Microproject 4.2.5 – Minimal UI pages with buttons for navigation
import machine # Load hardware pin/I2C library
import ssd1306 # Load OLED driver
import time # Load time library
i2c = machine.SoftI2C( # Create I2C bus
scl = machine.Pin(26), # SCL pin
sda = machine.Pin(5), # SDA pin
freq = 400000 # I2C frequency
) # End I2C
oled = ssd1306.SSD1306_I2C( # Create OLED display
128, # Width
32, # Height
i2c, # I2C bus
addr = 0x3C # Address
) # End OLED object
btn_next = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP) # Next page button on pin 27
btn_prev = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP) # Previous page button on pin 16
print("[UI] Buttons: NEXT=27, PREV=16") # Serial: confirm inputs
pages = ["HOME", "STATUS", "ADC"] # Page names for a simple UI
index = 0 # Current page index (start at HOME)
ticks = 0 # Simple heartbeat counter
def draw_home(): # Helper: render HOME page
oled.fill(0) # Clear screen
oled.text("HOME", 0, 0) # Title text
oled.text("Clu-Bots", 0, 16) # Subtitle text
oled.show() # Refresh display
print("[UI] HOME") # Serial: log page
def draw_status(t): # Helper: render STATUS page
oled.fill(0) # Clear screen
oled.text("STATUS", 0, 0) # Title
oled.text("Ticks:" + str(t), 0, 16) # Counter on second line
oled.show() # Refresh display
print("[UI] STATUS", t) # Serial: log page
def draw_adc(): # Helper: render ADC page
val = machine.ADC(machine.Pin(2)).read() # Read ADC2 quickly (simple demo)
oled.fill(0) # Clear screen
oled.text("ADC2", 0, 0) # Title
oled.text(str(val), 0, 16) # Raw value
oled.show() # Refresh display
print("[UI] ADC2", val) # Serial: log page
def show_page(): # Helper: select page to draw
name = pages[index] # Current page name
if name == "HOME": # If HOME page
draw_home() # Draw HOME
elif name == "STATUS": # If STATUS page
draw_status(ticks) # Draw STATUS with ticks
else: # Else ADC page
draw_adc() # Draw ADC simple view
while True: # UI loop: handle navigation
ticks = ticks + 1 # Increase heartbeat counter
show_page() # Render the current page
if btn_next.value() == 0: # If NEXT pressed (LOW)
index = (index + 1) % len(pages) # Advance to next page
time.sleep_ms(250) # Debounce to avoid multiple flips
if btn_prev.value() == 0: # If PREV pressed (LOW)
index = (index - 1) % len(pages) # Go to previous page (wrap around)
time.sleep_ms(250) # Debounce to avoid multiple flips
time.sleep_ms(400) # UI cadence for smooth updates
Reflection: A tiny UI makes the OLED feel useful, not just decorative.
Challenge: Add an outline frame on each page (rect(0,0,128,32,1)) and a small heartbeat pixel that toggles at (126,30).
Main project
0.91″ OLED display with text, graphics, sensor data, and a simple UI
- Initialization: SSD1306_I2C with width 128 and height 32 on SCL 26 and SDA 5.
- Text: 8×8 font, careful positioning to prevent clipping.
- Graphics: Lines and rectangles to structure information.
- Sensor: ADC demo rendered as raw, percent, and a simple bar.
- UI: Three pages (HOME, STATUS, ADC) with button navigation.
# Project 4.2 – Complete 0.91" OLED Display (text + graphics + sensor + UI)
import machine # Load hardware pin/I2C/ADC library
import ssd1306 # Load SSD1306 OLED driver
import time # Load time library
# I2C setup for OLED
i2c = machine.SoftI2C( # Create software I2C bus
scl = machine.Pin(26), # SCL pin on 26
sda = machine.Pin(5), # SDA pin on 5
freq = 400000 # 400 kHz I2C speed
) # End I2C setup
# OLED display object (128×32)
oled = ssd1306.SSD1306_I2C( # Create SSD1306 display object
128, # Width in pixels
32, # Height in pixels
i2c, # The I2C bus
addr = 0x3C # Common address for these modules
) # End OLED object creation
# Buttons for UI navigation
btn_next = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP) # Next page button
btn_prev = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP) # Previous page button
# ADC sensor (optional but included in project)
adc2 = machine.ADC(machine.Pin(2)) # ADC on pin 2 for sensor demo
adc2.atten(machine.ADC.ATTN_11DB) # Full voltage range to 3.3V
adc2.width(machine.ADC.WIDTH_12BIT) # 12-bit resolution (0–4095)
# UI state
pages = ["HOME", "STATUS", "ADC"] # Page list
index = 0 # Current page index
ticks = 0 # Heartbeat counter
def draw_home(): # Helper: render HOME page
oled.fill(0) # Clear screen
oled.rect(0, 0, 128, 32, 1) # Frame outline for consistency
oled.text("HOME", 2, 2) # Title at small inset (x=2,y=2)
oled.text("Clu-Bots", 2, 18) # Subtitle on second line
oled.show() # Refresh display
print("[Main] HOME") # Serial: log page
def draw_status(t): # Helper: render STATUS page
oled.fill(0) # Clear screen
oled.rect(0, 0, 128, 32, 1) # Frame around content
oled.text("STATUS", 2, 2) # Title
oled.text("Ticks:" + str(t), 2, 18) # Tick counter value
# Heartbeat pixel toggles based on t parity
hb = 1 if (t % 2 == 0) else 0 # Heartbeat state (1 or 0)
oled.pixel(126, 30, hb) # Heartbeat pixel near bottom-right
oled.show() # Refresh display
print("[Main] STATUS", t) # Serial: log page
def draw_adc(): # Helper: render ADC page
val = adc2.read() # Read raw sensor value
pct = int((val * 100) / 4095) # Convert to percentage
oled.fill(0) # Clear screen
oled.rect(0, 0, 128, 32, 1) # Frame around content
oled.text("ADC2", 2, 2) # Title
oled.text(str(val), 2, 18) # Raw value text
bar_w = int((pct * 120) / 100) # Map percent to bar width
oled.fill_rect(4, 26, bar_w, 4, 1) # Filled bar at bottom
oled.rect(4, 26, 120, 4, 1) # Bar outline for context
oled.show() # Refresh display
print("[Main] ADC2:", val, "(", pct, "%)") # Serial: log reading
def show_page(): # Helper: draw current page
name = pages[index] # Get current page name
if name == "HOME": # If HOME
draw_home() # Draw HOME
elif name == "STATUS": # If STATUS
draw_status(ticks) # Draw STATUS with ticks
else: # Else ADC
draw_adc() # Draw ADC bar view
# Welcome screen
oled.fill(0) # Clear screen
oled.text("OLED 128x32", 0, 0) # Welcome text line 1
oled.text("Ready!", 0, 16) # Welcome text line 2
oled.show() # Show welcome
time.sleep_ms(900) # Pause briefly
# Main UI loop
while True: # Continuous UI loop
ticks = ticks + 1 # Increment heartbeat
show_page() # Draw the current page
if btn_next.value() == 0: # If NEXT pressed (LOW)
index = (index + 1) % len(pages) # Advance to next page
time.sleep_ms(250) # Debounce
if btn_prev.value() == 0: # If PREV pressed (LOW)
index = (index - 1) % len(pages) # Go to previous page
time.sleep_ms(250) # Debounce
time.sleep_ms(400) # Smooth refresh cadence
External explanation
This project uses the official SSD1306 driver to draw text and graphics on a 128×32 OLED. You learned to place text safely with the 8×8 font, to frame content with rectangles and lines, and to visualize sensor data as both numbers and bars. A small page system (HOME, STATUS, ADC) keeps it organized and student‑friendly.
Story time
Your screen wakes up and starts speaking in pixels. It shows a heartbeat, draws a neat frame, and turns raw sensor numbers into a tiny progress bar. Suddenly, the robot’s “face” feels alive.
Debugging (2)
Debugging 4.2.A – Screen does not turn on
- Symptom: Nothing appears; the OLED looks black.
- Fix:
# Verify I2C pins and address
i2c = machine.SoftI2C(scl=machine.Pin(26), sda=machine.Pin(5), freq=400000) # Correct pins
oled = ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3C) # Common address
oled.fill(0); oled.text("OK", 0, 0); oled.show() # Minimal draw
# Ensure VCC and GND are connected; some modules need 5V on VCC but 3.3V also works on many
Debugging 4.2.B – Text outside the limits
- Symptom: Cropped or invisible text.
- Fix:
# Keep x within 0–127 and y within 0–24 (font is 8 px tall; last full line starts at y=24)
oled.text("Hello", 120, 0) # This will clip; reduce x
oled.text("Hello", 96, 0) # Fits (128 - 32 font width window)
# Plan layout: use x = 0, 32, 64, 96 anchors; y = 0, 8, 16, 24 rows
Final checklist
- OLED initializes on I2C with addr 0x3C and shows pixels.
- Static and dynamic text render cleanly within bounds.
- Lines and rectangles structure the screen without clutter.
- ADC values show as raw numbers, percent, and a bar.
- UI pages (HOME, STATUS, ADC) navigate smoothly with buttons.
Extras
- Student tip: Use anchors (x=0,32,64,96 and y=0,8,16,24) to keep text tidy.
- Instructor tip: Have students sketch their layouts on paper before coding; planning prevents clipping.
- Glossary:
- SSD1306: The common controller chip inside many small OLED displays.
- Buffer: The image memory that must be pushed with show() to update the screen.
- Frame: A complete screen layout for one view.
- Mini tips:
- Clear first (fill(0)), then draw, then show().
- Keep update cadence around 300–500 ms for readable changes.
- If you later send data to a PC, keep messages short and timestamped for clarity.