Skip to content

Commit 6b1dff6

Browse files
committed
cores/lcd/ssd1306: add a wrapper to send i2c address and control byte
1 parent adb725e commit 6b1dff6

File tree

1 file changed

+93
-70
lines changed

1 file changed

+93
-70
lines changed

lambdalib/cores/lcd/ssd1306.py

Lines changed: 93 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,60 @@
3636
SSD1306_SETCOMPINS = 0xDA
3737
SSD1306_SETVCOMDETECT = 0xDB
3838

39+
40+
class SSD1306_Wrapper(Elaboratable):
41+
def __init__(self):
42+
self.sink = stream.Endpoint([
43+
("d_cn", 1),
44+
("data", 8),
45+
])
46+
self.source = stream.Endpoint([
47+
("r_wn", 1),
48+
("data", 8),
49+
])
50+
51+
def elaborate(self, platform):
52+
sink = self.sink
53+
source = self.source
54+
55+
m = Module()
56+
57+
m.d.comb += source.r_wn.eq(0) # Write only
58+
59+
with m.FSM():
60+
# Send the I2C address and control byte
61+
with m.State("ADDR"):
62+
m.d.comb += [
63+
source.data .eq(SSD1306_I2C_ADDRESS << 1),
64+
source.valid.eq(sink.valid),
65+
]
66+
with m.If(source.valid & source.ready):
67+
m.next = "CONTROL"
68+
69+
with m.State("CONTROL"):
70+
m.d.comb += [
71+
# Control byte:
72+
# Command: 0x00: Co = 0, D/C# = 0
73+
# Data: 0x40: Co = 0, D/C# = 1
74+
source.data .eq(Mux(sink.d_cn, 0x40, 0x00)),
75+
source.valid.eq(sink.valid),
76+
]
77+
with m.If(source.valid & source.ready):
78+
m.next = "DATA"
79+
80+
with m.State("DATA"):
81+
m.d.comb += [
82+
source.data. eq(sink.data),
83+
source.valid.eq(sink.valid),
84+
source.last .eq(sink.last),
85+
sink .ready.eq(source.ready),
86+
]
87+
with m.If(source.valid & source.ready & source.last):
88+
m.next = "ADDR"
89+
90+
return m
91+
92+
3993
class SSD1306(Elaboratable):
4094
""" Driver for SSD1306 based LCD screen.
4195
@@ -48,19 +102,14 @@ class SSD1306(Elaboratable):
48102
The screen width in pixels.
49103
height : int
50104
The screen height in pixels.
51-
burst_len : int
52-
Specify the maximum amount of framebuffer bytes that can be
53-
sent at a time before closing the I2C transaction.
54-
0 means unlimited.
55105
por_init : bool
56106
When True, the screen is automatically initialized upon power on reset.
57107
when False, the user need to assert `reset` for one clock cycle.
58108
"""
59109

60-
def __init__(self, width, height, burst_len=0, por_init=True):
110+
def __init__(self, width, height, por_init=True):
61111
self.width = width
62112
self.height = height
63-
self.burst_len = burst_len
64113
self.por_init = por_init
65114

66115
# Table from https://github.com/rm-hull/luma.oled/blob/main/luma/oled/device/__init__.py
@@ -80,9 +129,6 @@ def __init__(self, width, height, burst_len=0, por_init=True):
80129
self._colstart = settings["colstart"]
81130
self._colend = self._colstart + width
82131

83-
if self.burst_len == 0:
84-
self.burst_len = self._size
85-
86132
self.reset = Signal()
87133
self.ready = Signal()
88134

@@ -96,16 +142,6 @@ def __init__(self, width, height, burst_len=0, por_init=True):
96142
])
97143
self.error = Signal()
98144

99-
def cmds_to_mem(self, cmds):
100-
mem = []
101-
102-
for cmd in cmds:
103-
mem.append(SSD1306_I2C_ADDRESS << 1) # Write
104-
mem.append(0x00) # Co = 0, D/C# = 0
105-
mem.append(cmd)
106-
107-
return mem
108-
109145
def elaborate(self, platform):
110146
sink = self.sink
111147
source = self.source
@@ -140,10 +176,7 @@ def elaborate(self, platform):
140176
SSD1306_NORMALDISPLAY,
141177
SSD1306_DISPLAYON,
142178
]
143-
blob = self.cmds_to_mem(cmds)
144-
145-
m.submodules.init = init = \
146-
LastInserter(3)(MemoryStreamReader(8, blob))
179+
m.submodules.init = init = MemoryStreamReader(8, cmds)
147180

148181
# Recipe for sending a framebuffer
149182
cmds = [
@@ -154,12 +187,13 @@ def elaborate(self, platform):
154187
0, # Page start address. (0 = reset)
155188
self._pages - 1, # Page end address.
156189
]
157-
blob = self.cmds_to_mem(cmds)
190+
m.submodules.display = display = MemoryStreamReader(8, cmds)
158191

159-
m.submodules.display = display = \
160-
LastInserter(3)(MemoryStreamReader(8, blob))
192+
# Instanciate the I2C address and control byte wrapper
193+
m.submodules.wrapper = wrapper = SSD1306_Wrapper()
194+
m.d.comb += wrapper.source.connect(source)
161195

162-
cnt = Signal(range(self.burst_len + 2))
196+
cnt = Signal(range(self._size))
163197

164198
with m.FSM():
165199
with m.State("UNKNOWN"):
@@ -178,10 +212,11 @@ def elaborate(self, platform):
178212
# Send the appropriate sequence to power on
179213
# and initialize the display.
180214
m.d.comb += [
181-
init.source.connect(source),
182-
source.r_wn.eq(0), # Write only
215+
init.source.connect(wrapper.sink, exclude={"last"}),
216+
wrapper.sink.d_cn.eq(0), # Commands
217+
wrapper.sink.last.eq(1),
183218
]
184-
with m.If(init.done & ~source.valid):
219+
with m.If(init.done & ~init.source.valid):
185220
m.next = "DISPLAY"
186221

187222
with m.State("DISPLAY"):
@@ -190,56 +225,44 @@ def elaborate(self, platform):
190225

191226
# Send the appropriate sequence to prepare
192227
# for a frame buffer write.
193-
with m.Elif(sink.valid): # ~sink.ready
228+
with m.Elif(~self.ready | sink.valid):
194229
m.d.comb += [
195-
display.source.connect(source),
196-
source.r_wn.eq(0), # Write only
230+
display.source.connect(wrapper.sink, exclude={"last"}),
231+
wrapper.sink.d_cn.eq(0), # Commands
232+
wrapper.sink.last.eq(1),
197233
]
198-
with m.If(display.done & ~source.valid):
234+
with m.If(display.done & ~display.source.valid):
199235
# On the first time after initialization
200236
# we want to clear the frame buffer to make
201237
# sure we do not display crap.
202-
# with m.If(~self.ready):
203-
# m.next = "CLEAR"
204-
# with m.Else():
205-
m.next = "FRAMEBUFFER"
238+
with m.If(~self.ready):
239+
m.next = "CLEAR"
240+
with m.Else():
241+
m.next = "FRAMEBUFFER"
206242

207-
with m.State("FRAMEBUFFER"):
243+
with m.State("CLEAR"):
208244
m.d.comb += [
209-
source.r_wn.eq(0),
210-
source.last.eq((cnt == self.burst_len+2-1) | sink.last),
245+
wrapper.sink.valid.eq(1),
246+
wrapper.sink.data .eq(0), # Black pixels
247+
wrapper.sink.last .eq(cnt == self._size-1),
248+
wrapper.sink.d_cn .eq(1), # Framebuffer data
211249
]
212-
213-
# Send the I2C address and control byte,
214-
# then send the frame buffer data up to burst length.
215-
with m.If(cnt == 0):
216-
m.d.comb += [
217-
source.data .eq(SSD1306_I2C_ADDRESS << 1),
218-
source.valid.eq(1),
219-
]
220-
with m.Elif(cnt == 1):
221-
m.d.comb += [
222-
source.data .eq(0x40), # Control byte: Co = 0, D/C# = 1
223-
source.valid.eq(1),
224-
]
225-
with m.Else():
226-
m.d.comb += [
227-
source.data .eq(sink.data),
228-
source.valid.eq(sink.valid),
229-
sink .ready.eq(source.ready),
230-
]
231-
232-
# End of burst detection
233-
# Reset the counter and stay in this state
234-
# to send the next burst, or go back to the
235-
# DISPLAY state when the end of the framebuffer is reached.
236-
with m.If(source.valid & source.ready):
237-
with m.If(~source.last):
250+
with m.If(wrapper.sink.ready):
251+
with m.If(~wrapper.sink.last):
238252
m.d.sync += cnt.eq(cnt + 1)
239253
with m.Else():
240254
m.d.sync += cnt.eq(0)
241-
with m.If(sink.last):
242-
m.d.comb += display.rewind.eq(1)
243-
m.next = "DISPLAY"
255+
m.d.sync += self.ready.eq(1)
256+
m.d.comb += display.rewind.eq(1)
257+
m.next = "DISPLAY"
258+
259+
with m.State("FRAMEBUFFER"):
260+
m.d.comb += [
261+
sink.connect(wrapper.sink),
262+
wrapper.sink.d_cn.eq(1),
263+
]
264+
with m.If(sink.valid & sink.ready & sink.last):
265+
m.d.comb += display.rewind.eq(1)
266+
m.next = "DISPLAY"
244267

245268
return m

0 commit comments

Comments
 (0)