diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index ce7d8f36018..df122905cc2 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -861,6 +861,7 @@ msgstr "" #: ports/analog/common-hal/busio/SPI.c #: ports/espressif/common-hal/espidf/__init__.c #: ports/nordic/common-hal/_bleio/__init__.c +#: shared-bindings/epaperdisplay/EPaperDisplay.c msgid "Invalid state" msgstr "" @@ -1737,8 +1738,10 @@ msgstr "" #: ports/raspberrypi/common-hal/usb_host/Port.c #: shared-bindings/digitalio/DigitalInOut.c shared-bindings/emmcio/EMMC.c #: shared-bindings/i2cioexpander/IOPin.c shared-bindings/microcontroller/Pin.c -#: shared-module/_airlift/__init__.c shared-module/max3421e/Max3421E.c -#: shared-module/usb/core/Device.c shared-module/usb_host_audio/USBIn.c +#: shared-module/_airlift/__init__.c +#: shared-module/epaperdisplay/EPaperDisplay.c +#: shared-module/max3421e/Max3421E.c shared-module/usb/core/Device.c +#: shared-module/usb_host_audio/USBIn.c msgid "%q in use" msgstr "" diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk index 52c9a271138..3c5c311c52f 100644 --- a/ports/atmel-samd/mpconfigport.mk +++ b/ports/atmel-samd/mpconfigport.mk @@ -39,6 +39,7 @@ CIRCUITPY_BUILTINS_POW3 ?= 0 CIRCUITPY_BUSDEVICE ?= 0 CIRCUITPY_COMPUTED_GOTO_SAVE_SPACE ?= 1 CIRCUITPY_COUNTIO ?= 0 +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 # Not enough RAM for framebuffers CIRCUITPY_FRAMEBUFFERIO ?= 0 CIRCUITPY_FREQUENCYIO ?= 0 @@ -123,6 +124,10 @@ endif CIRCUITPY_ALARM ?= 1 # Not enough room for both bitmaptools and bitmapfilter on most boards. CIRCUITPY_BITMAPFILTER ?= 0 +# Fast e-paper only fits on chips with 1MB flash. +ifeq ($(HAS_1MB_FLASH),0) +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif CIRCUITPY_FLOPPYIO ?= $(CIRCUITPY_FULL_BUILD) CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_FULL_BUILD) CIRCUITPY_LOAD_NATIVE ?= 1 diff --git a/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk b/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk index 3550957a7f4..0d9fdd6ca11 100644 --- a/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk +++ b/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk @@ -15,3 +15,6 @@ CIRCUITPY_ESP_PSRAM_FREQ = 80m # Out of internal RAM with audiopwmio CIRCUITPY_AUDIOPWMIO = 0 + +# Fast e-paper doesn't fit in internal RAM. +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk b/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk index 4f2eb15b7f4..fe5ea4a7da1 100644 --- a/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk +++ b/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk @@ -15,3 +15,6 @@ CIRCUITPY_ESP_PSRAM_FREQ = 80m # Out of internal RAM with audiopwmio CIRCUITPY_AUDIOPWMIO = 0 + +# Fast e-paper doesn't fit in internal RAM. +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 1e83c4f3eeb..2d32e0f6d3e 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -34,6 +34,11 @@ IDF_TARGET_ARCH = xtensa CROSS_COMPILE = xtensa-$(IDF_TARGET)-elf- endif +# Fast e-paper doesn't fit in flash on the C3, C5 and C6. +ifneq ($(filter $(IDF_TARGET),esp32c3 esp32c5 esp32c6),) +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif + # Use "internal flash" for CIRCUITPY drive. # Even thought it's an external flash chip, it appears as internal. INTERNAL_FLASH_FILESYSTEM = 1 diff --git a/ports/nordic/boards/TG-Watch/mpconfigboard.mk b/ports/nordic/boards/TG-Watch/mpconfigboard.mk index 212ab7edc20..011890898ac 100644 --- a/ports/nordic/boards/TG-Watch/mpconfigboard.mk +++ b/ports/nordic/boards/TG-Watch/mpconfigboard.mk @@ -20,3 +20,5 @@ FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_LC709203F FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_DRV2605 FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE_Apple_Notification_Center + +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/nordic/mpconfigport.mk b/ports/nordic/mpconfigport.mk index 80ebe319718..d424bf0fb4f 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -98,6 +98,8 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) CIRCUITPY_LTO_PARTITION = balanced CIRCUITPY_MSGPACK ?= 0 CIRCUITPY_ULAB_OPTIMIZE_SIZE ?= 1 + # Fast e-paper doesn't fit in what internal flash has left. + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 else CIRCUITPY_USB_AUDIO ?= 1 endif @@ -125,6 +127,7 @@ OPTIMIZATION_FLAGS ?= -Os CIRCUITPY_LTO = 1 CIRCUITPY_LTO_PARTITION = one ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 CIRCUITPY_FULL_BUILD ?= 0 CIRCUITPY_PULSEIO ?= 1 endif diff --git a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk index 3ab04a09833..95d228a8642 100644 --- a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_FLOPPYIO = 0 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 # The default is -O3. Change to -O2 because the build was overflowing. diff --git a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk index d47571e7950..c2e26d9caaf 100644 --- a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q32JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 # GPIO12-19 needed for picodvi, but GPIO16 is used for Wiznet. CIRCUITPY_PICODVI = 0 diff --git a/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk index 4a8343963bc..96f36b4a0c2 100644 --- a/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_FLOPPYIO = 0 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 # The default is -O3. Change to -O2 because the build was overflowing. diff --git a/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk index 68b621fdf97..9aa01792824 100644 --- a/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q32JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 # GPIO12-19 needed for picodvi, but GPIO16 is used for Wiznet. CIRCUITPY_PICODVI = 0 diff --git a/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk index d43195c0f2a..edd434fe119 100755 --- a/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "PY25Q16HB" CIRCUITPY_WIZNET = 1 CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 OPTIMIZATION_FLAGS = -O2 diff --git a/ports/stm/mpconfigport.mk b/ports/stm/mpconfigport.mk index aa5038964c5..5d73bc9ee3e 100644 --- a/ports/stm/mpconfigport.mk +++ b/ports/stm/mpconfigport.mk @@ -19,6 +19,11 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) OPTIMIZATION_FLAGS ?= -Os endif +# Fast e-paper doesn't fit in flash on the F401 and F411. +ifeq ($(MCU_VARIANT),$(filter $(MCU_VARIANT),STM32F401xE STM32F411xE)) + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif + ifeq ($(MCU_VARIANT),STM32F407xx) UF2_FAMILY_ID ?= 0x6d0922fa endif diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 4952d9f097a..55c5c130a7e 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -1180,6 +1180,7 @@ $(filter $(SRC_PATTERNS), \ _airlift/transport.c \ displayio/bus_core.c \ displayio/display_core.c \ + epaperdisplay/lag_drive.c \ usb/utf16le.c \ ) diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 9d9ccccb835..82f528d264a 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -414,6 +414,11 @@ typedef long mp_off_t; #define CIRCUITPY_BUSIO_SPI_ASYNC (0) #endif +// Normally set from CIRCUITPY_EPAPERDISPLAY_FAST in the .mk files. zephyr-cp doesn't set it. +#ifndef CIRCUITPY_EPAPERDISPLAY_FAST +#define CIRCUITPY_EPAPERDISPLAY_FAST (0) +#endif + // These CIRCUITPY_xxx values should all be defined in the *.mk files as being on or off. // So if any are not defined in *.mk, they'll throw an error here. diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index bc938243f72..4b86713a23c 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -317,6 +317,11 @@ CFLAGS += -DCIRCUITPY_QSPIBUS=$(CIRCUITPY_QSPIBUS) CIRCUITPY_EPAPERDISPLAY ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY=$(CIRCUITPY_EPAPERDISPLAY) +# Fast e-paper updates: EPaperDisplay's write_previous_ram_command, fast, lag_frames +# and clean(). Ports and boards short on flash turn it off. +CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) +CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_FAST=$(CIRCUITPY_EPAPERDISPLAY_FAST) + CIRCUITPY_I2CDISPLAYBUS ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_I2CDISPLAYBUS=$(CIRCUITPY_I2CDISPLAYBUS) diff --git a/shared-bindings/displayio/__init__.c b/shared-bindings/displayio/__init__.c index 12981cbbae0..b0cf5d3c911 100644 --- a/shared-bindings/displayio/__init__.c +++ b/shared-bindings/displayio/__init__.c @@ -84,11 +84,17 @@ //| Once you release a builtin display (one that is initialized automatically, not by user code), //| the display will remain released even if you restart the CircuitPython VM, for instance by typing ctrl-D. //| The display is not re-initialized until you do a hard reset or manually initialize it again. +//| +//| If an EPaperDisplay using ``lag_frames`` can't get its bus, this waits up to 30 seconds and then +//| raises `RuntimeError` with every display left as it was, so you can free the bus and call it again. //| """ //| ... //| //| static mp_obj_t displayio_release_displays(void) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + displayio_prepare_release_displays(); + #endif common_hal_displayio_release_displays(); return mp_const_none; } diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 7a1cf9aebd5..ee0d624573f 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -55,6 +55,7 @@ //| black_bits_inverted: bool = False, //| write_color_ram_command: Optional[int] = None, //| color_bits_inverted: bool = False, +//| write_previous_ram_command: Optional[int] = None, //| highlight_color: int = 0x000000, //| highlight_color2: int = 0x000000, //| refresh_display_command: Union[int, circuitpython_typing.ReadableBuffer], @@ -69,6 +70,9 @@ //| two_byte_sequence_length: bool = False, //| start_up_time: float = 0, //| address_little_endian: bool = False, +//| fast: bool = False, +//| lag_frames: int = 4, +//| clean_sequence: Optional[ReadableBuffer] = None, //| ) -> None: //| """Create a EPaperDisplay object on the given display bus (`fourwire.FourWire` or `paralleldisplaybus.ParallelBus`). //| @@ -99,6 +103,12 @@ //| :param bool black_bits_inverted: True if 0 bits are used to show black pixels. Otherwise, 1 means to show black. //| :param int write_color_ram_command: Command used to write pixels values into the update region //| :param bool color_bits_inverted: True if 0 bits are used to show the color. Otherwise, 1 means to show color. +//| :param int write_previous_ram_command: Command used to write the same pixel values again after each refresh. +//| Use it for controllers that compare new and previous RAM to do a partial update, such as the SSD1680 old data RAM (0x26). +//| When set, ``refresh()`` waits for the display to finish updating before it returns. +//| Requires a non-empty ``stop_sequence``. +//| Leave ``write_color_ram_command`` as None when it writes the same RAM, or it will overwrite the previous image. +//| Not accepted on boards without fast e-paper support. //| :param int highlight_color: RGB888 of source color to highlight with third ePaper color. //| :param int highlight_color2: RGB888 of source color to highlight with fourth ePaper color. //| :param int refresh_display_command: Command used to start a display refresh. Single int or byte-packed command sequence @@ -113,6 +123,26 @@ //| :param bool two_byte_sequence_length: When true, use two bytes to define sequence length //| :param float start_up_time: Time to wait after reset before sending commands //| :param bool address_little_endian: Send the least significant byte (not bit) of multi-byte addresses first. Ignored when ram is addressed with one byte +//| :param bool fast: When true, updates take a fraction of a second. The display is only reset and sent the +//| ``start_sequence`` before the first refresh, and the ``stop_sequence`` when it is released, so it can't be +//| empty. The previous RAM holds the frame from ``lag_frames`` refreshes ago instead of the last one, so each +//| changed pixel is driven for ``lag_frames`` refreshes, the same number toward black and toward white. Only +//| the box around the changed bytes is sent. ``refresh()`` returns as soon as the update starts. Poll ``busy`` +//| before the next one. If the image stops changing, call ``refresh()`` ``lag_frames`` more times to finish +//| the drive. Needs ``write_previous_ram_command``, ``busy_pin``, ``clean_sequence``, the window commands and +//| a monochrome display, and allocates ``lag_frames * width * height / 8`` bytes outside the VM heap until +//| the display is released. Not accepted on boards without fast e-paper support. +//| :param int lag_frames: How many refreshes each changed pixel is driven for, 1 to 32, when ``fast`` is true. +//| More gives darker motion with more smear and uses more memory. Ignored when ``fast`` is false. +//| :param ~circuitpython_typing.ReadableBuffer clean_sequence: Byte-packed commands for a full refresh, such as +//| one using the waveform stored in the display, for use when both RAM planes are white. It runs before the +//| first refresh with ``fast``. When the display is released or the code stops, the display finishes +//| its drive, goes white and runs this and the ``stop_sequence``. Display drivers should provide it. +//| It is only used with ``fast``. +//| +//| .. warning:: ``fast`` and ``write_previous_ram_command`` allow updates much faster than panel makers' +//| limits on refresh rate. Fast updates need a balanced waveform, and long-term wear on the panel +//| is unknown. //| """ //| ... //| @@ -124,7 +154,11 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_highlight_color, ARG_highlight_color2, ARG_refresh_display_command, ARG_refresh_time, ARG_busy_pin, ARG_busy_state, ARG_seconds_per_frame, ARG_always_toggle_chip_select, ARG_grayscale, ARG_advanced_color_epaper, ARG_spectra6, - ARG_two_byte_sequence_length, ARG_start_up_time, ARG_address_little_endian }; + ARG_two_byte_sequence_length, ARG_start_up_time, ARG_address_little_endian, + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_write_previous_ram_command, ARG_fast, ARG_lag_frames, ARG_clean_sequence, + #endif + }; static const mp_arg_t allowed_args[] = { { MP_QSTR_display_bus, MP_ARG_REQUIRED | MP_ARG_OBJ }, { MP_QSTR_start_sequence, MP_ARG_REQUIRED | MP_ARG_OBJ }, @@ -158,6 +192,12 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, { MP_QSTR_two_byte_sequence_length, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_start_up_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NEW_SMALL_INT(0)} }, { MP_QSTR_address_little_endian, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + #if CIRCUITPY_EPAPERDISPLAY_FAST + { MP_QSTR_write_previous_ram_command, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, + { MP_QSTR_fast, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + { MP_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 4} }, + { MP_QSTR_clean_sequence, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, + #endif }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -168,6 +208,14 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, mp_get_buffer_raise(args[ARG_start_sequence].u_obj, &start_bufinfo, MP_BUFFER_READ); mp_buffer_info_t stop_bufinfo; mp_get_buffer_raise(args[ARG_stop_sequence].u_obj, &stop_bufinfo, MP_BUFFER_READ); + #if CIRCUITPY_EPAPERDISPLAY_FAST + // A reset can follow these displays' last RAM write, and SSD168x controllers only commit the + // last byte when another command arrives. + if (stop_bufinfo.len == 0 && + (args[ARG_fast].u_bool || args[ARG_write_previous_ram_command].u_obj != mp_const_none)) { + mp_arg_error_invalid(MP_QSTR_stop_sequence); + } + #endif const mcu_pin_obj_t *busy_pin = validate_obj_is_free_pin_or_none(args[ARG_busy_pin].u_obj, MP_QSTR_busy_pin); @@ -177,6 +225,35 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, mp_raise_ValueError(MP_ERROR_TEXT("Display rotation must be in 90 degree increments")); } + #if CIRCUITPY_EPAPERDISPLAY_FAST + bool fast = args[ARG_fast].u_bool; + mp_int_t lag_frames = 0; + if (fast) { + lag_frames = mp_arg_validate_int_range(args[ARG_lag_frames].u_int, 1, 32, MP_QSTR_lag_frames); + // Each one a driver can leave out by mistake names itself. The rest are the display type. + qstr missing = MP_QSTRnull; + if (args[ARG_busy_pin].u_obj == mp_const_none) { + missing = MP_QSTR_busy_pin; + } else if (args[ARG_clean_sequence].u_obj == mp_const_none) { + missing = MP_QSTR_clean_sequence; + } else if (args[ARG_write_previous_ram_command].u_obj == mp_const_none) { + missing = MP_QSTR_write_previous_ram_command; + } else if (args[ARG_set_column_window_command].u_int == NO_COMMAND) { + missing = MP_QSTR_set_column_window_command; + } else if (args[ARG_set_row_window_command].u_int == NO_COMMAND) { + missing = MP_QSTR_set_row_window_command; + } else if (args[ARG_write_color_ram_command].u_obj != mp_const_none || + args[ARG_highlight_color].u_int != 0 || args[ARG_highlight_color2].u_int != 0 || + args[ARG_grayscale].u_bool || args[ARG_advanced_color_epaper].u_bool || + args[ARG_spectra6].u_bool) { + missing = MP_QSTR_fast; + } + if (missing != MP_QSTRnull) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), missing); + } + } + #endif // CIRCUITPY_EPAPERDISPLAY_FAST + // Check the bus before taking a display slot: construct would raise after the slot is in use. displayio_display_bus_validate(display_bus); @@ -193,6 +270,12 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, if (args[ARG_write_color_ram_command].u_obj != mp_const_none) { write_color_ram_command = mp_obj_get_int(args[ARG_write_color_ram_command].u_obj); } + #if CIRCUITPY_EPAPERDISPLAY_FAST + mp_int_t write_previous_ram_command = NO_COMMAND; + if (args[ARG_write_previous_ram_command].u_obj != mp_const_none) { + write_previous_ram_command = mp_obj_get_int(args[ARG_write_previous_ram_command].u_obj); + } + #endif bool two_byte_sequence_length = args[ARG_two_byte_sequence_length].u_bool; @@ -238,6 +321,9 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, construct_args.black_bits_inverted = args[ARG_black_bits_inverted].u_bool; construct_args.write_color_ram_command = write_color_ram_command; construct_args.color_bits_inverted = args[ARG_color_bits_inverted].u_bool; + #if CIRCUITPY_EPAPERDISPLAY_FAST + construct_args.write_previous_ram_command = write_previous_ram_command; + #endif construct_args.highlight_color = highlight_color; construct_args.highlight_color2 = highlight_color2; construct_args.refresh_sequence = refresh_buf; @@ -252,6 +338,16 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, construct_args.spectra6 = args[ARG_spectra6].u_bool; construct_args.two_byte_sequence_length = two_byte_sequence_length; construct_args.address_little_endian = args[ARG_address_little_endian].u_bool; + #if CIRCUITPY_EPAPERDISPLAY_FAST + construct_args.fast = fast; + construct_args.lag_frames = lag_frames; + if (args[ARG_clean_sequence].u_obj != mp_const_none) { + mp_buffer_info_t clean_bufinfo; + mp_get_buffer_raise(args[ARG_clean_sequence].u_obj, &clean_bufinfo, MP_BUFFER_READ); + construct_args.clean_sequence = clean_bufinfo.buf; + construct_args.clean_sequence_len = clean_bufinfo.len; + } + #endif common_hal_epaperdisplay_epaperdisplay_construct(self, &construct_args); return self; @@ -272,16 +368,35 @@ static mp_obj_t epaperdisplay_epaperdisplay_obj_show(mp_obj_t self_in, mp_obj_t MP_DEFINE_CONST_FUN_OBJ_2(epaperdisplay_epaperdisplay_show_obj, epaperdisplay_epaperdisplay_obj_show); //| def update_refresh_mode( -//| self, start_sequence: ReadableBuffer, seconds_per_frame: float = 180 +//| self, +//| start_sequence: ReadableBuffer, +//| seconds_per_frame: float = 180, +//| *, +//| refresh_sequence: Optional[ReadableBuffer] = None, //| ) -> None: //| """Updates the ``start_sequence`` and ``seconds_per_frame`` parameters to enable -//| varying the refresh mode of the display.""" +//| varying the refresh mode of the display. When ``fast`` is true, the display is +//| reset and sent the new ``start_sequence`` before the next refresh. +//| +//| :param ReadableBuffer start_sequence: Byte sequence sent to initialize the display before each refresh. +//| :param float seconds_per_frame: Minimum number of seconds between refreshes. +//| :param ReadableBuffer refresh_sequence: Byte sequence sent to start a refresh. +//| It uses the same format as ``start_sequence``. +//| When it is None, the current refresh sequence is kept. Not accepted on boards +//| without fast e-paper support.""" //| static mp_obj_t epaperdisplay_epaperdisplay_update_refresh_mode(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_start_sequence, ARG_seconds_per_frame }; + enum { ARG_start_sequence, ARG_seconds_per_frame, + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_refresh_sequence, + #endif + }; static const mp_arg_t allowed_args[] = { { MP_QSTR_start_sequence, MP_ARG_REQUIRED | MP_ARG_OBJ }, { MP_QSTR_seconds_per_frame, MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(180)} }, + #if CIRCUITPY_EPAPERDISPLAY_FAST + { MP_QSTR_refresh_sequence, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + #endif }; epaperdisplay_epaperdisplay_obj_t *self = native_display(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -292,27 +407,84 @@ static mp_obj_t epaperdisplay_epaperdisplay_update_refresh_mode(size_t n_args, c mp_get_buffer_raise(args[ARG_start_sequence].u_obj, &start_sequence, MP_BUFFER_READ); float seconds_per_frame = mp_obj_get_float(args[ARG_seconds_per_frame].u_obj); + mp_buffer_info_t *refresh_sequence_ptr = NULL; + #if CIRCUITPY_EPAPERDISPLAY_FAST + mp_buffer_info_t refresh_sequence; + if (args[ARG_refresh_sequence].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_refresh_sequence].u_obj, &refresh_sequence, MP_BUFFER_READ); + refresh_sequence_ptr = &refresh_sequence; + } + #endif + // Update parameters - epaperdisplay_epaperdisplay_change_refresh_mode_parameters(self, &start_sequence, seconds_per_frame); + epaperdisplay_epaperdisplay_change_refresh_mode_parameters(self, &start_sequence, refresh_sequence_ptr, seconds_per_frame); return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_KW(epaperdisplay_epaperdisplay_update_refresh_mode_obj, 1, epaperdisplay_epaperdisplay_update_refresh_mode); //| def refresh(self) -> None: //| """Refreshes the display immediately or raises an exception if too soon. Use -//| ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.""" +//| ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur. +//| +//| The minimum time between refreshes is ``seconds_per_frame``, 180 by default. With +//| ``fast``, set it to 0 and poll ``busy`` instead, since this returns once the update +//| starts. The first refresh also runs the ``clean_sequence``, which takes seconds, and an +//| interrupt during it raises when it ends.""" //| ... //| static mp_obj_t epaperdisplay_epaperdisplay_obj_refresh(mp_obj_t self_in) { epaperdisplay_epaperdisplay_obj_t *self = native_display(self_in); bool ok = common_hal_epaperdisplay_epaperdisplay_refresh(self); if (!ok) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->lag.frames > 0) { + // Interrupted is not too soon: raise what interrupted it. + mp_handle_pending(true); + } + #endif mp_raise_RuntimeError(MP_ERROR_TEXT("Refresh too soon")); } return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_1(epaperdisplay_epaperdisplay_refresh_obj, epaperdisplay_epaperdisplay_obj_refresh); +#if CIRCUITPY_EPAPERDISPLAY_FAST +//| def clean(self, frames: int = 8, *, full: bool = False) -> None: +//| """Remove ghosting from a display using ``fast``, with balanced drive. Not available +//| on boards without fast e-paper support. +//| +//| All pixels go to white, including the drive they are still owed. If ``full`` is True, +//| the ``clean_sequence`` is sent, followed by the ``start_sequence``. Then every pixel is +//| driven ``frames`` updates toward black and ``frames`` updates toward white. This blocks, +//| but a playing video keeps decoding. The next ``refresh()`` draws everything again. +//| +//| :param int frames: Updates in each direction. 0 skips the wipe. +//| :param bool full: Also do a full refresh with ``clean_sequence``.""" +//| ... +//| +static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_frames, ARG_full }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_frames, MP_ARG_INT, {.u_int = 8} }, + { MP_QSTR_full, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + }; + epaperdisplay_epaperdisplay_obj_t *self = native_display(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + if (self->lag.frames == 0) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Invalid state")); + } + mp_int_t frames = mp_arg_validate_int_range(args[ARG_frames].u_int, 0, 255, MP_QSTR_frames); + if (!common_hal_epaperdisplay_epaperdisplay_clean(self, frames, args[ARG_full].u_bool)) { + // Interrupted is not too soon: raise what interrupted it. + mp_handle_pending(true); + mp_raise_RuntimeError(MP_ERROR_TEXT("Refresh too soon")); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(epaperdisplay_epaperdisplay_clean_obj, 1, epaperdisplay_epaperdisplay_clean); +#endif // CIRCUITPY_EPAPERDISPLAY_FAST + //| time_to_refresh: float //| """Time, in fractional seconds, until the ePaper display can be refreshed.""" static mp_obj_t epaperdisplay_epaperdisplay_obj_get_time_to_refresh(mp_obj_t self_in) { @@ -331,7 +503,8 @@ MP_PROPERTY_GETTER(epaperdisplay_epaperdisplay_time_to_refresh_obj, //| busy: bool //| """True when the display is refreshing. This uses the ``busy_pin`` when available or the -//| ``refresh_time`` otherwise.""" +//| ``refresh_time`` otherwise. With ``fast``, a panel still busy after 30 seconds is +//| given up on, so this goes False and the next ``refresh()`` resets the panel and cleans it.""" static mp_obj_t epaperdisplay_epaperdisplay_obj_get_busy(mp_obj_t self_in) { epaperdisplay_epaperdisplay_obj_t *self = native_display(self_in); return mp_obj_new_bool(common_hal_epaperdisplay_epaperdisplay_get_busy(self)); @@ -427,6 +600,9 @@ static const mp_rom_map_elem_t epaperdisplay_epaperdisplay_locals_dict_table[] = { MP_ROM_QSTR(MP_QSTR_show), MP_ROM_PTR(&epaperdisplay_epaperdisplay_show_obj) }, { MP_ROM_QSTR(MP_QSTR_update_refresh_mode), MP_ROM_PTR(&epaperdisplay_epaperdisplay_update_refresh_mode_obj) }, { MP_ROM_QSTR(MP_QSTR_refresh), MP_ROM_PTR(&epaperdisplay_epaperdisplay_refresh_obj) }, + #if CIRCUITPY_EPAPERDISPLAY_FAST + { MP_ROM_QSTR(MP_QSTR_clean), MP_ROM_PTR(&epaperdisplay_epaperdisplay_clean_obj) }, + #endif { MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&epaperdisplay_epaperdisplay_width_obj) }, { MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&epaperdisplay_epaperdisplay_height_obj) }, diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 83f0b0377fe..a85e3139640 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -37,6 +37,7 @@ typedef struct { bool black_bits_inverted; uint16_t write_color_ram_command; bool color_bits_inverted; + uint16_t write_previous_ram_command; uint32_t highlight_color; uint32_t highlight_color2; const uint8_t *refresh_sequence; @@ -51,6 +52,10 @@ typedef struct { bool spectra6; bool two_byte_sequence_length; bool address_little_endian; + bool fast; + uint8_t lag_frames; + const uint8_t *clean_sequence; + uint32_t clean_sequence_len; } epaperdisplay_construct_args_t; #define EPAPERDISPLAY_CONSTRUCT_ARGS_DEFAULTS { \ @@ -75,6 +80,7 @@ typedef struct { .black_bits_inverted = false, \ .write_color_ram_command = NO_COMMAND, \ .color_bits_inverted = false, \ + .write_previous_ram_command = NO_COMMAND, \ .highlight_color = 0x000000, \ .highlight_color2 = 0x000000, \ .refresh_sequence = NULL, \ @@ -88,7 +94,11 @@ typedef struct { .acep = false, \ .spectra6 = false, \ .two_byte_sequence_length = false, \ - .address_little_endian = false \ + .address_little_endian = false, \ + .fast = false, \ + .lag_frames = 0, \ + .clean_sequence = NULL, \ + .clean_sequence_len = 0 \ } void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, @@ -96,6 +106,15 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_obj_t *self); +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Lag drive only. Brings every pixel to white with balanced drive. When full, sends the clean +// sequence and the start sequence. Then wipes each pixel frames toward black and frames toward +// white. Blocks with background tasks running. The next refresh redraws the whole display. +// Doesn't allocate. Holds any interrupt until it ends. Returns false, with any owed drive still +// owed, if the bus can't be had. +bool common_hal_epaperdisplay_epaperdisplay_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full); +#endif + mp_obj_t common_hal_epaperdisplay_epaperdisplay_get_root_group(epaperdisplay_epaperdisplay_obj_t *self); bool common_hal_epaperdisplay_epaperdisplay_set_root_group(epaperdisplay_epaperdisplay_obj_t *self, displayio_group_t *root_group); diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index 63d5aef01f0..b1bdebc9b0c 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -177,6 +177,11 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { #endif #if CIRCUITPY_EPAPERDISPLAY } else if (display_type == &epaperdisplay_epaperdisplay_type) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (keep_primary) { + epaperdisplay_epaperdisplay_end_held_bus(&displays[i].epaper_display); + } + #endif release_epaperdisplay(&displays[i].epaper_display); #endif #if CIRCUITPY_FRAMEBUFFERIO @@ -247,6 +252,16 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { } } +#if CIRCUITPY_EPAPERDISPLAY_FAST +void displayio_prepare_release_displays(void) { + for (uint8_t i = 0; i < CIRCUITPY_DISPLAY_LIMIT; i++) { + if (displays[i].display_base.type == &epaperdisplay_epaperdisplay_type) { + epaperdisplay_epaperdisplay_prepare_release(&displays[i].epaper_display); + } + } +} +#endif + void common_hal_displayio_release_displays(void) { common_hal_displayio_release_displays_impl(false); } diff --git a/shared-module/displayio/__init__.h b/shared-module/displayio/__init__.h index 4674bda582d..b15145b488d 100644 --- a/shared-module/displayio/__init__.h +++ b/shared-module/displayio/__init__.h @@ -118,6 +118,10 @@ extern displayio_group_t circuitpython_splash; void displayio_background(void); void reset_displays(void); +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Raises if a display using lag_frames can't finish its drive, before any display is released. +void displayio_prepare_release_displays(void); +#endif void displayio_gc_collect(void); primary_display_t *allocate_display(void); diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 3bd657f164f..0a0e40bd0a4 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -9,10 +9,15 @@ #include "py/gc.h" #include "py/runtime.h" #include "shared/runtime/interrupt_char.h" +#if CIRCUITPY_EPAPERDISPLAY_FAST && CIRCUITPY_WATCHDOG +#include "shared-bindings/watchdog/WatchDogTimer.h" +#endif #include "shared-bindings/displayio/ColorConverter.h" +#include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/time/__init__.h" #include "shared-module/displayio/__init__.h" +#include "shared-module/epaperdisplay/lag_drive.h" #include "supervisor/shared/display.h" #include "supervisor/shared/tick.h" @@ -25,6 +30,13 @@ #define DELAY 0x80 +// lag_drive.c calls these only when it is built. Otherwise they stay static, as before it. +#if CIRCUITPY_EPAPERDISPLAY_FAST +#define LAG_SHARED +#else +#define LAG_SHARED static +#endif + void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, const epaperdisplay_construct_args_t *args) { uint16_t color_depth = 1; @@ -64,10 +76,27 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla false /* data_as_commands */, args->always_toggle_chip_select, false /* SH1107_addressing */, args->address_little_endian); + #if CIRCUITPY_EPAPERDISPLAY_FAST + self->lag.slots = NULL; + self->lag.frames = 0; + self->lag.busy_deadline = 0; + self->lag.bus_held = false; + self->lag.state_lost = false; + self->lag.slot_overwriting = false; + self->lag.commit = LAG_COMMIT_NONE; + self->lag.holding_interrupts = false; + self->lag.held_exception = MP_OBJ_NULL; + self->clean_sequence = args->clean_sequence; + self->clean_sequence_len = args->clean_sequence_len; + #endif // CIRCUITPY_EPAPERDISPLAY_FAST + self->write_black_ram_command = args->write_black_ram_command; self->black_bits_inverted = args->black_bits_inverted; self->write_color_ram_command = args->write_color_ram_command; self->color_bits_inverted = args->color_bits_inverted; + #if CIRCUITPY_EPAPERDISPLAY_FAST + self->write_previous_ram_command = args->write_previous_ram_command; + #endif self->refresh_time = args->refresh_time * 1000; self->busy_state = args->busy_state; self->refreshing = false; @@ -75,6 +104,12 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla self->chip_select = args->always_toggle_chip_select ? CHIP_SELECT_TOGGLE_EVERY_BYTE : CHIP_SELECT_UNTOUCHED; self->grayscale = grayscale; + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (args->lag_frames > 0) { + epaperdisplay_lag_construct(self, args->lag_frames); + } + #endif // CIRCUITPY_EPAPERDISPLAY_FAST + self->start_sequence = args->start_sequence; self->start_sequence_len = args->start_sequence_len; self->start_up_time_ms = args->start_up_time * 1000; @@ -82,6 +117,11 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla self->stop_sequence_len = args->stop_sequence_len; self->refresh_sequence = args->refresh_sequence; self->refresh_sequence_len = args->refresh_sequence_len; + #if CIRCUITPY_EPAPERDISPLAY_FAST + self->fast = args->fast; + self->awake = false; + self->needs_start = false; + #endif self->busy.base.type = &mp_type_NoneType; self->two_byte_sequence_length = args->two_byte_sequence_length; @@ -132,17 +172,112 @@ uint16_t common_hal_epaperdisplay_epaperdisplay_get_height(epaperdisplay_epaperd return displayio_display_core_get_height(&self->core); } -static void wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { +#if CIRCUITPY_EPAPERDISPLAY_FAST +// BUSY can take a moment to rise after an update command, and reading it low before then would +// look like a finished update. If it never rises, nothing has started or it was too quick to see, +// so this returns false after 2 ms. Short enough that an interrupt doesn't have to end it. +bool epaperdisplay_epaperdisplay_wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { + for (uint8_t wait = 0; wait < 200; wait++) { + if (common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) { + return true; + } + common_hal_mcu_delay_us(10); + } + return false; +} +#endif // CIRCUITPY_EPAPERDISPLAY_FAST + +#if CIRCUITPY_EPAPERDISPLAY_FAST +// When a wait for BUSY or the bus gives up, or 0 for never. Lag drive always has an end, so a +// stuck panel can't hold up a caller that holds interrupts or has no way to be interrupted. +static uint64_t display_wait_deadline(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.busy_deadline != 0 || self->lag.frames == 0) { + return self->lag.busy_deadline; + } + return supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; +} + +// Between hold and release a waveform or a panel reset is under way, and cutting it short leaves +// the panel dark. Whatever interrupts (Ctrl-C, a reload, a watchdog) waits for the release. +bool epaperdisplay_epaperdisplay_interrupted(epaperdisplay_epaperdisplay_obj_t *self) { + if (!self->lag.holding_interrupts) { + return mp_hal_is_interrupted(); + } + if (mp_hal_is_interrupted()) { + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = MP_STATE_THREAD(mp_pending_exception); + } + MP_STATE_THREAD(mp_pending_exception) = MP_OBJ_NULL; + } + #if CIRCUITPY_WATCHDOG + // A stored waveform takes seconds, and a reset mid-waveform leaves the panel dark. These waits + // are bounded, so a hang still ends. + if (common_hal_watchdog_get_mode(&common_hal_mcu_watchdogtimer_obj) != WATCHDOGMODE_NONE) { + common_hal_watchdog_feed(&common_hal_mcu_watchdogtimer_obj); + } + #endif + return false; +} + +static bool display_send_command(epaperdisplay_epaperdisplay_obj_t *self, const uint8_t *command, + const uint8_t *data, uint32_t length) { + for (uint8_t attempt = 0; attempt < 3; attempt++) { + if (epaperdisplay_lag_send(self, DISPLAY_COMMAND, command, 1) && epaperdisplay_lag_send(self, DISPLAY_DATA, data, length)) { + return true; + } + // The send stopped, and it may have arrived. If it was an update, sending it again would + // drive every pixel a second time. BUSY high says it landed. A command with no data could + // be an update that BUSY hasn't shown yet, so don't risk it: the drive is now unknown. + if (epaperdisplay_epaperdisplay_wait_for_update_start(self)) { + return true; + } + if (length == 0) { + self->lag.state_lost = true; + return false; + } + } + return false; +} + +// A delay ends early when interrupted, which a held interrupt must not do. +void epaperdisplay_epaperdisplay_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t delay_ms) { + uint64_t start = supervisor_ticks_ms64(); + uint32_t elapsed = 0; + while (true) { + common_hal_time_delay_ms(delay_ms - elapsed); + elapsed = supervisor_ticks_ms64() - start; + if (!self->lag.holding_interrupts || elapsed >= delay_ms) { + return; + } + epaperdisplay_epaperdisplay_interrupted(self); + } +} +#else // CIRCUITPY_EPAPERDISPLAY_FAST +#define display_wait_deadline(self) ((uint64_t)0) +#define epaperdisplay_epaperdisplay_interrupted(self) mp_hal_is_interrupted() +#define display_send_command(self, command, data, length) ( \ + (self)->bus.send((self)->bus.bus, DISPLAY_COMMAND, (self)->chip_select, command, 1), \ + (self)->bus.send((self)->bus.bus, DISPLAY_DATA, (self)->chip_select, data, length), \ + true) +#define epaperdisplay_epaperdisplay_delay_ms(self, delay_ms) common_hal_time_delay_ms(delay_ms) +#endif // CIRCUITPY_EPAPERDISPLAY_FAST + +LAG_SHARED void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { if (self->busy.base.type == &mp_type_NoneType) { return; } + const uint64_t deadline = display_wait_deadline(self); while (common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state && - !mp_hal_is_interrupted()) { + !epaperdisplay_epaperdisplay_interrupted(self)) { + if (deadline != 0 && supervisor_ticks_ms64() > deadline) { + return; + } RUN_BACKGROUND_TASKS; } } -static void send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, +// True if every command went out. Lag drive counts an update only if its last command did. +LAG_SHARED bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len) { uint32_t i = 0; while (i < sequence_len) { @@ -155,13 +290,26 @@ static void send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, data_size = ((data_size & ~DELAY) << 8) + *(cmd + 2); data = cmd + 3; } - while (!displayio_display_bus_begin_transaction(&self->bus) && - !mp_hal_is_interrupted()) { + bool have_bus; + const uint64_t deadline = display_wait_deadline(self); + while (!(have_bus = displayio_display_bus_begin_transaction(&self->bus)) && + !epaperdisplay_epaperdisplay_interrupted(self) && (deadline == 0 || supervisor_ticks_ms64() <= deadline)) { RUN_BACKGROUND_TASKS; } - self->bus.send(self->bus.bus, DISPLAY_COMMAND, self->chip_select, cmd, 1); - self->bus.send(self->bus.bus, DISPLAY_DATA, self->chip_select, data, data_size); + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (!have_bus && self->lag.frames > 0) { + return false; + } + self->lag.bus_held = have_bus; + #endif // CIRCUITPY_EPAPERDISPLAY_FAST + const bool sent = display_send_command(self, cmd, data, data_size); displayio_display_bus_end_transaction(&self->bus); + #if CIRCUITPY_EPAPERDISPLAY_FAST + self->lag.bus_held = false; + #endif // CIRCUITPY_EPAPERDISPLAY_FAST + if (!sent) { + return false; + } uint16_t delay_length_ms = 0; if (delay) { data_size++; @@ -170,28 +318,89 @@ static void send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, delay_length_ms = 500; } } - common_hal_time_delay_ms(delay_length_ms); + epaperdisplay_epaperdisplay_delay_ms(self, delay_length_ms); if (should_wait_for_busy) { - wait_for_busy(self); + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->lag.frames > 0) { + epaperdisplay_epaperdisplay_wait_for_update_start(self); + } + #endif + epaperdisplay_epaperdisplay_wait_for_busy(self); } + #if !CIRCUITPY_EPAPERDISPLAY_FAST if (mp_hal_is_interrupted()) { - return; + return true; } + #endif i += 2 + data_size; if (self->two_byte_sequence_length) { i++; } + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (epaperdisplay_epaperdisplay_interrupted(self)) { + return i >= sequence_len; + } + #endif } + return true; } void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_epaperdisplay_obj_t *self, - mp_buffer_info_t *start_sequence, float seconds_per_frame) { + mp_buffer_info_t *start_sequence, mp_buffer_info_t *refresh_sequence, float seconds_per_frame) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->lag.frames > 0) { + // Finish the drive with the waveform that started it, and rewrite both planes after. + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + epaperdisplay_lag_flush(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); + nlr_pop(); + } else { + epaperdisplay_lag_raise(self, nlr.ret_val); + } + if (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE) { + // Still owed: keep the mode that started it. + if (!mp_hal_is_interrupted()) { + mp_raise_RuntimeError_varg(MP_ERROR_TEXT("%q in use"), MP_QSTR_display_bus); + } + return; + } + self->lag.force_full = true; + } + #endif // CIRCUITPY_EPAPERDISPLAY_FAST self->start_sequence = (uint8_t *)start_sequence->buf; self->start_sequence_len = start_sequence->len; + #if CIRCUITPY_EPAPERDISPLAY_FAST + // NULL keeps the current refresh sequence. + if (refresh_sequence != NULL) { + self->refresh_sequence = (uint8_t *)refresh_sequence->buf; + self->refresh_sequence_len = refresh_sequence->len; + } + #endif self->milliseconds_per_frame = seconds_per_frame * 1000; + #if CIRCUITPY_EPAPERDISPLAY_FAST + self->needs_start = true; + #endif +} + +#if CIRCUITPY_EPAPERDISPLAY_FAST +// For a panel kept awake. Ctrl-C raises from a send in progress, and that transaction is still +// open. The caller still has things to do first, so the interrupt raises later. +static void send_stop_sequence_guarded(epaperdisplay_epaperdisplay_obj_t *self) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + nlr_pop(); + } else { + if (!displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } + mp_sched_exception(nlr.ret_val); + } } +#endif // CIRCUITPY_EPAPERDISPLAY_FAST -static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { +LAG_SHARED void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { if (!displayio_display_bus_is_free(&self->bus)) { // Can't acquire display bus; skip updating this display. Try next display. return; @@ -201,15 +410,35 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla return; } + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->fast && self->awake) { + if (!self->needs_start) { + // The panel is still configured from the previous refresh. + return; + } + epaperdisplay_epaperdisplay_wait_for_busy(self); + // SSD168x controllers commit the last RAM byte when the next command arrives, so a reset + // can't come straight after a RAM write. + send_stop_sequence_guarded(self); + self->awake = false; + } + #endif + // run start sequence self->bus.bus_reset(self->bus.bus); - common_hal_time_delay_ms(self->start_up_time_ms); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); - send_command_sequence(self, true, self->start_sequence, self->start_sequence_len); - if (mp_hal_is_interrupted()) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len) || + epaperdisplay_epaperdisplay_interrupted(self)) { return; } + self->awake = true; + self->needs_start = false; + #else + epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len); + #endif } uint32_t common_hal_epaperdisplay_epaperdisplay_get_time_to_refresh(epaperdisplay_epaperdisplay_obj_t *self) { @@ -224,15 +453,79 @@ uint32_t common_hal_epaperdisplay_epaperdisplay_get_time_to_refresh(epaperdispla return self->milliseconds_per_frame - elapsed_time; } -static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdisplay_obj_t *self) { +#if CIRCUITPY_EPAPERDISPLAY_FAST +static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area, bool write_previous); + +// False if the panel is still updating when the wait is over. +static bool wait_for_update_end(epaperdisplay_epaperdisplay_obj_t *self) { + epaperdisplay_epaperdisplay_wait_for_update_start(self); + const uint64_t deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + bool busy; + while ((busy = common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) && + !mp_hal_is_interrupted() && supervisor_ticks_ms64() <= deadline) { + RUN_BACKGROUND_TASKS; + } + return !busy; +} + +static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { + // Actually refresh the display now that all pixel RAM has been updated. + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + const uint64_t refresh_start = supervisor_ticks_ms64(); + bool previous_written = true; + mp_obj_t raised = MP_OBJ_NULL; + + if (self->write_previous_ram_command != NO_COMMAND && areas != NULL) { + // Copy the new image into the previous image RAM so the next partial update compares + // against what is on screen. The controller ignores RAM writes while it is updating. + if (self->busy.base.type == &mp_type_NoneType) { + common_hal_time_delay_ms(self->refresh_time); + } else { + previous_written = wait_for_update_end(self); + } + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + while (areas != NULL && previous_written) { + previous_written = !mp_hal_is_interrupted() && + epaperdisplay_epaperdisplay_refresh_area(self, areas, true); + areas = areas->next; + } + nlr_pop(); + } else { + // Ctrl-C raises from a transfer in progress, and that transaction is still open. + raised = nlr.ret_val; + previous_written = false; + if (!displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } + } + } + + supervisor_enable_tick(); + self->refreshing = true; + + displayio_display_core_finish_refresh(&self->core); + // The update began before the wait above, and refresh_time and seconds_per_frame count from it. + self->core.last_refresh = refresh_start; + if (!previous_written) { + // The previous RAM is stale, so the next update has to write everything. + self->core.full_refresh = true; + } + if (raised != MP_OBJ_NULL) { + nlr_jump(raised); + } +} +#else +static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { // Actually refresh the display now that all pixel RAM has been updated. - send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); supervisor_enable_tick(); self->refreshing = true; displayio_display_core_finish_refresh(&self->core); } +#endif // CIRCUITPY_EPAPERDISPLAY_FAST mp_obj_t common_hal_epaperdisplay_epaperdisplay_get_bus(epaperdisplay_epaperdisplay_obj_t *self) { return self->bus.bus; @@ -267,7 +560,7 @@ mp_obj_t common_hal_epaperdisplay_epaperdisplay_get_root_group(epaperdisplay_epa return self->core.current_group; } -static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area) { +static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area, bool write_previous) { uint16_t buffer_size = 128; // In uint32_ts displayio_area_t clipped; @@ -302,7 +595,7 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay uint32_t mask[mask_length]; uint8_t passes = 1; - if (self->write_color_ram_command != NO_COMMAND) { + if (self->write_color_ram_command != NO_COMMAND && !write_previous) { passes = 2; } for (uint8_t pass = 0; pass < passes; pass++) { @@ -316,6 +609,11 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay if (pass == 1) { write_command = self->write_color_ram_command; } + #if CIRCUITPY_EPAPERDISPLAY_FAST + else if (write_previous) { + write_command = self->write_previous_ram_command; + } + #endif if (!displayio_display_bus_begin_transaction(&self->bus)) { // Display bus not available now. return false; @@ -426,13 +724,29 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ if (common_hal_digitalio_digitalinout_get_value(&self->busy) != self->busy_state) { supervisor_disable_tick(); self->refreshing = false; + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (!self->fast) { + // Run stop sequence but don't wait for busy because busy is set when sleeping. + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + } + #else // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + #endif } else { + #if CIRCUITPY_EPAPERDISPLAY_FAST + epaperdisplay_lag_give_up_if_stuck(self); + #endif return false; } } if (self->core.current_group == NULL) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + // Nothing to draw, but the drive already started is still owed. + if (self->lag.frames > 0 && self->awake) { + return epaperdisplay_lag_refresh(self, NULL); + } + #endif return true; } // Refresh at seconds per frame rate. @@ -444,13 +758,18 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ return false; } const displayio_area_t *current_area = epaperdisplay_epaperdisplay_get_refresh_areas(self); + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->lag.frames > 0) { + return epaperdisplay_lag_refresh(self, current_area); + } + #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (current_area == NULL) { return true; } if (self->acep) { epaperdisplay_epaperdisplay_start_refresh(self); _clean_area(self); - epaperdisplay_epaperdisplay_finish_refresh(self); + epaperdisplay_epaperdisplay_finish_refresh(self, NULL); while (self->refreshing && !mp_hal_is_interrupted()) { RUN_BACKGROUND_TASKS; } @@ -460,11 +779,12 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ } epaperdisplay_epaperdisplay_start_refresh(self); + const displayio_area_t *first_area = current_area; while (current_area != NULL) { - epaperdisplay_epaperdisplay_refresh_area(self, current_area); + epaperdisplay_epaperdisplay_refresh_area(self, current_area, false); current_area = current_area->next; } - epaperdisplay_epaperdisplay_finish_refresh(self); + epaperdisplay_epaperdisplay_finish_refresh(self, first_area); return true; } @@ -480,9 +800,21 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s if (refresh_done) { supervisor_disable_tick(); self->refreshing = false; + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (!self->fast) { + // Run stop sequence but don't wait for busy because busy is set when sleeping. + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + } + #else // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + #endif } + #if CIRCUITPY_EPAPERDISPLAY_FAST + else { + epaperdisplay_lag_give_up_if_stuck(self); + } + #endif } } @@ -492,13 +824,34 @@ bool common_hal_epaperdisplay_epaperdisplay_get_busy(epaperdisplay_epaperdisplay } void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + epaperdisplay_lag_shutdown(self); + epaperdisplay_lag_free(self); + #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (self->refreshing) { - wait_for_busy(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); supervisor_disable_tick(); self->refreshing = false; // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->fast) { + send_stop_sequence_guarded(self); + } else { + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + } + #else + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + #endif } + #if CIRCUITPY_EPAPERDISPLAY_FAST + else if (self->fast && self->awake) { + send_stop_sequence_guarded(self); + } + self->awake = false; + #endif + #if CIRCUITPY_EPAPERDISPLAY_FAST + epaperdisplay_lag_release_interrupts(self); + #endif // CIRCUITPY_EPAPERDISPLAY_FAST release_display_core(&self->core); if (self->busy.base.type == &digitalio_digitalinout_type) { @@ -506,9 +859,57 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { } } +#if CIRCUITPY_EPAPERDISPLAY_FAST +void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames == 0 || (!self->awake && !self->lag.state_lost)) { + return; + } + const uint64_t deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + while (!displayio_display_bus_is_free(&self->bus)) { + mp_handle_pending(true); + if (supervisor_ticks_ms64() > deadline) { + mp_raise_RuntimeError_varg(MP_ERROR_TEXT("%q in use"), MP_QSTR_display_bus); + } + RUN_BACKGROUND_TASKS; + } +} +#endif // CIRCUITPY_EPAPERDISPLAY_FAST + +#if CIRCUITPY_EPAPERDISPLAY_FAST +void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t *self) { + // The VM is over, so whatever holds the bus can never let it go: end it here. + if (self->fast && !displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + } +} +#endif // CIRCUITPY_EPAPERDISPLAY_FAST + void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { + #if CIRCUITPY_EPAPERDISPLAY_FAST + if (self->lag.frames > 0) { + // Only a primary display gets here; reset_displays() has released the others. The heap is + // still here but the sequences won't be, and the display would otherwise persist to draw + // the splash with them. Finish now and stay asleep. Its bus still holds the pins until + // the next VM ends or release_displays(). + epaperdisplay_epaperdisplay_end_held_bus(self); + release_epaperdisplay(self); + self->base.type = &mp_type_NoneType; + return; + } + #endif // CIRCUITPY_EPAPERDISPLAY_FAST displayio_display_core_set_root_group(&self->core, &circuitpython_splash); self->core.full_refresh = true; + #if CIRCUITPY_EPAPERDISPLAY_FAST + // Let the panel sleep after the next refresh when code isn't running. + if (self->fast && self->awake && !self->refreshing) { + // Nothing can catch a raise from a send. A reload may be pending. + epaperdisplay_epaperdisplay_end_held_bus(self); + send_stop_sequence_guarded(self); + self->awake = false; + } + self->fast = false; + #endif } void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t *self) { @@ -517,6 +918,10 @@ void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t gc_collect_ptr((void *)self->start_sequence); gc_collect_ptr((void *)self->stop_sequence); gc_collect_ptr((void *)self->refresh_sequence); + #if CIRCUITPY_EPAPERDISPLAY_FAST + gc_collect_ptr((void *)self->clean_sequence); + gc_collect_ptr(self->lag.held_exception); + #endif // CIRCUITPY_EPAPERDISPLAY_FAST } size_t maybe_refresh_epaperdisplay(void) { diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 7c72bf1f954..4b0d97bdf0a 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -13,6 +13,39 @@ #include "shared-module/displayio/display_core.h" #include "common-hal/digitalio/DigitalInOut.h" +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Lag drive, built on FAST's write_previous_ram_command. FAST writes the last frame to the +// previous RAM, so each changed pixel is driven once. Lag writes the frame from frames +// refreshes ago, so a changed pixel is driven frames times, the same number each way. +// The ring holds those frames. Only used when frames > 0. +typedef struct { + uint8_t **slots; + uint64_t busy_deadline; // When not 0, stop waiting for BUSY or the bus after this tick. + // What interrupted while interrupts are held. A send can raise one from the heap, so it is + // marked in epaperdisplay_epaperdisplay_collect_ptrs(). + mp_obj_t held_exception; + bool holding_interrupts; + bool bus_held; // Inside a transaction, so an exception must end it. + bool last_try; // Shutdown's reset after a stuck wait: the deadline is not renewed. + bool state_lost; // What the panel received is unknown, so it needs a stored-waveform refresh. + bool slot_overwriting; // The oldest frame in the ring is being replaced. + bool commit_drive; // The owed step has pixels to drive. + uint8_t commit; // What the newest step in the ring still has to send. + displayio_area_t commit_box; + uint64_t update_start; // When the running update was sent. + // What each of the last frames + 1 steps rendered, in pixels. + displayio_area_t *boxes; + uint32_t slot_size; + uint16_t stride; + uint16_t rows; + uint8_t frames; + uint8_t head; // Slot of the newest frame. + uint8_t box_index; // Entry in boxes for this step. + uint8_t settle; // Steps owed until every plane pair matches the newest frame. + bool force_full; // Write everything on the next step. +} epaperdisplay_lag_t; +#endif + typedef struct { mp_obj_base_t base; displayio_display_core_t core; @@ -29,6 +62,9 @@ typedef struct { uint16_t refresh_time; uint16_t write_black_ram_command; uint16_t write_color_ram_command; + #if CIRCUITPY_EPAPERDISPLAY_FAST + uint16_t write_previous_ram_command; + #endif uint8_t hue; bool busy_state; bool black_bits_inverted; @@ -37,14 +73,33 @@ typedef struct { bool grayscale; bool acep; bool two_byte_sequence_length; + #if CIRCUITPY_EPAPERDISPLAY_FAST + bool fast; + bool awake; + bool needs_start; // update_refresh_mode() changed the start sequence. + #endif display_chip_select_behavior_t chip_select; + #if CIRCUITPY_EPAPERDISPLAY_FAST + const uint8_t *clean_sequence; + uint32_t clean_sequence_len; + epaperdisplay_lag_t lag; + #endif } epaperdisplay_epaperdisplay_obj_t; void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_epaperdisplay_obj_t *self, - mp_buffer_info_t *start_sequence, float seconds_per_frame); + mp_buffer_info_t *start_sequence, mp_buffer_info_t *refresh_sequence, float seconds_per_frame); void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *self); void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self); void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self); +#if CIRCUITPY_EPAPERDISPLAY_FAST +// The VM is over: a fast display ends whatever transaction still holds its bus, since its +// stop sequence would wait for the bus forever. Other displays are left alone. Does not raise. +void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t *self); +// Waits up to 30 s for the bus so a release can finish the drive. Raises what interrupted it, or +// RuntimeError if the bus stays held, with the display and its drive unchanged. Only call it where +// an exception can be caught. Doesn't allocate. +void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj_t *self); +#endif size_t maybe_refresh_epaperdisplay(void); void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t *self); diff --git a/shared-module/epaperdisplay/lag_drive.c b/shared-module/epaperdisplay/lag_drive.c new file mode 100644 index 00000000000..69f875edefb --- /dev/null +++ b/shared-module/epaperdisplay/lag_drive.c @@ -0,0 +1,744 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/epaperdisplay/EPaperDisplay.h" + +#if CIRCUITPY_EPAPERDISPLAY_FAST + +#include "py/gc.h" +#include "py/runtime.h" +#include "shared/runtime/interrupt_char.h" +#include "shared-bindings/digitalio/DigitalInOut.h" +#include "shared-module/epaperdisplay/lag_drive.h" +#include "supervisor/port_heap.h" +#include "supervisor/shared/tick.h" + +#include +#include + +// SSD168x only: NOP. These controllers keep the last RAM byte until the next command and lose it to +// a reset, so a RAM write ends with this. Lag drive is only for displays where it is harmless. +#define LAG_RAM_COMMIT_COMMAND 0x7f + +static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self); +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full); + +static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { + return self->lag.busy_deadline != 0 && supervisor_ticks_ms64() > self->lag.busy_deadline; +} + +static void lag_hold_interrupts(epaperdisplay_epaperdisplay_obj_t *self) { + self->lag.holding_interrupts = true; + if (self->lag.busy_deadline == 0) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + } + epaperdisplay_epaperdisplay_interrupted(self); +} + +void epaperdisplay_lag_release_interrupts(epaperdisplay_epaperdisplay_obj_t *self) { + if (!self->lag.holding_interrupts) { + return; + } + self->lag.holding_interrupts = false; + self->lag.busy_deadline = 0; + if (self->lag.held_exception != MP_OBJ_NULL) { + mp_sched_exception(self->lag.held_exception); + self->lag.held_exception = MP_OBJ_NULL; + } +} + +// A transfer in progress raises when something interrupts. On a port with async SPI that can be +// any transfer, however short. While holding, that returns false: the transfer stopped partway, +// so the caller goes back to where the RAM or command can start again. +bool epaperdisplay_lag_send(epaperdisplay_epaperdisplay_obj_t *self, display_byte_type_t type, + const uint8_t *data, uint32_t length) { + if (!self->lag.holding_interrupts) { + self->bus.send(self->bus.bus, type, self->chip_select, data, length); + return true; + } + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->bus.send(self->bus.bus, type, self->chip_select, data, length); + nlr_pop(); + return true; + } + MP_STATE_THREAD(mp_pending_exception) = nlr.ret_val; + epaperdisplay_epaperdisplay_interrupted(self); + return false; +} + +// set_region_to_update() has its own transaction, so a raise from it leaves the bus locked. +static bool lag_set_region(epaperdisplay_epaperdisplay_obj_t *self, displayio_area_t *box) { + for (volatile uint8_t attempt = 0; attempt < 3; attempt++) { + // It skips its commands if it can't get the bus, which would leave the old window. + if (!displayio_display_bus_is_free(&self->bus)) { + return false; + } + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->lag.bus_held = true; + displayio_display_bus_set_region_to_update(&self->bus, &self->core, box); + self->lag.bus_held = false; + nlr_pop(); + return true; + } + if (self->lag.bus_held) { + if (!displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } + self->lag.bus_held = false; + } + if (!self->lag.holding_interrupts) { + nlr_jump(nlr.ret_val); + } + MP_STATE_THREAD(mp_pending_exception) = nlr.ret_val; + epaperdisplay_epaperdisplay_interrupted(self); + } + return false; +} + +// Lag drive. Plane 0x26 holds frame N-lag instead of N-1. Each step drives a pixel toward black +// where N is black and N-lag is white, toward white where N is white and N-lag is black, and not +// at all where they match. +// +// That keeps the drive balanced without counting. Each frame is used twice: pixels are driven +// toward it in the step where it is N, and away from it lag steps later, where it is N-lag. So a +// pixel that ends the way it started has been driven the same amount toward black and toward +// white. For example, lag 3 and a pixel that is black for 2 steps (both planes start white): +// +// step 1 2 3 4 5 6 +// frame N W B B W W W +// frame N-3 W W W W B B +// drive - B B - W W 2 toward black, 2 toward white +// +// A change that holds for lag steps or more is driven lag steps, so a bigger lag drives harder. +// When frames stop changing, lag more steps finish the drive. Each frame is rendered once into +// the ring and the planes are sent from there. +// +// The ring has lag slots, N-1 to N-lag, and N replaces N-lag in its slot. The box to send must +// be known before that, so it comes from the areas that were rendered, not from comparing frames. +// Frames N and N-lag can only differ inside the areas of the last lag steps. Sending the areas of +// the last lag + 1 steps also covers what the previous step left unequal, so the planes are +// equal outside the box: a byte there is either outside the last box too, so equal by induction, +// or was last written with frames that have not changed there since. + +void epaperdisplay_lag_construct(epaperdisplay_epaperdisplay_obj_t *self, uint8_t lag_frames) { + self->lag.stride = (self->core.area.x2 + 7) / 8; + self->lag.rows = self->core.area.y2; + self->lag.slot_size = self->lag.stride * self->lag.rows; + self->lag.frames = lag_frames; + self->lag.head = 0; + self->lag.box_index = 0; + self->lag.settle = 0; + // The panel's RAM is unknown, so the first step writes everything. + self->lag.force_full = true; + size_t meta_size = lag_frames * sizeof(uint8_t *) + (lag_frames + 1) * sizeof(displayio_area_t); + self->lag.slots = port_malloc(meta_size, false); + bool ok = self->lag.slots != NULL; + if (ok) { + memset(self->lag.slots, 0, meta_size); + self->lag.boxes = (displayio_area_t *)(self->lag.slots + lag_frames); + } + for (uint8_t i = 0; ok && i < lag_frames; i++) { + self->lag.slots[i] = port_malloc(self->lag.slot_size, true); + ok = self->lag.slots[i] != NULL; + } + if (!ok) { + epaperdisplay_lag_free(self); + self->base.type = &mp_type_NoneType; + m_malloc_fail(self->lag.slot_size); + } + // History starts white, the state a cleared panel is in. + for (uint8_t i = 0; i < lag_frames; i++) { + memset(self->lag.slots[i], self->black_bits_inverted ? 0x00 : 0xff, self->lag.slot_size); + } +} + +void epaperdisplay_lag_free(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.slots != NULL) { + for (uint8_t i = 0; i < self->lag.frames; i++) { + if (self->lag.slots[i] != NULL) { + port_free(self->lag.slots[i]); + } + } + port_free(self->lag.slots); + self->lag.slots = NULL; + } + self->lag.frames = 0; +} + +static void lag_render_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area, uint8_t *frame, bool white) { + displayio_area_t clipped; + if (!displayio_display_core_clip_area(&self->core, area, &clipped)) { + return; + } + if (white) { + for (int16_t row = clipped.y1; row < clipped.y2; row++) { + memset(frame + row * self->lag.stride + clipped.x1 / 8, self->black_bits_inverted ? 0x00 : 0xff, + displayio_area_width(&clipped) / 8); + } + return; + } + uint16_t width = displayio_area_width(&clipped); + uint16_t bytes_per_row = width / 8; + uint16_t rows_per_buffer = MAX(1, 128 * 32 / width); + size_t words = (rows_per_buffer * width + 31) / 32; + uint32_t buffer[words]; + uint32_t mask[words]; + + self->core.colorspace.grayscale = true; + self->core.colorspace.grayscale_bit = 7; + for (int16_t y = clipped.y1; y < clipped.y2; y += rows_per_buffer) { + displayio_area_t subrectangle = { + .x1 = clipped.x1, + .y1 = y, + .x2 = clipped.x2, + .y2 = MIN(y + rows_per_buffer, clipped.y2), + }; + memset(mask, 0, sizeof(mask)); + memset(buffer, 0, sizeof(buffer)); + displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + if (self->black_bits_inverted) { + for (size_t k = 0; k < words; k++) { + buffer[k] = ~buffer[k]; + } + } + const uint8_t *in = (const uint8_t *)buffer; + for (int16_t row = subrectangle.y1; row < subrectangle.y2; row++) { + memcpy(frame + row * self->lag.stride + clipped.x1 / 8, in, bytes_per_row); + in += bytes_per_row; + } + // Run background tasks like a regular refresh does. + RUN_BACKGROUND_TASKS; + } +} + +static bool lag_send_ram_commit(epaperdisplay_epaperdisplay_obj_t *self) { + const uint8_t command = LAG_RAM_COMMIT_COMMAND; + return epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1); +} + +// Rows are gathered so a narrow box doesn't cost one SPI transfer per row. +static bool lag_send_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, const uint8_t *frame, const displayio_area_t *box) { + if (!epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1)) { + return false; + } + uint16_t bytes_per_row = displayio_area_width(box) / 8; + const uint8_t *in = frame + box->y1 * self->lag.stride + box->x1 / 8; + if (bytes_per_row == self->lag.stride) { + return epaperdisplay_lag_send(self, DISPLAY_DATA, in, displayio_area_height(box) * bytes_per_row) && lag_send_ram_commit(self); + } + uint8_t chunk[512]; + size_t used = 0; + for (int16_t y = box->y1; y < box->y2; y++) { + if (used + bytes_per_row > sizeof(chunk)) { + if (!epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, used)) { + return false; + } + used = 0; + } + memcpy(chunk + used, in, bytes_per_row); + used += bytes_per_row; + in += self->lag.stride; + } + return epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, used) && lag_send_ram_commit(self); +} + +// A send stopped by a held interrupt leaves the RAM pointer partway, so start over from the region. +static bool lag_write_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, const uint8_t *frame, displayio_area_t *box) { + for (uint8_t attempt = 0; attempt < 3; attempt++) { + if (!lag_set_region(self, box) || !displayio_display_bus_begin_transaction(&self->bus)) { + return false; + } + self->lag.bus_held = true; + const bool sent = lag_send_plane(self, command, frame, box); + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + if (sent) { + return true; + } + } + return false; +} + +// Finish a step whose frame is already in the ring: frame N to its plane, then the update. Each +// part stays owed until it has gone out, so a pixel is only counted as driven if it was. Interrupts +// wait, since a send they stop can't be told from one that arrived. False if the bus can't be had. +static bool lag_commit_step(epaperdisplay_epaperdisplay_obj_t *self) { + const bool was_holding = self->lag.holding_interrupts; + lag_hold_interrupts(self); + bool ok = true; + if (self->lag.commit == LAG_COMMIT_PLANE) { + ok = lag_write_plane(self, self->write_black_ram_command, self->lag.slots[self->lag.head], &self->lag.commit_box); + if (ok) { + self->lag.commit = LAG_COMMIT_ACTIVATE; + } + } + if (ok && self->lag.commit == LAG_COMMIT_ACTIVATE) { + // Nothing can differ when the box is empty, so an update would only take time. + if (self->lag.commit_drive) { + ok = epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + if (ok) { + epaperdisplay_epaperdisplay_wait_for_update_start(self); + supervisor_enable_tick(); + self->refreshing = true; + self->lag.update_start = supervisor_ticks_ms64(); + } + } + if (ok) { + self->lag.commit = LAG_COMMIT_NONE; + } + } + if (!was_holding) { + epaperdisplay_lag_release_interrupts(self); + } + return ok; +} + +// Send one step: frame N-lag to the previous plane, then frame N, rendered from areas (may be +// NULL) over its slot, or white in those areas without rendering, to the other. The history +// doesn't advance if the bus can't be had before the slot is touched, so a retry does the same +// step. Once it has, the step is owed and true is returned, whatever happens to the rest. +static bool lag_step(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas, bool white) { + uint8_t lag = self->lag.frames; + uint8_t next = (self->lag.head + 1) % lag; + uint8_t *frame = self->lag.slots[next]; + displayio_area_t *boxes = self->lag.boxes; + uint8_t index = self->lag.box_index; + bool changed = areas != NULL; + + for (const displayio_area_t *area = areas; area != NULL; area = area->next) { + displayio_area_t clipped; + if (displayio_display_core_clip_area(&self->core, area, &clipped)) { + displayio_area_union(&boxes[index], &clipped, &boxes[index]); + } + } + // The oldest entry is lag steps back: only needed to make the planes equal again. + uint8_t oldest = (index + 1) % (lag + 1); + displayio_area_t drive_box = {0}; + for (uint8_t i = 0; i <= lag; i++) { + if (i != oldest) { + displayio_area_union(&drive_box, &boxes[i], &drive_box); + } + } + displayio_area_t write_box; + if (self->lag.force_full) { + displayio_display_core_clip_area(&self->core, &self->core.area, &write_box); + } else { + displayio_area_union(&drive_box, &boxes[oldest], &write_box); + } + bool write = !displayio_area_empty(&write_box); + + if (write && !lag_write_plane(self, self->write_previous_ram_command, frame, &write_box)) { + return false; + } + // N-1 and N-lag only differ inside the box. + self->lag.slot_overwriting = true; + if (write && next != self->lag.head) { + const uint8_t *in = self->lag.slots[self->lag.head] + write_box.y1 * self->lag.stride + write_box.x1 / 8; + uint8_t *out = frame + write_box.y1 * self->lag.stride + write_box.x1 / 8; + uint16_t bytes_per_row = displayio_area_width(&write_box) / 8; + for (int16_t y = write_box.y1; y < write_box.y2; y++) { + memcpy(out, in, bytes_per_row); + in += self->lag.stride; + out += self->lag.stride; + } + } + for (const displayio_area_t *area = areas; area != NULL; area = area->next) { + lag_render_area(self, area, frame, white); + } + // The slot no longer holds N-lag, so from here the step has to finish or that drive is lost. + self->lag.slot_overwriting = false; + self->lag.head = next; + self->lag.box_index = oldest; + boxes[oldest] = (displayio_area_t) {0}; + if (changed) { + self->lag.settle = lag; + } else if (self->lag.settle > 0) { + self->lag.settle--; + } + self->lag.force_full = false; + self->lag.commit_box = write_box; + self->lag.commit_drive = !displayio_area_empty(&drive_box); + self->lag.commit = write ? LAG_COMMIT_PLANE : LAG_COMMIT_ACTIVATE; + lag_commit_step(self); + return true; +} + +static bool lag_refresh_unguarded(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { + if (!self->awake || self->lag.state_lost) { + // The panel may still hold anything, including ghosts from a run that lost power. + if (!lag_clean(self, 0, true) || mp_hal_is_interrupted()) { + return false; + } + areas = &self->core.area; + } + if (self->lag.commit != LAG_COMMIT_NONE) { + // The areas stay marked, so the next refresh draws them. + return lag_commit_step(self); + } + if (areas == NULL && self->lag.settle == 0) { + return true; + } + if (self->needs_start) { + lag_hold_interrupts(self); + epaperdisplay_epaperdisplay_start_refresh(self); + epaperdisplay_lag_release_interrupts(self); + } else { + epaperdisplay_epaperdisplay_start_refresh(self); + } + bool ok = !self->needs_start && lag_step(self, areas, false); + if (ok) { + displayio_display_core_finish_refresh(&self->core); + } else { + self->core.refresh_in_progress = false; + } + return ok; +} + +// An update takes far less than the limit, so a panel still BUSY is stuck, and only a reset can +// help. That has to wait for the next refresh or the shutdown, since the bus may be in use here. +// Until then a script polling busy can go on. +void epaperdisplay_lag_give_up_if_stuck(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames > 0 && !self->lag.holding_interrupts && + supervisor_ticks_ms64() - self->lag.update_start > EPAPER_WAIT_TIMEOUT_MS) { + supervisor_disable_tick(); + self->refreshing = false; + self->awake = false; + self->lag.state_lost = true; + } +} + +// False if the update is still running: interrupted, or the panel is stuck. RAM writes and a reset +// would land on that update, so nothing more can be sent. +static bool lag_wait_idle(epaperdisplay_epaperdisplay_obj_t *self) { + epaperdisplay_epaperdisplay_wait_for_busy(self); + return common_hal_digitalio_digitalinout_get_value(&self->busy) != self->busy_state; +} + +// The deadline bounds each piece of work, not the whole drive, which can be 32 updates and a clean. +// Only called after progress, so a bus that is never free still runs out. +static void lag_renew_deadline(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.busy_deadline != 0 && !self->lag.last_try) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + } +} + +// Give every changed pixel the rest of its drive so the panel is left DC balanced. +void epaperdisplay_lag_flush(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames == 0 || !self->awake) { + return; + } + while (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE) { + if (!lag_wait_idle(self) || epaperdisplay_epaperdisplay_interrupted(self)) { + return; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + if (self->lag.commit != LAG_COMMIT_NONE ? !lag_commit_step(self) : !lag_step(self, NULL, false)) { + return; + } + lag_renew_deadline(self); + } +} + +bool epaperdisplay_lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + bool ok = lag_refresh_unguarded(self, areas); + nlr_pop(); + return ok; + } + epaperdisplay_lag_raise(self, nlr.ret_val); +} + +// Fill a whole RAM plane with one color from a small buffer. +static bool lag_send_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, bool white) { + if (!epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1)) { + return false; + } + uint8_t chunk[512]; + memset(chunk, white != self->black_bits_inverted ? 0xff : 0x00, sizeof(chunk)); + size_t remaining = self->lag.slot_size; + while (remaining > 0) { + size_t count = MIN(remaining, sizeof(chunk)); + if (!epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, count)) { + return false; + } + remaining -= count; + } + return lag_send_ram_commit(self); +} + +static bool lag_write_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, bool white) { + displayio_area_t box; + displayio_display_core_clip_area(&self->core, &self->core.area, &box); + for (uint8_t attempt = 0; attempt < 3; attempt++) { + if (!lag_set_region(self, &box) || !displayio_display_bus_begin_transaction(&self->bus)) { + return false; + } + self->lag.bus_held = true; + const bool sent = lag_send_solid(self, command, white); + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + if (sent) { + return true; + } + } + return false; +} + +// Updates with the planes as they are. Background tasks run, so a video keeps decoding. Returns how +// many went out. +static uint8_t lag_wipe_phase(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames) { + for (uint8_t i = 0; i < frames; i++) { + lag_renew_deadline(self); + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len)) { + return i; + } + epaperdisplay_epaperdisplay_wait_for_update_start(self); + if (!lag_wait_idle(self)) { + return i + 1; // A stuck panel: writes would be dropped, so nothing more can help. + } + } + return frames; +} + +// Both planes must be white. Every pixel gets frames toward black, then the same number toward white, +// and both planes are white again at the end. The caller holds interrupts, so the phases stay +// together. If that can't be done the drive is unknown, and only a stored-waveform refresh is safe. +static void lag_wipe(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames) { + bool ok = lag_write_solid(self, self->write_black_ram_command, false); + if (ok) { + const uint8_t toward_black = lag_wipe_phase(self, frames); + lag_renew_deadline(self); + ok = toward_black == frames && lag_wait_idle(self) && + lag_write_solid(self, self->write_previous_ram_command, false) && + lag_write_solid(self, self->write_black_ram_command, true) && + lag_wipe_phase(self, frames) == frames && + lag_write_solid(self, self->write_previous_ram_command, true); + } + if (!ok) { + self->lag.state_lost = true; + } +} + +// A normal step to white, then the owed steps. Everything changed so far gets all of its drive, +// and the ring and both planes end up white with nothing owed. False if interrupted first, with +// the drive still owed. +static bool lag_to_white(epaperdisplay_epaperdisplay_obj_t *self) { + while (true) { + if (!lag_wait_idle(self) || epaperdisplay_epaperdisplay_interrupted(self)) { + return false; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + if (self->lag.commit == LAG_COMMIT_NONE) { + break; + } + if (!lag_commit_step(self)) { + return false; + } + } + displayio_area_t whole = self->core.area; + whole.next = NULL; + if (!lag_step(self, &whole, true)) { + return false; + } + lag_renew_deadline(self); + epaperdisplay_lag_flush(self); + if (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE || !lag_wait_idle(self)) { + return false; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + return true; +} + +// An exception came out of a bus send (Ctrl-C during a long transfer raises) or a render. Without +// this the bus stays locked and chip select low, and everything after waits for it. The next +// step writes all of both planes, since this one may have stopped partway through either. +static void lag_abandon_step(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.bus_held) { + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + } + self->core.refresh_in_progress = false; + self->lag.force_full = true; + if (self->lag.slot_overwriting) { + // The drive it held can't be sent any more. + self->lag.slot_overwriting = false; + self->lag.state_lost = true; + } +} + +static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self) { + lag_abandon_step(self); + epaperdisplay_lag_release_interrupts(self); +} + +// Raise once: what was held, if anything, instead of both that and this. +MP_NORETURN void epaperdisplay_lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised) { + if (self->lag.held_exception != MP_OBJ_NULL) { + raised = self->lag.held_exception; + self->lag.held_exception = MP_OBJ_NULL; + } + lag_after_raise(self); + nlr_jump(raised); +} + +static bool lag_wake(epaperdisplay_epaperdisplay_obj_t *self) { + self->awake = false; + self->bus.bus_reset(self->bus.bus); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len)) { + return false; + } + self->awake = true; + self->needs_start = false; + return true; +} + +// What the panel received is unknown, so paying back could drive pixels the wrong way. Forget the +// drive and let the stored waveform, which is balanced by itself, bring the panel to white. +static bool lag_recover(epaperdisplay_epaperdisplay_obj_t *self) { + // An update that may have started shouldn't be cut. A stuck panel has already been waited for. + if (self->awake) { + epaperdisplay_epaperdisplay_wait_for_busy(self); + } + if (!lag_wake(self) || !lag_wait_idle(self)) { + return false; + } + lag_renew_deadline(self); + if (!lag_write_solid(self, self->write_previous_ram_command, true) || + !lag_write_solid(self, self->write_black_ram_command, true) || + !epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) || + !lag_wait_idle(self)) { + return false; + } + lag_renew_deadline(self); + if (!lag_wake(self)) { + return false; + } + for (uint8_t i = 0; i < self->lag.frames; i++) { + memset(self->lag.slots[i], self->black_bits_inverted ? 0x00 : 0xff, self->lag.slot_size); + } + memset(self->lag.boxes, 0, (self->lag.frames + 1) * sizeof(displayio_area_t)); + self->lag.settle = 0; + self->lag.commit = LAG_COMMIT_NONE; + self->lag.force_full = false; + self->lag.state_lost = false; + return true; +} + +// A clean can't stop partway, so an interrupt raises when it ends. False if the bus can't be had. +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full) { + if (!displayio_display_bus_is_free(&self->bus)) { + return false; + } + lag_hold_interrupts(self); + self->core.full_refresh = true; + bool ok; + if (self->lag.state_lost) { + ok = lag_recover(self); + } else { + ok = self->awake || lag_wake(self); + ok = ok && lag_to_white(self); + if (ok && full) { + lag_renew_deadline(self); + self->lag.force_full = true; + ok = epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) && lag_wait_idle(self); + if (ok) { + lag_renew_deadline(self); + ok = lag_wake(self); + lag_renew_deadline(self); + } + } + } + if (ok && frames > 0) { + lag_wipe(self, frames); + } + epaperdisplay_lag_release_interrupts(self); + return ok; +} + +bool common_hal_epaperdisplay_epaperdisplay_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + bool ok = lag_clean(self, frames, full); + nlr_pop(); + return ok; + } + epaperdisplay_lag_raise(self, nlr.ret_val); +} + +// Leave the panel white and balanced when the display goes away: release, or the end of the VM, +// where nothing can catch an exception. Interrupts stay held until release_epaperdisplay() has sent +// the stop sequence. All of it is bounded so a stuck panel can't hang shutdown. Doesn't allocate. +void epaperdisplay_lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames == 0 || (!self->awake && !self->lag.state_lost)) { + return; // Nothing was drawn. + } + lag_after_raise(self); // Anything still held from before is ours to end. + lag_hold_interrupts(self); + volatile bool done = false; + // Each raise abandons the step and the loop starts it again, up to 4 times. + volatile uint8_t raises = 0; + while (!done && !lag_deadline_passed(self) && raises < 4) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + if (self->lag.state_lost) { + done = lag_recover(self); + } else if (lag_to_white(self)) { + lag_renew_deadline(self); + done = epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len); + } + nlr_pop(); + RUN_BACKGROUND_TASKS; // Whatever holds the bus may let it go. + } else { + lag_abandon_step(self); + raises++; + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; + } + } + } + epaperdisplay_epaperdisplay_wait_for_busy(self); + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + // BUSY outlasted the wait. One reset may free the panel for the stored waveform. If BUSY stays + // high after it, nothing more is sent, and the deadline isn't renewed, so this is the last wait. + if (!done && raises < 4 && common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + self->lag.last_try = true; + self->awake = false; + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->bus.bus_reset(self->bus.bus); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); + if (lag_wait_idle(self)) { + lag_recover(self); + } + nlr_pop(); + } else { + lag_abandon_step(self); + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; + } + } + self->lag.last_try = false; + } +} + +#endif // CIRCUITPY_EPAPERDISPLAY_FAST diff --git a/shared-module/epaperdisplay/lag_drive.h b/shared-module/epaperdisplay/lag_drive.h new file mode 100644 index 00000000000..f3062593dce --- /dev/null +++ b/shared-module/epaperdisplay/lag_drive.h @@ -0,0 +1,60 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/epaperdisplay/EPaperDisplay.h" + +// Shared by EPaperDisplay.c and lag_drive.c only. + +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Sends a start, stop or refresh sequence. True if every command went out. +bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, + bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len); +// Runs the start sequence. A fast display that is already awake skips it unless the start +// sequence changed. +void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self); +// Waits for BUSY to clear or an interrupt. On a lag display the wait is bounded. +void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self); + +// Longer than one update or stored-waveform refresh takes. This bounds each wait for an update on +// a lag display, and the wait before writing the previous RAM. A stock display's waits have no bound. +#define EPAPER_WAIT_TIMEOUT_MS 30000 + +// What the newest lag step still has to send: nothing, its plane, or the update that shows it. +enum { LAG_COMMIT_NONE, LAG_COMMIT_PLANE, LAG_COMMIT_ACTIVATE }; + +// Delays, but while interrupts are held an interrupt doesn't cut it short. +void epaperdisplay_epaperdisplay_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t delay_ms); +// True if interrupted. While interrupts are held it saves the interrupt for the release and +// returns false. +bool epaperdisplay_epaperdisplay_interrupted(epaperdisplay_epaperdisplay_obj_t *self); +// True once BUSY rises after an update command. False if it doesn't within 2 ms. +bool epaperdisplay_epaperdisplay_wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self); + +// Allocates the frame ring outside the VM heap. Raises MemoryError if it can't. +void epaperdisplay_lag_construct(epaperdisplay_epaperdisplay_obj_t *self, uint8_t lag_frames); +// Sends the rest of the drive owed. Returns early if interrupted or the panel is stuck. +void epaperdisplay_lag_flush(epaperdisplay_epaperdisplay_obj_t *self); +// Frees the frame ring. +void epaperdisplay_lag_free(epaperdisplay_epaperdisplay_obj_t *self); +// Marks a panel BUSY for longer than EPAPER_WAIT_TIMEOUT_MS as lost, for the next refresh to reset. +void epaperdisplay_lag_give_up_if_stuck(epaperdisplay_epaperdisplay_obj_t *self); +// Abandons the step in progress, ends any hold on interrupts, then raises. An interrupt saved +// during the hold is raised instead of raised. +MP_NORETURN void epaperdisplay_lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised); +// Sends one lag step for areas, or only the owed drive when areas is NULL. False if the step +// couldn't be sent: interrupted, the panel is stuck, or the start sequence changed. The areas +// stay marked and the drive stays owed. +bool epaperdisplay_lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas); +// Ends a hold on interrupts and schedules any interrupt saved during it. +void epaperdisplay_lag_release_interrupts(epaperdisplay_epaperdisplay_obj_t *self); +// Sends bytes on the bus. False only while interrupts are held and the transfer was interrupted. +bool epaperdisplay_lag_send(epaperdisplay_epaperdisplay_obj_t *self, display_byte_type_t type, + const uint8_t *data, uint32_t length); +// Leaves the panel white and balanced when the display goes away. Doesn't raise or allocate. +void epaperdisplay_lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self); +#endif // CIRCUITPY_EPAPERDISPLAY_FAST