A small OLED gives Python a physical result you can see. It also makes several abstract ideas concrete: a program can depend on hardware, a screen is a grid of numbered positions, and drawing a picture is not the same as displaying it.
How can a Python program tell a physical screen exactly what and where to display?
We will use a Raspberry Pi Pico and a 128 × 64 SSD1306 OLED. The driver file, ssd1306.py, should already be installed on the Pico. We will use it without opening it yet. Understanding what a tool accomplishes often comes before understanding its implementation.
Start with this program in main.py:
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
# The OLED is already connected:
# GP0 -- SDA
# GP1 -- SCL
# 3V3 -- VCC
# GND -- GND
i2c = I2C(
0,
scl=Pin(1),
sda=Pin(0),
freq=200_000,
)
print("I2C devices:", i2c.scan())
oled = SSD1306_I2C(128, 64, i2c)
oled.fill(0)
oled.text("Hello, yush", 0, 0)
oled.text("Pico WH", 0, 16)
oled.show()
Connect the Hardware
Q1: What do GND, VCC, SCL, and SDA do?
Show answer
Hide answer
VCC supplies power to the OLED. GND gives the Pico and OLED a shared electrical reference.
SCL is the serial clock. It supplies the timing for I²C communication. SDA is the serial data line that carries information between the devices. Most of our useful data travels from the Pico to the OLED, although SDA can carry data in either direction.
You do not need to trace individual clock pulses or bits yet. For now, it is enough to know that SCL coordinates the conversation and SDA carries it.
Q2: Why is correct wiring a precondition?
Show answer
Hide answer
A precondition is something that must already be true before a program can work. Python can control signals at the Pico’s pins, but it cannot move a wire from the wrong pin to the right one.
The program therefore assumes that the hardware is connected correctly. If that assumption is false, the code may fail even when every Python statement is valid. Software often depends on facts outside itself; naming those facts makes a system easier to test.
Q3: Which holes on a breadboard are connected?
Imagine one numbered row:
A B C D E F G H I J
o o o o o o o o o o
gap
Show answer
Hide answer
On a typical breadboard, holes A through E in one numbered row form a connected group. Holes F through J form another. The center gap separates the two groups, and different numbered rows are separate.
Plan four continuous paths before applying power:
OLED SDA → GP0
OLED SCL → GP1
OLED VCC → 3V3
OLED GND → GND
The exact row numbers do not matter. Each signal needs an unbroken path, and unrelated signals must not share a connected row.
Establish I²C Communication
Q4: What should the starter program do?
Predict what will appear in the Shell and on the OLED. Then run the program and explain any difference.
Show answer
Hide answer
The Shell should usually report:
The OLED should display two lines:
This first run establishes that the complete system works. Once the known-good program succeeds, we can change one part at a time and learn from the result.
Q5: What does the 60 returned by i2c.scan() mean?
Show answer
Hide answer
It is the OLED’s I²C address, not a pin number or a count of devices. I²C allows several devices to share the same pair of communication lines, so each device needs an address.
The SSD1306 commonly uses hexadecimal address 0x3C. Python prints the same value here in decimal:
60 decimal = 0x3C hexadecimal
An empty list, [], means that no device answered the scan.
Q6: What would happen if SDA were disconnected?
Power off the Pico before moving a wire. Disconnect only SDA, then run the I²C setup and print(i2c.scan())—not the rest of the OLED program. Predict, run, and explain the result.
Show answer
Hide answer
With SDA connected, the scan commonly returns [60]. Without SDA, it should return [] because the OLED cannot answer.
The comparison gives us evidence that SDA is essential to the exchange. Reconnect SDA before continuing.
Q7: How would you translate the I²C setup into ordinary English?
i2c = I2C(
0,
scl=Pin(1),
sda=Pin(0),
freq=200_000,
)
Show answer
Hide answer
Create an I²C connection using the Pico’s I²C hardware controller 0. Carry the clock signal through GP1, carry the data signal through GP0, and use a clock frequency of 200,000 cycles per second.
The first 0 selects the RP2040’s I2C0 controller. It does not mean GP0; Pin(0) names GP0 separately.
Treat the Screen as a Grid
Q8: What do 128 and 64 describe?
oled = SSD1306_I2C(128, 64, i2c)
Show answer
Hide answer
They describe the display’s resolution: 128 pixels wide and 64 pixels tall. The screen is a rectangular grid with 128 columns and 64 rows.
It contains:
If only the width doubled to 256, the display would contain 16,384 pixels. Doubling one dimension doubles the total area.
Q9: Where are the corners of the screen?
The origin is at the top-left. As x increases, you move right. As y increases, you move down.
Find the top-left, top-right, bottom-left, bottom-right, and approximate center.
Show answer
Hide answer
(0, 0) top-left
(127, 0) top-right
(0, 63) bottom-left
(127, 63) bottom-right
(64, 32) approximate center
Both dimensions are even, so the exact geometric center lies between pixels. (64, 32) is a useful nearby position.
Q10: Why is the bottom-right pixel (127, 63), not (128, 64)?
Show answer
Hide answer
The coordinates use zero-based indexing. The 128 horizontal positions are numbered 0 through 127, and the 64 vertical positions are numbered 0 through 63.
The final valid index is therefore one less than the number of positions.
Place Pixels and Text
Q11: How can you turn on the four corner pixels and one near the center?
Use oled.pixel(x, y, 1). Calculate all five coordinates before you run the code.
Show answer
Hide answer
oled.fill(0)
oled.pixel(0, 0, 1)
oled.pixel(127, 0, 1)
oled.pixel(0, 63, 1)
oled.pixel(127, 63, 1)
oled.pixel(64, 32, 1)
oled.show()
Inspect the physical display. The pixels are tiny, but their positions should agree with the coordinate model.
Q12: What will happen if text begins at (125, 0)?
Predict, run, and explain:
oled.fill(0)
oled.text("HELLO", 125, 0)
oled.show()
Show answer
Hide answer
Only three horizontal positions—125, 126, and 127—remain. A letter requires a group of pixels, so the word cannot fit.
The coordinates passed to text() mark where the drawing begins. They do not place the entire word inside one pixel. Anything beyond the edge is not visible.
Q13: Why doesn’t (64, 32) center a message?
oled.fill(0)
oled.text("THIS IS A LONG MESSAGE", 64, 32)
oled.show()
Show answer
Hide answer
The point (64, 32) is near the center of the screen, but it becomes the text’s starting position. The message then extends to the right.
To center text, you need its width and must move the starting x-coordinate left by about half that width. Position and size are separate properties.
Q14: What is the difference between fill(0) and fill(1)?
Predict, run, and explain these two cases:
Show answer
Hide answer
This display is monochrome, so each pixel has two states:
fill(0) makes the entire drawing area black. fill(1) turns every pixel on. Why one bit can represent these states is a question for the next lesson.
Draw First, Display Second
Q15: Does text() immediately change the physical OLED?
Predict what will remain visible after this code runs:
oled.fill(0)
oled.show()
oled.text("TEST", 0, 0)
# Do not call oled.show() again.
Show answer
Hide answer
The display remains blank. text() changes an image being prepared in the Pico’s memory; it does not immediately update the physical OLED.
show() transfers the prepared image to the display. That separation gives the program time to assemble a complete frame before the viewer sees it.
The memory holding the prepared image is called a frame buffer. We will examine its representation later.
Q16: Which methods draw, and which method displays?
Sort these methods by job:
oled.fill(0)
oled.pixel(20, 20, 1)
oled.text("HELLO", 10, 10)
oled.show()
Show answer
Hide answer
fill(), pixel(), and text() are drawing operations. Each changes the frame buffer—the prepared picture in memory.
show() is a display operation. It sends that picture to the SSD1306 so the physical screen reflects it.
draw in memory → show the completed image
Read the Whole Program
Q17: What does SSD1306_I2C(128, 64, i2c) accomplish?
Show answer
Hide answer
It creates a Python object representing a 128 × 64 SSD1306 display and tells the driver to communicate through the connection stored in i2c.
That explanation describes what the constructor accomplishes. It does not explain how the driver implements the constructor—and it does not need to yet. A useful model can be accurate without exposing every layer beneath it.
Q18: What story does the complete program tell?
Show answer
Hide answer
It proceeds in four stages:
establish I²C communication
↓
create an object that represents the OLED
↓
prepare an image in memory
↓
send that image to the physical display
The wiring must already be correct. The program then establishes communication, describes the screen to the driver, draws into a frame buffer, and calls show() to make the result visible.
Build Your Own Screen
Q19: Can you place three messages deliberately?
Modify only the display portion of the starter program. Put one short message near the top, one near the middle, and one near the bottom. Sketch your expected result first. Then run it and adjust the coordinates.
Show answer
Hide answer
There is no single correct arrangement. A solution should have this shape:
oled.fill(0)
oled.text("MESSAGE 1", x1, y1)
oled.text("MESSAGE 2", x2, y2)
oled.text("MESSAGE 3", x3, y3)
oled.show()
Choose values that keep each message inside the screen. Be ready to explain why each coordinate is reasonable and what you changed after seeing the first result.
What Comes Next
Q20: What can we use without understanding yet?
Show answer
Hide answer
We can now wire the OLED, find it on the I²C bus, place pixels and text, and control when a prepared image becomes visible. Several useful questions remain open:
- Where is the frame buffer stored?
- How does text become pixels?
- How are those pixels packed into bytes?
- What travels along SDA when
show() runs? - What does the SSD1306 do with the data it receives?
Those questions lead naturally from drawing pictures to representing pictures as data:
pixel → bit → byte → frame buffer → display
For now, the essential model is enough: drawing methods prepare an image in memory, and show() sends it to the screen.