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,
41 __version__ = "0.0.0+auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
46 # pylint: disable=too-many-instance-attributes
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
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.
114 # Turn off auto-refresh as we init
115 self._auto_refresh = False
118 if single_byte_bounds:
122 self._core = _DisplayCore(
127 ram_height=ram_height,
131 color_depth=color_depth,
133 pixels_in_byte_share_row=pixels_in_byte_share_row,
134 bytes_per_cell=bytes_per_cell,
135 reverse_pixels_in_byte=reverse_pixels_in_byte,
136 reverse_bytes_in_word=reverse_bytes_in_word,
137 column_command=set_column_command,
138 row_command=set_row_command,
139 set_current_column_command=NO_COMMAND,
140 set_current_row_command=NO_COMMAND,
141 data_as_commands=data_as_commands,
142 always_toggle_chip_select=False,
143 sh1107_addressing=(SH1107_addressing and color_depth == 1),
144 address_little_endian=False,
147 self._write_ram_command = write_ram_command
148 self._brightness_command = brightness_command
149 self._first_manual_refresh = not auto_refresh
150 self._backlight_on_high = backlight_on_high
152 self._native_frames_per_second = native_frames_per_second
153 self._native_ms_per_frame = 1000 // native_frames_per_second
155 self._brightness = brightness
156 self._auto_refresh = auto_refresh
158 self._initialize(init_sequence)
160 self._current_group = None
161 self._last_refresh_call = 0
162 self._refresh_thread = None
163 self._colorconverter = ColorConverter()
165 self._backlight_type = None
166 if backlight_pin is not None:
168 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
170 # 100Hz looks decent and doesn't keep the CPU too busy
171 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
172 self._backlight_type = BACKLIGHT_PWM
174 # PWMOut not implemented on this platform
176 if self._backlight_type is None:
177 self._backlight_type = BACKLIGHT_IN_OUT
178 self._backlight = digitalio.DigitalInOut(backlight_pin)
179 self._backlight.switch_to_output()
180 self.brightness = brightness
181 if not circuitpython_splash._in_group:
182 self._set_root_group(circuitpython_splash)
183 self.auto_refresh = auto_refresh
185 def __new__(cls, *args, **kwargs):
186 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
190 display_instance = super().__new__(cls)
191 allocate_display(display_instance)
192 return display_instance
194 def _initialize(self, init_sequence):
196 while i < len(init_sequence):
197 command = init_sequence[i]
198 data_size = init_sequence[i + 1]
199 delay = (data_size & 0x80) > 0
202 if self._core.data_as_commands:
205 CHIP_SELECT_TOGGLE_EVERY_BYTE,
206 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
210 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
214 CHIP_SELECT_UNTOUCHED,
215 init_sequence[i + 2 : i + 2 + data_size],
220 delay_time_ms = init_sequence[i + 1 + data_size]
221 if delay_time_ms == 255:
223 time.sleep(delay_time_ms / 1000)
226 def _send_pixels(self, pixels):
227 if not self._core.data_as_commands:
230 CHIP_SELECT_TOGGLE_EVERY_BYTE,
231 bytes([self._write_ram_command]),
233 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
235 def show(self, group: Group) -> None:
236 """Switches to displaying the given group of layers. When group is None, the
237 default CircuitPython terminal will be shown.
240 group = circuitpython_splash
241 self._core.set_root_group(group)
243 def _set_root_group(self, root_group: Group) -> None:
244 ok = self._core.set_root_group(root_group)
246 raise ValueError("Group already used")
251 target_frames_per_second: Optional[int] = None,
252 minimum_frames_per_second: int = 0,
254 """When auto refresh is off, waits for the target frame rate and then refreshes the
255 display, returning True. If the call has taken too long since the last refresh call
256 for the given target frame rate, then the refresh returns False immediately without
257 updating the screen to hopefully help getting caught up.
259 If the time since the last successful refresh is below the minimum frame rate, then
260 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
262 When auto refresh is on, updates the display immediately. (The display will also
263 update without calls to this.)
265 maximum_ms_per_real_frame = 0xFFFFFFFF
266 if minimum_frames_per_second > 0:
267 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
269 if target_frames_per_second is None:
270 target_ms_per_frame = 0xFFFFFFFF
272 target_ms_per_frame = 1000 // target_frames_per_second
275 not self._auto_refresh
276 and not self._first_manual_refresh
277 and target_ms_per_frame != 0xFFFFFFFF
279 current_time = time.monotonic() * 1000
280 current_ms_since_real_refresh = current_time - self._core.last_refresh
281 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
282 raise RuntimeError("Below minimum frame rate")
283 current_ms_since_last_call = current_time - self._last_refresh_call
284 self._last_refresh_call = current_time
285 if current_ms_since_last_call > target_ms_per_frame:
288 remaining_time = target_ms_per_frame - (
289 current_ms_since_real_refresh % target_ms_per_frame
291 time.sleep(remaining_time / 1000)
292 self._first_manual_refresh = False
293 self._refresh_display()
296 def _refresh_display(self):
297 if not self._core.start_refresh():
300 areas_to_refresh = self._get_refresh_areas()
301 for area in areas_to_refresh:
302 self._refresh_area(area)
304 self._core.finish_refresh()
308 def _get_refresh_areas(self) -> list[Area]:
309 """Get a list of areas to be refreshed"""
311 if self._core.full_refresh:
312 areas.append(self._core.area)
313 elif self._core.current_group is not None:
314 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
319 def background(self):
320 """Run background refresh tasks. Do not call directly"""
323 and (time.monotonic() * 1000 - self._core.last_refresh)
324 > self._native_ms_per_frame
328 def _refresh_area(self, area) -> bool:
329 """Loop through dirty areas and redraw that area."""
330 # pylint: disable=too-many-locals
333 # Clip the area to the display by overlapping the areas.
334 # If there is no overlap then we're done.
335 if not self._core.clip_area(area, clipped):
338 rows_per_buffer = clipped.height()
339 pixels_per_word = 32 // self._core.colorspace.depth
340 pixels_per_buffer = clipped.size()
342 # We should have lots of memory
343 buffer_size = clipped.size() // pixels_per_word
346 # for SH1107 and other boundary constrained controllers
347 # write one single row at a time
348 if self._core.sh1107_addressing:
349 subrectangles = rows_per_buffer // 8
351 elif clipped.size() > buffer_size * pixels_per_word:
352 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
353 if rows_per_buffer == 0:
355 # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
357 self._core.colorspace.depth < 8
358 and self._core.colorspace.pixels_in_byte_share_row
360 pixels_per_byte = 8 // self._core.colorspace.depth
361 if rows_per_buffer % pixels_per_byte != 0:
362 rows_per_buffer -= rows_per_buffer % pixels_per_byte
363 subrectangles = clipped.height() // rows_per_buffer
364 if clipped.height() % rows_per_buffer != 0:
366 pixels_per_buffer = rows_per_buffer * clipped.width()
367 buffer_size = pixels_per_buffer // pixels_per_word
368 if pixels_per_buffer % pixels_per_word:
370 mask_length = (pixels_per_buffer // 8) + 1 # 1 bit per pixel + 1
371 remaining_rows = clipped.height()
373 for subrect_index in range(subrectangles):
376 clipped.y1 + rows_per_buffer * subrect_index,
378 clipped.y1 + rows_per_buffer * (subrect_index + 1),
380 if remaining_rows < rows_per_buffer:
381 subrectangle.y2 = subrectangle.y1 + remaining_rows
382 remaining_rows -= rows_per_buffer
383 self._core.set_region_to_update(subrectangle)
384 if self._core.colorspace.depth >= 8:
385 subrectangle_size_bytes = subrectangle.size() * (
386 self._core.colorspace.depth // 8
389 subrectangle_size_bytes = subrectangle.size() // (
390 8 // self._core.colorspace.depth
393 buffer = memoryview(bytearray([0] * (buffer_size * 4)))
394 mask = memoryview(bytearray([0] * mask_length))
395 self._core.fill_area(subrectangle, mask, buffer)
396 self._core.begin_transaction()
397 self._send_pixels(buffer[:subrectangle_size_bytes])
398 self._core.end_transaction()
401 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
402 """Extract the pixels from a single row"""
403 if self._core.colorspace.depth != 16:
404 raise ValueError("Display must have a 16 bit colorspace.")
406 area = Area(0, y, self._core.width, y + 1)
407 pixels_per_word = 32 // self._core.colorspace.depth
408 buffer_size = self._core.width // pixels_per_word
409 pixels_per_buffer = area.size()
410 if pixels_per_buffer % pixels_per_word:
413 buffer = bytearray([0] * (buffer_size * 4))
414 mask_length = (pixels_per_buffer // 32) + 1
415 mask = array("L", [0x00000000] * mask_length)
416 self._core.fill_area(area, mask, buffer)
419 def release(self) -> None:
420 """Release the display and free its resources"""
421 self.auto_refresh = False
422 self._core.release_display_core()
424 def reset(self) -> None:
425 """Reset the display"""
426 self.auto_refresh = True
427 circuitpython_splash.x = 0
428 circuitpython_splash.y = 0
429 if not circuitpython_splash._in_group: # pylint: disable=protected-access
430 self._set_root_group(circuitpython_splash)
433 def auto_refresh(self) -> bool:
434 """True when the display is refreshed automatically."""
435 return self._auto_refresh
438 def auto_refresh(self, value: bool):
439 self._first_manual_refresh = not value
440 self._auto_refresh = value
443 def brightness(self) -> float:
444 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
445 return self._brightness
448 def brightness(self, value: float):
449 if 0 <= float(value) <= 1.0:
450 if not self._backlight_on_high:
453 if self._backlight_type == BACKLIGHT_PWM:
454 self._backlight.duty_cycle = value * 0xFFFF
455 elif self._backlight_type == BACKLIGHT_IN_OUT:
456 self._backlight.value = value > 0.99
457 elif self._brightness_command is not None:
458 self._core.begin_transaction()
459 if self._core.data_as_commands:
462 CHIP_SELECT_TOGGLE_EVERY_BYTE,
463 bytes([self._brightness_command, round(0xFF * value)]),
468 CHIP_SELECT_TOGGLE_EVERY_BYTE,
469 bytes([self._brightness_command]),
472 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
474 self._core.end_transaction()
475 self._brightness = value
477 raise ValueError("Brightness must be between 0.0 and 1.0")
480 def width(self) -> int:
482 return self._core.get_width()
485 def height(self) -> int:
487 return self._core.get_height()
490 def rotation(self) -> int:
491 """The rotation of the display as an int in degrees."""
492 return self._core.get_rotation()
495 def rotation(self, value: int):
496 self._core.set_rotation(value)
499 def bus(self) -> _DisplayBus:
500 """Current Display Bus"""
501 return self._core.get_bus()