Raspberry Pi Pico 2/MicroPython to handle two SSD1306 OLEDs, in main thread and background thread.
This post list MicroPython exercises run on Raspberry Pi Pico 2 to handle two
SSD1306 OLEDs, one in main thread and another one in background thread.
Connection:
To install micropython_ssd1306 on the Raspberry Pi Pico 2, and simple exercise (mpy_pico2_ssd1306.py), read previous post.
Exercise Code:
Connection:
To install micropython_ssd1306 on the Raspberry Pi Pico 2, and simple exercise (mpy_pico2_ssd1306.py), read previous post.
Exercise Code:
# mpy_pico2_dual_ssd1306.py
# MicroPython exercise running on Raspberry Pi Pico 2 with RP2350
# to handle two 128x64 SSD1306 OLEDs, one by one.
#
# https://coxxect.blogspot.com/2026/10/raspberry-pi-pico-2micropython-to.html
#
import time
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
from framebuf import FrameBuffer
i2c0 = I2C(0)
print(i2c0)
print(i2c0.scan())
oled0 = SSD1306_I2C(128, 64, i2c0)
print(oled0.width, "x", oled0.height)
i2c1 = I2C(1)
print(i2c1)
print(i2c1.scan())
oled1 = SSD1306_I2C(128, 64, i2c1)
print(oled1.width, "x", oled1.height)
def fill_oled(oled_id, oled, ticks_ms, ticks_diff, sleep_ms):
start_time = time.ticks_ms()
for y in range(oled.height):
for x in range(oled.width):
oled.pixel(x, y, 1)
oled.show()
for y in range(oled.height):
for x in range(oled.width):
oled.pixel(x, y, 0)
oled.show()
end_time = time.ticks_ms()
duration = time.ticks_diff(end_time, start_time)
print(oled_id, "duration", duration)
oled.text(oled_id, 5, 10, 1)
oled.text(str(duration), 5, 20, 1)
oled.show()
sleep_ms(0)
fill_oled("oled0", oled0, time.ticks_ms, time.ticks_diff, time.sleep_ms)
fill_oled("oled1", oled1, time.ticks_ms, time.ticks_diff, time.sleep_ms)
# mpy_pico2_dual_ssd1306_thread.py
# MicroPython exercise running on Raspberry Pi Pico 2 with RP2350
# to handle two 128x64 SSD1306 OLEDs at the same time, one in main thread, another in background thread.
#
# https://coxxect.blogspot.com/2026/10/raspberry-pi-pico-2micropython-to.html
#
import time
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
from framebuf import FrameBuffer
import _thread
i2c0 = I2C(0)
print(i2c0)
print(i2c0.scan())
oled0 = SSD1306_I2C(128, 64, i2c0)
print(oled0.width, "x", oled0.height)
i2c1 = I2C(1)
print(i2c1)
print(i2c1.scan())
oled1 = SSD1306_I2C(128, 64, i2c1)
print(oled1.width, "x", oled1.height)
# use list as mutable flag
running = [True]
def fill_oled_thread(running, oled_id, oled, ticks_ms, ticks_diff, sleep_ms, get_thread_id):
try:
while running[0]:
fill_oled(oled_id, oled, ticks_ms, ticks_diff, sleep_ms, get_thread_id)
except Exception as e:
print("bg_thread exception:", e)
print("blink_thread stopped!")
_thread.start_new_thread(fill_oled_thread, (running, "oled1", oled1, time.ticks_ms, time.ticks_diff, time.sleep_ms, _thread.get_ident))
def fill_oled(oled_id, oled, ticks_ms, ticks_diff, sleep_ms, get_thread_id):
#thread_id = _thread.get_ident()
thread_id = get_thread_id()
start_time = ticks_ms()
for y in range(oled.height):
for x in range(oled.width):
oled.pixel(x, y, 1)
oled.show()
for y in range(oled.height):
for x in range(oled.width):
oled.pixel(x, y, 0)
oled.show()
end_time = ticks_ms()
duration = ticks_diff(end_time, start_time)
print(thread_id, oled_id, "duration", duration)
oled.text(str(thread_id) + " " + oled_id, 5, 45, 1)
oled.text(str(duration), 5, 55, 1)
oled.show()
sleep_ms(0)
for i in range(5):
fill_oled("oled0", oled0, time.ticks_ms, time.ticks_diff, time.sleep_ms, _thread.get_ident)
print("Stopping...")
running[0] = False
# wait a while before end
time.sleep_ms(10)
print("Stopped")
# mpy_pico2_ssd1306_cube.py
# MicroPython exercise running on Raspberry Pi Pico 2 with RP2350
# to display a floating/rotating cube on 128x64 SSD1306 OLEDs.
#
# https://coxxect.blogspot.com/2026/10/raspberry-pi-pico-2micropython-to.html
#
import math
import time
from machine import I2C
from ssd1306 import SSD1306_I2C
WIDTH = 128
HEIGHT = 64
# Use your existing I2C setup if it already works.
i2c = I2C(0)
# If needed, use explicit pins instead:
# i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
def rotating_floating_cube():
# Cube vertices in 3D space.
# Coordinates are from -1 to +1.
vertices = (
(-1.0, -1.0, -1.0), # 0
( 1.0, -1.0, -1.0), # 1
( 1.0, 1.0, -1.0), # 2
(-1.0, 1.0, -1.0), # 3
(-1.0, -1.0, 1.0), # 4
( 1.0, -1.0, 1.0), # 5
( 1.0, 1.0, 1.0), # 6
(-1.0, 1.0, 1.0) # 7
)
# Cube edges connect these vertex indexes.
edges = (
# Back face
(0, 1),
(1, 2),
(2, 3),
(3, 0),
# Front face
(4, 5),
(5, 6),
(6, 7),
(7, 4),
# Connecting edges
(0, 4),
(1, 5),
(2, 6),
(3, 7)
)
# Screen center.
cx = WIDTH // 2
cy = HEIGHT // 2
# Visual settings.
scale = 16.0 # Cube size on screen.
distance = 6.0 # Distance from camera.
fov = 4.0 # Perspective strength.
start_time = time.ticks_ms()
try:
while True:
# Time in seconds.
t = time.ticks_diff(time.ticks_ms(), start_time) / 1000
# Rotation angles.
angle_x = t * 1.1
angle_y = t * 0.9
angle_z = t * 0.6
# Floating/bobbing motion.
bob = math.sin(t * 2.0) * 4.0
# Precompute sine and cosine values for speed.
cos_x = math.cos(angle_x)
sin_x = math.sin(angle_x)
cos_y = math.cos(angle_y)
sin_y = math.sin(angle_y)
cos_z = math.cos(angle_z)
sin_z = math.sin(angle_z)
# Clear display buffer.
oled.fill(0)
# Optional simple shadow to enhance the floating effect.
shadow_width = int(34 - bob * 2)
if shadow_width < 10:
shadow_width = 10
elif shadow_width > 50:
shadow_width = 50
oled.hline(
cx - shadow_width // 2,
HEIGHT - 6,
shadow_width,
1
)
# Store projected 2D screen coordinates.
projected = []
for x, y, z in vertices:
# Rotate around X axis.
y1 = y * cos_x - z * sin_x
z1 = y * sin_x + z * cos_x
# Rotate around Y axis.
x1 = x * cos_y + z1 * sin_y
z2 = -x * sin_y + z1 * cos_y
# Rotate around Z axis.
x2 = x1 * cos_z - y1 * sin_z
y2 = x1 * sin_z + y1 * cos_z
# Move cube away from camera.
z3 = z2 + distance
# Safety against division by zero.
if z3 < 0.1:
z3 = 0.1
# Simple perspective projection.
factor = fov / z3
sx = int(cx + x2 * factor * scale)
sy = int(cy + y2 * factor * scale + bob)
projected.append((sx, sy))
# Draw cube edges.
for a, b in edges:
x_start, y_start = projected[a]
x_end, y_end = projected[b]
oled.line(
x_start,
y_start,
x_end,
y_end,
1
)
# Update OLED.
oled.show()
# Frame delay.
time.sleep_ms(20)
except KeyboardInterrupt:
# Clear screen on Ctrl+C.
oled.fill(0)
oled.show()
# Run the demo.
rotating_floating_cube()
#----------------------------
# mpy_pico2_dual_ssd1306_cube.py
#
# MicroPython exercise for Raspberry Pi Pico 2 / RP2350.
# Two 128x64 SSD1306 OLED displays.
#
# Main thread -> oled0
# Background thread -> oled1
#
# fill_oled() draws one frame of a floating/rotating cube.
# It is called repeatedly from both threads.
#
# https://coxxect.blogspot.com/2026/10/raspberry-pi-pico-2micropython-to.html
#----------------------------
import time
import math
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import _thread
#----------------------------
# Init I2C0 and OLED0
#----------------------------
i2c0 = I2C(0)
print(i2c0)
print(i2c0.scan())
oled0 = SSD1306_I2C(128, 64, i2c0)
print(oled0.width, "x", oled0.height)
#----------------------------
# Init I2C1 and OLED1
#----------------------------
i2c1 = I2C(1)
print(i2c1)
print(i2c1.scan())
oled1 = SSD1306_I2C(128, 64, i2c1)
print(oled1.width, "x", oled1.height)
#----------------------------
# Thread control flags
#----------------------------
# Mutable flag shared between threads.
running = [True]
# Added flag so main code can wait for background thread to stop.
thread_done = [False]
#----------------------------
# Cube model
#----------------------------
CUBE_VERTICES = (
(-1.0, -1.0, -1.0), # 0
( 1.0, -1.0, -1.0), # 1
( 1.0, 1.0, -1.0), # 2
(-1.0, 1.0, -1.0), # 3
(-1.0, -1.0, 1.0), # 4
( 1.0, -1.0, 1.0), # 5
( 1.0, 1.0, 1.0), # 6
(-1.0, 1.0, 1.0) # 7
)
CUBE_EDGES = (
# Back face
(0, 1),
(1, 2),
(2, 3),
(3, 0),
# Front face
(4, 5),
(5, 6),
(6, 7),
(7, 4),
# Connecting edges
(0, 4),
(1, 5),
(2, 6),
(3, 7)
)
#----------------------------
# Per-display animation state
#
# oled0 and oled1 must not share mutable animation state.
# Each display has its own start time, frame counter, etc.
#----------------------------
cube_state = {
"oled0": {
"start": time.ticks_ms(),
"frame": 0,
"duration": 0
},
"oled1": {
"start": time.ticks_ms(),
"frame": 0,
"duration": 0
}
}
#----------------------------
# Draw one cube frame
#----------------------------
def render_cube(oled_id, oled, ticks_ms, ticks_diff, get_thread_id):
state = cube_state[oled_id]
frame_start = ticks_ms()
# Time in seconds since this OLED animation started.
t = ticks_diff(ticks_ms(), state["start"]) / 1000
# Make the two OLEDs move a little differently,
# so it is obvious they are independent.
if oled_id == "oled1":
speed = 1.4
phase = 0.7
direction = -1.0
else:
speed = 1.0
phase = 0.0
direction = 1.0
angle_x = (t + phase) * 1.1 * speed * direction
angle_y = (t + phase) * 0.9 * speed
angle_z = (t + phase) * 0.6 * speed * direction
# Floating / bobbing motion.
bob = math.sin((t + phase) * 2.0) * 3.0
# Precompute trig values once per frame.
cos_x = math.cos(angle_x)
sin_x = math.sin(angle_x)
cos_y = math.cos(angle_y)
sin_y = math.sin(angle_y)
cos_z = math.cos(angle_z)
sin_z = math.sin(angle_z)
# Clear framebuffer.
oled.fill(0)
# Center the cube slightly lower to leave room for text.
cx = oled.width // 2
cy = (oled.height // 2) + 4
# For 128x64, this gives about 12.8.
scale = min(oled.width, oled.height) / 5.0
# Perspective settings.
distance = 6.0
fov = 4.0
# Simple shadow to emphasize floating.
shadow_width = int(26 - bob * 2)
if shadow_width < 12:
shadow_width = 12
elif shadow_width > 40:
shadow_width = 40
oled.hline(
cx - shadow_width // 2,
oled.height - 3,
shadow_width,
1
)
# Projected 2D coordinates.
px = [0] * len(CUBE_VERTICES)
py = [0] * len(CUBE_VERTICES)
for i in range(len(CUBE_VERTICES)):
x, y, z = CUBE_VERTICES[i]
# Rotate around X axis.
y1 = y * cos_x - z * sin_x
z1 = y * sin_x + z * cos_x
# Rotate around Y axis.
x1 = x * cos_y + z1 * sin_y
z2 = -x * sin_y + z1 * cos_y
# Rotate around Z axis.
x2 = x1 * cos_z - y1 * sin_z
y2 = x1 * sin_z + y1 * cos_z
# Move object away from camera.
z3 = z2 + distance
# Safety against divide-by-zero or negative projection.
if z3 < 0.1:
z3 = 0.1
# Perspective projection.
factor = fov / z3
sx = int(cx + x2 * factor * scale)
sy = int(cy + y2 * factor * scale + bob)
# Clamp to screen. This is not strictly required for the
# default settings, but it makes the demo safer if you
# change scale, distance, fov, etc.
if sx < 0:
sx = 0
elif sx >= oled.width:
sx = oled.width - 1
if sy < 0:
sy = 0
elif sy >= oled.height:
sy = oled.height - 1
px[i] = sx
py[i] = sy
# Draw cube edges.
for a, b in CUBE_EDGES:
oled.line(
px[a],
py[a],
px[b],
py[b],
1
)
# Show which OLED and thread this frame belongs to.
thread_id = get_thread_id()
oled.text(oled_id, 0, 0, 1)
tid = str(thread_id)
tx = oled.width - len(tid) * 8
if tx < 0:
tx = 0
oled.text(tid, tx, 0, 1)
# Update physical OLED.
oled.show()
# Save per-OLED statistics.
state["frame"] += 1
state["duration"] = ticks_diff(ticks_ms(), frame_start)
# Optional progress print. Comment this out if you want
# maximum animation performance.
#if state["frame"] % 100 == 0:
# print(thread_id, oled_id, "frame", state["frame"],
# "render_ms", state["duration"])
#----------------------------
# Your exercise function
#
# This is called from main thread and background thread.
# It now renders one cube frame, then sleeps briefly.
#----------------------------
def fill_oled(oled_id, oled, ticks_ms, ticks_diff, sleep_ms, get_thread_id):
render_cube(
oled_id,
oled,
ticks_ms,
ticks_diff,
get_thread_id
)
# Frame delay.
# Increase this if the animation is too fast.
# Decrease this if you want to try higher frame rate.
sleep_ms(20)
#----------------------------
# Background thread
#----------------------------
def fill_oled_thread(running, oled_id, oled, ticks_ms, ticks_diff, sleep_ms, get_thread_id):
try:
while running[0]:
fill_oled(
oled_id,
oled,
ticks_ms,
ticks_diff,
sleep_ms,
get_thread_id
)
except Exception as e:
print("bg_thread exception:", e)
finally:
thread_done[0] = True
print("fill_oled_thread stopped!")
#----------------------------
# Start background thread for oled1
#----------------------------
_thread.start_new_thread(
fill_oled_thread,
(
running,
"oled1",
oled1,
time.ticks_ms,
time.ticks_diff,
time.sleep_ms,
_thread.get_ident
)
)
#----------------------------
# Main thread drives oled0
#----------------------------
try:
# Run main-thread animation for 300 frames.
#
# For a shorter test, use:
# for i in range(20):
#
# For endless animation until Ctrl+C, use:
# while running[0]:
#
while running[0]:
fill_oled(
"oled0",
oled0,
time.ticks_ms,
time.ticks_diff,
time.sleep_ms,
_thread.get_ident
)
except KeyboardInterrupt:
pass
#----------------------------
# Stop background thread
#----------------------------
print("Stopping...")
running[0] = False
# Wait for background thread to finish.
wait_start = time.ticks_ms()
while not thread_done[0]:
if time.ticks_diff(time.ticks_ms(), wait_start) > 2000:
print("Warning: background thread did not stop quickly.")
break
time.sleep_ms(5)
print("Stopped")

Comments
Post a Comment