1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
22 from typing import Optional
25 import microcontroller
26 import circuitpython_typing
27 from ._displaycore import _DisplayCore
28 from ._displaybus import _DisplayBus
29 from ._colorconverter import ColorConverter
30 from ._group import Group
31 from ._structs import RectangleStruct
32 from ._area import Area
33 from ._constants import (
34 CHIP_SELECT_TOGGLE_EVERY_BYTE,
35 CHIP_SELECT_UNTOUCHED,
43 __version__ = "0.0.0+auto.0"
44 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48 # pylint: disable=too-many-instance-attributes
49 """This initializes a display and connects it into CircuitPython. Unlike other objects
50 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
51 This is done so that CircuitPython can use the display itself.
53 Most people should not use this class directly. Use a specific display driver instead
54 that will contain the initialization sequence at minimum.
59 display_bus: _DisplayBus,
60 init_sequence: circuitpython_typing.ReadableBuffer,
67 color_depth: int = 16,
68 grayscale: bool = False,
69 pixels_in_byte_share_row: bool = True,
70 bytes_per_cell: int = 1,
71 reverse_pixels_in_byte: bool = False,
72 reverse_bytes_in_word: bool = True,
73 set_column_command: int = 0x2A,
74 set_row_command: int = 0x2B,
75 write_ram_command: int = 0x2C,
76 backlight_pin: Optional[microcontroller.Pin] = None,
77 brightness_command: Optional[int] = None,
78 brightness: float = 1.0,
79 auto_brightness: bool = False,
80 single_byte_bounds: bool = False,
81 data_as_commands: bool = False,
82 auto_refresh: bool = True,
83 native_frames_per_second: int = 60,
84 backlight_on_high: bool = True,
85 SH1107_addressing: bool = False,
86 set_vertical_scroll: int = 0,
88 # pylint: disable=unused-argument,too-many-locals,invalid-name
89 """Create a Display object on the given display bus (`displayio.FourWire` or
90 `paralleldisplay.ParallelBus`).
92 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
93 with a command byte followed by a byte to determine the parameter count and if a
94 delay is need after. When the top bit of the second byte is 1, the next byte will be
95 the delay time in milliseconds. The remaining 7 bits are the parameter count
96 excluding any delay byte. The third through final bytes are the remaining command
97 parameters. The next byte will begin a new command definition. Here is a portion of
100 .. code-block:: python
103 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
104 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
105 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
106 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
108 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
110 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
111 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
112 Multiple byte literals (b”“) are merged together on load. The parens are needed to
113 allow byte literals on subsequent lines.
115 The initialization sequence should always leave the display memory access inline with
116 the scan of the display to minimize tearing artifacts.
118 # Turn off auto-refresh as we init
119 self._auto_refresh = False
122 if single_byte_bounds:
126 self._core = _DisplayCore(
131 ram_height=ram_height,
135 color_depth=color_depth,
137 pixels_in_byte_share_row=pixels_in_byte_share_row,
138 bytes_per_cell=bytes_per_cell,
139 reverse_pixels_in_byte=reverse_pixels_in_byte,
140 reverse_bytes_in_word=reverse_bytes_in_word,
141 column_command=set_column_command,
142 row_command=set_row_command,
143 set_current_column_command=NO_COMMAND,
144 set_current_row_command=NO_COMMAND,
145 data_as_commands=data_as_commands,
146 always_toggle_chip_select=False,
147 sh1107_addressing=(SH1107_addressing and color_depth == 1),
148 address_little_endian=False,
151 self._write_ram_command = write_ram_command
152 self._brightness_command = brightness_command
153 self._first_manual_refresh = not auto_refresh
154 self._backlight_on_high = backlight_on_high
156 self._native_frames_per_second = native_frames_per_second
157 self._native_ms_per_frame = 1000 // native_frames_per_second
159 self._auto_brightness = auto_brightness
160 self._brightness = brightness
161 self._auto_refresh = auto_refresh
163 self._initialize(init_sequence)
164 self._buffer = Image.new("RGB", (width, height))
165 self._current_group = None
166 self._last_refresh_call = 0
167 self._refresh_thread = None
168 if self._auto_refresh:
169 self.auto_refresh = True
170 self._colorconverter = ColorConverter()
172 self._backlight_type = None
173 if backlight_pin is not None:
175 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
177 # 100Hz looks decent and doesn't keep the CPU too busy
178 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
179 self._backlight_type = BACKLIGHT_PWM
181 # PWMOut not implemented on this platform
183 if self._backlight_type is None:
184 self._backlight_type = BACKLIGHT_IN_OUT
185 self._backlight = digitalio.DigitalInOut(backlight_pin)
186 self._backlight.switch_to_output()
187 self.brightness = brightness
189 def __new__(cls, *args, **kwargs):
190 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
194 display_instance = super().__new__(cls)
195 allocate_display(display_instance)
196 return display_instance
198 def _initialize(self, init_sequence):
200 while i < len(init_sequence):
201 command = init_sequence[i]
202 data_size = init_sequence[i + 1]
203 delay = (data_size & 0x80) > 0
206 if self._core.data_as_commands:
209 CHIP_SELECT_TOGGLE_EVERY_BYTE,
210 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
214 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
218 CHIP_SELECT_UNTOUCHED,
219 init_sequence[i + 2 : i + 2 + data_size],
224 delay_time_ms = init_sequence[i + 1 + data_size]
225 if delay_time_ms == 255:
227 time.sleep(delay_time_ms / 1000)
230 def _send_pixels(self, pixels):
231 if not self._core.data_as_commands:
234 CHIP_SELECT_TOGGLE_EVERY_BYTE,
235 bytes([self._write_ram_command]),
237 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
239 def show(self, group: Group) -> None:
240 """Switches to displaying the given group of layers. When group is None, the
241 default CircuitPython terminal will be shown.
243 self._core.show(group)
248 target_frames_per_second: Optional[int] = None,
249 minimum_frames_per_second: int = 0,
251 """When auto refresh is off, waits for the target frame rate and then refreshes the
252 display, returning True. If the call has taken too long since the last refresh call
253 for the given target frame rate, then the refresh returns False immediately without
254 updating the screen to hopefully help getting caught up.
256 If the time since the last successful refresh is below the minimum frame rate, then
257 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
259 When auto refresh is on, updates the display immediately. (The display will also
260 update without calls to this.)
262 maximum_ms_per_real_frame = 0xFFFFFFFF
263 if minimum_frames_per_second > 0:
264 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
266 if target_frames_per_second is None:
267 target_ms_per_frame = 0xFFFFFFFF
269 target_ms_per_frame = 1000 // target_frames_per_second
272 not self._auto_refresh
273 and not self._first_manual_refresh
274 and target_ms_per_frame != 0xFFFFFFFF
276 current_time = time.monotonic() * 1000
277 current_ms_since_real_refresh = current_time - self._core.last_refresh
278 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
279 raise RuntimeError("Below minimum frame rate")
280 current_ms_since_last_call = current_time - self._last_refresh_call
281 self._last_refresh_call = current_time
282 if current_ms_since_last_call > target_ms_per_frame:
285 remaining_time = target_ms_per_frame - (
286 current_ms_since_real_refresh % target_ms_per_frame
288 time.sleep(remaining_time / 1000)
289 self._first_manual_refresh = False
290 self._refresh_display()
293 def _refresh_display(self):
294 if not self._core.start_refresh():
297 # TODO: Likely move this to _refresh_area()
298 # Go through groups and and add each to buffer
300 if self._core.current_group is not None:
301 buffer = Image.new("RGBA", (self._core.width, self._core.height))
302 # Recursively have everything draw to the image
303 self._core.current_group._fill_area(
305 ) # pylint: disable=protected-access
306 # save image to buffer (or probably refresh buffer so we can compare)
307 self._buffer.paste(buffer)
309 areas_to_refresh = self._get_refresh_areas()
311 for area in areas_to_refresh:
312 self._refresh_area(area)
314 self._core.finish_refresh()
318 def _get_refresh_areas(self) -> list[Area]:
319 """Get a list of areas to be refreshed"""
321 if self._core.full_refresh:
322 areas.append(self._core.area)
323 elif self._core.current_group is not None:
324 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
329 def background(self):
330 """Run background refresh tasks. Do not call directly"""
333 and (time.monotonic() * 1000 - self._core.last_refresh)
334 > self._native_ms_per_frame
338 def _refresh_area(self, area) -> bool:
339 """Loop through dirty areas and redraw that area."""
340 # pylint: disable=too-many-locals
344 if not self._core.clip_area(area, clipped):
347 rows_per_buffer = clipped.height()
348 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
349 pixels_per_buffer = clipped.size()
353 if self._core.sh1107_addressing:
354 subrectangles = rows_per_buffer // 8
356 elif clipped.size() > buffer_size * pixels_per_word:
357 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
358 if rows_per_buffer == 0:
361 self._core.colorspace.depth < 8
362 and self._core.colorspace.pixels_in_byte_share_row
364 pixels_per_byte = 8 // self._core.colorspace.depth
365 if rows_per_buffer % pixels_per_byte != 0:
366 rows_per_buffer -= rows_per_buffer % pixels_per_byte
367 subrectangles = clipped.height() // rows_per_buffer
368 if clipped.height() % rows_per_buffer != 0:
370 pixels_per_buffer = rows_per_buffer * clipped.width()
371 buffer_size = pixels_per_buffer // pixels_per_word
372 if pixels_per_buffer % pixels_per_word:
375 buffer = bytearray(buffer_size)
376 mask_length = (pixels_per_buffer // 32) + 1
377 mask = bytearray(mask_length)
378 remaining_rows = clipped.height()
380 for subrect_index in range(subrectangles):
383 clipped.y1 + rows_per_buffer * subrect_index,
385 clipped.y1 + rows_per_buffer * (subrect_index + 1),
387 if remaining_rows < rows_per_buffer:
388 subrectangle.y2 = subrectangle.y1 + remaining_rows
389 self._core.set_region_to_update(subrectangle)
390 if self._core.colorspace.depth >= 8:
391 subrectangle_size_bytes = subrectangle.size() * (
392 self._core.colorspace.depth // 8
395 subrectangle_size_bytes = subrectangle.size() // (
396 8 // self._core.colorspace.depth
399 self._core.fill_area(subrectangle, mask, buffer)
401 self._core.begin_transaction()
402 self._send_pixels(buffer[:subrectangle_size_bytes])
403 self._core.end_transaction()
406 def _apply_rotation(self, rectangle):
407 """Adjust the rectangle coordinates based on rotation"""
408 if self._core.rotation == 90:
409 return RectangleStruct(
410 self._core.height - rectangle.y2,
412 self._core.height - rectangle.y1,
415 if self._core.rotation == 180:
416 return RectangleStruct(
417 self._core.width - rectangle.x2,
418 self._core.height - rectangle.y2,
419 self._core.width - rectangle.x1,
420 self._core.height - rectangle.y1,
422 if self._core.rotation == 270:
423 return RectangleStruct(
425 self._core.width - rectangle.x2,
427 self._core.width - rectangle.x1,
432 self, y: int, buffer: circuitpython_typing.WriteableBuffer
433 ) -> circuitpython_typing.WriteableBuffer:
434 """Extract the pixels from a single row"""
435 for x in range(0, self._core.width):
436 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
437 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
438 buffer[x * 2 + 1] = _rgb_565 & 0xFF
441 def release(self) -> None:
442 """Release the display and free its resources"""
443 self.auto_refresh = False
444 self._core.release_display_core()
446 def reset(self) -> None:
447 """Reset the display"""
448 self.auto_refresh = True
451 def auto_refresh(self) -> bool:
452 """True when the display is refreshed automatically."""
453 return self._auto_refresh
456 def auto_refresh(self, value: bool):
457 self._first_manual_refresh = not value
458 self._auto_refresh = value
461 def brightness(self) -> float:
462 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
463 When `auto_brightness` is True, the value of `brightness` will change automatically.
464 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
466 return self._brightness
469 def brightness(self, value: float):
470 if 0 <= float(value) <= 1.0:
471 if not self._backlight_on_high:
474 if self._backlight_type == BACKLIGHT_PWM:
475 self._backlight.duty_cycle = value * 0xFFFF
476 elif self._backlight_type == BACKLIGHT_IN_OUT:
477 self._backlight.value = value > 0.99
478 elif self._brightness_command is not None:
479 self._core.begin_transaction()
480 if self._core.data_as_commands:
483 CHIP_SELECT_TOGGLE_EVERY_BYTE,
484 bytes([self._brightness_command, 0xFF * value]),
489 CHIP_SELECT_TOGGLE_EVERY_BYTE,
490 bytes([self._brightness_command]),
493 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
495 self._core.end_transaction()
496 self._brightness = value
498 raise ValueError("Brightness must be between 0.0 and 1.0")
501 def auto_brightness(self) -> bool:
502 """True when the display brightness is adjusted automatically, based on an ambient
503 light sensor or other method. Note that some displays may have this set to True by
504 default, but not actually implement automatic brightness adjustment.
505 `auto_brightness` is set to False if `brightness` is set manually.
507 return self._auto_brightness
509 @auto_brightness.setter
510 def auto_brightness(self, value: bool):
511 self._auto_brightness = value
514 def width(self) -> int:
516 return self._core.get_width()
519 def height(self) -> int:
521 return self._core.get_height()
524 def rotation(self) -> int:
525 """The rotation of the display as an int in degrees."""
526 return self._core.get_rotation()
529 def rotation(self, value: int):
530 self._core.set_rotation(value)
533 def bus(self) -> _DisplayBus:
534 """Current Display Bus"""
535 return self._core.get_bus()