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 array import array
22 from typing import Optional
24 import microcontroller
25 from circuitpython_typing import WriteableBuffer, ReadableBuffer
26 from ._displaycore import _DisplayCore
27 from ._displaybus import _DisplayBus
28 from ._colorconverter import ColorConverter
29 from ._group import Group, circuitpython_splash
30 from ._area import Area
31 from ._constants import (
32 CHIP_SELECT_TOGGLE_EVERY_BYTE,
33 CHIP_SELECT_UNTOUCHED,
42 __version__ = "0.0.0+auto.0"
43 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
47 # pylint: disable=too-many-instance-attributes, too-many-statements
48 """This initializes a display and connects it into CircuitPython. Unlike other objects
49 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
50 This is done so that CircuitPython can use the display itself.
52 Most people should not use this class directly. Use a specific display driver instead
53 that will contain the initialization sequence at minimum.
58 display_bus: _DisplayBus,
59 init_sequence: ReadableBuffer,
66 color_depth: int = 16,
67 grayscale: bool = False,
68 pixels_in_byte_share_row: bool = True,
69 bytes_per_cell: int = 1,
70 reverse_pixels_in_byte: bool = False,
71 reverse_bytes_in_word: bool = True,
72 set_column_command: int = 0x2A,
73 set_row_command: int = 0x2B,
74 write_ram_command: int = 0x2C,
75 backlight_pin: Optional[microcontroller.Pin] = None,
76 brightness_command: Optional[int] = None,
77 brightness: float = 1.0,
78 single_byte_bounds: bool = False,
79 data_as_commands: bool = False,
80 auto_refresh: bool = True,
81 native_frames_per_second: int = 60,
82 backlight_on_high: bool = True,
83 SH1107_addressing: bool = False,
85 # pylint: disable=too-many-locals,invalid-name
86 """Create a Display object on the given display bus (`displayio.FourWire` or
87 `paralleldisplay.ParallelBus`).
89 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
90 with a command byte followed by a byte to determine the parameter count and if a
91 delay is need after. When the top bit of the second byte is 1, the next byte will be
92 the delay time in milliseconds. The remaining 7 bits are the parameter count
93 excluding any delay byte. The third through final bytes are the remaining command
94 parameters. The next byte will begin a new command definition. Here is a portion of
97 .. code-block:: python
100 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
101 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
102 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
103 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
105 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
107 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
108 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
109 Multiple byte literals (b”“) are merged together on load. The parens are needed to
110 allow byte literals on subsequent lines.
112 The initialization sequence should always leave the display memory access inline with
113 the scan of the display to minimize tearing artifacts.
115 # Turn off auto-refresh as we init
116 self._auto_refresh = False
119 if single_byte_bounds:
123 self._core = _DisplayCore(
128 ram_height=ram_height,
132 color_depth=color_depth,
134 pixels_in_byte_share_row=pixels_in_byte_share_row,
135 bytes_per_cell=bytes_per_cell,
136 reverse_pixels_in_byte=reverse_pixels_in_byte,
137 reverse_bytes_in_word=reverse_bytes_in_word,
138 column_command=set_column_command,
139 row_command=set_row_command,
140 set_current_column_command=NO_COMMAND,
141 set_current_row_command=NO_COMMAND,
142 data_as_commands=data_as_commands,
143 always_toggle_chip_select=False,
144 sh1107_addressing=(SH1107_addressing and color_depth == 1),
145 address_little_endian=False,
148 self._write_ram_command = write_ram_command
149 self._brightness_command = brightness_command
150 self._first_manual_refresh = not auto_refresh
151 self._backlight_on_high = backlight_on_high
153 self._native_frames_per_second = native_frames_per_second
154 self._native_ms_per_frame = 1000 // native_frames_per_second
156 self._brightness = brightness
157 self._auto_refresh = auto_refresh
160 while i < len(init_sequence):
161 command = init_sequence[i]
162 data_size = init_sequence[i + 1]
163 delay = (data_size & DELAY) != 0
165 while self._core.begin_transaction():
168 if self._core.data_as_commands:
169 full_command = bytearray(data_size + 1)
170 full_command[0] = command
171 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
174 CHIP_SELECT_TOGGLE_EVERY_BYTE,
179 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
183 CHIP_SELECT_UNTOUCHED,
184 init_sequence[i + 2 : i + 2 + data_size],
186 self._core.end_transaction()
190 delay_time_ms = init_sequence[i + 1 + data_size]
191 if delay_time_ms == 255:
193 time.sleep(delay_time_ms / 1000)
196 self._current_group = None
197 self._last_refresh_call = 0
198 self._refresh_thread = None
199 self._colorconverter = ColorConverter()
201 self._backlight_type = None
202 if backlight_pin is not None:
204 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
206 # 100Hz looks decent and doesn't keep the CPU too busy
207 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
208 self._backlight_type = BACKLIGHT_PWM
210 # PWMOut not implemented on this platform
212 if self._backlight_type is None:
213 self._backlight_type = BACKLIGHT_IN_OUT
214 self._backlight = digitalio.DigitalInOut(backlight_pin)
215 self._backlight.switch_to_output()
216 self.brightness = brightness
217 if not circuitpython_splash._in_group:
218 self._set_root_group(circuitpython_splash)
219 self.auto_refresh = auto_refresh
221 def __new__(cls, *args, **kwargs):
222 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
226 display_instance = super().__new__(cls)
227 allocate_display(display_instance)
228 return display_instance
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.
244 group = circuitpython_splash
245 self._core.set_root_group(group)
247 def _set_root_group(self, root_group: Group) -> None:
248 ok = self._core.set_root_group(root_group)
250 raise ValueError("Group already used")
255 target_frames_per_second: Optional[int] = None,
256 minimum_frames_per_second: int = 0,
258 """When auto refresh is off, waits for the target frame rate and then refreshes the
259 display, returning True. If the call has taken too long since the last refresh call
260 for the given target frame rate, then the refresh returns False immediately without
261 updating the screen to hopefully help getting caught up.
263 If the time since the last successful refresh is below the minimum frame rate, then
264 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
266 When auto refresh is on, updates the display immediately. (The display will also
267 update without calls to this.)
269 maximum_ms_per_real_frame = 0xFFFFFFFF
270 if minimum_frames_per_second > 0:
271 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
273 if target_frames_per_second is None:
274 target_ms_per_frame = 0xFFFFFFFF
276 target_ms_per_frame = 1000 // target_frames_per_second
279 not self._auto_refresh
280 and not self._first_manual_refresh
281 and target_ms_per_frame != 0xFFFFFFFF
283 current_time = time.monotonic() * 1000
284 current_ms_since_real_refresh = current_time - self._core.last_refresh
285 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
286 raise RuntimeError("Below minimum frame rate")
287 current_ms_since_last_call = current_time - self._last_refresh_call
288 self._last_refresh_call = current_time
289 if current_ms_since_last_call > target_ms_per_frame:
292 remaining_time = target_ms_per_frame - (
293 current_ms_since_real_refresh % target_ms_per_frame
295 time.sleep(remaining_time / 1000)
296 self._first_manual_refresh = False
297 self._refresh_display()
300 def _refresh_display(self):
301 if not self._core.start_refresh():
304 areas_to_refresh = self._get_refresh_areas()
305 for area in areas_to_refresh:
306 self._refresh_area(area)
308 self._core.finish_refresh()
312 def _get_refresh_areas(self) -> list[Area]:
313 """Get a list of areas to be refreshed"""
315 if self._core.full_refresh:
316 areas.append(self._core.area)
317 elif self._core.current_group is not None:
318 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
323 def background(self):
324 """Run background refresh tasks. Do not call directly"""
327 and (time.monotonic() * 1000 - self._core.last_refresh)
328 > self._native_ms_per_frame
332 def _refresh_area(self, area) -> bool:
333 """Loop through dirty areas and redraw that area."""
334 # pylint: disable=too-many-locals
337 # Clip the area to the display by overlapping the areas.
338 # If there is no overlap then we're done.
339 if not self._core.clip_area(area, clipped):
342 rows_per_buffer = clipped.height()
343 pixels_per_word = 32 // self._core.colorspace.depth
344 pixels_per_buffer = clipped.size()
346 # We should have lots of memory
347 buffer_size = clipped.size() // pixels_per_word
350 # for SH1107 and other boundary constrained controllers
351 # write one single row at a time
352 if self._core.sh1107_addressing:
353 subrectangles = rows_per_buffer // 8
355 elif clipped.size() > buffer_size * pixels_per_word:
356 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
357 if rows_per_buffer == 0:
359 # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
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:
374 mask_length = (pixels_per_buffer // 8) + 1 # 1 bit per pixel + 1
375 remaining_rows = clipped.height()
377 for subrect_index in range(subrectangles):
380 clipped.y1 + rows_per_buffer * subrect_index,
382 clipped.y1 + rows_per_buffer * (subrect_index + 1),
384 if remaining_rows < rows_per_buffer:
385 subrectangle.y2 = subrectangle.y1 + remaining_rows
386 remaining_rows -= rows_per_buffer
387 self._core.set_region_to_update(subrectangle)
388 if self._core.colorspace.depth >= 8:
389 subrectangle_size_bytes = subrectangle.size() * (
390 self._core.colorspace.depth // 8
393 subrectangle_size_bytes = subrectangle.size() // (
394 8 // self._core.colorspace.depth
397 buffer = memoryview(bytearray([0] * (buffer_size * 4)))
398 mask = memoryview(bytearray([0] * mask_length))
399 self._core.fill_area(subrectangle, mask, buffer)
400 self._core.begin_transaction()
401 self._send_pixels(buffer[:subrectangle_size_bytes])
402 self._core.end_transaction()
405 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
406 """Extract the pixels from a single row"""
407 if self._core.colorspace.depth != 16:
408 raise ValueError("Display must have a 16 bit colorspace.")
410 area = Area(0, y, self._core.width, y + 1)
411 pixels_per_word = 32 // self._core.colorspace.depth
412 buffer_size = self._core.width // pixels_per_word
413 pixels_per_buffer = area.size()
414 if pixels_per_buffer % pixels_per_word:
417 buffer = bytearray([0] * (buffer_size * 4))
418 mask_length = (pixels_per_buffer // 32) + 1
419 mask = array("L", [0x00000000] * mask_length)
420 self._core.fill_area(area, mask, buffer)
423 def release(self) -> None:
424 """Release the display and free its resources"""
425 self.auto_refresh = False
426 self._core.release_display_core()
428 def reset(self) -> None:
429 """Reset the display"""
430 self.auto_refresh = True
431 circuitpython_splash.x = 0
432 circuitpython_splash.y = 0
433 if not circuitpython_splash._in_group: # pylint: disable=protected-access
434 self._set_root_group(circuitpython_splash)
437 def auto_refresh(self) -> bool:
438 """True when the display is refreshed automatically."""
439 return self._auto_refresh
442 def auto_refresh(self, value: bool):
443 self._first_manual_refresh = not value
444 self._auto_refresh = value
447 def brightness(self) -> float:
448 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
449 return self._brightness
452 def brightness(self, value: float):
453 if 0 <= float(value) <= 1.0:
454 if not self._backlight_on_high:
457 if self._backlight_type == BACKLIGHT_PWM:
458 self._backlight.duty_cycle = value * 0xFFFF
459 elif self._backlight_type == BACKLIGHT_IN_OUT:
460 self._backlight.value = value > 0.99
461 elif self._brightness_command is not None:
462 okay = self._core.begin_transaction()
464 if self._core.data_as_commands:
467 CHIP_SELECT_TOGGLE_EVERY_BYTE,
468 bytes([self._brightness_command, round(0xFF * value)]),
473 CHIP_SELECT_TOGGLE_EVERY_BYTE,
474 bytes([self._brightness_command]),
477 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
479 self._core.end_transaction()
480 self._brightness = value
482 raise ValueError("Brightness must be between 0.0 and 1.0")
485 def width(self) -> int:
487 return self._core.get_width()
490 def height(self) -> int:
492 return self._core.get_height()
495 def rotation(self) -> int:
496 """The rotation of the display as an int in degrees."""
497 return self._core.get_rotation()
500 def rotation(self, value: int):
501 self._core.set_rotation(value)
504 def bus(self) -> _DisplayBus:
505 """Current Display Bus"""
506 return self._core.get_bus()