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:

# 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

Popular posts from this blog

Python/PyQt6 slideshow run on Windows 11

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library