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, too-many-branches
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.
116 if rotation % 90 != 0:
117 raise ValueError("Display rotation must be in 90 degree increments")
119 if SH1107_addressing and color_depth != 1:
120 raise ValueError("color_depth must be 1 when SH1107_addressing is True")
122 # Turn off auto-refresh as we init
123 self._auto_refresh = False
126 if single_byte_bounds:
130 self._core = _DisplayCore(
135 ram_height=ram_height,
139 color_depth=color_depth,
141 pixels_in_byte_share_row=pixels_in_byte_share_row,
142 bytes_per_cell=bytes_per_cell,
143 reverse_pixels_in_byte=reverse_pixels_in_byte,
144 reverse_bytes_in_word=reverse_bytes_in_word,
145 column_command=set_column_command,
146 row_command=set_row_command,
147 set_current_column_command=NO_COMMAND,
148 set_current_row_command=NO_COMMAND,
149 data_as_commands=data_as_commands,
150 always_toggle_chip_select=False,
151 sh1107_addressing=(SH1107_addressing and color_depth == 1),
152 address_little_endian=False,
155 self._write_ram_command = write_ram_command
156 self._brightness_command = brightness_command
157 self._first_manual_refresh = not auto_refresh
158 self._backlight_on_high = backlight_on_high
160 self._native_frames_per_second = native_frames_per_second
161 self._native_ms_per_frame = 1000 // native_frames_per_second
163 self._brightness = brightness
164 self._auto_refresh = auto_refresh
167 while i < len(init_sequence):
168 command = init_sequence[i]
169 data_size = init_sequence[i + 1]
170 delay = (data_size & DELAY) != 0
172 while self._core.begin_transaction():
175 if self._core.data_as_commands:
176 full_command = bytearray(data_size + 1)
177 full_command[0] = command
178 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
181 CHIP_SELECT_TOGGLE_EVERY_BYTE,
186 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
190 CHIP_SELECT_UNTOUCHED,
191 init_sequence[i + 2 : i + 2 + data_size],
193 self._core.end_transaction()
197 delay_time_ms = init_sequence[i + 1 + data_size]
198 if delay_time_ms == 255:
200 time.sleep(delay_time_ms / 1000)
203 self._current_group = None
204 self._last_refresh_call = 0
205 self._refresh_thread = None
206 self._colorconverter = ColorConverter()
208 self._backlight_type = None
209 if backlight_pin is not None:
211 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
213 # 100Hz looks decent and doesn't keep the CPU too busy
214 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
215 self._backlight_type = BACKLIGHT_PWM
217 # PWMOut not implemented on this platform
219 if self._backlight_type is None:
220 self._backlight_type = BACKLIGHT_IN_OUT
221 self._backlight = digitalio.DigitalInOut(backlight_pin)
222 self._backlight.switch_to_output()
223 self.brightness = brightness
224 if not circuitpython_splash._in_group:
225 self._set_root_group(circuitpython_splash)
226 self.auto_refresh = auto_refresh
228 def __new__(cls, *args, **kwargs):
229 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
233 display_instance = super().__new__(cls)
234 allocate_display(display_instance)
235 return display_instance
237 def _send_pixels(self, pixels):
238 if not self._core.data_as_commands:
241 CHIP_SELECT_TOGGLE_EVERY_BYTE,
242 bytes([self._write_ram_command]),
244 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
246 def show(self, group: Group) -> None:
247 """Switches to displaying the given group of layers. When group is None, the
248 default CircuitPython terminal will be shown.
251 group = circuitpython_splash
252 self._core.set_root_group(group)
254 def _set_root_group(self, root_group: Group) -> None:
255 ok = self._core.set_root_group(root_group)
257 raise ValueError("Group already used")
262 target_frames_per_second: Optional[int] = None,
263 minimum_frames_per_second: int = 0,
265 """When auto refresh is off, waits for the target frame rate and then refreshes the
266 display, returning True. If the call has taken too long since the last refresh call
267 for the given target frame rate, then the refresh returns False immediately without
268 updating the screen to hopefully help getting caught up.
270 If the time since the last successful refresh is below the minimum frame rate, then
271 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
273 When auto refresh is on, updates the display immediately. (The display will also
274 update without calls to this.)
276 maximum_ms_per_real_frame = 0xFFFFFFFF
277 if minimum_frames_per_second > 0:
278 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
280 if target_frames_per_second is None:
281 target_ms_per_frame = 0xFFFFFFFF
283 target_ms_per_frame = 1000 // target_frames_per_second
286 not self._auto_refresh
287 and not self._first_manual_refresh
288 and target_ms_per_frame != 0xFFFFFFFF
290 current_time = time.monotonic() * 1000
291 current_ms_since_real_refresh = current_time - self._core.last_refresh
292 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
293 raise RuntimeError("Below minimum frame rate")
294 current_ms_since_last_call = current_time - self._last_refresh_call
295 self._last_refresh_call = current_time
296 if current_ms_since_last_call > target_ms_per_frame:
299 remaining_time = target_ms_per_frame - (
300 current_ms_since_real_refresh % target_ms_per_frame
302 time.sleep(remaining_time / 1000)
303 self._first_manual_refresh = False
304 self._refresh_display()
307 def _refresh_display(self):
308 if not self._core.start_refresh():
311 areas_to_refresh = self._get_refresh_areas()
312 for area in areas_to_refresh:
313 self._refresh_area(area)
315 self._core.finish_refresh()
319 def _get_refresh_areas(self) -> list[Area]:
320 """Get a list of areas to be refreshed"""
322 if self._core.full_refresh:
323 areas.append(self._core.area)
324 elif self._core.current_group is not None:
325 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
330 def _background(self):
331 """Run background refresh tasks. Do not call directly"""
334 and (time.monotonic() * 1000 - self._core.last_refresh)
335 > self._native_ms_per_frame
339 def _refresh_area(self, area) -> bool:
340 """Loop through dirty areas and redraw that area."""
341 # pylint: disable=too-many-locals, too-many-branches
344 # Clip the area to the display by overlapping the areas.
345 # If there is no overlap then we're done.
346 if not self._core.clip_area(area, clipped):
349 rows_per_buffer = clipped.height()
350 pixels_per_word = 32 // self._core.colorspace.depth
351 pixels_per_buffer = clipped.size()
353 # We should have lots of memory
354 buffer_size = clipped.size() // pixels_per_word
357 # for SH1107 and other boundary constrained controllers
358 # write one single row at a time
359 if self._core.sh1107_addressing:
360 subrectangles = rows_per_buffer // 8
362 elif clipped.size() > buffer_size * pixels_per_word:
363 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
364 if rows_per_buffer == 0:
366 # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
368 self._core.colorspace.depth < 8
369 and self._core.colorspace.pixels_in_byte_share_row
371 pixels_per_byte = 8 // self._core.colorspace.depth
372 if rows_per_buffer % pixels_per_byte != 0:
373 rows_per_buffer -= rows_per_buffer % pixels_per_byte
374 subrectangles = clipped.height() // rows_per_buffer
375 if clipped.height() % rows_per_buffer != 0:
377 pixels_per_buffer = rows_per_buffer * clipped.width()
378 buffer_size = pixels_per_buffer // pixels_per_word
379 if pixels_per_buffer % pixels_per_word:
381 mask_length = (pixels_per_buffer // 8) + 1 # 1 bit per pixel + 1
382 remaining_rows = clipped.height()
384 for subrect_index in range(subrectangles):
387 clipped.y1 + rows_per_buffer * subrect_index,
389 clipped.y1 + rows_per_buffer * (subrect_index + 1),
391 if remaining_rows < rows_per_buffer:
392 subrectangle.y2 = subrectangle.y1 + remaining_rows
393 remaining_rows -= rows_per_buffer
394 self._core.set_region_to_update(subrectangle)
395 if self._core.colorspace.depth >= 8:
396 subrectangle_size_bytes = subrectangle.size() * (
397 self._core.colorspace.depth // 8
400 subrectangle_size_bytes = subrectangle.size() // (
401 8 // self._core.colorspace.depth
404 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
405 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
406 self._core.fill_area(subrectangle, mask, buffer)
408 # Can't acquire display bus; skip the rest of the data.
409 if not self._core.bus_free():
412 self._core.begin_transaction()
413 self._send_pixels(buffer[:subrectangle_size_bytes])
414 self._core.end_transaction()
417 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
418 """Extract the pixels from a single row"""
419 if self._core.colorspace.depth != 16:
420 raise ValueError("Display must have a 16 bit colorspace.")
422 area = Area(0, y, self._core.width, y + 1)
423 pixels_per_word = 32 // self._core.colorspace.depth
424 buffer_size = self._core.width // pixels_per_word
425 pixels_per_buffer = area.size()
426 if pixels_per_buffer % pixels_per_word:
429 buffer = bytearray([0] * (buffer_size * 4))
430 mask_length = (pixels_per_buffer // 32) + 1
431 mask = array("L", [0x00000000] * mask_length)
432 self._core.fill_area(area, mask, buffer)
435 def _release(self) -> None:
436 """Release the display and free its resources"""
437 self.auto_refresh = False
438 self._core.release_display_core()
440 def _reset(self) -> None:
441 """Reset the display"""
442 self.auto_refresh = True
443 circuitpython_splash.x = 0
444 circuitpython_splash.y = 0
445 if not circuitpython_splash._in_group: # pylint: disable=protected-access
446 self._set_root_group(circuitpython_splash)
449 def auto_refresh(self) -> bool:
450 """True when the display is refreshed automatically."""
451 return self._auto_refresh
454 def auto_refresh(self, value: bool):
455 self._first_manual_refresh = not value
456 self._auto_refresh = value
459 def brightness(self) -> float:
460 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
461 return self._brightness
464 def brightness(self, value: float):
465 if 0 <= float(value) <= 1.0:
466 if not self._backlight_on_high:
469 if self._backlight_type == BACKLIGHT_PWM:
470 self._backlight.duty_cycle = value * 0xFFFF
471 elif self._backlight_type == BACKLIGHT_IN_OUT:
472 self._backlight.value = value > 0.99
473 elif self._brightness_command is not None:
474 okay = self._core.begin_transaction()
476 if self._core.data_as_commands:
479 CHIP_SELECT_TOGGLE_EVERY_BYTE,
480 bytes([self._brightness_command, round(0xFF * value)]),
485 CHIP_SELECT_TOGGLE_EVERY_BYTE,
486 bytes([self._brightness_command]),
489 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
491 self._core.end_transaction()
492 self._brightness = value
494 raise ValueError("Brightness must be between 0.0 and 1.0")
497 def width(self) -> int:
499 return self._core.get_width()
502 def height(self) -> int:
504 return self._core.get_height()
507 def rotation(self) -> int:
508 """The rotation of the display as an int in degrees."""
509 return self._core.get_rotation()
512 def rotation(self, value: int):
514 raise ValueError("Display rotation must be in 90 degree increments")
515 self._core.set_rotation(value)
518 def bus(self) -> _DisplayBus:
519 """Current Display Bus"""
520 return self._core.get_bus()