1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
# Support for UC1701 (and similar) 128x64 graphics LCD displays
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
from . import font8x14
BACKGROUND_PRIORITY_CLOCK = 0x7FFFFFFF00000000
TextGlyphs = {"right_arrow": b"\x1a", "degrees": b"\xf8"}
class DisplayBase:
def __init__(self, io, columns=128, x_offset=0):
self.send = io.send
# framebuffers
self.columns = columns
self.x_offset = x_offset
self.vram = [bytearray(self.columns) for i in range(8)]
self.all_framebuffers = [
(self.vram[i], bytearray(b"~" * self.columns), i) for i in range(8)
]
# Cache fonts and icons in display byte order
self.font = [self._swizzle_bits(bytearray(c)) for c in font8x14.VGA_FONT]
self.icons = {}
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, page in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [
[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data)) if n != o
]
# Batch together changes that are close to each other
for i in range(len(diffs) - 2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i + 1]
if pos + 5 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i + 1]
# Transmit changes
for col_pos, count in diffs:
# Set Position registers
ra = 0xB0 | (page & 0x0F)
ca_msb = 0x10 | ((col_pos >> 4) & 0x0F)
ca_lsb = col_pos & 0x0F
self.send([ra, ca_msb, ca_lsb])
# Send Data
self.send(new_data[col_pos : col_pos + count], is_data=True)
old_data[:] = new_data
def _swizzle_bits(self, data):
# Convert from "rows of pixels" format to "columns of pixels"
top = bot = 0
for row in range(8):
spaced = (data[row] * 0x8040201008040201) & 0x8080808080808080
top |= spaced >> (7 - row)
spaced = (data[row + 8] * 0x8040201008040201) & 0x8080808080808080
bot |= spaced >> (7 - row)
bits_top = [(top >> s) & 0xFF for s in range(0, 64, 8)]
bits_bot = [(bot >> s) & 0xFF for s in range(0, 64, 8)]
return (bytearray(bits_top), bytearray(bits_bot))
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
icon = glyph_data.get("icon16x16")
if icon is not None:
top1, bot1 = self._swizzle_bits(icon[0])
top2, bot2 = self._swizzle_bits(icon[1])
self.icons[glyph_name] = (top1 + top2, bot1 + bot2)
def write_text(self, x, y, data):
if x + len(data) > 16:
data = data[: 16 - min(x, 16)]
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for c in bytearray(data):
bits_top, bits_bot = self.font[c]
page_top[pix_x : pix_x + 8] = bits_top
page_bot[pix_x : pix_x + 8] = bits_bot
pix_x += 8
def write_graphics(self, x, y, data):
if x >= 16 or y >= 4 or len(data) != 16:
return
bits_top, bits_bot = self._swizzle_bits(data)
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for i in range(8):
page_top[pix_x + i] ^= bits_top[i]
page_bot[pix_x + i] ^= bits_bot[i]
def write_glyph(self, x, y, glyph_name):
icon = self.icons.get(glyph_name)
if icon is not None and x < 15:
# Draw icon in graphics mode
pix_x = x * 8
pix_x += self.x_offset
page_idx = y * 2
self.vram[page_idx][pix_x : pix_x + 16] = icon[0]
self.vram[page_idx + 1][pix_x : pix_x + 16] = icon[1]
return 2
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def clear(self):
zeros = bytearray(self.columns)
for page in self.vram:
page[:] = zeros
def get_dimensions(self):
return (16, 4)
# IO wrapper for "4 wire" spi bus (spi bus with an extra data/control line)
class SPI4wire:
def __init__(self, config, data_pin_name):
self.spi = bus.MCU_SPI_from_config(config, 0, default_speed=10000000)
dc_pin = config.get(data_pin_name)
self.mcu_dc = bus.MCU_bus_digital_out(
self.spi.get_mcu(), dc_pin, self.spi.get_command_queue()
)
def send(self, cmds, is_data=False):
self.mcu_dc.update_digital_out(is_data, reqclock=BACKGROUND_PRIORITY_CLOCK)
self.spi.spi_send(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# IO wrapper for i2c bus
class I2C:
def __init__(self, config, default_addr):
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=default_addr, default_speed=400000
)
def send(self, cmds, is_data=False):
if is_data:
hdr = 0x40
else:
hdr = 0x00
cmds = bytearray(cmds)
cmds.insert(0, hdr)
self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# Helper code for toggling a reset pin on startup
class ResetHelper:
def __init__(self, pin_desc, io_bus):
self.mcu_reset = None
if pin_desc is None:
return
self.mcu_reset = bus.MCU_bus_digital_out(
io_bus.get_mcu(), pin_desc, io_bus.get_command_queue()
)
def init(self):
if self.mcu_reset is None:
return
mcu = self.mcu_reset.get_mcu()
curtime = mcu.get_printer().get_reactor().monotonic()
print_time = mcu.estimated_print_time(curtime)
# Toggle reset
minclock = mcu.print_time_to_clock(print_time + 0.100)
self.mcu_reset.update_digital_out(0, minclock=minclock)
minclock = mcu.print_time_to_clock(print_time + 0.200)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# Force a delay to any subsequent commands on the command queue
minclock = mcu.print_time_to_clock(print_time + 0.300)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# The UC1701 is a "4-wire" SPI display device
class UC1701(DisplayBase):
def __init__(self, config):
io = SPI4wire(config, "a0_pin")
DisplayBase.__init__(self, io)
self.contrast = config.getint("contrast", 40, minval=0, maxval=63)
self.reset = ResetHelper(config.get("rst_pin", None), io.spi)
def init(self):
self.reset.init()
init_cmds = [
0xE2, # System reset
0x40, # Set display to start at line 0
0xA0, # Set SEG direction
0xC8, # Set COM Direction
0xA2, # Set Bias = 1/9
0x2C, # Boost ON
0x2E, # Voltage regulator on
0x2F, # Voltage follower on
0xF8, # Set booster ratio
0x00, # Booster ratio value (4x)
0x23, # Set resistor ratio (3)
0x81, # Set Electronic Volume
self.contrast, # Electronic Volume value
0xAC, # Set static indicator off
0x00, # NOP
0xA6, # Disable Inverse
0xAF,
] # Set display enable
self.send(init_cmds)
self.send([0xA5]) # display all
self.send([0xA4]) # normal display
self.flush()
# The SSD1306 supports both i2c and "4-wire" spi
class SSD1306(DisplayBase):
def __init__(self, config, columns=128, x_offset=0):
cs_pin = config.get("cs_pin", None)
if cs_pin is None:
io = I2C(config, 60)
io_bus = io.i2c
else:
io = SPI4wire(config, "dc_pin")
io_bus = io.spi
self.reset = ResetHelper(config.get("reset_pin", None), io_bus)
DisplayBase.__init__(self, io, columns, x_offset)
self.contrast = config.getint("contrast", 239, minval=0, maxval=255)
self.vcomh = config.getint("vcomh", 0, minval=0, maxval=63)
self.invert = config.getboolean("invert", False)
def init(self):
self.reset.init()
init_cmds = [
0xAE, # Display off
0xD5,
0x80, # Set oscillator frequency
0xA8,
0x3F, # Set multiplex ratio
0xD3,
0x00, # Set display offset
0x40, # Set display start line
0x8D,
0x14, # Charge pump setting
0x20,
0x02, # Set Memory addressing mode
0xA1, # Set Segment re-map
0xC8, # Set COM output scan direction
0xDA,
0x12, # Set COM pins hardware configuration
0x81,
self.contrast, # Set contrast control
0xD9,
0xA1, # Set pre-charge period
0xDB,
self.vcomh, # Set VCOMH deselect level
0x2E, # Deactivate scroll
0xA4, # Output ram to display
0xA7 if self.invert else 0xA6, # Set normal/invert
0xAF, # Display on
]
self.send(init_cmds)
self.flush()
# the SH1106 is SSD1306 compatible with up to 132 columns
class SH1106(SSD1306):
def __init__(self, config):
x_offset = config.getint("x_offset", 0, minval=0, maxval=3)
SSD1306.__init__(self, config, 132, x_offset=x_offset)
|