Multi SSD1306 OLED, on Raspberry Pi Pico 2/MicroPython

My exercise on Raspberry Pi Pico 2 (RP2350) running MicroPython v1.24.0-preview.321, to display on two 128x64 SSD1315 I2C OLED (SSD1306 compatible), and one 128x64 SSD1306 SPI OLED. The 3rd, 4th and 5th exercises display on three OLEDs as one framebuf. In the 4th and 5th exercises, groupedOLED_class is implemented, make it easier to append OLEDs at runtime.

Two I2C SSD1306 OLED share the same I2C bus with different address.


Connection:

Exercise code:

mpyPico2_tri_oled_hello.py, a simple hello world display on 3 OLED separately.
"""
MicroPython/Raspberry Pi Pico 2 exercise
display on three OLED, 0.96" 128x64 SSD1306 OLED
- two I2C OLED share one I2C with different I2C address
- and one SPI OLED.

To install MicroPython ssd1306 driver on Pico 2. read:
https://coxxect.blogspot.com/2024/09/i2c-oled-ssd1306ssd1315-screen-with.html

Two I2C OLEDs are connected to the same I2C(0) with different I2C address
The I2C address of the left SSD1306 I2C OLED = 0x3d
The I2C address of the mid SSD1306 I2C OLED = 0x3c (default)
SCL - GP5
SDA - GP4

SPI OLED in the right is connected to SPI(1)
SCK  - GP10
MOSI - GP11
MISO - GP8  (Not use)

"""
import sys
import os
import time
import ssd1306

DISP_WIDTH=128
DISP_HEIGHT=64

# notice that:
# parameter cs have to be provided to ssd1306.SSD1306_SPI(),
# so I assign a GP8 as dummt cs, no need connect it.
dc  = machine.Pin(13)
res = machine.Pin(12)
cs  = machine.Pin(9)   #dummy (any un-used pin), no connection

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

package_info = ssd1306.__name__
try:
    package_info = package_info + " ver:" + ssd1306.__version__
except AttributeError as exc:
    print("AttributeError!", exc)

print("ssd1306 package: ", package_info)
print("====================================")
oled_i2c = machine.I2C(0)
print("oled_i2c:", oled_i2c)
oled_spi = machine.SPI(1)
print("oled_spi:", oled_spi)
print()

oled_ssd1306_i2c_0 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c, 0x3d)
oled_ssd1306_i2c = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c)
oled_ssd1306_spi = ssd1306.SSD1306_SPI(DISP_WIDTH, DISP_HEIGHT, oled_spi, dc, res, cs)
oled_ssd1306_i2c_0.fill(1)
oled_ssd1306_i2c.fill(1)
oled_ssd1306_spi.fill(1)
oled_ssd1306_i2c_0.show()
oled_ssd1306_i2c.show()
oled_ssd1306_spi.show()
time.sleep(1)
oled_ssd1306_i2c_0.fill(0)
oled_ssd1306_i2c.fill(0)
oled_ssd1306_spi.fill(0)
oled_ssd1306_i2c_0.show()
oled_ssd1306_i2c.show()
oled_ssd1306_spi.show()
time.sleep(1)

oled_ssd1306_i2c_0.rect(1, 1, DISP_WIDTH-2, 25, 1)
oled_ssd1306_i2c.rect(1, 1, DISP_WIDTH-2, 25, 1)
oled_ssd1306_spi.rect(1, 1, DISP_WIDTH-2, 25, 1)

oled_ssd1306_i2c_0.text('Hello, World!', 5, 5, 1)
oled_ssd1306_i2c_0.text(oled_ssd1306_i2c.__qualname__, 5, 15, 1)
oled_ssd1306_i2c_0.show()
oled_ssd1306_i2c.text('Hello, World!', 5, 5, 1)
oled_ssd1306_i2c.text(oled_ssd1306_i2c.__qualname__, 5, 15, 1)
oled_ssd1306_i2c.show()
oled_ssd1306_spi.text('Hello, World!', 5, 5, 1)
oled_ssd1306_spi.text(oled_ssd1306_spi.__qualname__, 5, 15, 1)
oled_ssd1306_spi.show()
    
while True:
    for y in range(9):
        oled_ssd1306_i2c_0.scroll(0, y)
        oled_ssd1306_i2c.scroll(0, y)
        oled_ssd1306_spi.scroll(0, y)
        oled_ssd1306_i2c_0.show()
        oled_ssd1306_i2c.show()
        oled_ssd1306_spi.show()
        time.sleep(0.2)
    for y in range(9):
        oled_ssd1306_i2c_0.scroll(0, -y)
        oled_ssd1306_i2c.scroll(0, -y)
        oled_ssd1306_spi.scroll(0, -y)
        oled_ssd1306_i2c_0.show()
        oled_ssd1306_i2c.show()
        oled_ssd1306_spi.show()
        time.sleep(0.2)       

print("~ bye ~")


mpyPico2_tri_oled_polygon3.py, display stars (polygon) on 3 OLED separately.
"""
MicroPython/Raspberry Pi Pico 2 exercise
display on three OLED, 0.96" 128x64 SSD1306 OLED
- two I2C OLED share one I2C with different I2C address
- and one SPI OLED.

Try different num_of_ends on all three OLED.

To install MicroPython ssd1306 driver on Pico 2. read:
https://coxxect.blogspot.com/2024/09/i2c-oled-ssd1306ssd1315-screen-with.html

Two I2C OLEDs are connected to the same I2C(0) with different I2C address
The I2C address of the left SSD1306 I2C OLED = 0x3d
The I2C address of the mid SSD1306 I2C OLED = 0x3c (default)
SCL - GP5
SDA - GP4

SPI OLED in the right is connected to SPI(1)
SCK  - GP10
MOSI - GP11
MISO - GP8  (Not use)
"""
import sys
import os
import time
import ssd1306
from array import array
import math

DISP_WIDTH=128
DISP_HEIGHT=64

# notice that:
# parameter cs have to be provided to ssd1306.SSD1306_SPI(),
# so I assign a GP8 as dummt cs, no need connect it.
dc  = machine.Pin(13)
res = machine.Pin(12)
cs  = machine.Pin(9)   #dummy (any un-used pin), no connection

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

package_info = ssd1306.__name__
try:
    package_info = package_info + " ver:" + ssd1306.__version__
except AttributeError as exc:
    print("AttributeError!", exc)

print("ssd1306 package: ", package_info)
print("====================================")
oled_i2c = machine.I2C(0)
print("oled_i2c:", oled_i2c)
oled_spi = machine.SPI(1)
print("oled_spi:", oled_spi)
print()

oled_ssd1306_i2c_0 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c, 0x3d)
oled_ssd1306_i2c = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c)
oled_ssd1306_spi = ssd1306.SSD1306_SPI(DISP_WIDTH, DISP_HEIGHT, oled_spi, dc, res, cs)
oled_ssd1306_i2c_0.fill(1)
oled_ssd1306_i2c.fill(1)
oled_ssd1306_spi.fill(1)
oled_ssd1306_i2c_0.text(oled_ssd1306_i2c_0.__qualname__, 5, 5, 0)
oled_ssd1306_i2c.text(oled_ssd1306_i2c.__qualname__, 5, 5, 0)
oled_ssd1306_spi.text(oled_ssd1306_spi.__qualname__, 5, 5, 0)
oled_ssd1306_i2c_0.show()
oled_ssd1306_i2c.show()
oled_ssd1306_spi.show()
time.sleep(3)
oled_ssd1306_i2c_0.fill(0)
oled_ssd1306_i2c.fill(0)
oled_ssd1306_spi.fill(0)
oled_ssd1306_i2c_0.text(oled_ssd1306_i2c_0.__qualname__, 5, 5, 1)
oled_ssd1306_i2c.text(oled_ssd1306_i2c.__qualname__, 5, 5, 1)
oled_ssd1306_spi.text(oled_ssd1306_spi.__qualname__, 5, 5, 1)
oled_ssd1306_i2c_0.show()
oled_ssd1306_i2c.show()
oled_ssd1306_spi.show()
time.sleep(3)

# === myEquPolygonClass class ===
class myEquPolygonClass:
    
    def __init__(self, num_of_ends, r, angel=0):
        self.num_of_ends = num_of_ends
        self.r = r
        self.angel = angel
        
        self.coords_list = []
        self.reCal_coords_list()
        
    def update_coords_list(self, num_of_ends, r, angel=0):
        self.num_of_ends = num_of_ends
        self.r = r
        self.angel = angel
        
        self.coords_list.clear()
        self.reCal_coords_list()
        
    def reCal_coords_list(self):
        for end in range(self.num_of_ends):
            angle_in_radians = math.radians((360 * end/self.num_of_ends) + self.angel)
            #print("degree of end", end, angle_in_radians)
            #print(angle_in_radians, ":", math.sin(angle_in_radians), math.cos(angle_in_radians))
            self.coords_list.append(int(self.r * math.sin(angle_in_radians)))
            self.coords_list.append(int(self.r * math.cos(angle_in_radians)))
        
    def show(self, screen, offset_x=0, offset_y=0, fill=0):
        coords_array = array('h', self.coords_list)
        
        screen.poly(offset_x, offset_y,
                    coords_array,
                    1,        # color
                    fill)     # optional fill: True - fill, False - not fill
        screen.show()
# === End of myFixedPolygonClass ===

# === myStarClass class ===
class myStarClass:
    
    def __init__(self, num_of_ends, r1, r2, angel=0):
        self.num_of_ends = num_of_ends
        self.r1 = r1
        self.r2 = r2
        self.angel = angel
        
        self.coords_list = []
        self.reCal_coords_list()
        
    def update_coords_list(self, num_of_ends, r1, r2, angel=0):
        self.num_of_ends = num_of_ends
        self.r1 = r1
        self.r2 = r2
        self.angel = angel
        
        self.coords_list.clear()
        self.reCal_coords_list()
        
    def reCal_coords_list(self):
        
        # if self.num_of_ends ==0, this code will raise error of:
        # ZeroDivisionError: divide by zero
        half_angel_per_end = (360/self.num_of_ends)/2
        
        for end in range(self.num_of_ends):
            angle_in_radians = math.radians((360 * end/self.num_of_ends) + self.angel)
            self.coords_list.append(int(self.r1 * math.sin(angle_in_radians)))
            self.coords_list.append(int(self.r1 * math.cos(angle_in_radians)))
            
            angle_in_radians = math.radians((360 * end/self.num_of_ends) + half_angel_per_end + self.angel)
            self.coords_list.append(int(self.r2 * math.sin(angle_in_radians)))
            self.coords_list.append(int(self.r2 * math.cos(angle_in_radians)))
        
    def show(self, screen, offset_x=0, offset_y=0, fill=0):
        coords_array = array('h', self.coords_list)
        
        screen.poly(offset_x, offset_y,
                    coords_array,
                    1,        # color
                    fill)     # optional fill: True - fill, False - not fill
        screen.show()
# === End of myFixedPolygonClass ===

center_x_i2c_0 = int(oled_ssd1306_i2c_0.width/2)
center_y_i2c_0 = int(oled_ssd1306_i2c_0.height/2)
center_x_i2c = int(oled_ssd1306_i2c.width/2)
center_y_i2c = int(oled_ssd1306_i2c.height/2)
center_x_spi = int(oled_ssd1306_spi.width/2)
center_y_spi = int(oled_ssd1306_spi.height/2)

init_num_of_ends = 3
init_angel = 0
init_r1 = 30
init_r2 = 10
myStar1 = myStarClass(num_of_ends = init_num_of_ends,
                      r1 = init_r1, r2 = init_r2,
                      angel = init_angel)

myStar1.show(oled_ssd1306_i2c_0, center_x_i2c_0, center_y_i2c_0, 0)
myStar1.show(oled_ssd1306_i2c, center_x_i2c, center_y_i2c, 0)
myStar1.show(oled_ssd1306_spi, center_x_spi, center_y_spi, 0)

angel = init_angel

num_of_ends = init_num_of_ends
r1 = init_r1
r2 = init_r2
r2_min = 5
r2_max = 30
r2_dir = 1
while True:

    myStar1.update_coords_list(num_of_ends, r1, r2, angel)
    
    oled_ssd1306_i2c_0.fill(0)
    oled_ssd1306_i2c.fill(0)
    oled_ssd1306_spi.fill(0)
    myStar1.show(oled_ssd1306_i2c_0, center_x_i2c_0, center_y_i2c_0)
    myStar1.show(oled_ssd1306_i2c, center_x_i2c, center_y_i2c)
    myStar1.show(oled_ssd1306_spi, center_x_spi, center_y_spi)
    
    num_of_ends = num_of_ends + 1
    if num_of_ends > 10:
        num_of_ends = 1
            
    time.sleep(0.1)
     
print("~ bye ~")


mpyPico2_triOLED_one_framebuf.py, three OLEDs as one FrameBuffer.
"""
MicroPython/Raspberry Pi Pico 2 exercise
display on three OLED, 0.96" 128x64 SSD1306 OLED
- two I2C OLED share one I2C with different I2C address
- and one SPI OLED.

Three OLEDs as one FrameBuffer

To install MicroPython ssd1306 driver on Pico 2. read:
https://coxxect.blogspot.com/2024/09/i2c-oled-ssd1306ssd1315-screen-with.html

Two I2C OLEDs are connected to the same I2C(0) with different I2C address
The I2C address of the left SSD1306 I2C OLED = 0x3d
The I2C address of the mid SSD1306 I2C OLED = 0x3c (default)
SCL - GP5
SDA - GP4

SPI OLED in the right is connected to SPI(1)
SCK  - GP10
MOSI - GP11
MISO - GP8  (Not use)
"""
import sys
import os
import time
import ssd1306
import framebuf

DISP_WIDTH=128
DISP_HEIGHT=64

# notice that:
# parameter cs have to be provided to ssd1306.SSD1306_SPI(),
# so I assign a GP8 as dummt cs, no need connect it.
dc  = machine.Pin(13)
res = machine.Pin(12)
cs  = machine.Pin(9)   #dummy (any un-used pin), no connection

sys_msg_1 = sys.implementation[0] + " " + os.uname()[3]
sys_msg_2 = "run on " + os.uname()[4]
print("====================================")
print(sys_msg_1)
print(sys_msg_2)
print("====================================")
package_info = ssd1306.__name__
try:
    package_info = package_info + " ver:" + ssd1306.__version__
except AttributeError as exc:
    print("AttributeError!", exc)

print("ssd1306 package: ", package_info)
print("====================================")
oled_i2c = machine.I2C(0)
print("oled_i2c:", oled_i2c)
oled_spi = machine.SPI(1)
print("oled_spi:", oled_spi)
oled_0 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c, 0x3d)
oled_1 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c)
oled_2 = ssd1306.SSD1306_SPI(DISP_WIDTH, DISP_HEIGHT, oled_spi, dc, res, cs)


print("oled_0 I2C address:",
      oled_0.addr, "/", hex(oled_0.addr))

print("oled_1 I2C address:",
      oled_1.addr, "/", hex(oled_1.addr))

"""
fb structure:
                |<--------------- fb_width=128 x 3 = 384 --------------->|
    
 -------------- +==================+==================+==================+
 ^              | 128x64           | 128x64           | 128x64           |
 fb_height=64   |                  |                  |                  |
 v--------------+==================+==================+==================+
                ^                  ^                  ^
                fb0_x_offset=0     fb1_x_offset=128   fb2_x_offset=256
                
                I2C SSD1306        I2C SSD1306       SPI SSD1306
                addr = 0x3d        addr = 0x3c
                                          (default)
    
"""
# Grouped FrameBuffer = 384x64
fb_width=384
fb_height=64

fb0_x_offset=0
fb1_x_offset=128
fb2_x_offset=256

buffer = bytearray(fb_width*fb_height)
fb = framebuf.FrameBuffer(buffer, fb_width, fb_height, framebuf.MONO_HLSB)

# Raspberry Pi logo as 32x32 bytearray
pi_fb_width=32
pi_fb_height=32
pi_buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")

# Load the raspberry pi logo into the framebuffer (the image is 32x32)
pi_fb = framebuf.FrameBuffer(pi_buffer, 32, 32, framebuf.MONO_HLSB)

def update_display():
    oled_0.blit(fb, fb0_x_offset, 0)
    oled_1.blit(fb, -fb1_x_offset, 0)
    oled_2.blit(fb, -fb2_x_offset, 0)
    oled_0.show()
    oled_1.show()
    oled_2.show()

fb.fill(1)
update_display()
time.sleep(0.5)
fb.fill(0)
update_display()
time.sleep(0.5)
    
fb.rect(0, 0, fb_width, fb_height, 1)
fb.text(sys_msg_1, 3, 10, 1)
fb.text(sys_msg_2, 3, 20, 1)
fb.text(package_info, 3, 30, 1)
update_display()
time.sleep(3)

update_display()
time.sleep(1)
fb.fill(0)
time.sleep(0.5)

blitx = 0
blity = 0
xdir = 1
xlim = fb_width-pi_fb_width
ydir = 1
ylim = fb_height-pi_fb_height
step_x = 2
step_y = 1

while True:
    fb.blit(pi_fb, blitx, blity)
    update_display()
    
    #prepare next round
    blitx = blitx + (xdir*step_x)
    if blitx < 0:
        blitx=1
        xdir=1
    if blitx >= xlim:
        blitx=xlim-1
        xdir=-1
        
    blity = blity + (ydir*step_y)
    if blity < 0:
        blity=1
        ydir=1
    if blity >= ylim:
        blity=ylim-1
        ydir=-1
        
    time.sleep(0.01)

print("~ bye ~")


mpyPico2_multiOLED_one_framebuf.py, groupedOLED_class is implemented, easier to append OLEDs at runtime.
"""
MicroPython/Raspberry Pi Pico 2 exercise
display on three OLED, 0.96" 128x64 SSD1306 OLED
- two I2C OLED share one I2C with different I2C address
- and one SPI OLED.

Three OLEDs as one FrameBuffer

groupedOLED_class is implemented, easier to append OLEDs at runtime.

To install MicroPython ssd1306 driver on Pico 2. read:
https://coxxect.blogspot.com/2024/09/i2c-oled-ssd1306ssd1315-screen-with.html

Two I2C OLEDs are connected to the same I2C(0) with different I2C address
The I2C address of the left SSD1306 I2C OLED = 0x3d
The I2C address of the mid SSD1306 I2C OLED = 0x3c (default)
SCL - GP5
SDA - GP4

SPI OLED in the right is connected to SPI(1)
SCK  - GP10
MOSI - GP11
MISO - GP8  (Not use)
"""
import sys
import os
import time
import ssd1306
import framebuf

DISP_WIDTH=128
DISP_HEIGHT=64

# notice that:
# parameter cs have to be provided to ssd1306.SSD1306_SPI(),
# so I assign a GP8 as dummt cs, no need connect it.
dc  = machine.Pin(13)
res = machine.Pin(12)
cs  = machine.Pin(9)   #dummy (any un-used pin), no connection

sys_msg_1 = sys.implementation[0] + " " + os.uname()[3]
sys_msg_2 = "run on " + os.uname()[4]
print("====================================")
print(sys_msg_1)
print(sys_msg_2)
print("====================================")
package_info = ssd1306.__name__
try:
    package_info = package_info + " ver:" + ssd1306.__version__
except AttributeError as exc:
    print("AttributeError!", exc)

print("ssd1306 package: ", package_info)
print("====================================")
oled_i2c = machine.I2C(0)
print("oled_i2c:", oled_i2c)
oled_spi = machine.SPI(1)
print("oled_spi:", oled_spi)
oled_0 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c, 0x3d)
oled_1 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c)
oled_2 = ssd1306.SSD1306_SPI(DISP_WIDTH, DISP_HEIGHT, oled_spi, dc, res, cs)


print("oled_0 I2C address:",
      oled_0.addr, "/", hex(oled_0.addr))

print("oled_1 I2C address:",
      oled_1.addr, "/", hex(oled_1.addr))

"""
fb structure:
                |<--------------- fb_width=128 x 3 = 384 --------------->|
    
 -------------- +==================+==================+==================+
 ^              | 128x64           | 128x64           | 128x64           |
 fb_height=64   |                  |                  |                  |
 v--------------+==================+==================+==================+
                ^                  ^                  ^
                fb0_x_offset=0     fb1_x_offset=128   fb2_x_offset=256
                
                I2C SSD1306        I2C SSD1306       SPI SSD1306
                addr = 0x3d        addr = 0x3c
                                          (default)
    
"""
class groupedOLED_class():
    def __init__(self):
        self.OLED_group = []
        self.reCreateFrameBuffer()
        
    def reCreateFrameBuffer(self):
        if len(self.OLED_group) == 0:
            self.width = 0
            self.height = 0
            self.fb = None;
            print("OLED_group is empty")
        else:
            self.height = self.OLED_group[0].height
            fb_width = 0
            for oled in self.OLED_group:
                fb_width = fb_width + oled.width
            self.width = fb_width
                
            self.fb = framebuf.FrameBuffer(bytearray(self.width*self.height),
                                           self.width,
                                           self.height,
                                           framebuf.MONO_HLSB)
            print("new FrameBuffer created:", self.width, "x", self.height) 
        
    def append(self, new_oled):
        self.OLED_group.append(new_oled)
        self.reCreateFrameBuffer()
        
    def update_display(self):
        next_x_offset = 0
        for oled in self.OLED_group:
            oled.blit(self.fb, -next_x_offset, 0)
            next_x_offset = next_x_offset + oled.width
        for oled in self.OLED_group:
            oled.show()



# Raspberry Pi logo as 32x32 bytearray
pi_fb_width=32
pi_fb_height=32
pi_buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")

# Load the raspberry pi logo into the framebuffer (the image is 32x32)
pi_fb = framebuf.FrameBuffer(pi_buffer, 32, 32, framebuf.MONO_HLSB)

groupedOLED = groupedOLED_class()
groupedOLED.append(oled_0)
groupedOLED.append(oled_1)
groupedOLED.append(oled_2)

print("groupedOLED: ", groupedOLED.width, "x", groupedOLED.height)

groupedOLED.fb.fill(1)
groupedOLED.update_display()
time.sleep(0.5)
groupedOLED.fb.fill(0)
groupedOLED.update_display()
time.sleep(0.5)

def test_msg():
    groupedOLED.fb.fill(0)
    groupedOLED.fb.rect(0, 0, groupedOLED.width, groupedOLED.height, 1)
    groupedOLED.fb.text(sys_msg_1, 3, 10, 1)
    groupedOLED.fb.text(sys_msg_2, 3, 20, 1)
    groupedOLED.fb.text(package_info, 3, 30, 1)
    groupedOLED.update_display()
    time.sleep(2)
    
test_msg()
    

blitx = 1
blity = 1
xdir = 1
xlim = groupedOLED.width-pi_fb_width
ydir = 1
ylim = groupedOLED.height-pi_fb_height
step_x = 2
step_y = 1

while True:
    groupedOLED.fb.blit(pi_fb, blitx, blity)
    groupedOLED.update_display()
    
    #prepare next round
    blitx = blitx + (xdir*step_x)
    if blitx <= 0:
        blitx=1
        xdir=1
    if blitx >= xlim:
        blitx=xlim-1
        xdir=-1
        
    blity = blity + (ydir*step_y)
    if blity <= 0:
        blity=1
        ydir=1
    if blity >= ylim:
        blity=ylim-1
        ydir=-1
        
    time.sleep(0.01)

print("~ bye ~")


mpyPico2_multiOLED_test.py, append OLEDs at runtime, with various test: hline, vline, line, rect ellipse, circle.
"""
MicroPython/Raspberry Pi Pico 2 exercise
display on three OLED, 0.96" 128x64 SSD1306 OLED
- two I2C OLED share one I2C with different I2C address
- and one SPI OLED.

Three OLEDs as one FrameBuffer,
with various test: hline, vline, line, rect, ellipse, circle

groupedOLED_class is implemented, easier to append OLEDs at runtime.

To install MicroPython ssd1306 driver on Pico 2. read:
https://coxxect.blogspot.com/2024/09/i2c-oled-ssd1306ssd1315-screen-with.html

Two I2C OLEDs are connected to the same I2C(0) with different I2C address
The I2C address of the left SSD1306 I2C OLED = 0x3d
The I2C address of the mid SSD1306 I2C OLED = 0x3c (default)
SCL - GP5
SDA - GP4

SPI OLED in the right is connected to SPI(1)
SCK  - GP10
MOSI - GP11
MISO - GP8  (Not use)

ref. MicroPython framebuf:
https://docs.micropython.org/en/latest/library/framebuf.html
"""
import sys
import os
import time
import ssd1306
import framebuf

DISP_WIDTH=128
DISP_HEIGHT=64

# notice that:
# parameter cs have to be provided to ssd1306.SSD1306_SPI(),
# so I assign a GP8 as dummt cs, no need connect it.
dc  = machine.Pin(13)
res = machine.Pin(12)
cs  = machine.Pin(9)   #dummy (any un-used pin), no connection

sys_msg_1 = sys.implementation[0] + " " + os.uname()[3]
sys_msg_2 = "run on " + os.uname()[4]
print("====================================")
print(sys_msg_1)
print(sys_msg_2)
print("====================================")
package_info = ssd1306.__name__
try:
    package_info = package_info + " ver:" + ssd1306.__version__
except AttributeError as exc:
    print("AttributeError!", exc)

print("ssd1306 package: ", package_info)
print("====================================")
oled_i2c = machine.I2C(0)
print("oled_i2c:", oled_i2c)
oled_spi = machine.SPI(1)
print("oled_spi:", oled_spi)
oled_0 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c, 0x3d)
oled_1 = ssd1306.SSD1306_I2C(DISP_WIDTH, DISP_HEIGHT, oled_i2c)
oled_2 = ssd1306.SSD1306_SPI(DISP_WIDTH, DISP_HEIGHT, oled_spi, dc, res, cs)

print("oled_0 I2C address:",
      oled_0.addr, "/", hex(oled_0.addr))

print("oled_1 I2C address:",
      oled_1.addr, "/", hex(oled_1.addr))

"""
fb structure:
                |<--------------- fb_width=128 x 3 = 384 --------------->|
    
 -------------- +==================+==================+==================+
 ^              | 128x64           | 128x64           | 128x64           |
 fb_height=64   |                  |                  |                  |
 v--------------+==================+==================+==================+
                ^                  ^                  ^
                fb0_x_offset=0     fb1_x_offset=128   fb2_x_offset=256
                
                I2C SSD1306        I2C SSD1306       SPI SSD1306
                addr = 0x3d        addr = 0x3c
                                          (default)
    
"""
class groupedOLED_class():
    def __init__(self):
        self.OLED_group = []
        self.reCreateFrameBuffer()
        
    def reCreateFrameBuffer(self):
        if len(self.OLED_group) == 0:
            self.width = 0
            self.height = 0
            self.fb = None;
            print("OLED_group is empty")
        else:
            self.height = self.OLED_group[0].height
            fb_width = 0
            for oled in self.OLED_group:
                fb_width = fb_width + oled.width
            self.width = fb_width
                
            self.fb = framebuf.FrameBuffer(bytearray(self.width*self.height),
                                           self.width,
                                           self.height,
                                           framebuf.MONO_HLSB)
            print("new FrameBuffer created:", self.width, "x", self.height) 
        
    def append(self, new_oled):
        self.OLED_group.append(new_oled)
        self.reCreateFrameBuffer()
        
    def update_display(self):
        next_x_offset = 0
        for oled in self.OLED_group:
            oled.blit(self.fb, -next_x_offset, 0)
            next_x_offset = next_x_offset + oled.width
        for oled in self.OLED_group:
            oled.show()



# Raspberry Pi logo as 32x32 bytearray
pi_fb_width=32
pi_fb_height=32
pi_buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")

# Load the raspberry pi logo into the framebuffer (the image is 32x32)
pi_fb = framebuf.FrameBuffer(pi_buffer, 32, 32, framebuf.MONO_HLSB)

def clear_screen():
    groupedOLED.fb.fill(0)
    groupedOLED.update_display()

def test_hline(steps=10):
    clear_screen()
    time.sleep(0.2)
    x = 10
    y = 0
    w = 0
    for i in range(steps):
        groupedOLED.fb.hline(x,
                             int(groupedOLED.height * i/steps),
                             int(groupedOLED.width * i/steps),
                             1)
        groupedOLED.update_display()
        time.sleep(0.1)
        
def test_vline(steps=10):
    clear_screen()
    time.sleep(0.2)
    x = 0
    y = 5
    w = 0
    for i in range(steps): 
        groupedOLED.fb.vline(int(groupedOLED.width * i/steps),
                             y,
                             int(groupedOLED.height * i/steps),
                             1)
        groupedOLED.update_display()
        time.sleep(0.1)
        
def test_line(steps=10):
    clear_screen()
    time.sleep(0.2)
    for i in range(steps):
        groupedOLED.fb.line(int(groupedOLED.width * i/steps),
                            0,
                            groupedOLED.width-1,
                            int(groupedOLED.height * i/steps),
                            1)
        groupedOLED.update_display()
        time.sleep(0.1)
        
def test_rect(steps=10):
    clear_screen()
    time.sleep(0.2)
    for i in range(steps):
        groupedOLED.fb.rect(int(groupedOLED.width * i/steps),
                            int(groupedOLED.height * i/steps),
                            int(groupedOLED.width * (steps - 2*i)/steps),
                            int(groupedOLED.height * (steps - 2*i)/steps),
                            1)
        groupedOLED.update_display()
        time.sleep(0.1)
        
def test_ellipse(steps=10):
    clear_screen()
    time.sleep(0.2)
    for i in range(steps):
        x = int(groupedOLED.width/2)
        y = int(groupedOLED.height/2)
        xr = int(groupedOLED.width/2 * i/steps)
        yr = int(groupedOLED.height/2 * i/steps)
        
        # in my test, xr, yr cannot be 0
        xr = 1 if xr == 0 else xr
        yr = 1 if xr == 0 else yr
        groupedOLED.fb.ellipse(x, y, xr, yr, 1)
        
        groupedOLED.update_display()
        time.sleep(0.1)
        
def test_circle(steps=10):
    clear_screen()
    time.sleep(0.2)
    for i in range(steps):
        x = int(groupedOLED.width * i/steps)
        y = int(groupedOLED.height/2)
        r = int(groupedOLED.height/2 * i/steps)
        
        # in my test, r (xr, yr in ellipse) cannot be 0
        r = 1 if r == 0 else r
        groupedOLED.fb.ellipse(x, y, r, r, 1)
        
        groupedOLED.update_display()
        time.sleep(0.1)

def grouped_test():
    clear_screen()
    time.sleep(0.5)
    test_hline()
    time.sleep(0.5)
    test_vline(30)
    time.sleep(0.5)
    test_line(15)
    time.sleep(0.5)
    test_rect(15)
    time.sleep(0.5)
    test_ellipse(8)
    time.sleep(0.5)
    test_circle(20)

groupedOLED = groupedOLED_class()
groupedOLED.append(oled_0)
print("groupedOLED: ", groupedOLED.width, "x", groupedOLED.height)
grouped_test()

groupedOLED.append(oled_1)
print("groupedOLED: ", groupedOLED.width, "x", groupedOLED.height)
grouped_test()

groupedOLED.append(oled_2)
print("groupedOLED: ", groupedOLED.width, "x", groupedOLED.height)
grouped_test()

print("~ bye ~")



Comments

Popular posts from this blog

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

my dev.tools - FNIRSI 2C23T 3-in-1 Dual Channel Oscilloscope/Multimeter/Signal Generator