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 array import array
23 from typing import Optional
25 import microcontroller
26 from circuitpython_typing import WriteableBuffer, ReadableBuffer
27 from ._displaycore import _DisplayCore
28 from ._displaybus import _DisplayBus
29 from ._colorconverter import ColorConverter
30 from ._group import Group, circuitpython_splash
31 from ._area import Area
32 from ._constants import (
33 CHIP_SELECT_TOGGLE_EVERY_BYTE,
34 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
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
159 self._initialize(init_sequence)
161 self._current_group = None
162 self._last_refresh_call = 0
163 self._refresh_thread = None
164 self._colorconverter = ColorConverter()
166 self._backlight_type = None
167 if backlight_pin is not None:
169 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
171 # 100Hz looks decent and doesn't keep the CPU too busy
172 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
173 self._backlight_type = BACKLIGHT_PWM
175 # PWMOut not implemented on this platform
177 if self._backlight_type is None:
178 self._backlight_type = BACKLIGHT_IN_OUT
179 self._backlight = digitalio.DigitalInOut(backlight_pin)
180 self._backlight.switch_to_output()
181 self.brightness = brightness
182 if not circuitpython_splash._in_group:
183 self._set_root_group(circuitpython_splash)
184 self.auto_refresh = auto_refresh
186 def __new__(cls, *args, **kwargs):
187 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
191 display_instance = super().__new__(cls)
192 allocate_display(display_instance)
193 return display_instance
195 def _initialize(self, init_sequence):
197 while i < len(init_sequence):
198 command = init_sequence[i]
199 data_size = init_sequence[i + 1]
200 delay = (data_size & 0x80) > 0
203 if self._core.data_as_commands:
206 CHIP_SELECT_TOGGLE_EVERY_BYTE,
207 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
211 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
215 CHIP_SELECT_UNTOUCHED,
216 init_sequence[i + 2 : i + 2 + data_size],
221 delay_time_ms = init_sequence[i + 1 + data_size]
222 if delay_time_ms == 255:
224 time.sleep(delay_time_ms / 1000)
227 def _send_pixels(self, pixels):
228 if not self._core.data_as_commands:
231 CHIP_SELECT_TOGGLE_EVERY_BYTE,
232 bytes([self._write_ram_command]),
234 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
236 def show(self, group: Group) -> None:
237 """Switches to displaying the given group of layers. When group is None, the
238 default CircuitPython terminal will be shown.
241 group = circuitpython_splash
242 self._core.set_root_group(group)
244 def _set_root_group(self, root_group: Group) -> None:
245 ok = self._core.set_root_group(root_group)
247 raise ValueError("Group already used")
252 target_frames_per_second: Optional[int] = None,
253 minimum_frames_per_second: int = 0,
255 """When auto refresh is off, waits for the target frame rate and then refreshes the
256 display, returning True. If the call has taken too long since the last refresh call
257 for the given target frame rate, then the refresh returns False immediately without
258 updating the screen to hopefully help getting caught up.
260 If the time since the last successful refresh is below the minimum frame rate, then
261 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
263 When auto refresh is on, updates the display immediately. (The display will also
264 update without calls to this.)
266 maximum_ms_per_real_frame = 0xFFFFFFFF
267 if minimum_frames_per_second > 0:
268 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
270 if target_frames_per_second is None:
271 target_ms_per_frame = 0xFFFFFFFF
273 target_ms_per_frame = 1000 // target_frames_per_second
276 not self._auto_refresh
277 and not self._first_manual_refresh
278 and target_ms_per_frame != 0xFFFFFFFF
280 current_time = time.monotonic() * 1000
281 current_ms_since_real_refresh = current_time - self._core.last_refresh
282 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
283 raise RuntimeError("Below minimum frame rate")
284 current_ms_since_last_call = current_time - self._last_refresh_call
285 self._last_refresh_call = current_time
286 if current_ms_since_last_call > target_ms_per_frame:
289 remaining_time = target_ms_per_frame - (
290 current_ms_since_real_refresh % target_ms_per_frame
292 time.sleep(remaining_time / 1000)
293 self._first_manual_refresh = False
294 self._refresh_display()
297 def _refresh_display(self):
298 if not self._core.start_refresh():
301 areas_to_refresh = self._get_refresh_areas()
302 for area in areas_to_refresh:
303 self._refresh_area(area)
305 self._core.finish_refresh()
309 def _get_refresh_areas(self) -> list[Area]:
310 """Get a list of areas to be refreshed"""
312 if self._core.full_refresh:
313 areas.append(self._core.area)
314 elif self._core.current_group is not None:
315 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
320 def background(self):
321 """Run background refresh tasks. Do not call directly"""
324 and (time.monotonic() * 1000 - self._core.last_refresh)
325 > self._native_ms_per_frame
329 def _refresh_area(self, area) -> bool:
330 """Loop through dirty areas and redraw that area."""
331 # pylint: disable=too-many-locals
335 # Clip the area to the display by overlapping the areas.
336 # If there is no overlap then we're done.
337 if not self._core.clip_area(area, clipped):
340 rows_per_buffer = clipped.height()
341 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
342 pixels_per_buffer = clipped.size()
345 # for SH1107 and other boundary constrained controllers
346 # write one single row at a time
347 if self._core.sh1107_addressing:
348 subrectangles = rows_per_buffer // 8
350 elif clipped.size() > buffer_size * pixels_per_word:
351 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
352 if rows_per_buffer == 0:
354 # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
356 self._core.colorspace.depth < 8
357 and self._core.colorspace.pixels_in_byte_share_row
359 pixels_per_byte = 8 // self._core.colorspace.depth
360 if rows_per_buffer % pixels_per_byte != 0:
361 rows_per_buffer -= rows_per_buffer % pixels_per_byte
362 subrectangles = clipped.height() // rows_per_buffer
363 if clipped.height() % rows_per_buffer != 0:
365 pixels_per_buffer = rows_per_buffer * clipped.width()
366 buffer_size = pixels_per_buffer // pixels_per_word
367 if pixels_per_buffer % pixels_per_word:
370 # TODO: Optimize with memoryview
371 mask_length = (pixels_per_buffer // 8) + 1 # 1 bit per pixel + 1
372 remaining_rows = clipped.height()
374 for subrect_index in range(subrectangles):
377 clipped.y1 + rows_per_buffer * subrect_index,
379 clipped.y1 + rows_per_buffer * (subrect_index + 1),
381 if remaining_rows < rows_per_buffer:
382 subrectangle.y2 = subrectangle.y1 + remaining_rows
383 remaining_rows -= rows_per_buffer
384 self._core.set_region_to_update(subrectangle)
385 if self._core.colorspace.depth >= 8:
386 subrectangle_size_bytes = subrectangle.size() * (
387 self._core.colorspace.depth // 8
390 subrectangle_size_bytes = subrectangle.size() // (
391 8 // self._core.colorspace.depth
394 buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
395 mask = bytearray([0] * mask_length)
396 self._core.fill_area(subrectangle, mask, buffer)
397 self._core.begin_transaction()
398 self._send_pixels(buffer[:subrectangle_size_bytes])
399 self._core.end_transaction()
402 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
403 """Extract the pixels from a single row"""
404 if self._core.colorspace.depth != 16:
405 raise ValueError("Display must have a 16 bit colorspace.")
407 area = Area(0, y, self._core.width, y + 1)
408 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
409 buffer_size = self._core.width // pixels_per_word
410 pixels_per_buffer = area.size()
411 if pixels_per_buffer % pixels_per_word:
414 buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
415 mask_length = (pixels_per_buffer // 32) + 1
416 mask = array("L", [0x00000000] * mask_length)
417 self._core.fill_area(area, mask, buffer)
420 def release(self) -> None:
421 """Release the display and free its resources"""
422 self.auto_refresh = False
423 self._core.release_display_core()
425 def reset(self) -> None:
426 """Reset the display"""
427 self.auto_refresh = True
428 circuitpython_splash.x = 0
429 circuitpython_splash.y = 0
430 if not circuitpython_splash._in_group: # pylint: disable=protected-access
431 self._set_root_group(circuitpython_splash)
434 def auto_refresh(self) -> bool:
435 """True when the display is refreshed automatically."""
436 return self._auto_refresh
439 def auto_refresh(self, value: bool):
440 self._first_manual_refresh = not value
441 self._auto_refresh = value
444 def brightness(self) -> float:
445 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
446 return self._brightness
449 def brightness(self, value: float):
450 if 0 <= float(value) <= 1.0:
451 if not self._backlight_on_high:
454 if self._backlight_type == BACKLIGHT_PWM:
455 self._backlight.duty_cycle = value * 0xFFFF
456 elif self._backlight_type == BACKLIGHT_IN_OUT:
457 self._backlight.value = value > 0.99
458 elif self._brightness_command is not None:
459 self._core.begin_transaction()
460 if self._core.data_as_commands:
463 CHIP_SELECT_TOGGLE_EVERY_BYTE,
464 bytes([self._brightness_command, 0xFF * value]),
469 CHIP_SELECT_TOGGLE_EVERY_BYTE,
470 bytes([self._brightness_command]),
473 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
475 self._core.end_transaction()
476 self._brightness = value
478 raise ValueError("Brightness must be between 0.0 and 1.0")
481 def width(self) -> int:
483 return self._core.get_width()
486 def height(self) -> int:
488 return self._core.get_height()
491 def rotation(self) -> int:
492 """The rotation of the display as an int in degrees."""
493 return self._core.get_rotation()
496 def rotation(self, value: int):
497 self._core.set_rotation(value)
500 def bus(self) -> _DisplayBus:
501 """Current Display Bus"""
502 return self._core.get_bus()