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
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)
160 self._current_group = None
161 self._last_refresh_call = 0
162 self._refresh_thread = None
163 if self._auto_refresh:
164 self.auto_refresh = True
165 self._colorconverter = ColorConverter()
167 self._backlight_type = None
168 if backlight_pin is not None:
170 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
172 # 100Hz looks decent and doesn't keep the CPU too busy
173 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
174 self._backlight_type = BACKLIGHT_PWM
176 # PWMOut not implemented on this platform
178 if self._backlight_type is None:
179 self._backlight_type = BACKLIGHT_IN_OUT
180 self._backlight = digitalio.DigitalInOut(backlight_pin)
181 self._backlight.switch_to_output()
182 self.brightness = brightness
184 def __new__(cls, *args, **kwargs):
185 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
189 display_instance = super().__new__(cls)
190 allocate_display(display_instance)
191 return display_instance
193 def _initialize(self, init_sequence):
195 while i < len(init_sequence):
196 command = init_sequence[i]
197 data_size = init_sequence[i + 1]
198 delay = (data_size & 0x80) > 0
201 if self._core.data_as_commands:
204 CHIP_SELECT_TOGGLE_EVERY_BYTE,
205 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
209 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
213 CHIP_SELECT_UNTOUCHED,
214 init_sequence[i + 2 : i + 2 + data_size],
219 delay_time_ms = init_sequence[i + 1 + data_size]
220 if delay_time_ms == 255:
222 time.sleep(delay_time_ms / 1000)
225 def _send_pixels(self, pixels):
226 if not self._core.data_as_commands:
229 CHIP_SELECT_TOGGLE_EVERY_BYTE,
230 bytes([self._write_ram_command]),
232 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
234 def show(self, group: Group) -> None:
235 """Switches to displaying the given group of layers. When group is None, the
236 default CircuitPython terminal will be shown.
238 self._core.show(group)
243 target_frames_per_second: Optional[int] = None,
244 minimum_frames_per_second: int = 0,
246 """When auto refresh is off, waits for the target frame rate and then refreshes the
247 display, returning True. If the call has taken too long since the last refresh call
248 for the given target frame rate, then the refresh returns False immediately without
249 updating the screen to hopefully help getting caught up.
251 If the time since the last successful refresh is below the minimum frame rate, then
252 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
254 When auto refresh is on, updates the display immediately. (The display will also
255 update without calls to this.)
257 maximum_ms_per_real_frame = 0xFFFFFFFF
258 if minimum_frames_per_second > 0:
259 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
261 if target_frames_per_second is None:
262 target_ms_per_frame = 0xFFFFFFFF
264 target_ms_per_frame = 1000 // target_frames_per_second
267 not self._auto_refresh
268 and not self._first_manual_refresh
269 and target_ms_per_frame != 0xFFFFFFFF
271 current_time = time.monotonic() * 1000
272 current_ms_since_real_refresh = current_time - self._core.last_refresh
273 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
274 raise RuntimeError("Below minimum frame rate")
275 current_ms_since_last_call = current_time - self._last_refresh_call
276 self._last_refresh_call = current_time
277 if current_ms_since_last_call > target_ms_per_frame:
280 remaining_time = target_ms_per_frame - (
281 current_ms_since_real_refresh % target_ms_per_frame
283 time.sleep(remaining_time / 1000)
284 self._first_manual_refresh = False
285 self._refresh_display()
288 def _refresh_display(self):
289 if not self._core.start_refresh():
292 areas_to_refresh = self._get_refresh_areas()
293 for area in areas_to_refresh:
294 self._refresh_area(area)
296 self._core.finish_refresh()
300 def _get_refresh_areas(self) -> list[Area]:
301 """Get a list of areas to be refreshed"""
303 if self._core.full_refresh:
304 areas.append(self._core.area)
305 elif self._core.current_group is not None:
306 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
311 def background(self):
312 """Run background refresh tasks. Do not call directly"""
315 and (time.monotonic() * 1000 - self._core.last_refresh)
316 > self._native_ms_per_frame
320 def _refresh_area(self, area) -> bool:
321 """Loop through dirty areas and redraw that area."""
322 # pylint: disable=too-many-locals
326 if not self._core.clip_area(area, clipped):
329 rows_per_buffer = clipped.height()
330 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
331 pixels_per_buffer = clipped.size()
335 if self._core.sh1107_addressing:
336 subrectangles = rows_per_buffer // 8
338 elif clipped.size() > buffer_size * pixels_per_word:
339 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
340 if rows_per_buffer == 0:
343 self._core.colorspace.depth < 8
344 and self._core.colorspace.pixels_in_byte_share_row
346 pixels_per_byte = 8 // self._core.colorspace.depth
347 if rows_per_buffer % pixels_per_byte != 0:
348 rows_per_buffer -= rows_per_buffer % pixels_per_byte
349 subrectangles = clipped.height() // rows_per_buffer
350 if clipped.height() % rows_per_buffer != 0:
352 pixels_per_buffer = rows_per_buffer * clipped.width()
353 buffer_size = pixels_per_buffer // pixels_per_word
354 if pixels_per_buffer % pixels_per_word:
357 buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
358 mask_length = (pixels_per_buffer // 32) + 1
359 mask = array("L", [0] * mask_length)
360 remaining_rows = clipped.height()
362 for subrect_index in range(subrectangles):
365 clipped.y1 + rows_per_buffer * subrect_index,
367 clipped.y1 + rows_per_buffer * (subrect_index + 1),
369 if remaining_rows < rows_per_buffer:
370 subrectangle.y2 = subrectangle.y1 + remaining_rows
371 self._core.set_region_to_update(subrectangle)
372 if self._core.colorspace.depth >= 8:
373 subrectangle_size_bytes = subrectangle.size() * (
374 self._core.colorspace.depth // 8
377 subrectangle_size_bytes = subrectangle.size() // (
378 8 // self._core.colorspace.depth
381 self._core.fill_area(subrectangle, mask, buffer)
383 self._core.begin_transaction()
384 self._send_pixels(buffer[:subrectangle_size_bytes])
385 self._core.end_transaction()
388 def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
389 """Extract the pixels from a single row"""
390 if self._core.colorspace.depth != 16:
391 raise ValueError("Display must have a 16 bit colorspace.")
393 area = Area(0, y, self._core.width, y + 1)
394 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
395 buffer_size = self._core.width // pixels_per_word
396 pixels_per_buffer = area.size()
397 if pixels_per_buffer % pixels_per_word:
400 buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
401 mask_length = (pixels_per_buffer // 32) + 1
402 mask = array("L", [0x00000000] * mask_length)
403 self._core.fill_area(area, mask, buffer)
406 def release(self) -> None:
407 """Release the display and free its resources"""
408 self.auto_refresh = False
409 self._core.release_display_core()
411 def reset(self) -> None:
412 """Reset the display"""
413 self.auto_refresh = True
416 def auto_refresh(self) -> bool:
417 """True when the display is refreshed automatically."""
418 return self._auto_refresh
421 def auto_refresh(self, value: bool):
422 self._first_manual_refresh = not value
423 self._auto_refresh = value
426 def brightness(self) -> float:
427 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
428 return self._brightness
431 def brightness(self, value: float):
432 if 0 <= float(value) <= 1.0:
433 if not self._backlight_on_high:
436 if self._backlight_type == BACKLIGHT_PWM:
437 self._backlight.duty_cycle = value * 0xFFFF
438 elif self._backlight_type == BACKLIGHT_IN_OUT:
439 self._backlight.value = value > 0.99
440 elif self._brightness_command is not None:
441 self._core.begin_transaction()
442 if self._core.data_as_commands:
445 CHIP_SELECT_TOGGLE_EVERY_BYTE,
446 bytes([self._brightness_command, 0xFF * value]),
451 CHIP_SELECT_TOGGLE_EVERY_BYTE,
452 bytes([self._brightness_command]),
455 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
457 self._core.end_transaction()
458 self._brightness = value
460 raise ValueError("Brightness must be between 0.0 and 1.0")
463 def width(self) -> int:
465 return self._core.get_width()
468 def height(self) -> int:
470 return self._core.get_height()
473 def rotation(self) -> int:
474 """The rotation of the display as an int in degrees."""
475 return self._core.get_rotation()
478 def rotation(self, value: int):
479 self._core.set_rotation(value)
482 def bus(self) -> _DisplayBus:
483 """Current Display Bus"""
484 return self._core.get_bus()