Skip to content

Commit 00ea1db

Browse files
committed
Add SSD1306 module and example
1 parent d047ae5 commit 00ea1db

3 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# FILE: SSD1306-example.py
2+
# AUTHOR: Soldered
3+
# BRIEF: An example showing various functionalities of the SSD1306 OLED display
4+
# WORKS WITH: Display OLED I2C 0.96" SSD1306 (Blue screen color): www.solde.red/333100
5+
# Display OLED I2C 0.96" SSD1306 (White screen color): www.solde.red/333099
6+
# LAST UPDATED: 2025-06-10
7+
8+
# Import required libraries
9+
from ssd1306 import SSD1306 # OLED display driver
10+
import time # For delays and timing
11+
import random # For random number generation
12+
import framebuf # For frame buffer operations
13+
from machine import I2C,Pin
14+
15+
# Initialize the OLED display
16+
display = SSD1306() # Creates an instance of the SSD1306 OLED display
17+
18+
# If you aren't using the Qwiic connector, manually enter your I2C pins
19+
# i2c = I2C(0, scl=Pin(22), sda=Pin(21))
20+
# display = SSD1306(i2c)
21+
22+
23+
# Bitmap data for a 16x16 pixel logo
24+
# Stored as a bytearray where each byte represents 8 vertical pixels
25+
LOGO_WIDTH = 16 # Width of the logo in pixels
26+
LOGO_HEIGHT = 16 # Height of the logo in pixels
27+
logo_bmp = bytearray([
28+
0b00000000, 0b11000000, 0b00000001, 0b11000000, 0b00000001, 0b11000000, 0b00000011, 0b11100000,
29+
0b11110011, 0b11100000, 0b11111110, 0b11111000, 0b01111110, 0b11111111, 0b00110011, 0b10011111,
30+
0b00011111, 0b11111100, 0b00001101, 0b01110000, 0b00011011, 0b10100000, 0b00111111, 0b11100000,
31+
0b00111111, 0b11110000, 0b01111100, 0b11110000, 0b01110000, 0b01110000, 0b00000000, 0b00110000])
32+
33+
def test_draw_line():
34+
"""Demonstrates drawing lines in various directions on the display"""
35+
display.fill(0) # Clear display (0 = black, 1 = white)
36+
37+
# Draw lines from top-left corner to right edge, moving down
38+
for i in range(0, display.width, 4):
39+
display.line(0, 0, i, display.height-1, 1) # Draw line (x0,y0 to x1,y1, color)
40+
display.show() # Update the display
41+
time.sleep_ms(1) # Short delay for animation effect
42+
43+
# Draw lines from top-left corner to bottom edge, moving right
44+
for i in range(0, display.height, 4):
45+
display.line(0, 0, display.width-1, i, 1)
46+
display.show()
47+
time.sleep_ms(1)
48+
49+
time.sleep_ms(250) # Pause before next pattern
50+
display.fill(0) # Clear display
51+
52+
# Draw lines from bottom-left corner to right edge, moving up
53+
for i in range(0, display.width, 4):
54+
display.line(0, display.height-1, i, 0, 1)
55+
display.show()
56+
time.sleep_ms(1)
57+
58+
# Draw lines from bottom-left corner to top edge, moving right
59+
for i in range(display.height-1, -1, -4): # Count down from bottom
60+
display.line(0, display.height-1, display.width-1, i, 1)
61+
display.show()
62+
time.sleep_ms(1)
63+
64+
time.sleep_ms(250)
65+
display.fill(0)
66+
67+
# Draw lines from bottom-right corner to left edge, moving up
68+
for i in range(display.width-1, -1, -4):
69+
display.line(display.width-1, display.height-1, i, 0, 1)
70+
display.show()
71+
time.sleep_ms(1)
72+
73+
# Draw lines from bottom-right corner to top edge, moving left
74+
for i in range(display.height-1, -1, -4):
75+
display.line(display.width-1, display.height-1, 0, i, 1)
76+
display.show()
77+
time.sleep_ms(1)
78+
79+
time.sleep_ms(250)
80+
display.fill(0)
81+
82+
# Draw lines from top-right corner to left edge, moving down
83+
for i in range(0, display.height, 4):
84+
display.line(display.width-1, 0, 0, i, 1)
85+
display.show()
86+
time.sleep_ms(1)
87+
88+
# Draw lines from top-right corner to bottom edge, moving left
89+
for i in range(0, display.width, 4):
90+
display.line(display.width-1, 0, i, display.height-1, 1)
91+
display.show()
92+
time.sleep_ms(1)
93+
94+
time.sleep(2) # Final pause before ending function
95+
96+
def test_draw_rect():
97+
"""Demonstrates drawing rectangles of decreasing size"""
98+
display.fill(0)
99+
100+
# Draw concentric rectangles starting from the outside
101+
for i in range(0, display.height//2, 2):
102+
display.rect(i, i, display.width-2*i, display.height-2*i, 1)
103+
display.show()
104+
time.sleep_ms(1)
105+
106+
time.sleep(2)
107+
108+
def test_fill_rect():
109+
"""Demonstrates drawing filled rectangles with alternating colors"""
110+
display.fill(0)
111+
112+
for i in range(0, display.height//2, 3):
113+
# Draw filled rectangle (inverse alternates between white and black)
114+
display.fill_rect(i, i, display.width-2*i, display.height-2*i, 1)
115+
display.show()
116+
time.sleep_ms(1)
117+
display.fill_rect(i, i, display.width-2*i, display.height-2*i, 0) # Inverse
118+
display.show()
119+
time.sleep_ms(1)
120+
121+
time.sleep(2)
122+
123+
def test_draw_round_rect():
124+
"""Demonstrates drawing rectangles (simulating rounded corners)"""
125+
# Note: MicroPython's framebuf doesn't have native rounded rectangles
126+
# So we use regular rectangles as an approximation
127+
display.fill(0)
128+
129+
for i in range(0, display.height//2-2, 2):
130+
display.rect(i, i, display.width-2*i, display.height-2*i, 1)
131+
display.show()
132+
time.sleep_ms(1)
133+
134+
time.sleep(2)
135+
136+
def test_fill_round_rect():
137+
"""Demonstrates filled rectangles (simulating rounded corners)"""
138+
display.fill(0)
139+
140+
for i in range(0, display.height//2-2, 2):
141+
display.fill_rect(i, i, display.width-2*i, display.height-2*i, 1)
142+
display.show()
143+
time.sleep_ms(1)
144+
display.fill_rect(i, i, display.width-2*i, display.height-2*i, 0) # Inverse
145+
display.show()
146+
time.sleep_ms(1)
147+
148+
time.sleep(2)
149+
150+
def test_draw_triangle():
151+
"""Demonstrates drawing triangles of increasing size"""
152+
display.fill(0)
153+
154+
for i in range(0, max(display.width, display.height)//2, 5):
155+
# Draw triangle by connecting three lines
156+
display.line(display.width//2, display.height//2-i,
157+
display.width//2-i, display.height//2+i, 1)
158+
display.line(display.width//2-i, display.height//2+i,
159+
display.width//2+i, display.height//2+i, 1)
160+
display.line(display.width//2+i, display.height//2+i,
161+
display.width//2, display.height//2-i, 1)
162+
display.show()
163+
time.sleep_ms(1)
164+
165+
time.sleep(2)
166+
167+
def test_fill_triangle():
168+
"""Demonstrates filled triangles using horizontal lines as an approximation"""
169+
display.fill(0)
170+
171+
for i in range(max(display.width, display.height)//2, 0, -5):
172+
# Since MicroPython doesn't have fill_triangle, we approximate with lines
173+
for y in range(display.height//2-i, display.height//2+i):
174+
# Calculate left and right x-coordinates for each horizontal line
175+
x_start = display.width//2 - i + (y - (display.height//2-i))
176+
x_end = display.width//2 + i - (y - (display.height//2-i))
177+
display.line(x_start, y, x_end, y, 1)
178+
display.show()
179+
time.sleep_ms(1)
180+
display.fill(0)
181+
display.show()
182+
time.sleep_ms(1)
183+
184+
time.sleep(2)
185+
186+
def test_draw_char():
187+
"""Demonstrates drawing text characters on the display"""
188+
display.fill(0)
189+
display.text("Hello World!", 0, 0, 1) # Text, x, y, color
190+
display.show()
191+
time.sleep(2)
192+
193+
def test_draw_styles():
194+
"""Demonstrates different text styles and formatting"""
195+
display.fill(0)
196+
display.text("Hello, world!", 0, 0, 1) # Simple text
197+
display.text(str(3.141592), 0, 10, 1) # Number conversion
198+
display.text("0xDEADBEEF", 0, 20, 1) # Hexadecimal value
199+
display.show()
200+
time.sleep(2)
201+
202+
def test_scroll_text():
203+
"""Demonstrates scrolling text horizontally"""
204+
display.fill(0)
205+
display.text("scrolling...", 10, 0, 1)
206+
display.show()
207+
time.sleep(0.1)
208+
209+
# Scroll text left
210+
for i in range(0, display.width):
211+
display.scroll(-1, 0) # Scroll x by -1, y by 0
212+
display.show()
213+
time.sleep_ms(50)
214+
215+
time.sleep(1)
216+
217+
def test_draw_bitmap():
218+
"""Demonstrates drawing a bitmap image (the predefined logo)"""
219+
display.fill(0)
220+
# Create a frame buffer from our bitmap data
221+
buf = framebuf.FrameBuffer(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, framebuf.MONO_HLSB)
222+
# Blit (copy) the buffer to the display, centered
223+
display.blit(buf, (display.width - LOGO_WIDTH) // 2, (display.height - LOGO_HEIGHT) // 2)
224+
display.show()
225+
time.sleep(1)
226+
227+
# Main program execution starts here
228+
display.fill(0) # Clear display
229+
display.text("Starting...", 0, 0, 1) # Show startup message
230+
display.show()
231+
time.sleep(2) # Pause so user can see the message
232+
233+
# Run all demonstration functions
234+
test_draw_line() # Line drawing demo
235+
test_draw_rect() # Rectangle outline demo
236+
test_fill_rect() # Filled rectangle demo
237+
test_draw_round_rect() # Rounded rectangle demo (approximation)
238+
test_fill_round_rect() # Filled rounded rectangle demo
239+
test_draw_triangle() # Triangle outline demo
240+
test_fill_triangle() # Filled triangle demo
241+
test_draw_char() # Text drawing demo
242+
test_draw_styles() # Text formatting demo
243+
test_scroll_text() # Text scrolling demo
244+
test_draw_bitmap() # Bitmap drawing demo
245+
246+
# Demonstrate display inversion
247+
display.invert(1) # Invert display colors
248+
time.sleep(1)
249+
display.invert(0) # Restore normal colors
250+
time.sleep(1)

SSD1306/SSD1306/ssd1306.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# FILE: ssd1306.py
2+
# AUTHOR: Adafruit
3+
# MODIFIED BY: Josip Šimun Kuči @ Soldered
4+
# BRIEF: A MicroPython module for the SSD1306 display that communicates via I2C
5+
# LAST UPDATED: 2025-06-10
6+
7+
from micropython import const
8+
import framebuf
9+
from os import uname
10+
from machine import I2C,Pin
11+
SSD1306_WIDTH=128
12+
SSD1306_HEIGHT=64
13+
14+
# register definitions
15+
SET_CONTRAST = const(0x81)
16+
SET_ENTIRE_ON = const(0xA4)
17+
SET_NORM_INV = const(0xA6)
18+
SET_DISP = const(0xAE)
19+
SET_MEM_ADDR = const(0x20)
20+
SET_COL_ADDR = const(0x21)
21+
SET_PAGE_ADDR = const(0x22)
22+
SET_DISP_START_LINE = const(0x40)
23+
SET_SEG_REMAP = const(0xA0)
24+
SET_MUX_RATIO = const(0xA8)
25+
SET_IREF_SELECT = const(0xAD)
26+
SET_COM_OUT_DIR = const(0xC0)
27+
SET_DISP_OFFSET = const(0xD3)
28+
SET_COM_PIN_CFG = const(0xDA)
29+
SET_DISP_CLK_DIV = const(0xD5)
30+
SET_PRECHARGE = const(0xD9)
31+
SET_VCOM_DESEL = const(0xDB)
32+
SET_CHARGE_PUMP = const(0x8D)
33+
34+
35+
# Subclassing FrameBuffer provides support for graphics primitives
36+
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
37+
class SSD1306(framebuf.FrameBuffer):
38+
def __init__(self, i2c=None, addr=0x3C, external_vcc=False):
39+
if i2c != None:
40+
self.i2c = i2c
41+
else:
42+
if uname().sysname == "esp32" or uname().sysname == "esp8266":
43+
self.i2c = I2C(0, scl=Pin(22), sda=Pin(21))
44+
else:
45+
raise Exception("Board not recognized, enter I2C pins manually")
46+
self.addr = addr
47+
self.temp = bytearray(2)
48+
self.write_list = [b"\x40", None] # Co=0, D/C#=1
49+
self.width = SSD1306_WIDTH
50+
self.height = SSD1306_HEIGHT
51+
self.external_vcc = external_vcc
52+
self.pages = self.height // 8
53+
self.buffer = bytearray(self.pages * self.width)
54+
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
55+
self.init_display()
56+
57+
def init_display(self):
58+
for cmd in (
59+
SET_DISP, # display off
60+
# address setting
61+
SET_MEM_ADDR,
62+
0x00, # horizontal
63+
# resolution and layout
64+
SET_DISP_START_LINE, # start at line 0
65+
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
66+
SET_MUX_RATIO,
67+
self.height - 1,
68+
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
69+
SET_DISP_OFFSET,
70+
0x00,
71+
SET_COM_PIN_CFG,
72+
0x02 if self.width > 2 * self.height else 0x12,
73+
# timing and driving scheme
74+
SET_DISP_CLK_DIV,
75+
0x80,
76+
SET_PRECHARGE,
77+
0x22 if self.external_vcc else 0xF1,
78+
SET_VCOM_DESEL,
79+
0x30, # 0.83*Vcc
80+
# display
81+
SET_CONTRAST,
82+
0xFF, # maximum
83+
SET_ENTIRE_ON, # output follows RAM contents
84+
SET_NORM_INV, # not inverted
85+
SET_IREF_SELECT,
86+
0x30, # enable internal IREF during display on
87+
# charge pump
88+
SET_CHARGE_PUMP,
89+
0x10 if self.external_vcc else 0x14,
90+
SET_DISP | 0x01, # display on
91+
): # on
92+
self.write_cmd(cmd)
93+
self.fill(0)
94+
self.show()
95+
96+
def poweroff(self):
97+
self.write_cmd(SET_DISP)
98+
99+
def poweron(self):
100+
self.write_cmd(SET_DISP | 0x01)
101+
102+
def contrast(self, contrast):
103+
self.write_cmd(SET_CONTRAST)
104+
self.write_cmd(contrast)
105+
106+
def invert(self, invert):
107+
self.write_cmd(SET_NORM_INV | (invert & 1))
108+
109+
def rotate(self, rotate):
110+
self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
111+
self.write_cmd(SET_SEG_REMAP | (rotate & 1))
112+
113+
def write_cmd(self, cmd):
114+
self.temp[0] = 0x80 # Co=1, D/C#=0
115+
self.temp[1] = cmd
116+
self.i2c.writeto(self.addr, self.temp)
117+
118+
def write_data(self, buf):
119+
self.write_list[1] = buf
120+
self.i2c.writevto(self.addr, self.write_list)
121+
122+
def show(self):
123+
x0 = 0
124+
x1 = self.width - 1
125+
if self.width != 128:
126+
# narrow displays use centred columns
127+
col_offset = (128 - self.width) // 2
128+
x0 += col_offset
129+
x1 += col_offset
130+
self.write_cmd(SET_COL_ADDR)
131+
self.write_cmd(x0)
132+
self.write_cmd(x1)
133+
self.write_cmd(SET_PAGE_ADDR)
134+
self.write_cmd(0)
135+
self.write_cmd(self.pages - 1)
136+
self.write_data(self.buffer)

0 commit comments

Comments
 (0)