|
| 1 | +import math |
| 2 | +import framebuf |
| 3 | +import utime as time |
| 4 | +from micropython import const |
| 5 | + |
| 6 | +# a few register definitions |
| 7 | +_SET_CONTRAST = const(0x81) |
| 8 | +_SET_NORM_INV = const(0xa6) |
| 9 | +_SET_DISP = const(0xae) |
| 10 | +_SET_SCAN_DIR = const(0xc0) |
| 11 | +_SET_SEG_REMAP = const(0xa0) |
| 12 | +_LOW_COLUMN_ADDRESS = const(0x00) |
| 13 | +_HIGH_COLUMN_ADDRESS = const(0x10) |
| 14 | +_SET_PAGE_ADDRESS = const(0xB0) |
| 15 | + |
| 16 | + |
| 17 | +class SH1106(framebuf.FrameBuffer): |
| 18 | + def __init__(self, width, height, external_vcc, rotate=0): |
| 19 | + self.width = width |
| 20 | + self.height = height |
| 21 | + self.external_vcc = external_vcc |
| 22 | + self.flip_en = rotate == 180 or rotate == 270 |
| 23 | + self.rotate90 = rotate == 90 or rotate == 270 |
| 24 | + self.pages = self.height // 8 |
| 25 | + self.bufsize = self.pages * self.width |
| 26 | + self.buffer = bytearray(self.bufsize) |
| 27 | + self.pages_to_update = 0 |
| 28 | + |
| 29 | + if self.rotate90: |
| 30 | + self.displaybuf = bytearray(self.bufsize) |
| 31 | + # HMSB is required to keep the bit order in the render buffer |
| 32 | + # compatible with byte-for-byte remapping to the display buffer, |
| 33 | + # which is in VLSB. Else we'd have to copy bit-by-bit! |
| 34 | + super().__init__(self.buffer, self.height, self.width, |
| 35 | + framebuf.MONO_HMSB) |
| 36 | + else: |
| 37 | + self.displaybuf = self.buffer |
| 38 | + super().__init__(self.buffer, self.width, self.height, |
| 39 | + framebuf.MONO_VLSB) |
| 40 | + |
| 41 | + # flip() was called rotate() once, provide backwards compatibility. |
| 42 | + self.rotate = self.flip |
| 43 | + self.init_display() |
| 44 | + self.back_light(255) |
| 45 | + |
| 46 | + def init_display(self): |
| 47 | + self.reset() |
| 48 | + self.fill(0) |
| 49 | + self.show() |
| 50 | + self.poweron() |
| 51 | + # rotate90 requires a call to flip() for setting up. |
| 52 | + self.flip(self.flip_en) |
| 53 | + |
| 54 | + def poweroff(self): |
| 55 | + self.write_cmd(_SET_DISP | 0x00) |
| 56 | + |
| 57 | + def poweron(self): |
| 58 | + self.write_cmd(_SET_DISP | 0x01) |
| 59 | + if self.delay: |
| 60 | + time.sleep_ms(self.delay) |
| 61 | + |
| 62 | + def flip(self, flag=None, update=True): |
| 63 | + if flag is None: |
| 64 | + flag = not self.flip_en |
| 65 | + mir_v = flag ^ self.rotate90 |
| 66 | + mir_h = flag |
| 67 | + self.write_cmd(_SET_SEG_REMAP | (0x01 if mir_v else 0x00)) |
| 68 | + self.write_cmd(_SET_SCAN_DIR | (0x08 if mir_h else 0x00)) |
| 69 | + self.flip_en = flag |
| 70 | + if update: |
| 71 | + self.show(True) # full update |
| 72 | + |
| 73 | + def sleep(self, value): |
| 74 | + self.write_cmd(_SET_DISP | (not value)) |
| 75 | + |
| 76 | + def contrast(self, contrast): |
| 77 | + self.write_cmd(_SET_CONTRAST) |
| 78 | + self.write_cmd(contrast) |
| 79 | + |
| 80 | + def back_light(self, value): |
| 81 | + """ |
| 82 | + 背光调节 |
| 83 | +
|
| 84 | + Args: |
| 85 | + value: 背光等级 0 ~ 255 |
| 86 | + """ |
| 87 | + self.contrast(value) |
| 88 | + |
| 89 | + def rotate(self, rotate): |
| 90 | + """ |
| 91 | + 设置显示旋转 |
| 92 | +
|
| 93 | + Args: |
| 94 | + rotate(int): |
| 95 | + - 0-Portrait |
| 96 | + - 1-Upper right printing left (backwards) (X Flip) |
| 97 | + - 2-Inverted Portrait |
| 98 | + - 3-Lower left printing up (backwards) (Y Flip) |
| 99 | + """ |
| 100 | + rotate %= 4 |
| 101 | + mir_v = False |
| 102 | + mir_h = False |
| 103 | + if rotate == 0: |
| 104 | + mir_v = True |
| 105 | + mir_h = True |
| 106 | + elif rotate == 1: |
| 107 | + mir_h = True |
| 108 | + elif rotate == 2: |
| 109 | + pass |
| 110 | + elif rotate == 3: |
| 111 | + mir_v = True |
| 112 | + self.write_cmd(_SET_SEG_REMAP | (0x01 if mir_v else 0x00)) |
| 113 | + self.write_cmd(_SET_SCAN_DIR | (0x08 if mir_h else 0x00)) |
| 114 | + self.show() |
| 115 | + |
| 116 | + def invert(self, invert): |
| 117 | + """ |
| 118 | + Invert mode, If true, switch to invert mode (black-on-white), else normal mode (white-on-black) |
| 119 | + """ |
| 120 | + self.write_cmd(_SET_NORM_INV | (invert & 1)) |
| 121 | + |
| 122 | + def show(self, full_update=False): |
| 123 | + # self.* lookups in loops take significant time (~4fps). |
| 124 | + (w, p, db, rb) = (self.width, self.pages, |
| 125 | + self.displaybuf, self.buffer) |
| 126 | + if self.rotate90: |
| 127 | + for i in range(self.bufsize): |
| 128 | + db[w * (i % p) + (i // p)] = rb[i] |
| 129 | + if full_update: |
| 130 | + pages_to_update = (1 << self.pages) - 1 |
| 131 | + else: |
| 132 | + pages_to_update = self.pages_to_update |
| 133 | + # print("Updating pages: {:08b}".format(pages_to_update)) |
| 134 | + for page in range(self.pages): |
| 135 | + if (pages_to_update & (1 << page)): |
| 136 | + self.write_cmd(_SET_PAGE_ADDRESS | page) |
| 137 | + self.write_cmd(_LOW_COLUMN_ADDRESS | 2) |
| 138 | + self.write_cmd(_HIGH_COLUMN_ADDRESS | 0) |
| 139 | + self.write_data(db[(w * page):(w * page + w)]) |
| 140 | + self.pages_to_update = 0 |
| 141 | + |
| 142 | + def pixel(self, x, y, color=None): |
| 143 | + if color is None: |
| 144 | + return super().pixel(x, y) |
| 145 | + else: |
| 146 | + super().pixel(x, y, color) |
| 147 | + page = y // 8 |
| 148 | + self.pages_to_update |= 1 << page |
| 149 | + |
| 150 | + def text(self, text, x, y, color=1): |
| 151 | + super().text(text, x, y, color) |
| 152 | + self.register_updates(y, y + 7) |
| 153 | + |
| 154 | + def line(self, x0, y0, x1, y1, color): |
| 155 | + super().line(x0, y0, x1, y1, color) |
| 156 | + self.register_updates(y0, y1) |
| 157 | + |
| 158 | + def hline(self, x, y, w, color): |
| 159 | + super().hline(x, y, w, color) |
| 160 | + self.register_updates(y) |
| 161 | + |
| 162 | + def vline(self, x, y, h, color): |
| 163 | + super().vline(x, y, h, color) |
| 164 | + self.register_updates(y, y + h - 1) |
| 165 | + |
| 166 | + def fill(self, color): |
| 167 | + super().fill(color) |
| 168 | + self.pages_to_update = (1 << self.pages) - 1 |
| 169 | + |
| 170 | + def blit(self, fbuf, x, y, key=-1, palette=None): |
| 171 | + super().blit(fbuf, x, y, key, palette) |
| 172 | + self.register_updates(y, y + self.height) |
| 173 | + |
| 174 | + def scroll(self, x, y): |
| 175 | + # my understanding is that scroll() does a full screen change |
| 176 | + super().scroll(x, y) |
| 177 | + self.pages_to_update = (1 << self.pages) - 1 |
| 178 | + |
| 179 | + def fill_rect(self, x, y, w, h, color): |
| 180 | + super().fill_rect(x, y, w, h, color) |
| 181 | + self.register_updates(y, y + h - 1) |
| 182 | + |
| 183 | + def rect(self, x, y, w, h, color): |
| 184 | + super().rect(x, y, w, h, color) |
| 185 | + self.register_updates(y, y + h - 1) |
| 186 | + |
| 187 | + def circle(self, x, y, radius, c, section=100): |
| 188 | + """ |
| 189 | + 画圆 |
| 190 | +
|
| 191 | + Args: |
| 192 | + c: 颜色 |
| 193 | + x: 中心 x 坐标 |
| 194 | + y: 中心 y 坐标 |
| 195 | + radius: 半径 |
| 196 | + section: 分段 |
| 197 | + """ |
| 198 | + arr = [] |
| 199 | + for m in range(section + 1): |
| 200 | + _x = round(radius * math.cos((2 * math.pi / section) * m - math.pi) + x) |
| 201 | + _y = round(radius * math.sin((2 * math.pi / section) * m - math.pi) + y) |
| 202 | + arr.append([_x, _y]) |
| 203 | + for i in range(len(arr) - 1): |
| 204 | + self.line(*arr[i], *arr[i + 1], c) |
| 205 | + |
| 206 | + def fill_circle(self, x, y, radius, c): |
| 207 | + """ |
| 208 | + 画填充圆 |
| 209 | +
|
| 210 | + Args: |
| 211 | + c: 颜色 |
| 212 | + x: 中心 x 坐标 |
| 213 | + y: 中心 y 坐标 |
| 214 | + radius: 半径 |
| 215 | + """ |
| 216 | + rsq = radius * radius |
| 217 | + for _x in range(radius): |
| 218 | + _y = int(math.sqrt(rsq - _x * _x)) # 计算 y 坐标 |
| 219 | + y0 = y - _y |
| 220 | + end_y = y0 + _y * 2 |
| 221 | + y0 = max(0, min(y0, self.height)) # 将 y0 限制在画布的范围内 |
| 222 | + length = abs(end_y - y0) + 1 |
| 223 | + self.vline(x + _x, y0, length, c) # 绘制左右两侧的垂直线 |
| 224 | + self.vline(x - _x, y0, length, c) |
| 225 | + |
| 226 | + def register_updates(self, y0, y1=None): |
| 227 | + # this function takes the top and optional bottom address of the changes made |
| 228 | + # and updates the pages_to_change list with any changed pages |
| 229 | + # that are not yet on the list |
| 230 | + start_page = max(0, y0 // 8) |
| 231 | + end_page = max(0, y1 // 8) if y1 is not None else start_page |
| 232 | + # rearrange start_page and end_page if coordinates were given from bottom to top |
| 233 | + if start_page > end_page: |
| 234 | + start_page, end_page = end_page, start_page |
| 235 | + for page in range(start_page, end_page + 1): |
| 236 | + self.pages_to_update |= 1 << page |
| 237 | + |
| 238 | + def reset(self, res): |
| 239 | + if res is not None: |
| 240 | + res(1) |
| 241 | + time.sleep_ms(1) |
| 242 | + res(0) |
| 243 | + time.sleep_ms(20) |
| 244 | + res(1) |
| 245 | + time.sleep_ms(20) |
| 246 | + |
| 247 | + |
| 248 | +class SH1106_I2C(SH1106): |
| 249 | + def __init__(self, width, height, i2c, res=None, addr=0x3c, |
| 250 | + rotate=0, external_vcc=False, delay=0): |
| 251 | + self.i2c = i2c |
| 252 | + self.addr = addr |
| 253 | + self.res = res |
| 254 | + self.temp = bytearray(2) |
| 255 | + self.delay = delay |
| 256 | + if res is not None: |
| 257 | + res.init(res.OUT, value=1) |
| 258 | + super().__init__(width, height, external_vcc, rotate) |
| 259 | + |
| 260 | + def write_cmd(self, cmd): |
| 261 | + self.temp[0] = 0x80 # Co=1, D/C#=0 |
| 262 | + self.temp[1] = cmd |
| 263 | + self.i2c.writeto(self.addr, self.temp) |
| 264 | + |
| 265 | + def write_data(self, buf): |
| 266 | + self.i2c.writeto(self.addr, b'\x40' + buf) |
| 267 | + |
| 268 | + def reset(self): |
| 269 | + super().reset(self.res) |
| 270 | + |
| 271 | + |
| 272 | +class SH1106_SPI(SH1106): |
| 273 | + def __init__(self, width, height, spi, dc, res=None, cs=None, |
| 274 | + rotate=0, external_vcc=False, delay=0): |
| 275 | + dc.init(dc.OUT, value=0) |
| 276 | + if res is not None: |
| 277 | + res.init(res.OUT, value=0) |
| 278 | + if cs is not None: |
| 279 | + cs.init(cs.OUT, value=1) |
| 280 | + self.spi = spi |
| 281 | + self.dc = dc |
| 282 | + self.res = res |
| 283 | + self.cs = cs |
| 284 | + self.delay = delay |
| 285 | + super().__init__(width, height, external_vcc, rotate) |
| 286 | + |
| 287 | + def write_cmd(self, cmd): |
| 288 | + if self.cs is not None: |
| 289 | + self.cs(1) |
| 290 | + self.dc(0) |
| 291 | + self.cs(0) |
| 292 | + self.spi.write(bytearray([cmd])) |
| 293 | + self.cs(1) |
| 294 | + else: |
| 295 | + self.dc(0) |
| 296 | + self.spi.write(bytearray([cmd])) |
| 297 | + |
| 298 | + def write_data(self, buf): |
| 299 | + if self.cs is not None: |
| 300 | + self.cs(1) |
| 301 | + self.dc(1) |
| 302 | + self.cs(0) |
| 303 | + self.spi.write(buf) |
| 304 | + self.cs(1) |
| 305 | + else: |
| 306 | + self.dc(1) |
| 307 | + self.spi.write(buf) |
| 308 | + |
| 309 | + def reset(self): |
| 310 | + super().reset(self.res) |
0 commit comments