🤖 Level 4 – Mobile Robotics

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

PartHow many?Pin connection (R32)
D1 R321USB cable
OLED 0.91″ 128×32 (SSD1306 I2C)1SCL → Pin 26, SDA → Pin 5, VCC 3.3–5V, GND
Wires4Match pins carefully
Optional sensor (ADC demo)1Joystick 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.
On this page