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
21 from typing import Optional
23 import microcontroller
24 from circuitpython_typing import WriteableBuffer, ReadableBuffer
25 from ._displaycore import _DisplayCore
26 from ._displaybus import _DisplayBus
27 from ._colorconverter import ColorConverter
28 from ._group import Group, circuitpython_splash
29 from ._area import Area
30 from ._constants import (
31 CHIP_SELECT_TOGGLE_EVERY_BYTE,
32 CHIP_SELECT_UNTOUCHED,
41 __version__ = "0.0.0+auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
46 # pylint: disable=too-many-instance-attributes, too-many-statements
47 """This initializes a display and connects it into CircuitPython. Unlike other objects
48 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
49 This is done so that CircuitPython can use the display itself.
51 Most people should not use this class directly. Use a specific display driver instead
52 that will contain the initialization sequence at minimum.
57 display_bus: _DisplayBus,
58 init_sequence: ReadableBuffer,
65 color_depth: int = 16,
66 grayscale: bool = False,
67 pixels_in_byte_share_row: bool = True,
68 bytes_per_cell: int = 1,
69 reverse_pixels_in_byte: bool = False,
70 reverse_bytes_in_word: bool = True,
71 set_column_command: int = 0x2A,
72 set_row_command: int = 0x2B,
73 write_ram_command: int = 0x2C,
74 backlight_pin: Optional[microcontroller.Pin] = None,
75 brightness_command: Optional[int] = None,
76 brightness: float = 1.0,
77 single_byte_bounds: bool = False,
78 data_as_commands: bool = False,
79 auto_refresh: bool = True,
80 native_frames_per_second: int = 60,
81 backlight_on_high: bool = True,
82 SH1107_addressing: bool = False,
84 # pylint: disable=too-many-locals,invalid-name, too-many-branches
85 """Create a Display object on the given display bus (`displayio.FourWire` or
86 `paralleldisplay.ParallelBus`).
88 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
89 with a command byte followed by a byte to determine the parameter count and if a
90 delay is need after. When the top bit of the second byte is 1, the next byte will be
91 the delay time in milliseconds. The remaining 7 bits are the parameter count
92 excluding any delay byte. The third through final bytes are the remaining command
93 parameters. The next byte will begin a new command definition. Here is a portion of
96 .. code-block:: python
99 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
100 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
101 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
102 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
104 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
106 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
107 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
108 Multiple byte literals (b”“) are merged together on load. The parens are needed to
109 allow byte literals on subsequent lines.
111 The initialization sequence should always leave the display memory access inline with
112 the scan of the display to minimize tearing artifacts.
115 if rotation % 90 != 0:
116 raise ValueError("Display rotation must be in 90 degree increments")
118 if SH1107_addressing and color_depth != 1:
119 raise ValueError("color_depth must be 1 when SH1107_addressing is True")
121 # Turn off auto-refresh as we init
122 self._auto_refresh = False
125 if single_byte_bounds:
129 self._core = _DisplayCore(
134 ram_height=ram_height,
138 color_depth=color_depth,
140 pixels_in_byte_share_row=pixels_in_byte_share_row,
141 bytes_per_cell=bytes_per_cell,
142 reverse_pixels_in_byte=reverse_pixels_in_byte,
143 reverse_bytes_in_word=reverse_bytes_in_word,
144 column_command=set_column_command,
145 row_command=set_row_command,
146 set_current_column_command=NO_COMMAND,
147 set_current_row_command=NO_COMMAND,
148 data_as_commands=data_as_commands,
149 always_toggle_chip_select=False,
150 sh1107_addressing=(SH1107_addressing and color_depth == 1),
151 address_little_endian=False,
154 self._write_ram_command = write_ram_command
155 self._brightness_command = brightness_command
156 self._first_manual_refresh = not auto_refresh
157 self._backlight_on_high = backlight_on_high
159 self._native_frames_per_second = native_frames_per_second
160 self._native_ms_per_frame = 1000 // native_frames_per_second
162 self._brightness = brightness
163 self._auto_refresh = auto_refresh
166 while i < len(init_sequence):
167 command = init_sequence[i]
168 data_size = init_sequence[i + 1]
169 delay = (data_size & DELAY) != 0
171 while self._core.begin_transaction():
174 if self._core.data_as_commands:
175 full_command = bytearray(data_size + 1)
176 full_command[0] = command
177 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
180 CHIP_SELECT_TOGGLE_EVERY_BYTE,
185 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
189 CHIP_SELECT_UNTOUCHED,
190 init_sequence[i + 2 : i + 2 + data_size],
192 self._core.end_transaction()
196 delay_time_ms = init_sequence[i + 1 + data_size]
197 if delay_time_ms == 255:
199 time.sleep(delay_time_ms / 1000)
202 self._current_group = None
203 self._last_refresh_call = 0
204 self._refresh_thread = None
205 self._colorconverter = ColorConverter()
207 self._backlight_type = None
208 if backlight_pin is not None:
210 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
212 # 100Hz looks decent and doesn't keep the CPU too busy
213 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
214 self._backlight_type = BACKLIGHT_PWM
216 # PWMOut not implemented on this platform
218 if self._backlight_type is None:
219 self._backlight_type = BACKLIGHT_IN_OUT
220 self._backlight = digitalio.DigitalInOut(backlight_pin)
221 self._backlight.switch_to_output()
222 self.brightness = brightness
223 if not circuitpython_splash._in_group:
224 self._set_root_group(circuitpython_splash)
225 self.auto_refresh = auto_refresh
227 def __new__(cls, *args, **kwargs):
228 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
232 display_instance = super().__new__(cls)
233 allocate_display(display_instance)
234 return display_instance
236 def _send_pixels(self, pixels):
237 if not self._core.data_as_commands:
240 CHIP_SELECT_TOGGLE_EVERY_BYTE,
241 bytes([self._write_ram_command]),
243 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
245 def show(self, group: Group) -> None:
247 .. note:: `show()` is deprecated and will be removed when CircuitPython 9.0.0
248 is released. Use ``.root_group = group`` instead.
250 Switches to displaying the given group of layers. When group is None, the
251 default CircuitPython terminal will be shown.
253 :param Group group: The group to show.
257 group = circuitpython_splash
258 self._core.set_root_group(group)
260 def _set_root_group(self, root_group: Group) -> None:
261 ok = self._core.set_root_group(root_group)
263 raise ValueError("Group already used")
268 target_frames_per_second: Optional[int] = None,
269 minimum_frames_per_second: int = 0,
271 """When auto refresh is off, waits for the target frame rate and then refreshes the
272 display, returning True. If the call has taken too long since the last refresh call
273 for the given target frame rate, then the refresh returns False immediately without
274 updating the screen to hopefully help getting caught up.
276 If the time since the last successful refresh is below the minimum frame rate, then
277 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
279 When auto refresh is on, updates the display immediately. (The display will also
280 update without calls to this.)
282 maximum_ms_per_real_frame = 0xFFFFFFFF
283 if minimum_frames_per_second > 0:
284 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
286 if target_frames_per_second is None:
287 target_ms_per_frame = 0xFFFFFFFF
289 target_ms_per_frame = 1000 // target_frames_per_second
292 not self._auto_refresh
293 and not self._first_manual_refresh
294 and target_ms_per_frame != 0xFFFFFFFF
296 current_time = time.monotonic() * 1000
297 current_ms_since_real_refresh = current_time - self._core.last_refresh
298 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
299 raise RuntimeError("Below minimum frame rate")
300 current_ms_since_last_call = current_time - self._last_refresh_call
301 self._last_refresh_call = current_time
302 if current_ms_since_last_call > target_ms_per_frame:
305 remaining_time = target_ms_per_frame - (
306 current_ms_since_real_refresh % target_ms_per_frame
308 time.sleep(remaining_time / 1000)
309 self._first_manual_refresh = False
310 self._refresh_display()
313 def _refresh_display(self):
314 if not self._core.start_refresh():
317 areas_to_refresh = self._get_refresh_areas()
318 for area in areas_to_refresh:
319 self._refresh_area(area)
321 self._core.finish_refresh()
325 def _get_refresh_areas(self) -> list[Area]:
326 """Get a list of areas to be refreshed"""
328 if self._core.full_refresh:
329 areas.append(self._core.area)
330 elif self._core.current_group is not None:
331 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
336 def _background(self):
337 """Run background refresh tasks. Do not call directly"""
340 and (time.monotonic() * 1000 - self._core.last_refresh)
341 > self._native_ms_per_frame
345 def _refresh_area(self, area) -> bool:
346 """Loop through dirty areas and redraw that area."""
347 # pylint: disable=too-many-locals, too-many-branches
350 # Clip the area to the display by overlapping the areas.
351 # If there is no overlap then we're done.
352 if not self._core.clip_area(area, clipped):
355 rows_per_buffer = clipped.height()
356 pixels_per_word = 32 // self._core.colorspace.depth
357 pixels_per_buffer = clipped.size()
359 # We should have lots of memory
360 buffer_size = clipped.size() // pixels_per_word
363 # for SH1107 and other boundary constrained controllers
364 # write one single row at a time
365 if self._core.sh1107_addressing:
366 subrectangles = rows_per_buffer // 8
368 elif clipped.size() > buffer_size * pixels_per_word:
369 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
370 if rows_per_buffer == 0:
372 # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
374 self._core.colorspace.depth < 8
375 and self._core.colorspace.pixels_in_byte_share_row
377 pixels_per_byte = 8 // self._core.colorspace.depth
378 if rows_per_buffer % pixels_per_byte != 0:
379 rows_per_buffer -= rows_per_buffer % pixels_per_byte
380 subrectangles = clipped.height() // rows_per_buffer
381 if clipped.height() % rows_per_buffer != 0:
383 pixels_per_buffer = rows_per_buffer * clipped.width()
384 buffer_size = pixels_per_buffer // pixels_per_word
385 if pixels_per_buffer % pixels_per_word:
387 mask_length = (pixels_per_buffer // 32) + 1 # 1 bit per pixel + 1
388 remaining_rows = clipped.height()
390 for subrect_index in range(subrectangles):
393 y1=clipped.y1 + rows_per_buffer * subrect_index,
395 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
397 if remaining_rows < rows_per_buffer:
398 subrectangle.y2 = subrectangle.y1 + remaining_rows
399 remaining_rows -= rows_per_buffer
400 self._core.set_region_to_update(subrectangle)
401 if self._core.colorspace.depth >= 8:
402 subrectangle_size_bytes = subrectangle.size() * (
403 self._core.colorspace.depth // 8
406 subrectangle_size_bytes = subrectangle.size() // (
407 8 // self._core.colorspace.depth
410 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
411 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
412 self._core.fill_area(subrectangle, mask, buffer)
414 # Can't acquire display bus; skip the rest of the data.
415 if not self._core.bus_free():
418 self._core.begin_transaction()
419 self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
420 self._core.end_transaction()
423 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
424 """Extract the pixels from a single row"""
425 if self._core.colorspace.depth != 16:
426 raise ValueError("Display must have a 16 bit colorspace.")
428 area = Area(0, y, self._core.width, y + 1)
429 pixels_per_word = 32 // self._core.colorspace.depth
430 buffer_size = self._core.width // pixels_per_word
431 pixels_per_buffer = area.size()
432 if pixels_per_buffer % pixels_per_word:
435 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
436 mask_length = (pixels_per_buffer // 32) + 1
437 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
438 self._core.fill_area(area, mask, buffer)
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
449 circuitpython_splash.x = 0
450 circuitpython_splash.y = 0
451 if not circuitpython_splash._in_group: # pylint: disable=protected-access
452 self._set_root_group(circuitpython_splash)
455 def auto_refresh(self) -> bool:
456 """True when the display is refreshed automatically."""
457 return self._auto_refresh
460 def auto_refresh(self, value: bool):
461 self._first_manual_refresh = not value
462 self._auto_refresh = value
465 def brightness(self) -> float:
466 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
467 return self._brightness
470 def brightness(self, value: float):
471 if 0 <= float(value) <= 1.0:
472 if not self._backlight_on_high:
475 if self._backlight_type == BACKLIGHT_PWM:
476 self._backlight.duty_cycle = value * 0xFFFF
477 elif self._backlight_type == BACKLIGHT_IN_OUT:
478 self._backlight.value = value > 0.99
479 elif self._brightness_command is not None:
480 okay = self._core.begin_transaction()
482 if self._core.data_as_commands:
485 CHIP_SELECT_TOGGLE_EVERY_BYTE,
486 bytes([self._brightness_command, round(0xFF * value)]),
491 CHIP_SELECT_TOGGLE_EVERY_BYTE,
492 bytes([self._brightness_command]),
495 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
497 self._core.end_transaction()
498 self._brightness = value
500 raise ValueError("Brightness must be between 0.0 and 1.0")
503 def width(self) -> int:
505 return self._core.get_width()
508 def height(self) -> int:
510 return self._core.get_height()
513 def rotation(self) -> int:
514 """The rotation of the display as an int in degrees."""
515 return self._core.get_rotation()
518 def rotation(self, value: int):
520 raise ValueError("Display rotation must be in 90 degree increments")
521 transposed = self._core.rotation in (90, 270)
522 will_transposed = value in (90, 270)
523 if transposed != will_transposed:
524 self._core.width, self._core.height = self._core.height, self._core.width
525 self._core.set_rotation(value)
526 if self._core.current_group is not None:
527 self._core.current_group._update_transform( # pylint: disable=protected-access
532 def bus(self) -> _DisplayBus:
533 """Current Display Bus"""
534 return self._core.get_bus()
537 def root_group(self) -> Group:
539 The root group on the display.
540 If the root group is set to `displayio.CIRCUITPYTHON_TERMINAL`, the default
541 CircuitPython terminal will be shown.
542 If the root group is set to ``None``, no output will be shown.
544 return self._core.current_group
547 def root_group(self, new_group: Group) -> None:
548 self._set_root_group(new_group)