Skip to content

OLED Display Graphics

Drawing Shapes on SSD1306

OLED displays, such as the 128x32 pixel SSD1306, use monochrome pixels (black & white) for rendering graphics and text. MicroPython's ssd1306 library provides functions to draw pixels and text.

Understanding the Display Grid (128x32)

  • The OLED screen has 128 columns (X-axis) and 32 rows (Y-axis).
  • The top-left corner (0,0) is the origin.
  • X-axis moves right (0 → 127).
  • Y-axis moves down (0 → 31).

Visual Illustration:

   (0,0)┌────────── X-axis ──────────► (0,127)
        │  Text and Shapes Here
 Y-axis │  
        |
      (31,0)

Drawing Pixels

The most basic way to draw is by setting individual pixels.

oled.pixel(10, 10, 1)  # Set pixel at (10,10) to white (1)
oled.pixel(20, 15, 1)  # Set pixel at (20,15) to white (1)
oled.show()  # Refresh display

📝 Tip: oled.pixel(x, y, color)

  • x, y → Position of the pixel
  • color = 1 (white) or 0 (black)

Drawing a Line with Only pixel()

Since we don't have hline() or vline(), we manually turn on pixels.

Horizontal Line

for x in range(10, 100):  # Draw from (10,15) to (99,15)
    oled.pixel(x, 15, 1)

oled.show()

Vertical Line

for y in range(5, 25):  # Draw from (30,5) to (30,24)
    oled.pixel(30, y, 1)

oled.show()

Diagonal Line

for i in range(20):  # Diagonal from (10,10) to (30,30)
    oled.pixel(10 + i, 10 + i, 1)

oled.show()

Thicker line

To draw thicker lines, simply repeat drawing at adjacent Y-coordinates:

line_width = 3  # example thickness  
for y_offset in range(line_width):  # draw 3 lines stacked vertically 
    for x in range(0, 92):         
        oled.pixel(x, 10 + y_offset, 1)

oled.show()
  • This code draws a horizontal line 3 pixels thick starting at y=10.

Drawing a Rectangle with pixel()

To draw a rectangle, we manually create four edges using loops.

x0, y0 = 20, 5  # Top-left corner
w, h = 40, 15   # Width & height

# Top & bottom edges
for x in range(x0, x0 + w):
    oled.pixel(x, y0, 1)  # Top edge
    oled.pixel(x, y0 + h - 1, 1)  # Bottom edge

# Left & right edges
for y in range(y0, y0 + h):
    oled.pixel(x0, y, 1)  # Left edge
    oled.pixel(x0 + w - 1, y, 1)  # Right edge

oled.show()

Filled Rectangle with pixel()

x0, y0 = 20, 5  # Top-left corner
w, h = 40, 15   # Width & height

for x in range(x0, x0 + w):
    for y in range(y0, y0 + h):
        oled.pixel(x, y, 1)  # Fill pixels

oled.show()

Draw custom pixel patterns

Below is an example that draws a wider car—taking advantage of the 128×32 display—using only the pixel() function. In this example, the car is defined as a list of strings where each asterisk (*) represents a pixel that should be turned on. The pattern is 40 characters wide and 7 rows tall, and it is centered on the display.

# Define a wider car pattern (each string is exactly 40 characters long)
car_pattern = [
    "            ****************            ",  # 40 characters
    "       ******             ******        ",  # 40 characters
    "       ******             ******        ",  # 40 characters
    "       ******             ******        ",  # 40 characters
    "     ******************************     ",  # 40 characters
    "    *                          *******  ",  # 40 characters
    "    *                          *******  ",  # 40 characters
    "    *                          *******  ",  # 40 characters
    "    *                          *******  ",  # 40 characters
    "    **********************************  ",  # 40 characters
    "        *****             *****         ",  # 40 characters
    "         ***               ***          "   # 40 characters
]


# Calculate starting positions to center the pattern on the display
pattern_width = len(car_pattern[0])   # Should be 40
pattern_height = len(car_pattern)       # Now 12 rows
start_x = (128 - pattern_width) // 2    # Center horizontally
start_y = (32 - pattern_height) // 2    # Center vertically

# Draw the car pattern using only the pixel() function
for row_index, row in enumerate(car_pattern):
    for col_index, char in enumerate(row):
        if char == "*":  # Turn on pixel wherever there is an asterisk
            oled.pixel(start_x + col_index, start_y + row_index, 1)

oled.show()  # Refresh the display to show the car

How It Works

  1. OLED Initialization:
    We clear the display using oled.fill(0).

  2. Car Pattern Definition:
    The car is represented as a list of 11 strings (rows), each exactly 40 characters wide. In this pattern, every asterisk (*) indicates that the corresponding pixel should be lit.

  3. Centering the Pattern:
    The code calculates start_x and start_y so that the 40×11 pattern is centered on the 128×32 display.

  4. Drawing the Pattern:
    The code loops through each row and each column of the pattern. For each asterisk, the corresponding pixel on the OLED is turned on using oled.pixel().

  5. Display Update:
    Finally, oled.show() refreshes the screen so that the drawn car appears.

This approach uses only the pixel() function to draw a recognizable, wider car that takes advantage of the full width of the 128×32 OLED display. Enjoy experimenting with pixel art!


Professional Context: Industrial & Automotive Displays

Your 128×32 OLED is great for learning, but professional embedded systems use displays designed for reliability, readability, and harsh environments. Here's how they compare:

Display Comparison

Feature SSD1306 OLED Industrial HMI Automotive Cluster Aerospace/Medical
Resolution 128×32 / 128×64 320×240 to 1920×1080 1920×720+ 640×480 to 4K
Technology OLED monochrome TFT LCD color TFT/OLED + GPU LCD with redundancy
Brightness 100-150 cd/m² 500-1000 cd/m² 500-1000 cd/m² (sunlight readable) 200-500 cd/m²
Temperature -20 to +70°C -30 to +80°C -40 to +85°C -55 to +85°C
Interface I2C (slow) SPI/Parallel/LVDS LVDS/HDMI Redundant ARINC
Update rate ~10 fps (I2C limited) 30-60 fps 60 fps 30 fps minimum
Lifetime 10k-50k hours 50k-100k hours 50k+ hours 100k+ hours
Sunlight readable No Optional Yes (required) Yes (often)
Touch No Resistive/Capacitive Capacitive Sometimes
Price ~$3 $50-500 $100-1000+ $500-5000+

Why Your OLED Is Slow

I2C at 400 kHz:
    128×32 = 4096 pixels = 512 bytes
    512 bytes × 8 bits × (1/400kHz) = ~10ms minimum
    + Address, commands, overhead = ~15-20ms per frame
    → Maximum ~50-60 fps theoretical, ~10-30 fps practical

SPI at 10 MHz:
    Same data in ~0.4ms
    → Much faster updates possible

Parallel RGB interface:
    24 bits/pixel streamed continuously
    → Full video rate, but many pins required

Automotive Instrument Clusters

Modern car dashboards are sophisticated graphics systems:

Your OLED:
    CPU: RP2040 (no graphics acceleration)
    Memory: 512 bytes framebuffer
    Update: pixel() calls in Python loop
    Result: Simple shapes, text, ~10 fps

Automotive cluster:
    CPU: Dedicated graphics processor (GPU)
    Memory: 64-512 MB video RAM
    Update: Hardware-accelerated 2D/3D
    Result: Smooth gauges, animations, video, 60 fps

    Plus:
    - Automatic brightness (ambient light sensor)
    - Anti-glare coating
    - Wide viewing angle
    - Redundant backlight
    - Functional safety (shows warnings reliably)

Industrial HMI Panels

Factory automation uses rugged touchscreen panels:

Feature Your OLED Industrial HMI
Enclosure None (bare PCB) IP65/IP67 sealed
Shock/vibration Not rated IEC 60068 tested
EMC None CE/FCC certified
Input None Multi-touch, glove-compatible
Software Custom MicroPython SCADA/PLC integration
Diagnostics None Built-in self-test
Lifespan ? 7+ years guaranteed

Graphics Acceleration

Your Pico draws pixels one at a time. Professional systems use hardware:

# Your approach (software rendering):
for x in range(128):
    for y in range(32):
        oled.pixel(x, y, calculate_color(x, y))
# Time: ~100ms for full screen

# Hardware-accelerated (conceptual):
gpu.draw_rectangle(0, 0, 128, 32, color)
gpu.draw_text("Speed: 50", font, x, y)
gpu.render()  # GPU does all work
# Time: <1ms

What the Industry Uses

Manufacturer Product Application
Solomon Systech SSD1306/SSD1351 Small OLED modules
Bosch Instrument clusters Automotive dashboards
Continental HMI displays Vehicle infotainment
Siemens SIMATIC HMI Industrial automation
Garmin G1000 Aviation glass cockpit
Advantech Industrial panels Factory automation

Hardware Limits Principle

What Software Can and Cannot Fix

Software CAN improve: - Flicker → double buffering (draw to memory, swap) - Slow updates → only redraw changed regions - Memory usage → streaming partial updates - Readability → better fonts, contrast optimization

Software CANNOT fix: - I2C bandwidth limit → need SPI or parallel interface - 128×32 resolution → need different display - Monochrome → need color display hardware - No sunlight readability → need high-brightness + anti-reflective display - Touch input → need touchscreen hardware - Burn-in (OLED aging) → inherent to OLED technology

The lesson: You can optimize your drawing code to update at 30 fps instead of 10 fps. But you cannot software-upgrade a 128×32 monochrome display to show video or work in sunlight—that requires different hardware.

Real Example: Update Speed

Task SSD1306 via I2C Industrial TFT
Clear screen ~15ms <1ms
Draw text (10 chars) ~5ms <0.1ms
Full redraw ~20ms (50 fps max) <1ms (1000+ fps possible)
Animation Choppy Smooth
Video playback Impossible Easy

Your robot's OLED showing sensor values at 10 Hz is perfectly fine. A car dashboard showing smooth speedometer needle movement needs GPU hardware.


Further Reading