From 34b53b9aedcb3d4eb31d500de3fa4ab34ceb55fc Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:00:42 -0700 Subject: [PATCH 01/10] EPaperDisplay: allow update_refresh_mode to change refresh_sequence Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 26 ++++++++++++++++--- shared-module/epaperdisplay/EPaperDisplay.c | 7 ++++- shared-module/epaperdisplay/EPaperDisplay.h | 2 +- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index b8858ac552f..081202d493a 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -268,16 +268,27 @@ 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. +//| +//| :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.""" //| 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, ARG_refresh_sequence }; 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)} }, + { MP_QSTR_refresh_sequence, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, }; epaperdisplay_epaperdisplay_obj_t *self = native_display(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -288,8 +299,15 @@ 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; + mp_buffer_info_t *refresh_sequence_ptr = NULL; + 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; + } + // 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); diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 3b3908d1fb2..8da25337378 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -185,9 +185,14 @@ static void send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, } 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) { self->start_sequence = (uint8_t *)start_sequence->buf; self->start_sequence_len = start_sequence->len; + // 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; + } self->milliseconds_per_frame = seconds_per_frame * 1000; } diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 7c72bf1f954..75760785134 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -41,7 +41,7 @@ typedef struct { } 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); From 3fea298a3cd42846d9fead199aba706ffbb7fe65 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:01:37 -0700 Subject: [PATCH 02/10] epaperdisplay: add write_previous_ram_command After a refresh finishes, write the same areas again with this command so the controller's previous image RAM matches the screen. This lets SSD168x style partial updates diff against what is actually shown. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 14 ++- shared-bindings/epaperdisplay/EPaperDisplay.h | 2 + shared-module/epaperdisplay/EPaperDisplay.c | 85 +++++++++++++++++-- shared-module/epaperdisplay/EPaperDisplay.h | 1 + 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 081202d493a..9a269f3dd2e 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -54,6 +54,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], @@ -98,6 +99,11 @@ //| :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. //| :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 @@ -120,7 +126,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, ARG_ram_width, ARG_ram_height, ARG_colstart, ARG_rowstart, ARG_rotation, ARG_set_column_window_command, ARG_set_row_window_command, ARG_set_current_column_command, ARG_set_current_row_command, ARG_write_black_ram_command, ARG_black_bits_inverted, - ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_highlight_color, ARG_highlight_color2, + ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_write_previous_ram_command, 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 }; @@ -143,6 +149,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, { MP_QSTR_black_bits_inverted, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_write_color_ram_command, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, { MP_QSTR_color_bits_inverted, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + { MP_QSTR_write_previous_ram_command, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, { MP_QSTR_highlight_color, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0x000000} }, { MP_QSTR_highlight_color2, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0x000000} }, { MP_QSTR_refresh_display_command, MP_ARG_OBJ | MP_ARG_REQUIRED }, @@ -189,6 +196,10 @@ 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); } + 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); + } bool two_byte_sequence_length = args[ARG_two_byte_sequence_length].u_bool; @@ -234,6 +245,7 @@ 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; + construct_args.write_previous_ram_command = write_previous_ram_command; construct_args.highlight_color = highlight_color; construct_args.highlight_color2 = highlight_color2; construct_args.refresh_sequence = refresh_buf; diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 83f0b0377fe..54b4c3e069a 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; @@ -75,6 +76,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, \ diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 8da25337378..001dcaf0abf 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -10,6 +10,7 @@ #include "py/runtime.h" #include "shared/runtime/interrupt_char.h" #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" @@ -68,6 +69,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla 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; + self->write_previous_ram_command = args->write_previous_ram_command; self->refresh_time = args->refresh_time * 1000; self->busy_state = args->busy_state; self->refreshing = false; @@ -132,6 +134,22 @@ uint16_t common_hal_epaperdisplay_epaperdisplay_get_height(epaperdisplay_epaperd return displayio_display_core_get_height(&self->core); } +// A stuck panel can't hold up what this driver waits for on its own account. +#define EPAPER_WAIT_TIMEOUT_MS 30000 + +// 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. +static bool 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; +} + static void wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { if (self->busy.base.type == &mp_type_NoneType) { return; @@ -229,14 +247,66 @@ 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) { +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) { + 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. 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); + } } mp_obj_t common_hal_epaperdisplay_epaperdisplay_get_bus(epaperdisplay_epaperdisplay_obj_t *self) { @@ -272,7 +342,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; @@ -307,7 +377,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++) { @@ -320,6 +390,8 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay uint8_t write_command = self->write_black_ram_command; if (pass == 1) { write_command = self->write_color_ram_command; + } else if (write_previous) { + write_command = self->write_previous_ram_command; } if (!displayio_display_bus_begin_transaction(&self->bus)) { // Display bus not available now. @@ -455,7 +527,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ 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; } @@ -465,11 +537,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; } diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 75760785134..34d9b4d8f80 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -29,6 +29,7 @@ typedef struct { uint16_t refresh_time; uint16_t write_black_ram_command; uint16_t write_color_ram_command; + uint16_t write_previous_ram_command; uint8_t hue; bool busy_state; bool black_bits_inverted; From 819974a3430c303b30eae286d5404bf98ab93b2b Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:02:22 -0700 Subject: [PATCH 03/10] epaperdisplay: add keep_awake to skip reset and sleep between refreshes A keep_awake display ends a transaction still holding its bus when the VM ends, since Ctrl-C can leave one open and the stop sequence would otherwise wait for the bus forever. The stop sequence is also guarded when it follows a refresh. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 18 ++++- shared-bindings/epaperdisplay/EPaperDisplay.h | 4 +- shared-module/displayio/__init__.c | 3 + shared-module/epaperdisplay/EPaperDisplay.c | 68 +++++++++++++++++-- shared-module/epaperdisplay/EPaperDisplay.h | 6 ++ 5 files changed, 91 insertions(+), 8 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 9a269f3dd2e..9ec580c7a86 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -69,6 +69,7 @@ //| two_byte_sequence_length: bool = False, //| start_up_time: float = 0, //| address_little_endian: bool = False, +//| keep_awake: bool = False, //| ) -> None: //| """Create a EPaperDisplay object on the given display bus (`fourwire.FourWire` or `paralleldisplaybus.ParallelBus`). //| @@ -118,6 +119,10 @@ //| :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 keep_awake: When true, the display is only reset and sent the ``start_sequence`` before the first refresh. +//| The ``stop_sequence`` is not sent after each refresh. It is sent when the display is released. +//| It is also sent before the reset that ``update_refresh_mode()`` asks for, so it can't be empty. +//| This keeps the display's settings and RAM between refreshes so they can be faster. //| """ //| ... //| @@ -129,7 +134,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_write_previous_ram_command, 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, ARG_keep_awake }; 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 }, @@ -164,6 +169,7 @@ 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} }, + { MP_QSTR_keep_awake, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, }; 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); @@ -174,6 +180,12 @@ 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); + // 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_keep_awake].u_bool || args[ARG_write_previous_ram_command].u_obj != mp_const_none)) { + mp_arg_error_invalid(MP_QSTR_stop_sequence); + } const mcu_pin_obj_t *busy_pin = validate_obj_is_free_pin_or_none(args[ARG_busy_pin].u_obj, MP_QSTR_busy_pin); @@ -260,6 +272,7 @@ 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; + construct_args.keep_awake = args[ARG_keep_awake].u_bool; common_hal_epaperdisplay_epaperdisplay_construct(self, &construct_args); return self; @@ -287,7 +300,8 @@ MP_DEFINE_CONST_FUN_OBJ_2(epaperdisplay_epaperdisplay_show_obj, epaperdisplay_ep //| 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 ``keep_awake`` 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. diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 54b4c3e069a..9ae58b82f10 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -52,6 +52,7 @@ typedef struct { bool spectra6; bool two_byte_sequence_length; bool address_little_endian; + bool keep_awake; } epaperdisplay_construct_args_t; #define EPAPERDISPLAY_CONSTRUCT_ARGS_DEFAULTS { \ @@ -90,7 +91,8 @@ typedef struct { .acep = false, \ .spectra6 = false, \ .two_byte_sequence_length = false, \ - .address_little_endian = false \ + .address_little_endian = false, \ + .keep_awake = false \ } void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index 9819b613cab..72c7a9b8af0 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -170,6 +170,9 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { #endif #if CIRCUITPY_EPAPERDISPLAY } else if (display_type == &epaperdisplay_epaperdisplay_type) { + if (keep_primary) { + epaperdisplay_epaperdisplay_end_held_bus(&displays[i].epaper_display); + } release_epaperdisplay(&displays[i].epaper_display); #endif #if CIRCUITPY_FRAMEBUFFERIO diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 001dcaf0abf..508fe96fb06 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -84,6 +84,9 @@ 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; + self->keep_awake = args->keep_awake; + self->awake = false; + self->needs_start = false; self->busy.base.type = &mp_type_NoneType; self->two_byte_sequence_length = args->two_byte_sequence_length; @@ -212,6 +215,22 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep self->refresh_sequence_len = refresh_sequence->len; } self->milliseconds_per_frame = seconds_per_frame * 1000; + self->needs_start = true; +} + +// 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) { + 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); + } } static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { @@ -224,6 +243,17 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla return; } + if (self->keep_awake && self->awake) { + if (!self->needs_start) { + // The panel is still configured from the previous refresh. + return; + } + // 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; + } + // run start sequence self->bus.bus_reset(self->bus.bus); @@ -233,6 +263,8 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla if (mp_hal_is_interrupted()) { return; } + self->awake = true; + self->needs_start = false; } uint32_t common_hal_epaperdisplay_epaperdisplay_get_time_to_refresh(epaperdisplay_epaperdisplay_obj_t *self) { @@ -503,8 +535,10 @@ 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; - // 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 (!self->keep_awake) { + // 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); + } } else { return false; } @@ -558,8 +592,10 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s if (refresh_done) { 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 (!self->keep_awake) { + // 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); + } } } } @@ -575,8 +611,15 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *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 (self->keep_awake) { + send_stop_sequence_guarded(self); + } else { + send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + } + } else if (self->keep_awake && self->awake) { + send_stop_sequence_guarded(self); } + self->awake = false; release_display_core(&self->core); if (self->busy.base.type == &digitalio_digitalinout_type) { @@ -584,9 +627,24 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { } } +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->keep_awake && !displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } +} + void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { displayio_display_core_set_root_group(&self->core, &circuitpython_splash); self->core.full_refresh = true; + // Let the panel sleep after the next refresh when code isn't running. + if (self->keep_awake && 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->keep_awake = false; } void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t *self) { diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 34d9b4d8f80..1e48dcb52b0 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -38,6 +38,9 @@ typedef struct { bool grayscale; bool acep; bool two_byte_sequence_length; + bool keep_awake; + bool awake; + bool needs_start; // update_refresh_mode() changed the start sequence. display_chip_select_behavior_t chip_select; } epaperdisplay_epaperdisplay_obj_t; @@ -46,6 +49,9 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep 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); +// The VM is over: a keep_awake 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); size_t maybe_refresh_epaperdisplay(void); void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t *self); From f2ffcb426000dfec32c60ed630066c3dd2fe56f9 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 04/10] epaperdisplay: add lag_frames drive The previous RAM holds frame N-lag instead of N-1, so each changed pixel is driven for lag_frames updates, the same number toward black and toward white. Only the box around changed bytes is sent. clean_sequence runs before the first refresh and when the display is released, leaving the panel white and balanced. A waveform or panel reset cut short leaves the panel dark, so while one is under way an interrupt waits and is raised when it ends. A send it stops starts over. Waits are bounded per update so a stuck panel can't hang shutdown, and the watchdog is fed while interrupts are held. CIRCUITPY_EPAPERDISPLAY_LAG gates it, off by default and on for Feather RP2040 ThinkInk. clean_sequence is accepted on every build. lag_frames raises ValueError where the drive is off. Co-Authored-By: Claude Opus 5.5 --- .../mpconfigboard.mk | 2 + py/circuitpy_mpconfig.mk | 4 + shared-bindings/epaperdisplay/EPaperDisplay.c | 75 +- shared-bindings/epaperdisplay/EPaperDisplay.h | 8 +- shared-module/epaperdisplay/EPaperDisplay.c | 879 +++++++++++++++++- shared-module/epaperdisplay/EPaperDisplay.h | 29 + 6 files changed, 976 insertions(+), 21 deletions(-) diff --git a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk index 1f71dcb76bf..1caf2f862ee 100644 --- a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk +++ b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk @@ -7,3 +7,5 @@ CHIP_VARIANT = RP2040 CHIP_FAMILY = rp2 EXTERNAL_FLASH_DEVICES = "GD25Q64C,W25Q64JVxQ" + +CIRCUITPY_EPAPERDISPLAY_LAG = 1 diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index e6903fcf6a0..516a0f89b90 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -310,6 +310,10 @@ CFLAGS += -DCIRCUITPY_QSPIBUS=$(CIRCUITPY_QSPIBUS) CIRCUITPY_EPAPERDISPLAY ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY=$(CIRCUITPY_EPAPERDISPLAY) +# Fast e-paper updates with balanced drive: EPaperDisplay's lag_frames and clean(). +CIRCUITPY_EPAPERDISPLAY_LAG ?= 0 +CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_LAG=$(CIRCUITPY_EPAPERDISPLAY_LAG) + CIRCUITPY_I2CDISPLAYBUS ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_I2CDISPLAYBUS=$(CIRCUITPY_I2CDISPLAYBUS) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 9ec580c7a86..777c22791e7 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -70,6 +70,8 @@ //| start_up_time: float = 0, //| address_little_endian: bool = False, //| keep_awake: bool = False, +//| lag_frames: int = 0, +//| clean_sequence: Optional[ReadableBuffer] = None, //| ) -> None: //| """Create a EPaperDisplay object on the given display bus (`fourwire.FourWire` or `paralleldisplaybus.ParallelBus`). //| @@ -123,6 +125,19 @@ //| The ``stop_sequence`` is not sent after each refresh. It is sent when the display is released. //| It is also sent before the reset that ``update_refresh_mode()`` asks for, so it can't be empty. //| This keeps the display's settings and RAM between refreshes so they can be faster. +//| :param int lag_frames: When more than 0, 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. Implies ``keep_awake``. 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. +//| Only on builds with ``CIRCUITPY_EPAPERDISPLAY_LAG``. Elsewhere, more than 0 raises `ValueError`. +//| :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 ``lag_frames``. 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 accepted on every build and only used with ``lag_frames``. //| """ //| ... //| @@ -134,7 +149,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_write_previous_ram_command, 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_keep_awake }; + ARG_two_byte_sequence_length, ARG_start_up_time, ARG_address_little_endian, ARG_keep_awake, ARG_lag_frames, ARG_clean_sequence }; 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 }, @@ -170,6 +185,8 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, { 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} }, { MP_QSTR_keep_awake, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + { MP_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0} }, + { MP_QSTR_clean_sequence, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, }; 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); @@ -195,6 +212,37 @@ 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")); } + mp_int_t lag_frames = mp_arg_validate_int_range(args[ARG_lag_frames].u_int, 0, 32, MP_QSTR_lag_frames); + #if CIRCUITPY_EPAPERDISPLAY_LAG + if (lag_frames > 0) { + // 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_lag_frames; + } + if (missing != MP_QSTRnull) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), missing); + } + } + #else + if (lag_frames > 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_lag_frames); + } + #endif + primary_display_t *disp = allocate_display_or_raise(); epaperdisplay_epaperdisplay_obj_t *self = &disp->epaper_display; @@ -272,7 +320,14 @@ 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; - construct_args.keep_awake = args[ARG_keep_awake].u_bool; + construct_args.keep_awake = args[ARG_keep_awake].u_bool || lag_frames > 0; + 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; + } common_hal_epaperdisplay_epaperdisplay_construct(self, &construct_args); return self; @@ -340,13 +395,24 @@ MP_DEFINE_CONST_FUN_OBJ_KW(epaperdisplay_epaperdisplay_update_refresh_mode_obj, //| 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 +//| ``lag_frames``, 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_LAG + 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; @@ -371,7 +437,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 ``lag_frames``, 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)); diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 9ae58b82f10..829215369f0 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -53,6 +53,9 @@ typedef struct { bool two_byte_sequence_length; bool address_little_endian; bool keep_awake; + uint8_t lag_frames; + const uint8_t *clean_sequence; + uint32_t clean_sequence_len; } epaperdisplay_construct_args_t; #define EPAPERDISPLAY_CONSTRUCT_ARGS_DEFAULTS { \ @@ -92,7 +95,10 @@ typedef struct { .spectra6 = false, \ .two_byte_sequence_length = false, \ .address_little_endian = false, \ - .keep_awake = false \ + .keep_awake = false, \ + .lag_frames = 0, \ + .clean_sequence = NULL, \ + .clean_sequence_len = 0 \ } void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 508fe96fb06..6c54931c2c3 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -9,12 +9,18 @@ #include "py/gc.h" #include "py/runtime.h" #include "shared/runtime/interrupt_char.h" +#if CIRCUITPY_EPAPERDISPLAY_LAG && 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 "supervisor/shared/display.h" +#if CIRCUITPY_EPAPERDISPLAY_LAG +#include "supervisor/port_heap.h" +#endif #include "supervisor/shared/tick.h" #if CIRCUITPY_TINYUSB @@ -26,6 +32,32 @@ #define DELAY 0x80 +// One update or stored-waveform refresh takes less. 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 + +#if CIRCUITPY_EPAPERDISPLAY_LAG +// 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 +#endif + +#if CIRCUITPY_EPAPERDISPLAY_LAG +enum { LAG_COMMIT_NONE, LAG_COMMIT_PLANE, LAG_COMMIT_ACTIVATE }; +#endif + +#if CIRCUITPY_EPAPERDISPLAY_LAG +static void lag_construct(epaperdisplay_epaperdisplay_obj_t *self, uint8_t lag_frames); +static void lag_flush(epaperdisplay_epaperdisplay_obj_t *self); +static void lag_give_up_if_stuck(epaperdisplay_epaperdisplay_obj_t *self); +static void lag_free(epaperdisplay_epaperdisplay_obj_t *self); +static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self); +static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self); +static MP_NORETURN void lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised); +static bool lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas); +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self); +#endif // CIRCUITPY_EPAPERDISPLAY_LAG + void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, const epaperdisplay_construct_args_t *args) { uint16_t color_depth = 1; @@ -65,6 +97,20 @@ 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_LAG + 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_LAG + 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; @@ -77,6 +123,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_LAG + if (args->lag_frames > 0) { + lag_construct(self, args->lag_frames); + } + #endif // CIRCUITPY_EPAPERDISPLAY_LAG + self->start_sequence = args->start_sequence; self->start_sequence_len = args->start_sequence_len; self->start_up_time_ms = args->start_up_time * 1000; @@ -137,9 +189,6 @@ uint16_t common_hal_epaperdisplay_epaperdisplay_get_height(epaperdisplay_epaperd return displayio_display_core_get_height(&self->core); } -// A stuck panel can't hold up what this driver waits for on its own account. -#define EPAPER_WAIT_TIMEOUT_MS 30000 - // 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. @@ -153,17 +202,171 @@ static bool wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { return false; } +#if CIRCUITPY_EPAPERDISPLAY_LAG +static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { + return self->lag_busy_deadline != 0 && supervisor_ticks_ms64() > self->lag_busy_deadline; +} + +// 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. +static bool display_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 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; + } + display_interrupted(self); +} + +static void 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. +static bool 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; + display_interrupted(self); + 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 (lag_send(self, DISPLAY_COMMAND, command, 1) && 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 (wait_for_update_start(self)) { + return true; + } + if (length == 0) { + self->lag_state_lost = true; + return false; + } + } + 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; + display_interrupted(self); + } + return false; +} + +// A delay ends early when interrupted, which a held interrupt must not do. +static void display_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; + } + display_interrupted(self); + } +} +#else // CIRCUITPY_EPAPERDISPLAY_LAG +#define display_wait_deadline(self) ((uint64_t)0) +#define display_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 display_delay_ms(self, delay_ms) common_hal_time_delay_ms(delay_ms) +#endif // CIRCUITPY_EPAPERDISPLAY_LAG + static void 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()) { + !display_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. +static bool 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) { @@ -176,13 +379,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)) && + !display_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_LAG + if (!have_bus && self->lag_frames > 0) { + return false; + } + self->lag_bus_held = have_bus; + #endif // CIRCUITPY_EPAPERDISPLAY_LAG + const bool sent = display_send_command(self, cmd, data, data_size); displayio_display_bus_end_transaction(&self->bus); + #if CIRCUITPY_EPAPERDISPLAY_LAG + self->lag_bus_held = false; + #endif // CIRCUITPY_EPAPERDISPLAY_LAG + if (!sent) { + return false; + } uint16_t delay_length_ms = 0; if (delay) { data_size++; @@ -191,22 +407,49 @@ static void send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, delay_length_ms = 500; } } - common_hal_time_delay_ms(delay_length_ms); + display_delay_ms(self, delay_length_ms); if (should_wait_for_busy) { + #if CIRCUITPY_EPAPERDISPLAY_LAG + if (self->lag_frames > 0) { + wait_for_update_start(self); + } + #endif wait_for_busy(self); } - if (mp_hal_is_interrupted()) { - return; - } i += 2 + data_size; if (self->two_byte_sequence_length) { i++; } + if (display_interrupted(self)) { + return i >= sequence_len; + } } + return true; } void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_epaperdisplay_obj_t *self, mp_buffer_info_t *start_sequence, mp_buffer_info_t *refresh_sequence, float seconds_per_frame) { + #if CIRCUITPY_EPAPERDISPLAY_LAG + 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) { + lag_flush(self); + wait_for_busy(self); + nlr_pop(); + } else { + 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_LAG self->start_sequence = (uint8_t *)start_sequence->buf; self->start_sequence_len = start_sequence->len; // NULL keeps the current refresh sequence. @@ -248,6 +491,7 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla // The panel is still configured from the previous refresh. return; } + 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); @@ -257,10 +501,10 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla // run start sequence self->bus.bus_reset(self->bus.bus); - common_hal_time_delay_ms(self->start_up_time_ms); + display_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 (!send_command_sequence(self, true, self->start_sequence, self->start_sequence_len) || + display_interrupted(self)) { return; } self->awake = true; @@ -540,10 +784,19 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } } else { + #if CIRCUITPY_EPAPERDISPLAY_LAG + lag_give_up_if_stuck(self); + #endif return false; } } if (self->core.current_group == NULL) { + #if CIRCUITPY_EPAPERDISPLAY_LAG + // Nothing to draw, but the drive already started is still owed. + if (self->lag_frames > 0 && self->awake) { + return lag_refresh(self, NULL); + } + #endif return true; } // Refresh at seconds per frame rate. @@ -555,6 +808,11 @@ 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_LAG + if (self->lag_frames > 0) { + return lag_refresh(self, current_area); + } + #endif // CIRCUITPY_EPAPERDISPLAY_LAG if (current_area == NULL) { return true; } @@ -597,6 +855,11 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } } + #if CIRCUITPY_EPAPERDISPLAY_LAG + else { + lag_give_up_if_stuck(self); + } + #endif } } @@ -606,6 +869,10 @@ bool common_hal_epaperdisplay_epaperdisplay_get_busy(epaperdisplay_epaperdisplay } void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { + #if CIRCUITPY_EPAPERDISPLAY_LAG + lag_shutdown(self); + lag_free(self); + #endif // CIRCUITPY_EPAPERDISPLAY_LAG if (self->refreshing) { wait_for_busy(self); supervisor_disable_tick(); @@ -620,6 +887,9 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { send_stop_sequence_guarded(self); } self->awake = false; + #if CIRCUITPY_EPAPERDISPLAY_LAG + lag_release_interrupts(self); + #endif // CIRCUITPY_EPAPERDISPLAY_LAG release_display_core(&self->core); if (self->busy.base.type == &digitalio_digitalinout_type) { @@ -631,10 +901,24 @@ void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t // The VM is over, so whatever holds the bus can never let it go: end it here. if (self->keep_awake && !displayio_display_bus_is_free(&self->bus)) { displayio_display_bus_end_transaction(&self->bus); + #if CIRCUITPY_EPAPERDISPLAY_LAG + self->lag_bus_held = false; + #endif } } void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { + #if CIRCUITPY_EPAPERDISPLAY_LAG + if (self->lag_frames > 0) { + // 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 release_displays(), as for any display that persists. + epaperdisplay_epaperdisplay_end_held_bus(self); + release_epaperdisplay(self); + self->base.type = &mp_type_NoneType; + return; + } + #endif // CIRCUITPY_EPAPERDISPLAY_LAG displayio_display_core_set_root_group(&self->core, &circuitpython_splash); self->core.full_refresh = true; // Let the panel sleep after the next refresh when code isn't running. @@ -653,7 +937,570 @@ 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_LAG + gc_collect_ptr((void *)self->clean_sequence); + gc_collect_ptr(self->lag_held_exception); + #endif // CIRCUITPY_EPAPERDISPLAY_LAG +} + +#if CIRCUITPY_EPAPERDISPLAY_LAG +// Lag drive. Plane 0x26 holds frame N-lag instead of N-1, so a pixel that changes is driven for +// lag steps, then driven the other way by the same number of steps if it changes back. 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. + +static void 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) { + 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); + } +} + +static void 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 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 (!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 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 (!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 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 = send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + if (ok) { + 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) { + 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) || 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); + 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. +static void 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) { + 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_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. +static void 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) || display_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); + } +} + +static bool 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; + } + 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 (!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 (!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; +} + +// 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) || display_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); + 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); + lag_release_interrupts(self); +} + +// Raise once: what was held, if anything, instead of both that and this. +static MP_NORETURN void 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); + display_delay_ms(self, self->start_up_time_ms); + if (!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) { + 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) || + !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) { + 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) { + lag_renew_deadline(self); + self->lag_force_full = true; + ok = 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); + } + } + } + lag_release_interrupts(self); + return ok; +} + +// 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. +static void 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 = 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; + } + } + } + wait_for_busy(self); + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } } +#endif // CIRCUITPY_EPAPERDISPLAY_LAG size_t maybe_refresh_epaperdisplay(void) { for (uint8_t i = 0; i < CIRCUITPY_DISPLAY_LIMIT; i++) { diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 1e48dcb52b0..ee2867a56b6 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -42,6 +42,35 @@ typedef struct { bool awake; bool needs_start; // update_refresh_mode() changed the start sequence. display_chip_select_behavior_t chip_select; + #if CIRCUITPY_EPAPERDISPLAY_LAG + // Lag drive. Only used when lag_frames > 0. The ring has lag_frames slots: frames N-1 to + // N-lag_frames. + uint8_t **lag_slots; + const uint8_t *clean_sequence; + uint32_t clean_sequence_len; + uint64_t lag_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 lag_held_exception; + bool lag_holding_interrupts; + bool lag_bus_held; // Inside a transaction, so an exception must end it. + bool lag_state_lost; // What the panel received is unknown, so it needs a stored-waveform refresh. + bool lag_slot_overwriting; // The oldest frame in the ring is being replaced. + bool lag_commit_drive; // The owed step has pixels to drive. + uint8_t lag_commit; // What the newest step in the ring still has to send. + displayio_area_t lag_commit_box; + uint64_t lag_update_start; // When the running update was sent. + // What each of the last lag_frames + 1 steps rendered, in pixels. + displayio_area_t *lag_boxes; + uint32_t lag_slot_size; + uint16_t lag_stride; + uint16_t lag_rows; + uint8_t lag_frames; + uint8_t lag_head; // Slot of the newest frame. + uint8_t lag_box_index; // Entry in lag_boxes for this step. + uint8_t lag_settle; // Steps owed until every plane pair matches the newest frame. + bool lag_force_full; // Write everything on the next step. + #endif } epaperdisplay_epaperdisplay_obj_t; void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_epaperdisplay_obj_t *self, From 539e4067679dad3e682529558621082c96d2e6bc Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 05/10] epaperdisplay: add clean() for balanced wipes with lag drive Brings every pixel to white, optionally runs clean_sequence, then drives every pixel frames updates each way. For scripts to remove ghosting while running. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 40 +++++++++++++ shared-bindings/epaperdisplay/EPaperDisplay.h | 9 +++ shared-module/epaperdisplay/EPaperDisplay.c | 56 +++++++++++++++++-- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 777c22791e7..79520783d68 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -419,6 +419,43 @@ static mp_obj_t epaperdisplay_epaperdisplay_obj_refresh(mp_obj_t self_in) { } MP_DEFINE_CONST_FUN_OBJ_1(epaperdisplay_epaperdisplay_refresh_obj, epaperdisplay_epaperdisplay_obj_refresh); +//| def clean(self, frames: int = 8, *, full: bool = False) -> None: +//| """Remove ghosting from a display using ``lag_frames``, with balanced drive. Only on builds +//| with ``CIRCUITPY_EPAPERDISPLAY_LAG``. +//| +//| 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``.""" +//| ... +//| +#if CIRCUITPY_EPAPERDISPLAY_LAG +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 + //| 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) { @@ -534,6 +571,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_LAG + { 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 829215369f0..63c65960e10 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -106,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_LAG +// 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/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 6c54931c2c3..7c369b16c07 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -55,7 +55,7 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self); static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self); static MP_NORETURN void lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised); static bool lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas); -static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self); +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full); #endif // CIRCUITPY_EPAPERDISPLAY_LAG void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, @@ -1201,7 +1201,7 @@ static bool lag_step(epaperdisplay_epaperdisplay_obj_t *self, const displayio_ar 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) || mp_hal_is_interrupted()) { + if (!lag_clean(self, 0, true) || mp_hal_is_interrupted()) { return false; } areas = &self->core.area; @@ -1323,6 +1323,41 @@ static bool lag_write_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t com 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 (!send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len)) { + return i; + } + 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. @@ -1436,7 +1471,7 @@ static bool lag_recover(epaperdisplay_epaperdisplay_obj_t *self) { } // 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) { +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full) { if (!displayio_display_bus_is_free(&self->bus)) { return false; } @@ -1448,7 +1483,7 @@ static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self) { } else { ok = self->awake || lag_wake(self); ok = ok && lag_to_white(self); - if (ok) { + if (ok && full) { lag_renew_deadline(self); self->lag_force_full = true; ok = send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) && lag_wait_idle(self); @@ -1459,10 +1494,23 @@ static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self) { } } } + if (ok && frames > 0) { + lag_wipe(self, frames); + } 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; + } + 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. From 8898cc9025815b4d0ffde3e07451820862985064 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 06/10] displayio: raise before releasing a lag display whose bus stays held release_displays() lost the owed drive and the stop sequence silently when something held the bus for the whole 30 s bound. Wait for the bus first, and raise with every display untouched, so the script can free it and try again. The end of the VM can't catch an exception, and doesn't need to: nothing can still hold the bus by then. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/displayio/__init__.c | 6 ++++++ shared-module/displayio/__init__.c | 10 +++++++++ shared-module/displayio/__init__.h | 4 ++++ shared-module/epaperdisplay/EPaperDisplay.c | 23 ++++++++++++++++++--- shared-module/epaperdisplay/EPaperDisplay.h | 6 ++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/shared-bindings/displayio/__init__.c b/shared-bindings/displayio/__init__.c index 12981cbbae0..5d79c104b36 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_LAG + displayio_prepare_release_displays(); + #endif common_hal_displayio_release_displays(); return mp_const_none; } diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index 72c7a9b8af0..047fda3bf1c 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -243,6 +243,16 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { } } +#if CIRCUITPY_EPAPERDISPLAY_LAG +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..e61060ff26f 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_LAG +// 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 7c369b16c07..f5472b76044 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -897,6 +897,22 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { } } +#if CIRCUITPY_EPAPERDISPLAY_LAG +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_LAG + 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->keep_awake && !displayio_display_bus_is_free(&self->bus)) { @@ -910,9 +926,10 @@ void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_LAG if (self->lag_frames > 0) { - // 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 release_displays(), as for any display that persists. + // 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; diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index ee2867a56b6..0442e81f6d8 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -81,6 +81,12 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self); // The VM is over: a keep_awake 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); +#if CIRCUITPY_EPAPERDISPLAY_LAG +// 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); From 7316a558e9845cd3f9375ff8993284711857f21c Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 15:03:27 -0800 Subject: [PATCH 07/10] epaperdisplay: try one reset when shutdown finds BUSY stuck The wait used the whole deadline, so a panel a reset would free got no clean. Reset once, wait once more with no renewal, and run the stored waveform if BUSY falls. If it stays high, nothing more is sent. Co-Authored-By: Claude Opus 5.5 --- shared-module/epaperdisplay/EPaperDisplay.c | 24 ++++++++++++++++++++- shared-module/epaperdisplay/EPaperDisplay.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index f5472b76044..7db03980d04 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -1269,7 +1269,7 @@ static bool lag_wait_idle(epaperdisplay_epaperdisplay_obj_t *self) { // 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) { + if (self->lag_busy_deadline != 0 && !self->lag_last_try) { self->lag_busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; } } @@ -1564,6 +1564,28 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { 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); + display_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_LAG diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 0442e81f6d8..ba650ead480 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -54,6 +54,7 @@ typedef struct { mp_obj_t lag_held_exception; bool lag_holding_interrupts; bool lag_bus_held; // Inside a transaction, so an exception must end it. + bool lag_last_try; // Shutdown's reset after a stuck wait: the deadline is not renewed. bool lag_state_lost; // What the panel received is unknown, so it needs a stored-waveform refresh. bool lag_slot_overwriting; // The oldest frame in the ring is being replaced. bool lag_commit_drive; // The owed step has pixels to drive. From 2d8485c35a64e4d6cc47e453bcca555413f34c94 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 2 Nov 2026 13:53:06 -0800 Subject: [PATCH 08/10] epaperdisplay: add CIRCUITPY_EPAPERDISPLAY_FAST, off on atmel-samd The faster refresh options (write_previous_ram_command, keep_awake and update_refresh_mode's refresh_sequence) cost about 900 bytes, which no longer fits on several SAMD21 and SAMD51 boards. With the flag off, EPaperDisplay builds as it did before. Lag drive needs it. Co-Authored-By: Claude Opus 5.5 --- ports/atmel-samd/mpconfigport.mk | 1 + py/circuitpy_mpconfig.mk | 7 +++ shared-bindings/epaperdisplay/EPaperDisplay.c | 43 ++++++++++++-- shared-module/displayio/__init__.c | 2 + shared-module/epaperdisplay/EPaperDisplay.c | 56 ++++++++++++++++++- shared-module/epaperdisplay/EPaperDisplay.h | 6 ++ 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk index 29a881d2d98..7d14227571a 100644 --- a/ports/atmel-samd/mpconfigport.mk +++ b/ports/atmel-samd/mpconfigport.mk @@ -13,6 +13,7 @@ CIRCUITPY_LTO = 1 CIRCUITPY_KEYPAD_DEMUX ?= 0 CIRCUITPY_LVFONTIO ?= 0 +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 CIRCUITPY_DIGITALINOUT_PROTOCOL = 0 diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 516a0f89b90..1d5d1484d7c 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -310,9 +310,16 @@ CFLAGS += -DCIRCUITPY_QSPIBUS=$(CIRCUITPY_QSPIBUS) CIRCUITPY_EPAPERDISPLAY ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY=$(CIRCUITPY_EPAPERDISPLAY) +# Faster e-paper refreshes: EPaperDisplay's write_previous_ram_command and keep_awake. +CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) +CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_FAST=$(CIRCUITPY_EPAPERDISPLAY_FAST) + # Fast e-paper updates with balanced drive: EPaperDisplay's lag_frames and clean(). CIRCUITPY_EPAPERDISPLAY_LAG ?= 0 CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_LAG=$(CIRCUITPY_EPAPERDISPLAY_LAG) +ifeq ($(CIRCUITPY_EPAPERDISPLAY_LAG)$(CIRCUITPY_EPAPERDISPLAY_FAST),10) +$(error CIRCUITPY_EPAPERDISPLAY_LAG needs CIRCUITPY_EPAPERDISPLAY_FAST) +endif CIRCUITPY_I2CDISPLAYBUS ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_I2CDISPLAYBUS=$(CIRCUITPY_I2CDISPLAYBUS) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 79520783d68..ce7ef4a3708 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -107,6 +107,7 @@ //| 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 available on all boards. //| :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 @@ -125,6 +126,7 @@ //| The ``stop_sequence`` is not sent after each refresh. It is sent when the display is released. //| It is also sent before the reset that ``update_refresh_mode()`` asks for, so it can't be empty. //| This keeps the display's settings and RAM between refreshes so they can be faster. +//| Not available on all boards. //| :param int lag_frames: When more than 0, 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 @@ -132,12 +134,13 @@ //| ``lag_frames`` more times to finish the drive. Implies ``keep_awake``. 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. -//| Only on builds with ``CIRCUITPY_EPAPERDISPLAY_LAG``. Elsewhere, more than 0 raises `ValueError`. +//| Only on builds with ``CIRCUITPY_EPAPERDISPLAY_LAG``. Elsewhere, more than 0 raises `ValueError`, +//| and boards without ``keep_awake`` don't accept it. //| :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 ``lag_frames``. 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 accepted on every build and only used with ``lag_frames``. +//| It is accepted wherever ``keep_awake`` is and only used with ``lag_frames``. //| """ //| ... //| @@ -146,10 +149,18 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, ARG_ram_width, ARG_ram_height, ARG_colstart, ARG_rowstart, ARG_rotation, ARG_set_column_window_command, ARG_set_row_window_command, ARG_set_current_column_command, ARG_set_current_row_command, ARG_write_black_ram_command, ARG_black_bits_inverted, - ARG_write_color_ram_command, ARG_color_bits_inverted, ARG_write_previous_ram_command, ARG_highlight_color, ARG_highlight_color2, + ARG_write_color_ram_command, ARG_color_bits_inverted, + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_write_previous_ram_command, + #endif + 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_keep_awake, ARG_lag_frames, ARG_clean_sequence }; + ARG_two_byte_sequence_length, ARG_start_up_time, ARG_address_little_endian, + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_keep_awake, 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 }, @@ -169,7 +180,9 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, { MP_QSTR_black_bits_inverted, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_write_color_ram_command, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, { MP_QSTR_color_bits_inverted, 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} }, + #endif { MP_QSTR_highlight_color, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0x000000} }, { MP_QSTR_highlight_color2, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0x000000} }, { MP_QSTR_refresh_display_command, MP_ARG_OBJ | MP_ARG_REQUIRED }, @@ -184,9 +197,11 @@ 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_keep_awake, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0} }, { 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); @@ -197,12 +212,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_keep_awake].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); @@ -212,6 +229,7 @@ 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 mp_int_t lag_frames = mp_arg_validate_int_range(args[ARG_lag_frames].u_int, 0, 32, MP_QSTR_lag_frames); #if CIRCUITPY_EPAPERDISPLAY_LAG if (lag_frames > 0) { @@ -241,7 +259,8 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, if (lag_frames > 0) { mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_lag_frames); } - #endif + #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST primary_display_t *disp = allocate_display_or_raise(); epaperdisplay_epaperdisplay_obj_t *self = &disp->epaper_display; @@ -256,10 +275,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; @@ -305,7 +326,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; @@ -320,6 +343,7 @@ 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.keep_awake = args[ARG_keep_awake].u_bool || lag_frames > 0; construct_args.lag_frames = lag_frames; if (args[ARG_clean_sequence].u_obj != mp_const_none) { @@ -328,6 +352,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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; @@ -362,14 +387,16 @@ MP_DEFINE_CONST_FUN_OBJ_2(epaperdisplay_epaperdisplay_show_obj, epaperdisplay_ep //| :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.""" +//| When it is None, the current refresh sequence is kept. Not available on all boards.""" //| 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, ARG_refresh_sequence }; 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)]; @@ -380,12 +407,16 @@ 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); + #if CIRCUITPY_EPAPERDISPLAY_FAST mp_buffer_info_t refresh_sequence; + #endif mp_buffer_info_t *refresh_sequence_ptr = NULL; + #if CIRCUITPY_EPAPERDISPLAY_FAST 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, refresh_sequence_ptr, seconds_per_frame); diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index 047fda3bf1c..976663b7ec5 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -170,9 +170,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 diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 7db03980d04..ffcc29dbc9f 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -115,7 +115,9 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla 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; @@ -136,9 +138,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->keep_awake = args->keep_awake; 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; @@ -189,6 +193,7 @@ uint16_t common_hal_epaperdisplay_epaperdisplay_get_height(epaperdisplay_epaperd return displayio_display_core_get_height(&self->core); } +#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. @@ -201,6 +206,7 @@ static bool wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { } return false; } +#endif // CIRCUITPY_EPAPERDISPLAY_FAST #if CIRCUITPY_EPAPERDISPLAY_LAG static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { @@ -452,15 +458,20 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep #endif // CIRCUITPY_EPAPERDISPLAY_LAG 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) { @@ -475,6 +486,7 @@ static void send_stop_sequence_guarded(epaperdisplay_epaperdisplay_obj_t *self) mp_sched_exception(nlr.ret_val); } } +#endif // CIRCUITPY_EPAPERDISPLAY_FAST static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { if (!displayio_display_bus_is_free(&self->bus)) { @@ -486,6 +498,7 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla return; } + #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->keep_awake && self->awake) { if (!self->needs_start) { // The panel is still configured from the previous refresh. @@ -497,6 +510,7 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla send_stop_sequence_guarded(self); self->awake = false; } + #endif // run start sequence self->bus.bus_reset(self->bus.bus); @@ -507,8 +521,10 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla display_interrupted(self)) { return; } + #if CIRCUITPY_EPAPERDISPLAY_FAST self->awake = true; self->needs_start = false; + #endif } uint32_t common_hal_epaperdisplay_epaperdisplay_get_time_to_refresh(epaperdisplay_epaperdisplay_obj_t *self) { @@ -523,6 +539,7 @@ uint32_t common_hal_epaperdisplay_epaperdisplay_get_time_to_refresh(epaperdispla return self->milliseconds_per_frame - elapsed_time; } +#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. @@ -584,6 +601,17 @@ static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdispl 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); + + 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; @@ -666,9 +694,12 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay uint8_t write_command = self->write_black_ram_command; if (pass == 1) { write_command = self->write_color_ram_command; - } else if (write_previous) { + } + #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; @@ -779,10 +810,15 @@ 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->keep_awake) { // 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); } + #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); + #endif } else { #if CIRCUITPY_EPAPERDISPLAY_LAG lag_give_up_if_stuck(self); @@ -850,10 +886,15 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s if (refresh_done) { supervisor_disable_tick(); self->refreshing = false; + #if CIRCUITPY_EPAPERDISPLAY_FAST if (!self->keep_awake) { // 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); } + #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); + #endif } #if CIRCUITPY_EPAPERDISPLAY_LAG else { @@ -878,15 +919,22 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { supervisor_disable_tick(); self->refreshing = false; // Run stop sequence but don't wait for busy because busy is set when sleeping. + #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->keep_awake) { send_stop_sequence_guarded(self); } else { send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } - } else if (self->keep_awake && self->awake) { + #else + send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + #endif + } + #if CIRCUITPY_EPAPERDISPLAY_FAST + else if (self->keep_awake && self->awake) { send_stop_sequence_guarded(self); } self->awake = false; + #endif #if CIRCUITPY_EPAPERDISPLAY_LAG lag_release_interrupts(self); #endif // CIRCUITPY_EPAPERDISPLAY_LAG @@ -913,6 +961,7 @@ void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj } #endif // CIRCUITPY_EPAPERDISPLAY_LAG +#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->keep_awake && !displayio_display_bus_is_free(&self->bus)) { @@ -922,6 +971,7 @@ void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t #endif } } +#endif // CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_LAG @@ -938,6 +988,7 @@ void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) #endif // CIRCUITPY_EPAPERDISPLAY_LAG 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->keep_awake && self->awake && !self->refreshing) { // Nothing can catch a raise from a send. A reload may be pending. @@ -946,6 +997,7 @@ void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) self->awake = false; } self->keep_awake = false; + #endif } void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t *self) { diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index ba650ead480..75ac486e0aa 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -29,7 +29,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; @@ -38,9 +40,11 @@ typedef struct { bool grayscale; bool acep; bool two_byte_sequence_length; + #if CIRCUITPY_EPAPERDISPLAY_FAST bool keep_awake; bool awake; bool needs_start; // update_refresh_mode() changed the start sequence. + #endif display_chip_select_behavior_t chip_select; #if CIRCUITPY_EPAPERDISPLAY_LAG // Lag drive. Only used when lag_frames > 0. The ring has lag_frames slots: frames N-1 to @@ -79,9 +83,11 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep 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 keep_awake 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); +#endif #if CIRCUITPY_EPAPERDISPLAY_LAG // 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 From 8d19a332f9f8463b592ef44dc4f4b629ca53fd87 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 2 Nov 2026 16:42:09 -0800 Subject: [PATCH 09/10] Update circuitpython.pot after merging video-gif Co-Authored-By: Claude Opus 5.5 --- locale/circuitpython.pot | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 5c541171090..82dbf2295df 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 "" @@ -1735,8 +1736,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 "" From 8cbd70ed263445cd53b17ab5fa6f908a9d871382 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 3 Nov 2026 08:56:10 -0800 Subject: [PATCH 10/10] Warn that fast e-paper updates exceed panel limits Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index ce7ef4a3708..287f770f4cd 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -141,6 +141,10 @@ //| first refresh with ``lag_frames``. 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 accepted wherever ``keep_awake`` is and only used with ``lag_frames``. +//| +//| .. warning:: ``keep_awake``, ``write_previous_ram_command`` and ``lag_frames`` 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. //| """ //| ... //|