From 30774daeb6b6750240356c29cf774f354fa522cf Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:00:42 -0700 Subject: [PATCH 01/24] 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 412d4a1d01829fd3f7f91e7150bf73c56f771c5f Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:01:37 -0700 Subject: [PATCH 02/24] 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 96ac94956519647ca24b1c8cb22763d6e2f100ed Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 29 Oct 2026 08:02:22 -0700 Subject: [PATCH 03/24] 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 eeb1fb9c72b..f90926b179a 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -173,6 +173,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 d774bbecc0ad5ccb2fec9119ef53e5289be6c5b6 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 04/24] 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 021a923ab78..e33824911ad 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -313,6 +313,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 68d85871d90d54e5a7f8ac5dc50957ec44119391 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 05/24] 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 c544326ca73eacba4f636c00ed36f9626bb8a35b Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 06:59:49 -0800 Subject: [PATCH 06/24] 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 f90926b179a..6e061cf353d 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -246,6 +246,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 66578b29cac95d108d6f0110079c1ca8b6e27668 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 1 Nov 2026 15:03:27 -0800 Subject: [PATCH 07/24] 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 550d9d9198dbed49ee40dd0f931289e1c4916fce Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 2 Nov 2026 13:53:06 -0800 Subject: [PATCH 08/24] 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 e33824911ad..84f4341bcab 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -313,9 +313,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 6e061cf353d..b0b4d260730 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -173,9 +173,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 d67afe7862fb677146f29130a9e27869b2606494 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 2 Nov 2026 16:42:09 -0800 Subject: [PATCH 09/24] 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 e13866c98bc..56c67d1f501 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 35edbc844676d734d63a1e12e8a46f840c858aa4 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 3 Nov 2026 08:56:10 -0800 Subject: [PATCH 10/24] 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. //| """ //| ... //| From 0eebc9351e4b7f0a69b990889579cb4ad0a429c8 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 3 Nov 2026 18:47:54 -0800 Subject: [PATCH 11/24] epaperdisplay: explain how lag drive relates to FAST Co-Authored-By: Claude Opus 5.5 --- shared-module/epaperdisplay/EPaperDisplay.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 75ac486e0aa..03ad9028ff1 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -47,8 +47,10 @@ typedef struct { #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 - // N-lag_frames. + // Lag drive, built on FAST's write_previous_ram_command. FAST writes the last frame to the + // previous RAM, so each changed pixel is driven once. Lag writes the frame from lag_frames + // refreshes ago, so a changed pixel is driven lag_frames times, the same number each way. + // The ring holds those frames. Only used when lag_frames > 0. uint8_t **lag_slots; const uint8_t *clean_sequence; uint32_t clean_sequence_len; From cce6090e2fec01dcb977dd411e6b718aad606a7e Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 3 Nov 2026 18:53:48 -0800 Subject: [PATCH 12/24] epaperdisplay: same API on every build The fast and lag arguments and clean() now exist on every build. Builds without them raise NotImplementedError when they are used. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 287f770f4cd..d8f34d93d94 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -107,7 +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. +//| Raises `NotImplementedError` on boards without it. //| :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 @@ -126,7 +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. +//| Raises `NotImplementedError` on boards without it. //| :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 @@ -134,13 +134,12 @@ //| ``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`, -//| and boards without ``keep_awake`` don't accept it. +//| Raises `NotImplementedError` on boards without 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 wherever ``keep_awake`` is and only used with ``lag_frames``. +//| It is 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, @@ -153,18 +152,12 @@ 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, - #if CIRCUITPY_EPAPERDISPLAY_FAST - ARG_write_previous_ram_command, - #endif + 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, - #if CIRCUITPY_EPAPERDISPLAY_FAST - ARG_keep_awake, ARG_lag_frames, ARG_clean_sequence, - #endif - }; + 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 }, @@ -184,9 +177,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} }, - #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 }, @@ -201,15 +192,27 @@ 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); + #if !CIRCUITPY_EPAPERDISPLAY_FAST + if (args[ARG_write_previous_ram_command].u_obj != mp_const_none) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_write_previous_ram_command); + } + if (args[ARG_keep_awake].u_bool) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_keep_awake); + } + #endif + #if !CIRCUITPY_EPAPERDISPLAY_LAG + if (args[ARG_lag_frames].u_int != 0) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_lag_frames); + } + #endif + mp_obj_t display_bus = args[ARG_display_bus].u_obj; mp_buffer_info_t start_bufinfo; @@ -259,10 +262,6 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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 // CIRCUITPY_EPAPERDISPLAY_LAG #endif // CIRCUITPY_EPAPERDISPLAY_FAST @@ -391,16 +390,15 @@ 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. Not available on all boards.""" +//| When it is None, the current refresh sequence is kept. Raises `NotImplementedError` +//| on boards without it.""" //| 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)]; @@ -411,16 +409,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); - #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) { + #if !CIRCUITPY_EPAPERDISPLAY_FAST + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_refresh_sequence); + #endif 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); @@ -455,8 +452,8 @@ 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``. +//| """Remove ghosting from a display using ``lag_frames``, with balanced drive. Raises +//| `NotImplementedError` on boards without ``lag_frames``. //| //| 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 @@ -467,8 +464,10 @@ MP_DEFINE_CONST_FUN_OBJ_1(epaperdisplay_epaperdisplay_refresh_obj, epaperdisplay //| :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) { + #if !CIRCUITPY_EPAPERDISPLAY_LAG + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_clean); + #else enum { ARG_frames, ARG_full }; static const mp_arg_t allowed_args[] = { { MP_QSTR_frames, MP_ARG_INT, {.u_int = 8} }, @@ -487,9 +486,9 @@ static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t mp_raise_RuntimeError(MP_ERROR_TEXT("Refresh too soon")); } return mp_const_none; + #endif } 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.""" @@ -606,9 +605,7 @@ 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) }, From ef990ecfb7e5017bf6716ecef33eec5952b2b373 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 4 Nov 2026 07:32:03 -0800 Subject: [PATCH 13/24] epaperdisplay: fast updates on only for espressif and raspberrypi Those are the ports with e-paper boards. Other ports keep the same API and raise NotImplementedError, which saves flash on small boards such as the nRF52840 dongles that overflowed. Co-Authored-By: Claude Opus 5.5 --- ports/atmel-samd/mpconfigport.mk | 1 - ports/espressif/mpconfigport.mk | 1 + ports/raspberrypi/mpconfigport.mk | 1 + py/circuitpy_mpconfig.mk | 3 ++- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk index 7d14227571a..29a881d2d98 100644 --- a/ports/atmel-samd/mpconfigport.mk +++ b/ports/atmel-samd/mpconfigport.mk @@ -13,7 +13,6 @@ CIRCUITPY_LTO = 1 CIRCUITPY_KEYPAD_DEMUX ?= 0 CIRCUITPY_LVFONTIO ?= 0 -CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 CIRCUITPY_DIGITALINOUT_PROTOCOL = 0 diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 18208b43bbd..0734036619f 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -57,6 +57,7 @@ CIRCUITPY_FULL_BUILD ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 +CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # If SSL is enabled, it's mbedtls diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index efb5f77e9ee..76942632a5b 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -10,6 +10,7 @@ CIRCUITPY_RP2PIO ?= 1 CIRCUITPY_ASYNC_SPI ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 +CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) CIRCUITPY_FLOPPYIO ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_DISPLAYIO) diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 84f4341bcab..2eed4f05e61 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -314,7 +314,8 @@ 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) +# On only in ports with e-paper boards and room for it. +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_FAST=$(CIRCUITPY_EPAPERDISPLAY_FAST) # Fast e-paper updates with balanced drive: EPaperDisplay's lag_frames and clean(). From af6334e6064dcc0d87cc493a5552a6e84ffdfde1 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 4 Nov 2026 10:53:20 -0800 Subject: [PATCH 14/24] epaperdisplay: lag drive on for espressif and raspberrypi It was only on for the Feather RP2040 ThinkInk, but external panels need it too. It costs about 7 KB of flash and 80 bytes of RAM. The frame memory is only allocated when lag_frames is used. Co-Authored-By: Claude Opus 5.5 --- ports/espressif/mpconfigport.mk | 1 + .../boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk | 2 -- ports/raspberrypi/mpconfigport.mk | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 0734036619f..612fae8303f 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -58,6 +58,7 @@ CIRCUITPY_FULL_BUILD ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) +CIRCUITPY_EPAPERDISPLAY_LAG ?= $(CIRCUITPY_EPAPERDISPLAY_FAST) CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # If SSL is enabled, it's mbedtls diff --git a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk index 1caf2f862ee..1f71dcb76bf 100644 --- a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk +++ b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk @@ -7,5 +7,3 @@ CHIP_VARIANT = RP2040 CHIP_FAMILY = rp2 EXTERNAL_FLASH_DEVICES = "GD25Q64C,W25Q64JVxQ" - -CIRCUITPY_EPAPERDISPLAY_LAG = 1 diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 76942632a5b..c145f9d2509 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -11,6 +11,7 @@ CIRCUITPY_ASYNC_SPI ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) +CIRCUITPY_EPAPERDISPLAY_LAG ?= $(CIRCUITPY_EPAPERDISPLAY_FAST) CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) CIRCUITPY_FLOPPYIO ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_DISPLAYIO) From 4c2e2277ca717d87b87170d041158817413860b0 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 4 Nov 2026 10:53:35 -0800 Subject: [PATCH 15/24] epaperdisplay: explain why lag drive stays balanced Each frame is driven toward once and away from once, lag steps later. Adds a small example of a pixel that changes back early. Co-Authored-By: Claude Opus 5.5 --- shared-module/epaperdisplay/EPaperDisplay.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index ffcc29dbc9f..8d8709d1d8d 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -1013,9 +1013,23 @@ void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t } #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. +// Lag drive. Plane 0x26 holds frame N-lag instead of N-1. Each step drives a pixel toward black +// where N is black and N-lag is white, toward white where N is white and N-lag is black, and not +// at all where they match. +// +// That keeps the drive balanced without counting. Each frame is used twice: pixels are driven +// toward it in the step where it is N, and away from it lag steps later, where it is N-lag. So a +// pixel that ends the way it started has been driven the same amount toward black and toward +// white. For example, lag 3 and a pixel that is black for 2 steps (both planes start white): +// +// step 1 2 3 4 5 6 +// frame N W B B W W W +// frame N-3 W W W W B B +// drive - B B - W W 2 toward black, 2 toward white +// +// A change that holds for lag steps or more is driven lag steps, so a bigger lag drives harder. +// When frames stop changing, lag more steps finish the drive. Each frame is rendered once into +// the ring and the planes are sent from there. // // The ring has lag slots, N-1 to N-lag, and N replaces N-lag in its slot. The box to send must // be known before that, so it comes from the areas that were rendered, not from comparing frames. From 84230dfa99ed25d1849fdff7255f96d89f35bd8c Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 5 Nov 2026 14:21:16 -0800 Subject: [PATCH 16/24] epaperdisplay: fold lag drive into CIRCUITPY_EPAPERDISPLAY_FAST Lag drive is what makes fast updates safe for the panel, so it no longer has its own build flag. CIRCUITPY_EPAPERDISPLAY_LAG is gone and its code builds with CIRCUITPY_EPAPERDISPLAY_FAST. FAST stays on only for espressif and raspberrypi, so atmel-samd and nordic get neither. Co-Authored-By: Claude Opus 5.5 --- ports/espressif/mpconfigport.mk | 1 - ports/raspberrypi/mpconfigport.mk | 1 - py/circuitpy_mpconfig.mk | 11 +-- shared-bindings/displayio/__init__.c | 2 +- shared-bindings/epaperdisplay/EPaperDisplay.c | 8 +- shared-bindings/epaperdisplay/EPaperDisplay.h | 2 +- shared-module/displayio/__init__.c | 2 +- shared-module/displayio/__init__.h | 2 +- shared-module/epaperdisplay/EPaperDisplay.c | 76 +++++++++---------- shared-module/epaperdisplay/EPaperDisplay.h | 4 +- 10 files changed, 47 insertions(+), 62 deletions(-) diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 612fae8303f..0734036619f 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -58,7 +58,6 @@ CIRCUITPY_FULL_BUILD ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) -CIRCUITPY_EPAPERDISPLAY_LAG ?= $(CIRCUITPY_EPAPERDISPLAY_FAST) CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # If SSL is enabled, it's mbedtls diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index c145f9d2509..76942632a5b 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -11,7 +11,6 @@ CIRCUITPY_ASYNC_SPI ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) -CIRCUITPY_EPAPERDISPLAY_LAG ?= $(CIRCUITPY_EPAPERDISPLAY_FAST) CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) CIRCUITPY_FLOPPYIO ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_DISPLAYIO) diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 2eed4f05e61..e8c339d43b8 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -313,18 +313,11 @@ 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. -# On only in ports with e-paper boards and room for it. +# Fast e-paper updates: EPaperDisplay's write_previous_ram_command, keep_awake, lag_frames +# and clean(). On only in ports with e-paper boards and room for it. CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 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/displayio/__init__.c b/shared-bindings/displayio/__init__.c index 5d79c104b36..b0cf5d3c911 100644 --- a/shared-bindings/displayio/__init__.c +++ b/shared-bindings/displayio/__init__.c @@ -92,7 +92,7 @@ //| //| static mp_obj_t displayio_release_displays(void) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST displayio_prepare_release_displays(); #endif common_hal_displayio_release_displays(); diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index d8f34d93d94..648ab99f196 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -206,8 +206,6 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, if (args[ARG_keep_awake].u_bool) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_keep_awake); } - #endif - #if !CIRCUITPY_EPAPERDISPLAY_LAG if (args[ARG_lag_frames].u_int != 0) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_lag_frames); } @@ -238,7 +236,6 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, #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) { // Each one a driver can leave out by mistake names itself. The rest are the display type. qstr missing = MP_QSTRnull; @@ -262,7 +259,6 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), missing); } } - #endif // CIRCUITPY_EPAPERDISPLAY_LAG #endif // CIRCUITPY_EPAPERDISPLAY_FAST primary_display_t *disp = allocate_display_or_raise(); @@ -439,7 +435,7 @@ 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 CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag_frames > 0) { // Interrupted is not too soon: raise what interrupted it. mp_handle_pending(true); @@ -465,7 +461,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(epaperdisplay_epaperdisplay_refresh_obj, epaperdisplay //| ... //| static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - #if !CIRCUITPY_EPAPERDISPLAY_LAG + #if !CIRCUITPY_EPAPERDISPLAY_FAST mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_clean); #else enum { ARG_frames, ARG_full }; diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 63c65960e10..3cd63f63305 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -106,7 +106,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_obj_t *self); -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST // Lag drive only. Brings every pixel to white with balanced drive. When full, sends the clean // sequence and the start sequence. Then wipes each pixel frames toward black and frames toward // white. Blocks with background tasks running. The next refresh redraws the whole display. diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index b0b4d260730..5ce7c511d0b 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -248,7 +248,7 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { } } -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST void displayio_prepare_release_displays(void) { for (uint8_t i = 0; i < CIRCUITPY_DISPLAY_LIMIT; i++) { if (displays[i].display_base.type == &epaperdisplay_epaperdisplay_type) { diff --git a/shared-module/displayio/__init__.h b/shared-module/displayio/__init__.h index e61060ff26f..b15145b488d 100644 --- a/shared-module/displayio/__init__.h +++ b/shared-module/displayio/__init__.h @@ -118,7 +118,7 @@ extern displayio_group_t circuitpython_splash; void displayio_background(void); void reset_displays(void); -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST // Raises if a display using lag_frames can't finish its drive, before any display is released. void displayio_prepare_release_displays(void); #endif diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 8d8709d1d8d..266eb7a8c0d 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -9,7 +9,7 @@ #include "py/gc.h" #include "py/runtime.h" #include "shared/runtime/interrupt_char.h" -#if CIRCUITPY_EPAPERDISPLAY_LAG && CIRCUITPY_WATCHDOG +#if CIRCUITPY_EPAPERDISPLAY_FAST && CIRCUITPY_WATCHDOG #include "shared-bindings/watchdog/WatchDogTimer.h" #endif #include "shared-bindings/displayio/ColorConverter.h" @@ -18,7 +18,7 @@ #include "shared-bindings/time/__init__.h" #include "shared-module/displayio/__init__.h" #include "supervisor/shared/display.h" -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST #include "supervisor/port_heap.h" #endif #include "supervisor/shared/tick.h" @@ -36,17 +36,17 @@ // 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 +#if CIRCUITPY_EPAPERDISPLAY_FAST // 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 +#if CIRCUITPY_EPAPERDISPLAY_FAST enum { LAG_COMMIT_NONE, LAG_COMMIT_PLANE, LAG_COMMIT_ACTIVATE }; #endif -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST 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); @@ -56,7 +56,7 @@ 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, uint8_t frames, bool full); -#endif // CIRCUITPY_EPAPERDISPLAY_LAG +#endif // CIRCUITPY_EPAPERDISPLAY_FAST void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, const epaperdisplay_construct_args_t *args) { @@ -97,7 +97,7 @@ 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 + #if CIRCUITPY_EPAPERDISPLAY_FAST self->lag_slots = NULL; self->lag_frames = 0; self->lag_busy_deadline = 0; @@ -109,7 +109,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla 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 + #endif // CIRCUITPY_EPAPERDISPLAY_FAST self->write_black_ram_command = args->write_black_ram_command; self->black_bits_inverted = args->black_bits_inverted; @@ -125,11 +125,11 @@ 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 CIRCUITPY_EPAPERDISPLAY_FAST if (args->lag_frames > 0) { lag_construct(self, args->lag_frames); } - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST self->start_sequence = args->start_sequence; self->start_sequence_len = args->start_sequence_len; @@ -208,7 +208,7 @@ static bool wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { } #endif // CIRCUITPY_EPAPERDISPLAY_FAST -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { return self->lag_busy_deadline != 0 && supervisor_ticks_ms64() > self->lag_busy_deadline; } @@ -347,7 +347,7 @@ static void display_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t d display_interrupted(self); } } -#else // CIRCUITPY_EPAPERDISPLAY_LAG +#else // CIRCUITPY_EPAPERDISPLAY_FAST #define display_wait_deadline(self) ((uint64_t)0) #define display_interrupted(self) mp_hal_is_interrupted() #define display_send_command(self, command, data, length) ( \ @@ -355,7 +355,7 @@ static void display_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t d (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 +#endif // CIRCUITPY_EPAPERDISPLAY_FAST static void wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { if (self->busy.base.type == &mp_type_NoneType) { @@ -391,17 +391,17 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, !display_interrupted(self) && (deadline == 0 || supervisor_ticks_ms64() <= deadline)) { RUN_BACKGROUND_TASKS; } - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST if (!have_bus && self->lag_frames > 0) { return false; } self->lag_bus_held = have_bus; - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST const bool sent = display_send_command(self, cmd, data, data_size); displayio_display_bus_end_transaction(&self->bus); - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST self->lag_bus_held = false; - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (!sent) { return false; } @@ -415,7 +415,7 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, } display_delay_ms(self, delay_length_ms); if (should_wait_for_busy) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag_frames > 0) { wait_for_update_start(self); } @@ -435,7 +435,7 @@ static bool 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, mp_buffer_info_t *refresh_sequence, float seconds_per_frame) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag_frames > 0) { // Finish the drive with the waveform that started it, and rewrite both planes after. nlr_buf_t nlr; @@ -455,7 +455,7 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep } self->lag_force_full = true; } - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST self->start_sequence = (uint8_t *)start_sequence->buf; self->start_sequence_len = start_sequence->len; #if CIRCUITPY_EPAPERDISPLAY_FAST @@ -820,14 +820,14 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); #endif } else { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST lag_give_up_if_stuck(self); #endif return false; } } if (self->core.current_group == NULL) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST // Nothing to draw, but the drive already started is still owed. if (self->lag_frames > 0 && self->awake) { return lag_refresh(self, NULL); @@ -844,11 +844,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 CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag_frames > 0) { return lag_refresh(self, current_area); } - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (current_area == NULL) { return true; } @@ -896,7 +896,7 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); #endif } - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST else { lag_give_up_if_stuck(self); } @@ -910,10 +910,10 @@ bool common_hal_epaperdisplay_epaperdisplay_get_busy(epaperdisplay_epaperdisplay } void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST lag_shutdown(self); lag_free(self); - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (self->refreshing) { wait_for_busy(self); supervisor_disable_tick(); @@ -935,9 +935,9 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { } self->awake = false; #endif - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST lag_release_interrupts(self); - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST release_display_core(&self->core); if (self->busy.base.type == &digitalio_digitalinout_type) { @@ -945,7 +945,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { } } -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj_t *self) { if (self->lag_frames == 0 || (!self->awake && !self->lag_state_lost)) { return; @@ -959,22 +959,20 @@ void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj RUN_BACKGROUND_TASKS; } } -#endif // CIRCUITPY_EPAPERDISPLAY_LAG +#endif // CIRCUITPY_EPAPERDISPLAY_FAST #if CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t *self) { // The VM is over, so whatever holds the bus can never let it go: end it here. if (self->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 } } #endif // CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag_frames > 0) { // Only a primary display gets here; reset_displays() has released the others. The heap is // still here but the sequences won't be, and the display would otherwise persist to draw @@ -985,7 +983,7 @@ void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) self->base.type = &mp_type_NoneType; return; } - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST displayio_display_core_set_root_group(&self->core, &circuitpython_splash); self->core.full_refresh = true; #if CIRCUITPY_EPAPERDISPLAY_FAST @@ -1006,13 +1004,13 @@ 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 + #if CIRCUITPY_EPAPERDISPLAY_FAST gc_collect_ptr((void *)self->clean_sequence); gc_collect_ptr(self->lag_held_exception); - #endif // CIRCUITPY_EPAPERDISPLAY_LAG + #endif // CIRCUITPY_EPAPERDISPLAY_FAST } -#if CIRCUITPY_EPAPERDISPLAY_LAG +#if CIRCUITPY_EPAPERDISPLAY_FAST // Lag drive. Plane 0x26 holds frame N-lag instead of N-1. Each step drives a pixel toward black // where N is black and N-lag is white, toward white where N is white and N-lag is black, and not // at all where they match. @@ -1653,7 +1651,7 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { self->lag_last_try = false; } } -#endif // CIRCUITPY_EPAPERDISPLAY_LAG +#endif // CIRCUITPY_EPAPERDISPLAY_FAST 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 03ad9028ff1..12b31e82adb 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -46,7 +46,7 @@ typedef struct { bool needs_start; // update_refresh_mode() changed the start sequence. #endif display_chip_select_behavior_t chip_select; - #if CIRCUITPY_EPAPERDISPLAY_LAG + #if CIRCUITPY_EPAPERDISPLAY_FAST // Lag drive, built on FAST's write_previous_ram_command. FAST writes the last frame to the // previous RAM, so each changed pixel is driven once. Lag writes the frame from lag_frames // refreshes ago, so a changed pixel is driven lag_frames times, the same number each way. @@ -90,7 +90,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self); // 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 +#if CIRCUITPY_EPAPERDISPLAY_FAST // 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. From ef18e3183d583aee5f053e231202db0bf08d10f2 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 5 Nov 2026 14:52:45 -0800 Subject: [PATCH 17/24] epaperdisplay: replace keep_awake with fast, lag_frames 4 by default fast=True is now the one switch for fast updates. It keeps the display awake between refreshes and drives with lag, so keep_awake is gone. lag_frames defaults to 4, must be 1 to 32 with fast, and is ignored without it. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index 648ab99f196..e0814fb1492 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -69,8 +69,8 @@ //| two_byte_sequence_length: bool = False, //| start_up_time: float = 0, //| address_little_endian: bool = False, -//| keep_awake: bool = False, -//| lag_frames: int = 0, +//| fast: bool = False, +//| lag_frames: int = 4, //| clean_sequence: Optional[ReadableBuffer] = None, //| ) -> None: //| """Create a EPaperDisplay object on the given display bus (`fourwire.FourWire` or `paralleldisplaybus.ParallelBus`). @@ -122,28 +122,26 @@ //| :param bool two_byte_sequence_length: When true, use two bytes to define sequence length //| :param float start_up_time: Time to wait after reset before sending commands //| :param bool address_little_endian: Send the least significant byte (not bit) of multi-byte addresses first. Ignored when ram is addressed with one byte -//| :param bool 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. -//| Raises `NotImplementedError` on boards without it. -//| :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. -//| Raises `NotImplementedError` on boards without it. +//| :param bool fast: When true, updates take a fraction of a second. The display is only reset and sent the +//| ``start_sequence`` before the first refresh, and the ``stop_sequence`` when it is released, so it can't be +//| empty. The previous RAM holds the frame from ``lag_frames`` refreshes ago instead of the last one, so each +//| changed pixel is driven for ``lag_frames`` refreshes, the same number toward black and toward white. Only +//| the box around the changed bytes is sent. ``refresh()`` returns as soon as the update starts. Poll ``busy`` +//| before the next one. If the image stops changing, call ``refresh()`` ``lag_frames`` more times to finish +//| the drive. Needs ``write_previous_ram_command``, ``busy_pin``, ``clean_sequence``, the window commands and +//| a monochrome display, and allocates ``lag_frames * width * height / 8`` bytes outside the VM heap until +//| the display is released. Raises `NotImplementedError` on boards without it. +//| :param int lag_frames: How many refreshes each changed pixel is driven for, 1 to 32, when ``fast`` is true. +//| More gives darker motion with more smear and uses more memory. Ignored when ``fast`` is false. //| :param ~circuitpython_typing.ReadableBuffer clean_sequence: Byte-packed commands for a full refresh, such as //| one using the waveform stored in the display, for use when both RAM planes are white. It runs before the -//| first refresh with ``lag_frames``. When the display is released or the code stops, the display finishes +//| first refresh with ``fast``. When the display is released or the code stops, the display finishes //| its drive, goes white and runs this and the ``stop_sequence``. Display drivers should provide it. -//| It is only used with ``lag_frames``. +//| It is only used with ``fast``. //| -//| .. 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. +//| .. warning:: ``fast`` and ``write_previous_ram_command`` allow updates much faster than panel makers' +//| limits on refresh rate. Fast updates need a balanced waveform, and long-term wear on the panel +//| is unknown. //| """ //| ... //| @@ -157,7 +155,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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_fast, 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 }, @@ -192,8 +190,8 @@ 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_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0} }, + { MP_QSTR_fast, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + { MP_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 4} }, { MP_QSTR_clean_sequence, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -203,11 +201,8 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, if (args[ARG_write_previous_ram_command].u_obj != mp_const_none) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_write_previous_ram_command); } - if (args[ARG_keep_awake].u_bool) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_keep_awake); - } - if (args[ARG_lag_frames].u_int != 0) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_lag_frames); + if (args[ARG_fast].u_bool) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_fast); } #endif @@ -221,7 +216,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, // 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)) { + (args[ARG_fast].u_bool || args[ARG_write_previous_ram_command].u_obj != mp_const_none)) { mp_arg_error_invalid(MP_QSTR_stop_sequence); } #endif @@ -235,8 +230,10 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, } #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 (lag_frames > 0) { + bool fast = args[ARG_fast].u_bool; + mp_int_t lag_frames = 0; + if (fast) { + lag_frames = mp_arg_validate_int_range(args[ARG_lag_frames].u_int, 1, 32, MP_QSTR_lag_frames); // Each one a driver can leave out by mistake names itself. The rest are the display type. qstr missing = MP_QSTRnull; if (args[ARG_busy_pin].u_obj == mp_const_none) { @@ -253,7 +250,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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; + missing = MP_QSTR_fast; } if (missing != MP_QSTRnull) { mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), missing); @@ -343,7 +340,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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.keep_awake = fast; construct_args.lag_frames = lag_frames; if (args[ARG_clean_sequence].u_obj != mp_const_none) { mp_buffer_info_t clean_bufinfo; @@ -379,7 +376,7 @@ 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. When ``keep_awake`` is true, the display is +//| varying the refresh mode of the display. When ``fast`` is true, the display is //| reset and sent the new ``start_sequence`` before the next refresh. //| //| :param ReadableBuffer start_sequence: Byte sequence sent to initialize the display before each refresh. @@ -426,7 +423,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(epaperdisplay_epaperdisplay_update_refresh_mode_obj, //| ``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 +//| ``fast``, set it to 0 and poll ``busy`` instead, since this returns once the update //| starts. The first refresh also runs the ``clean_sequence``, which takes seconds, and an //| interrupt during it raises when it ends.""" //| ... @@ -448,8 +445,8 @@ 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. Raises -//| `NotImplementedError` on boards without ``lag_frames``. +//| """Remove ghosting from a display using ``fast``, with balanced drive. Raises +//| `NotImplementedError` on boards without ``fast``. //| //| 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 @@ -504,7 +501,7 @@ 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. With ``lag_frames``, a panel still busy after 30 seconds is +//| ``refresh_time`` otherwise. With ``fast``, a panel still busy after 30 seconds is //| given up on, so this goes False and the next ``refresh()`` resets the panel and cleans it.""" static mp_obj_t epaperdisplay_epaperdisplay_obj_get_busy(mp_obj_t self_in) { epaperdisplay_epaperdisplay_obj_t *self = native_display(self_in); From 1ce8bb267c245c7dc5303b90d2453a305409bd84 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 9 Nov 2026 19:17:29 -0800 Subject: [PATCH 18/24] epaperdisplay: turn fast e-paper on by default CIRCUITPY_EPAPERDISPLAY_FAST now follows CIRCUITPY_EPAPERDISPLAY, so new ports get it automatically. It is turned off where flash is tight: samd21, SAMx5x chips without 1MB flash (the same HAS_1MB_FLASH gate as ulab), and nordic boards that keep their filesystem in internal flash. The espressif and raspberrypi lines that set the same default are dropped. Add a default of 0 in circuitpy_mpconfig.h, since zephyr-cp builds the module without setting the flag. The feature cost 5.3 KB on espruino_pico, the tightest board that gains it, which keeps 16 KB free. Co-Authored-By: Claude Opus 5.5 --- ports/atmel-samd/mpconfigport.mk | 5 +++++ ports/espressif/mpconfigport.mk | 1 - ports/nordic/mpconfigport.mk | 3 +++ ports/raspberrypi/mpconfigport.mk | 1 - py/circuitpy_mpconfig.h | 5 +++++ py/circuitpy_mpconfig.mk | 6 +++--- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk index 29a881d2d98..fff5310cfdb 100644 --- a/ports/atmel-samd/mpconfigport.mk +++ b/ports/atmel-samd/mpconfigport.mk @@ -39,6 +39,7 @@ CIRCUITPY_BUILTINS_POW3 ?= 0 CIRCUITPY_BUSDEVICE ?= 0 CIRCUITPY_COMPUTED_GOTO_SAVE_SPACE ?= 1 CIRCUITPY_COUNTIO ?= 0 +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 # Not enough RAM for framebuffers CIRCUITPY_FRAMEBUFFERIO ?= 0 CIRCUITPY_FREQUENCYIO ?= 0 @@ -123,6 +124,10 @@ endif CIRCUITPY_ALARM ?= 1 # Not enough room for both bitmaptools and bitmapfilter on most boards. CIRCUITPY_BITMAPFILTER ?= 0 +# Fast e-paper only fits on chips with 1MB flash. +ifeq ($(HAS_1MB_FLASH),0) +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif CIRCUITPY_FLOPPYIO ?= $(CIRCUITPY_FULL_BUILD) CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_FULL_BUILD) CIRCUITPY_LOAD_NATIVE ?= 1 diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 0734036619f..18208b43bbd 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -57,7 +57,6 @@ CIRCUITPY_FULL_BUILD ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 -CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) CIRCUITPY_BUSIO_SPI_ASYNC ?= 1 # If SSL is enabled, it's mbedtls diff --git a/ports/nordic/mpconfigport.mk b/ports/nordic/mpconfigport.mk index 3f762b4eb68..b96621a3021 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -97,6 +97,8 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) CIRCUITPY_LTO = 1 CIRCUITPY_LTO_PARTITION = balanced CIRCUITPY_ULAB_OPTIMIZE_SIZE ?= 1 + # Fast e-paper doesn't fit in what internal flash has left. + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 else CIRCUITPY_USB_AUDIO ?= 1 endif @@ -124,6 +126,7 @@ OPTIMIZATION_FLAGS ?= -Os CIRCUITPY_LTO = 1 CIRCUITPY_LTO_PARTITION = one ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 CIRCUITPY_FULL_BUILD ?= 0 CIRCUITPY_PULSEIO ?= 1 endif diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 76942632a5b..efb5f77e9ee 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -10,7 +10,6 @@ CIRCUITPY_RP2PIO ?= 1 CIRCUITPY_ASYNC_SPI ?= 1 # The 24 KB stack has room for a larger display refresh buffer. CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE ?= 2048 -CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) CIRCUITPY_NEOPIXEL_WRITE ?= $(CIRCUITPY_RP2PIO) CIRCUITPY_FLOPPYIO ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_DISPLAYIO) diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 8649b3e65a2..4dc4d02c0a8 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -414,6 +414,11 @@ typedef long mp_off_t; #define CIRCUITPY_BUSIO_SPI_ASYNC (0) #endif +// Normally set from CIRCUITPY_EPAPERDISPLAY_FAST in the .mk files. zephyr-cp doesn't set it. +#ifndef CIRCUITPY_EPAPERDISPLAY_FAST +#define CIRCUITPY_EPAPERDISPLAY_FAST (0) +#endif + // These CIRCUITPY_xxx values should all be defined in the *.mk files as being on or off. // So if any are not defined in *.mk, they'll throw an error here. diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index e8c339d43b8..6a8f31f68e4 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -313,9 +313,9 @@ CFLAGS += -DCIRCUITPY_QSPIBUS=$(CIRCUITPY_QSPIBUS) CIRCUITPY_EPAPERDISPLAY ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY=$(CIRCUITPY_EPAPERDISPLAY) -# Fast e-paper updates: EPaperDisplay's write_previous_ram_command, keep_awake, lag_frames -# and clean(). On only in ports with e-paper boards and room for it. -CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +# Fast e-paper updates: EPaperDisplay's write_previous_ram_command, fast, lag_frames +# and clean(). Ports and boards short on flash turn it off. +CIRCUITPY_EPAPERDISPLAY_FAST ?= $(CIRCUITPY_EPAPERDISPLAY) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY_FAST=$(CIRCUITPY_EPAPERDISPLAY_FAST) CIRCUITPY_I2CDISPLAYBUS ?= $(CIRCUITPY_DISPLAYIO) From be778a7332456c22f182e54936640f8ec25ed592 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 9 Nov 2026 19:20:45 -0800 Subject: [PATCH 19/24] epaperdisplay: merge the two fast-only header blocks Per review. Also call the display "fast" in the end_held_bus comment, matching the renamed argument. Co-Authored-By: Claude Opus 5.5 --- shared-module/epaperdisplay/EPaperDisplay.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 12b31e82adb..9accf885b76 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -86,11 +86,9 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s 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 +// The VM is over: a fast display ends whatever transaction still holds its bus, since its // stop sequence would wait for the bus forever. Other displays are left alone. Does not raise. void epaperdisplay_epaperdisplay_end_held_bus(epaperdisplay_epaperdisplay_obj_t *self); -#endif -#if CIRCUITPY_EPAPERDISPLAY_FAST // 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. From 4e258dfe453bab0e44724cfc2ea3f41ed0c54b13 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 9 Nov 2026 19:23:30 -0800 Subject: [PATCH 20/24] epaperdisplay: move lag drive state into its own struct Per review. The lag_* members of the display object become an epaperdisplay_lag_t member named lag, with the prefix dropped (self->lag_head is now self->lag.head). No behavior change: the ThinkInk firmware is the same size. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 4 +- shared-module/epaperdisplay/EPaperDisplay.c | 300 +++++++++--------- shared-module/epaperdisplay/EPaperDisplay.h | 62 ++-- 3 files changed, 186 insertions(+), 180 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index e0814fb1492..b61fa81468e 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -433,7 +433,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_obj_refresh(mp_obj_t self_in) { bool ok = common_hal_epaperdisplay_epaperdisplay_refresh(self); if (!ok) { #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->lag_frames > 0) { + if (self->lag.frames > 0) { // Interrupted is not too soon: raise what interrupted it. mp_handle_pending(true); } @@ -469,7 +469,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t 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) { + 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); diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 266eb7a8c0d..a78b6dbfb41 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -98,15 +98,15 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla false /* SH1107_addressing */, args->address_little_endian); #if CIRCUITPY_EPAPERDISPLAY_FAST - self->lag_slots = NULL; - self->lag_frames = 0; - self->lag_busy_deadline = 0; - self->lag_bus_held = false; - self->lag_state_lost = false; - self->lag_slot_overwriting = false; - self->lag_commit = LAG_COMMIT_NONE; - self->lag_holding_interrupts = false; - self->lag_held_exception = MP_OBJ_NULL; + self->lag.slots = NULL; + self->lag.frames = 0; + self->lag.busy_deadline = 0; + self->lag.bus_held = false; + self->lag.state_lost = false; + self->lag.slot_overwriting = false; + self->lag.commit = LAG_COMMIT_NONE; + self->lag.holding_interrupts = false; + self->lag.held_exception = MP_OBJ_NULL; self->clean_sequence = args->clean_sequence; self->clean_sequence_len = args->clean_sequence_len; #endif // CIRCUITPY_EPAPERDISPLAY_FAST @@ -210,14 +210,14 @@ static bool wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_FAST static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { - return self->lag_busy_deadline != 0 && supervisor_ticks_ms64() > self->lag_busy_deadline; + 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; + if (self->lag.busy_deadline != 0 || self->lag.frames == 0) { + return self->lag.busy_deadline; } return supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; } @@ -225,12 +225,12 @@ static uint64_t display_wait_deadline(epaperdisplay_epaperdisplay_obj_t *self) { // 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) { + 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); + 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; } @@ -245,22 +245,22 @@ static bool display_interrupted(epaperdisplay_epaperdisplay_obj_t *self) { } 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; + 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) { + 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; + 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; } } @@ -269,7 +269,7 @@ static void lag_release_interrupts(epaperdisplay_epaperdisplay_obj_t *self) { // 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) { + if (!self->lag.holding_interrupts) { self->bus.send(self->bus.bus, type, self->chip_select, data, length); return true; } @@ -297,7 +297,7 @@ static bool display_send_command(epaperdisplay_epaperdisplay_obj_t *self, const return true; } if (length == 0) { - self->lag_state_lost = true; + self->lag.state_lost = true; return false; } } @@ -313,19 +313,19 @@ static bool lag_set_region(epaperdisplay_epaperdisplay_obj_t *self, displayio_ar } nlr_buf_t nlr; if (nlr_push(&nlr) == 0) { - self->lag_bus_held = true; + self->lag.bus_held = true; displayio_display_bus_set_region_to_update(&self->bus, &self->core, box); - self->lag_bus_held = false; + self->lag.bus_held = false; nlr_pop(); return true; } - if (self->lag_bus_held) { + 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; + self->lag.bus_held = false; } - if (!self->lag_holding_interrupts) { + if (!self->lag.holding_interrupts) { nlr_jump(nlr.ret_val); } MP_STATE_THREAD(mp_pending_exception) = nlr.ret_val; @@ -341,7 +341,7 @@ static void display_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t d while (true) { common_hal_time_delay_ms(delay_ms - elapsed); elapsed = supervisor_ticks_ms64() - start; - if (!self->lag_holding_interrupts || elapsed >= delay_ms) { + if (!self->lag.holding_interrupts || elapsed >= delay_ms) { return; } display_interrupted(self); @@ -392,15 +392,15 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, RUN_BACKGROUND_TASKS; } #if CIRCUITPY_EPAPERDISPLAY_FAST - if (!have_bus && self->lag_frames > 0) { + if (!have_bus && self->lag.frames > 0) { return false; } - self->lag_bus_held = have_bus; + self->lag.bus_held = have_bus; #endif // CIRCUITPY_EPAPERDISPLAY_FAST const bool sent = display_send_command(self, cmd, data, data_size); displayio_display_bus_end_transaction(&self->bus); #if CIRCUITPY_EPAPERDISPLAY_FAST - self->lag_bus_held = false; + self->lag.bus_held = false; #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (!sent) { return false; @@ -416,7 +416,7 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, display_delay_ms(self, delay_length_ms); if (should_wait_for_busy) { #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->lag_frames > 0) { + if (self->lag.frames > 0) { wait_for_update_start(self); } #endif @@ -436,7 +436,7 @@ static bool 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, mp_buffer_info_t *refresh_sequence, float seconds_per_frame) { #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->lag_frames > 0) { + 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) { @@ -446,14 +446,14 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep } else { lag_raise(self, nlr.ret_val); } - if (self->lag_settle > 0 || self->lag_commit != LAG_COMMIT_NONE) { + 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; + self->lag.force_full = true; } #endif // CIRCUITPY_EPAPERDISPLAY_FAST self->start_sequence = (uint8_t *)start_sequence->buf; @@ -829,7 +829,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ if (self->core.current_group == NULL) { #if CIRCUITPY_EPAPERDISPLAY_FAST // Nothing to draw, but the drive already started is still owed. - if (self->lag_frames > 0 && self->awake) { + if (self->lag.frames > 0 && self->awake) { return lag_refresh(self, NULL); } #endif @@ -845,7 +845,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ } const displayio_area_t *current_area = epaperdisplay_epaperdisplay_get_refresh_areas(self); #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->lag_frames > 0) { + if (self->lag.frames > 0) { return lag_refresh(self, current_area); } #endif // CIRCUITPY_EPAPERDISPLAY_FAST @@ -947,7 +947,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj_t *self) { - if (self->lag_frames == 0 || (!self->awake && !self->lag_state_lost)) { + if (self->lag.frames == 0 || (!self->awake && !self->lag.state_lost)) { return; } const uint64_t deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; @@ -966,14 +966,14 @@ 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); - self->lag_bus_held = false; + self->lag.bus_held = false; } } #endif // CIRCUITPY_EPAPERDISPLAY_FAST void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->lag_frames > 0) { + if (self->lag.frames > 0) { // Only a primary display gets here; reset_displays() has released the others. The heap is // still here but the sequences won't be, and the display would otherwise persist to draw // the splash with them. Finish now and stay asleep. Its bus still holds the pins until @@ -1006,7 +1006,7 @@ void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t gc_collect_ptr((void *)self->refresh_sequence); #if CIRCUITPY_EPAPERDISPLAY_FAST gc_collect_ptr((void *)self->clean_sequence); - gc_collect_ptr(self->lag_held_exception); + gc_collect_ptr(self->lag.held_exception); #endif // CIRCUITPY_EPAPERDISPLAY_FAST } @@ -1037,48 +1037,48 @@ void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t // 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; + 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; + 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; + 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); + 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; + 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); + 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); + 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]); + 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; + port_free(self->lag.slots); + self->lag.slots = NULL; } - self->lag_frames = 0; + self->lag.frames = 0; } static void lag_render_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area, uint8_t *frame, bool white) { @@ -1088,7 +1088,7 @@ static void lag_render_area(epaperdisplay_epaperdisplay_obj_t *self, const displ } 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, + memset(frame + row * self->lag.stride + clipped.x1 / 8, self->black_bits_inverted ? 0x00 : 0xff, displayio_area_width(&clipped) / 8); } return; @@ -1119,7 +1119,7 @@ static void lag_render_area(epaperdisplay_epaperdisplay_obj_t *self, const displ } 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); + 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. @@ -1138,8 +1138,8 @@ static bool lag_send_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t comm 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) { + 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]; @@ -1153,7 +1153,7 @@ static bool lag_send_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t comm } memcpy(chunk + used, in, bytes_per_row); used += bytes_per_row; - in += self->lag_stride; + in += self->lag.stride; } return lag_send(self, DISPLAY_DATA, chunk, used) && lag_send_ram_commit(self); } @@ -1164,10 +1164,10 @@ static bool lag_write_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t com if (!lag_set_region(self, box) || !displayio_display_bus_begin_transaction(&self->bus)) { return false; } - self->lag_bus_held = true; + 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; + self->lag.bus_held = false; if (sent) { return true; } @@ -1179,28 +1179,28 @@ static bool lag_write_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t com // 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; + 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 (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; + self->lag.commit = LAG_COMMIT_ACTIVATE; } } - 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) { + 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(); + self->lag.update_start = supervisor_ticks_ms64(); } } if (ok) { - self->lag_commit = LAG_COMMIT_NONE; + self->lag.commit = LAG_COMMIT_NONE; } } if (!was_holding) { @@ -1214,11 +1214,11 @@ static bool lag_commit_step(epaperdisplay_epaperdisplay_obj_t *self) { // 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; + 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) { @@ -1236,7 +1236,7 @@ static bool lag_step(epaperdisplay_epaperdisplay_obj_t *self, const displayio_ar } } displayio_area_t write_box; - if (self->lag_force_full) { + 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); @@ -1247,51 +1247,51 @@ static bool lag_step(epaperdisplay_epaperdisplay_obj_t *self, const displayio_ar 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; + 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; + 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; + 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; + 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) { + if (!self->awake || self->lag.state_lost) { // The panel may still hold anything, including ghosts from a run that lost power. if (!lag_clean(self, 0, true) || mp_hal_is_interrupted()) { return false; } areas = &self->core.area; } - if (self->lag_commit != LAG_COMMIT_NONE) { + 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) { + if (areas == NULL && self->lag.settle == 0) { return true; } if (self->needs_start) { @@ -1314,12 +1314,12 @@ static bool lag_refresh_unguarded(epaperdisplay_epaperdisplay_obj_t *self, const // 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) { + 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; + self->lag.state_lost = true; } } @@ -1333,17 +1333,17 @@ 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 && !self->lag_last_try) { - self->lag_busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + if (self->lag.busy_deadline != 0 && !self->lag.last_try) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; } } // Give every changed pixel the rest of its drive so the panel is left DC balanced. static void lag_flush(epaperdisplay_epaperdisplay_obj_t *self) { - if (self->lag_frames == 0 || !self->awake) { + if (self->lag.frames == 0 || !self->awake) { return; } - while (self->lag_settle > 0 || self->lag_commit != LAG_COMMIT_NONE) { + while (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE) { if (!lag_wait_idle(self) || display_interrupted(self)) { return; } @@ -1351,7 +1351,7 @@ static void lag_flush(epaperdisplay_epaperdisplay_obj_t *self) { supervisor_disable_tick(); self->refreshing = false; } - if (self->lag_commit != LAG_COMMIT_NONE ? !lag_commit_step(self) : !lag_step(self, NULL, false)) { + if (self->lag.commit != LAG_COMMIT_NONE ? !lag_commit_step(self) : !lag_step(self, NULL, false)) { return; } lag_renew_deadline(self); @@ -1375,7 +1375,7 @@ static bool lag_send_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t comm } uint8_t chunk[512]; memset(chunk, white != self->black_bits_inverted ? 0xff : 0x00, sizeof(chunk)); - size_t remaining = self->lag_slot_size; + 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)) { @@ -1393,10 +1393,10 @@ static bool lag_write_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t com if (!lag_set_region(self, &box) || !displayio_display_bus_begin_transaction(&self->bus)) { return false; } - self->lag_bus_held = true; + 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; + self->lag.bus_held = false; if (sent) { return true; } @@ -1435,7 +1435,7 @@ static void lag_wipe(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames) { lag_write_solid(self, self->write_previous_ram_command, true); } if (!ok) { - self->lag_state_lost = true; + self->lag.state_lost = true; } } @@ -1451,7 +1451,7 @@ static bool lag_to_white(epaperdisplay_epaperdisplay_obj_t *self) { supervisor_disable_tick(); self->refreshing = false; } - if (self->lag_commit == LAG_COMMIT_NONE) { + if (self->lag.commit == LAG_COMMIT_NONE) { break; } if (!lag_commit_step(self)) { @@ -1465,7 +1465,7 @@ static bool lag_to_white(epaperdisplay_epaperdisplay_obj_t *self) { } lag_renew_deadline(self); lag_flush(self); - if (self->lag_settle > 0 || self->lag_commit != LAG_COMMIT_NONE || !lag_wait_idle(self)) { + if (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE || !lag_wait_idle(self)) { return false; } if (self->refreshing) { @@ -1479,16 +1479,16 @@ static bool lag_to_white(epaperdisplay_epaperdisplay_obj_t *self) { // 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) { + if (self->lag.bus_held) { displayio_display_bus_end_transaction(&self->bus); - self->lag_bus_held = false; + self->lag.bus_held = false; } self->core.refresh_in_progress = false; - self->lag_force_full = true; - if (self->lag_slot_overwriting) { + 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; + self->lag.slot_overwriting = false; + self->lag.state_lost = true; } } @@ -1499,9 +1499,9 @@ static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *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; + 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); @@ -1540,14 +1540,14 @@ static bool lag_recover(epaperdisplay_epaperdisplay_obj_t *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); + 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; + 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; } @@ -1559,14 +1559,14 @@ static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, b lag_hold_interrupts(self); self->core.full_refresh = true; bool ok; - if (self->lag_state_lost) { + if (self->lag.state_lost) { ok = lag_recover(self); } else { ok = self->awake || lag_wake(self); ok = ok && lag_to_white(self); if (ok && full) { lag_renew_deadline(self); - self->lag_force_full = true; + 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); @@ -1596,7 +1596,7 @@ bool common_hal_epaperdisplay_epaperdisplay_clean(epaperdisplay_epaperdisplay_ob // 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)) { + 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. @@ -1607,7 +1607,7 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { while (!done && !lag_deadline_passed(self) && raises < 4) { nlr_buf_t nlr; if (nlr_push(&nlr) == 0) { - if (self->lag_state_lost) { + if (self->lag.state_lost) { done = lag_recover(self); } else if (lag_to_white(self)) { lag_renew_deadline(self); @@ -1618,8 +1618,8 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { } else { lag_abandon_step(self); raises++; - if (self->lag_held_exception == MP_OBJ_NULL) { - self->lag_held_exception = nlr.ret_val; + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; } } } @@ -1631,8 +1631,8 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { // 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->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) { @@ -1644,11 +1644,11 @@ static void lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { nlr_pop(); } else { lag_abandon_step(self); - if (self->lag_held_exception == MP_OBJ_NULL) { - self->lag_held_exception = nlr.ret_val; + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; } } - self->lag_last_try = false; + self->lag.last_try = false; } } #endif // CIRCUITPY_EPAPERDISPLAY_FAST diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 9accf885b76..1377599e8ff 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -13,6 +13,39 @@ #include "shared-module/displayio/display_core.h" #include "common-hal/digitalio/DigitalInOut.h" +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Lag drive, built on FAST's write_previous_ram_command. FAST writes the last frame to the +// previous RAM, so each changed pixel is driven once. Lag writes the frame from frames +// refreshes ago, so a changed pixel is driven frames times, the same number each way. +// The ring holds those frames. Only used when frames > 0. +typedef struct { + uint8_t **slots; + uint64_t busy_deadline; // When not 0, stop waiting for BUSY or the bus after this tick. + // What interrupted while interrupts are held. A send can raise one from the heap, so it is + // marked in epaperdisplay_epaperdisplay_collect_ptrs(). + mp_obj_t held_exception; + bool holding_interrupts; + bool bus_held; // Inside a transaction, so an exception must end it. + bool last_try; // Shutdown's reset after a stuck wait: the deadline is not renewed. + bool state_lost; // What the panel received is unknown, so it needs a stored-waveform refresh. + bool slot_overwriting; // The oldest frame in the ring is being replaced. + bool commit_drive; // The owed step has pixels to drive. + uint8_t commit; // What the newest step in the ring still has to send. + displayio_area_t commit_box; + uint64_t update_start; // When the running update was sent. + // What each of the last frames + 1 steps rendered, in pixels. + displayio_area_t *boxes; + uint32_t slot_size; + uint16_t stride; + uint16_t rows; + uint8_t frames; + uint8_t head; // Slot of the newest frame. + uint8_t box_index; // Entry in boxes for this step. + uint8_t settle; // Steps owed until every plane pair matches the newest frame. + bool force_full; // Write everything on the next step. +} epaperdisplay_lag_t; +#endif + typedef struct { mp_obj_base_t base; displayio_display_core_t core; @@ -47,36 +80,9 @@ typedef struct { #endif display_chip_select_behavior_t chip_select; #if CIRCUITPY_EPAPERDISPLAY_FAST - // Lag drive, built on FAST's write_previous_ram_command. FAST writes the last frame to the - // previous RAM, so each changed pixel is driven once. Lag writes the frame from lag_frames - // refreshes ago, so a changed pixel is driven lag_frames times, the same number each way. - // The ring holds those frames. Only used when lag_frames > 0. - 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_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. - 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. + epaperdisplay_lag_t lag; #endif } epaperdisplay_epaperdisplay_obj_t; From 659ff40d35af86b360f5779d785df2a6fc26fe04 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 9 Nov 2026 19:24:15 -0800 Subject: [PATCH 21/24] epaperdisplay: rename the keep_awake member to fast It holds the fast argument, which replaced keep_awake. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 2 +- shared-bindings/epaperdisplay/EPaperDisplay.h | 4 ++-- shared-module/epaperdisplay/EPaperDisplay.c | 18 +++++++++--------- shared-module/epaperdisplay/EPaperDisplay.h | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index b61fa81468e..6bde369e786 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -340,7 +340,7 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, 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 = fast; + construct_args.fast = fast; construct_args.lag_frames = lag_frames; if (args[ARG_clean_sequence].u_obj != mp_const_none) { mp_buffer_info_t clean_bufinfo; diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.h b/shared-bindings/epaperdisplay/EPaperDisplay.h index 3cd63f63305..a85e3139640 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.h +++ b/shared-bindings/epaperdisplay/EPaperDisplay.h @@ -52,7 +52,7 @@ typedef struct { bool spectra6; bool two_byte_sequence_length; bool address_little_endian; - bool keep_awake; + bool fast; uint8_t lag_frames; const uint8_t *clean_sequence; uint32_t clean_sequence_len; @@ -95,7 +95,7 @@ typedef struct { .spectra6 = false, \ .two_byte_sequence_length = false, \ .address_little_endian = false, \ - .keep_awake = false, \ + .fast = false, \ .lag_frames = 0, \ .clean_sequence = NULL, \ .clean_sequence_len = 0 \ diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index a78b6dbfb41..d0731938cfa 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -139,7 +139,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla 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->fast = args->fast; self->awake = false; self->needs_start = false; #endif @@ -499,7 +499,7 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla } #if CIRCUITPY_EPAPERDISPLAY_FAST - if (self->keep_awake && self->awake) { + if (self->fast && self->awake) { if (!self->needs_start) { // The panel is still configured from the previous refresh. return; @@ -811,7 +811,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ supervisor_disable_tick(); self->refreshing = false; #if CIRCUITPY_EPAPERDISPLAY_FAST - if (!self->keep_awake) { + if (!self->fast) { // 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); } @@ -887,7 +887,7 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s supervisor_disable_tick(); self->refreshing = false; #if CIRCUITPY_EPAPERDISPLAY_FAST - if (!self->keep_awake) { + if (!self->fast) { // 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); } @@ -920,7 +920,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { 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) { + if (self->fast) { send_stop_sequence_guarded(self); } else { send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); @@ -930,7 +930,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { #endif } #if CIRCUITPY_EPAPERDISPLAY_FAST - else if (self->keep_awake && self->awake) { + else if (self->fast && self->awake) { send_stop_sequence_guarded(self); } self->awake = false; @@ -964,7 +964,7 @@ void epaperdisplay_epaperdisplay_prepare_release(epaperdisplay_epaperdisplay_obj #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)) { + if (self->fast && !displayio_display_bus_is_free(&self->bus)) { displayio_display_bus_end_transaction(&self->bus); self->lag.bus_held = false; } @@ -988,13 +988,13 @@ void epaperdisplay_epaperdisplay_reset(epaperdisplay_epaperdisplay_obj_t *self) 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) { + if (self->fast && self->awake && !self->refreshing) { // Nothing can catch a raise from a send. A reload may be pending. epaperdisplay_epaperdisplay_end_held_bus(self); send_stop_sequence_guarded(self); self->awake = false; } - self->keep_awake = false; + self->fast = false; #endif } diff --git a/shared-module/epaperdisplay/EPaperDisplay.h b/shared-module/epaperdisplay/EPaperDisplay.h index 1377599e8ff..4b0d97bdf0a 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.h +++ b/shared-module/epaperdisplay/EPaperDisplay.h @@ -74,7 +74,7 @@ typedef struct { bool acep; bool two_byte_sequence_length; #if CIRCUITPY_EPAPERDISPLAY_FAST - bool keep_awake; + bool fast; bool awake; bool needs_start; // update_refresh_mode() changed the start sequence. #endif From 54401e4f6893288191cd5840c5605d2d1956e20c Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 9 Nov 2026 19:35:04 -0800 Subject: [PATCH 22/24] epaperdisplay: move the lag drive into lag_drive.c Per review. The lag drive section and the helpers only it uses move verbatim from EPaperDisplay.c into lag_drive.c. The functions the two files share are declared, with what they return, in a private lag_drive.h, and get module-prefixed names: lag_refresh() becomes epaperdisplay_lag_refresh() and wait_for_busy() becomes epaperdisplay_epaperdisplay_wait_for_busy(). lag_drive.c compiles to nothing without CIRCUITPY_EPAPERDISPLAY_FAST, so zephyr-cp, which builds every file in the module, is unaffected. Co-Authored-By: Claude Opus 5.5 --- py/circuitpy_defns.mk | 1 + shared-module/epaperdisplay/EPaperDisplay.c | 833 ++------------------ shared-module/epaperdisplay/lag_drive.c | 744 +++++++++++++++++ shared-module/epaperdisplay/lag_drive.h | 60 ++ 4 files changed, 849 insertions(+), 789 deletions(-) create mode 100644 shared-module/epaperdisplay/lag_drive.c create mode 100644 shared-module/epaperdisplay/lag_drive.h diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 0eaa5d85dc3..2d5f263c81d 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -1144,6 +1144,7 @@ $(filter $(SRC_PATTERNS), \ _airlift/transport.c \ displayio/bus_core.c \ displayio/display_core.c \ + epaperdisplay/lag_drive.c \ usb/utf16le.c \ ) diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index d0731938cfa..c23c2e7ec71 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -17,10 +17,8 @@ #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/time/__init__.h" #include "shared-module/displayio/__init__.h" +#include "shared-module/epaperdisplay/lag_drive.h" #include "supervisor/shared/display.h" -#if CIRCUITPY_EPAPERDISPLAY_FAST -#include "supervisor/port_heap.h" -#endif #include "supervisor/shared/tick.h" #if CIRCUITPY_TINYUSB @@ -32,32 +30,6 @@ #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_FAST -// 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_FAST -enum { LAG_COMMIT_NONE, LAG_COMMIT_PLANE, LAG_COMMIT_ACTIVATE }; -#endif - -#if CIRCUITPY_EPAPERDISPLAY_FAST -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, uint8_t frames, bool full); -#endif // CIRCUITPY_EPAPERDISPLAY_FAST - void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, const epaperdisplay_construct_args_t *args) { uint16_t color_depth = 1; @@ -127,7 +99,7 @@ void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdispla #if CIRCUITPY_EPAPERDISPLAY_FAST if (args->lag_frames > 0) { - lag_construct(self, args->lag_frames); + epaperdisplay_lag_construct(self, args->lag_frames); } #endif // CIRCUITPY_EPAPERDISPLAY_FAST @@ -197,7 +169,7 @@ uint16_t common_hal_epaperdisplay_epaperdisplay_get_height(epaperdisplay_epaperd // 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) { +bool epaperdisplay_epaperdisplay_wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { for (uint8_t wait = 0; wait < 200; wait++) { if (common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) { return true; @@ -209,10 +181,6 @@ static bool wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self) { #endif // CIRCUITPY_EPAPERDISPLAY_FAST #if CIRCUITPY_EPAPERDISPLAY_FAST -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) { @@ -224,7 +192,7 @@ static uint64_t display_wait_deadline(epaperdisplay_epaperdisplay_obj_t *self) { // 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) { +bool epaperdisplay_epaperdisplay_interrupted(epaperdisplay_epaperdisplay_obj_t *self) { if (!self->lag.holding_interrupts) { return mp_hal_is_interrupted(); } @@ -244,56 +212,16 @@ static bool display_interrupted(epaperdisplay_epaperdisplay_obj_t *self) { 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)) { + if (epaperdisplay_lag_send(self, DISPLAY_COMMAND, command, 1) && epaperdisplay_lag_send(self, DISPLAY_DATA, data, length)) { return true; } // The send stopped, and it may have arrived. If it was an update, sending it again would // drive every pixel a second time. BUSY high says it landed. A command with no data could // be an update that BUSY hasn't shown yet, so don't risk it: the drive is now unknown. - if (wait_for_update_start(self)) { + if (epaperdisplay_epaperdisplay_wait_for_update_start(self)) { return true; } if (length == 0) { @@ -304,38 +232,8 @@ static bool display_send_command(epaperdisplay_epaperdisplay_obj_t *self, const 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) { +void epaperdisplay_epaperdisplay_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t delay_ms) { uint64_t start = supervisor_ticks_ms64(); uint32_t elapsed = 0; while (true) { @@ -344,26 +242,26 @@ static void display_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t d if (!self->lag.holding_interrupts || elapsed >= delay_ms) { return; } - display_interrupted(self); + epaperdisplay_epaperdisplay_interrupted(self); } } #else // CIRCUITPY_EPAPERDISPLAY_FAST #define display_wait_deadline(self) ((uint64_t)0) -#define display_interrupted(self) mp_hal_is_interrupted() +#define epaperdisplay_epaperdisplay_interrupted(self) mp_hal_is_interrupted() #define display_send_command(self, command, data, length) ( \ (self)->bus.send((self)->bus.bus, DISPLAY_COMMAND, (self)->chip_select, command, 1), \ (self)->bus.send((self)->bus.bus, DISPLAY_DATA, (self)->chip_select, data, length), \ true) -#define display_delay_ms(self, delay_ms) common_hal_time_delay_ms(delay_ms) +#define epaperdisplay_epaperdisplay_delay_ms(self, delay_ms) common_hal_time_delay_ms(delay_ms) #endif // CIRCUITPY_EPAPERDISPLAY_FAST -static void wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { +void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { if (self->busy.base.type == &mp_type_NoneType) { return; } const uint64_t deadline = display_wait_deadline(self); while (common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state && - !display_interrupted(self)) { + !epaperdisplay_epaperdisplay_interrupted(self)) { if (deadline != 0 && supervisor_ticks_ms64() > deadline) { return; } @@ -372,7 +270,7 @@ static void wait_for_busy(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 epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len) { uint32_t i = 0; while (i < sequence_len) { @@ -388,7 +286,7 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, 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)) { + !epaperdisplay_epaperdisplay_interrupted(self) && (deadline == 0 || supervisor_ticks_ms64() <= deadline)) { RUN_BACKGROUND_TASKS; } #if CIRCUITPY_EPAPERDISPLAY_FAST @@ -413,20 +311,20 @@ static bool send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, delay_length_ms = 500; } } - display_delay_ms(self, delay_length_ms); + epaperdisplay_epaperdisplay_delay_ms(self, delay_length_ms); if (should_wait_for_busy) { #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag.frames > 0) { - wait_for_update_start(self); + epaperdisplay_epaperdisplay_wait_for_update_start(self); } #endif - wait_for_busy(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); } i += 2 + data_size; if (self->two_byte_sequence_length) { i++; } - if (display_interrupted(self)) { + if (epaperdisplay_epaperdisplay_interrupted(self)) { return i >= sequence_len; } } @@ -440,11 +338,11 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep // 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); + epaperdisplay_lag_flush(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); nlr_pop(); } else { - lag_raise(self, nlr.ret_val); + epaperdisplay_lag_raise(self, nlr.ret_val); } if (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE) { // Still owed: keep the mode that started it. @@ -477,7 +375,7 @@ void epaperdisplay_epaperdisplay_change_refresh_mode_parameters(epaperdisplay_ep 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); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); nlr_pop(); } else { if (!displayio_display_bus_is_free(&self->bus)) { @@ -488,7 +386,7 @@ static void send_stop_sequence_guarded(epaperdisplay_epaperdisplay_obj_t *self) } #endif // CIRCUITPY_EPAPERDISPLAY_FAST -static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { +void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { if (!displayio_display_bus_is_free(&self->bus)) { // Can't acquire display bus; skip updating this display. Try next display. return; @@ -504,7 +402,7 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla // The panel is still configured from the previous refresh. return; } - wait_for_busy(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); // SSD168x controllers commit the last RAM byte when the next command arrives, so a reset // can't come straight after a RAM write. send_stop_sequence_guarded(self); @@ -515,10 +413,10 @@ static void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdispla // run start sequence self->bus.bus_reset(self->bus.bus); - display_delay_ms(self, self->start_up_time_ms); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); - if (!send_command_sequence(self, true, self->start_sequence, self->start_sequence_len) || - display_interrupted(self)) { + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len) || + epaperdisplay_epaperdisplay_interrupted(self)) { return; } #if CIRCUITPY_EPAPERDISPLAY_FAST @@ -544,7 +442,7 @@ static bool epaperdisplay_epaperdisplay_refresh_area(epaperdisplay_epaperdisplay // 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); + epaperdisplay_epaperdisplay_wait_for_update_start(self); const uint64_t deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; bool busy; while ((busy = common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) && @@ -556,7 +454,7 @@ static bool wait_for_update_end(epaperdisplay_epaperdisplay_obj_t *self) { static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { // Actually refresh the display now that all pixel RAM has been updated. - send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); const uint64_t refresh_start = supervisor_ticks_ms64(); bool previous_written = true; mp_obj_t raised = MP_OBJ_NULL; @@ -604,7 +502,7 @@ static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdispl #else static void epaperdisplay_epaperdisplay_finish_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { // Actually refresh the display now that all pixel RAM has been updated. - send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); supervisor_enable_tick(); self->refreshing = true; @@ -813,15 +711,15 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ #if CIRCUITPY_EPAPERDISPLAY_FAST if (!self->fast) { // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } #else // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); #endif } else { #if CIRCUITPY_EPAPERDISPLAY_FAST - lag_give_up_if_stuck(self); + epaperdisplay_lag_give_up_if_stuck(self); #endif return false; } @@ -830,7 +728,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ #if CIRCUITPY_EPAPERDISPLAY_FAST // Nothing to draw, but the drive already started is still owed. if (self->lag.frames > 0 && self->awake) { - return lag_refresh(self, NULL); + return epaperdisplay_lag_refresh(self, NULL); } #endif return true; @@ -846,7 +744,7 @@ bool common_hal_epaperdisplay_epaperdisplay_refresh(epaperdisplay_epaperdisplay_ const displayio_area_t *current_area = epaperdisplay_epaperdisplay_get_refresh_areas(self); #if CIRCUITPY_EPAPERDISPLAY_FAST if (self->lag.frames > 0) { - return lag_refresh(self, current_area); + return epaperdisplay_lag_refresh(self, current_area); } #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (current_area == NULL) { @@ -889,16 +787,16 @@ void epaperdisplay_epaperdisplay_background(epaperdisplay_epaperdisplay_obj_t *s #if CIRCUITPY_EPAPERDISPLAY_FAST if (!self->fast) { // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } #else // Run stop sequence but don't wait for busy because busy is set when sleeping. - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); #endif } #if CIRCUITPY_EPAPERDISPLAY_FAST else { - lag_give_up_if_stuck(self); + epaperdisplay_lag_give_up_if_stuck(self); } #endif } @@ -911,11 +809,11 @@ bool common_hal_epaperdisplay_epaperdisplay_get_busy(epaperdisplay_epaperdisplay void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { #if CIRCUITPY_EPAPERDISPLAY_FAST - lag_shutdown(self); - lag_free(self); + epaperdisplay_lag_shutdown(self); + epaperdisplay_lag_free(self); #endif // CIRCUITPY_EPAPERDISPLAY_FAST if (self->refreshing) { - wait_for_busy(self); + epaperdisplay_epaperdisplay_wait_for_busy(self); supervisor_disable_tick(); self->refreshing = false; // Run stop sequence but don't wait for busy because busy is set when sleeping. @@ -923,10 +821,10 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { if (self->fast) { send_stop_sequence_guarded(self); } else { - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); } #else - send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); + epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->stop_sequence, self->stop_sequence_len); #endif } #if CIRCUITPY_EPAPERDISPLAY_FAST @@ -936,7 +834,7 @@ void release_epaperdisplay(epaperdisplay_epaperdisplay_obj_t *self) { self->awake = false; #endif #if CIRCUITPY_EPAPERDISPLAY_FAST - lag_release_interrupts(self); + epaperdisplay_lag_release_interrupts(self); #endif // CIRCUITPY_EPAPERDISPLAY_FAST release_display_core(&self->core); @@ -1010,649 +908,6 @@ void epaperdisplay_epaperdisplay_collect_ptrs(epaperdisplay_epaperdisplay_obj_t #endif // CIRCUITPY_EPAPERDISPLAY_FAST } -#if CIRCUITPY_EPAPERDISPLAY_FAST -// Lag drive. Plane 0x26 holds frame N-lag instead of N-1. Each step drives a pixel toward black -// where N is black and N-lag is white, toward white where N is white and N-lag is black, and not -// at all where they match. -// -// That keeps the drive balanced without counting. Each frame is used twice: pixels are driven -// toward it in the step where it is N, and away from it lag steps later, where it is N-lag. So a -// pixel that ends the way it started has been driven the same amount toward black and toward -// white. For example, lag 3 and a pixel that is black for 2 steps (both planes start white): -// -// step 1 2 3 4 5 6 -// frame N W B B W W W -// frame N-3 W W W W B B -// drive - B B - W W 2 toward black, 2 toward white -// -// A change that holds for lag steps or more is driven lag steps, so a bigger lag drives harder. -// When frames stop changing, lag more steps finish the drive. Each frame is rendered once into -// the ring and the planes are sent from there. -// -// The ring has lag slots, N-1 to N-lag, and N replaces N-lag in its slot. The box to send must -// be known before that, so it comes from the areas that were rendered, not from comparing frames. -// Frames N and N-lag can only differ inside the areas of the last lag steps. Sending the areas of -// the last lag + 1 steps also covers what the previous step left unequal, so the planes are -// equal outside the box: a byte there is either outside the last box too, so equal by induction, -// or was last written with frames that have not changed there since. - -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, 0, true) || mp_hal_is_interrupted()) { - return false; - } - areas = &self->core.area; - } - if (self->lag.commit != LAG_COMMIT_NONE) { - // The areas stay marked, so the next refresh draws them. - return lag_commit_step(self); - } - if (areas == NULL && self->lag.settle == 0) { - return true; - } - if (self->needs_start) { - lag_hold_interrupts(self); - epaperdisplay_epaperdisplay_start_refresh(self); - 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.last_try) { - self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; - } -} - -// Give every changed pixel the rest of its drive so the panel is left DC balanced. -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; -} - -// 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. -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, uint8_t frames, bool full) { - if (!displayio_display_bus_is_free(&self->bus)) { - return false; - } - lag_hold_interrupts(self); - self->core.full_refresh = true; - bool ok; - if (self->lag.state_lost) { - ok = lag_recover(self); - } else { - ok = self->awake || lag_wake(self); - ok = ok && lag_to_white(self); - if (ok && full) { - lag_renew_deadline(self); - self->lag.force_full = true; - ok = send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) && lag_wait_idle(self); - if (ok) { - lag_renew_deadline(self); - ok = lag_wake(self); - lag_renew_deadline(self); - } - } - } - if (ok && frames > 0) { - lag_wipe(self, frames); - } - 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. -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; - } - // 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_FAST - size_t maybe_refresh_epaperdisplay(void) { for (uint8_t i = 0; i < CIRCUITPY_DISPLAY_LIMIT; i++) { if (displays[i].epaper_display.base.type != &epaperdisplay_epaperdisplay_type || diff --git a/shared-module/epaperdisplay/lag_drive.c b/shared-module/epaperdisplay/lag_drive.c new file mode 100644 index 00000000000..69f875edefb --- /dev/null +++ b/shared-module/epaperdisplay/lag_drive.c @@ -0,0 +1,744 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/epaperdisplay/EPaperDisplay.h" + +#if CIRCUITPY_EPAPERDISPLAY_FAST + +#include "py/gc.h" +#include "py/runtime.h" +#include "shared/runtime/interrupt_char.h" +#include "shared-bindings/digitalio/DigitalInOut.h" +#include "shared-module/epaperdisplay/lag_drive.h" +#include "supervisor/port_heap.h" +#include "supervisor/shared/tick.h" + +#include +#include + +// SSD168x only: NOP. These controllers keep the last RAM byte until the next command and lose it to +// a reset, so a RAM write ends with this. Lag drive is only for displays where it is harmless. +#define LAG_RAM_COMMIT_COMMAND 0x7f + +static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self); +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full); + +static bool lag_deadline_passed(epaperdisplay_epaperdisplay_obj_t *self) { + return self->lag.busy_deadline != 0 && supervisor_ticks_ms64() > self->lag.busy_deadline; +} + +static void lag_hold_interrupts(epaperdisplay_epaperdisplay_obj_t *self) { + self->lag.holding_interrupts = true; + if (self->lag.busy_deadline == 0) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + } + epaperdisplay_epaperdisplay_interrupted(self); +} + +void epaperdisplay_lag_release_interrupts(epaperdisplay_epaperdisplay_obj_t *self) { + if (!self->lag.holding_interrupts) { + return; + } + self->lag.holding_interrupts = false; + self->lag.busy_deadline = 0; + if (self->lag.held_exception != MP_OBJ_NULL) { + mp_sched_exception(self->lag.held_exception); + self->lag.held_exception = MP_OBJ_NULL; + } +} + +// A transfer in progress raises when something interrupts. On a port with async SPI that can be +// any transfer, however short. While holding, that returns false: the transfer stopped partway, +// so the caller goes back to where the RAM or command can start again. +bool epaperdisplay_lag_send(epaperdisplay_epaperdisplay_obj_t *self, display_byte_type_t type, + const uint8_t *data, uint32_t length) { + if (!self->lag.holding_interrupts) { + self->bus.send(self->bus.bus, type, self->chip_select, data, length); + return true; + } + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->bus.send(self->bus.bus, type, self->chip_select, data, length); + nlr_pop(); + return true; + } + MP_STATE_THREAD(mp_pending_exception) = nlr.ret_val; + epaperdisplay_epaperdisplay_interrupted(self); + return false; +} + +// set_region_to_update() has its own transaction, so a raise from it leaves the bus locked. +static bool lag_set_region(epaperdisplay_epaperdisplay_obj_t *self, displayio_area_t *box) { + for (volatile uint8_t attempt = 0; attempt < 3; attempt++) { + // It skips its commands if it can't get the bus, which would leave the old window. + if (!displayio_display_bus_is_free(&self->bus)) { + return false; + } + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->lag.bus_held = true; + displayio_display_bus_set_region_to_update(&self->bus, &self->core, box); + self->lag.bus_held = false; + nlr_pop(); + return true; + } + if (self->lag.bus_held) { + if (!displayio_display_bus_is_free(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } + self->lag.bus_held = false; + } + if (!self->lag.holding_interrupts) { + nlr_jump(nlr.ret_val); + } + MP_STATE_THREAD(mp_pending_exception) = nlr.ret_val; + epaperdisplay_epaperdisplay_interrupted(self); + } + return false; +} + +// Lag drive. Plane 0x26 holds frame N-lag instead of N-1. Each step drives a pixel toward black +// where N is black and N-lag is white, toward white where N is white and N-lag is black, and not +// at all where they match. +// +// That keeps the drive balanced without counting. Each frame is used twice: pixels are driven +// toward it in the step where it is N, and away from it lag steps later, where it is N-lag. So a +// pixel that ends the way it started has been driven the same amount toward black and toward +// white. For example, lag 3 and a pixel that is black for 2 steps (both planes start white): +// +// step 1 2 3 4 5 6 +// frame N W B B W W W +// frame N-3 W W W W B B +// drive - B B - W W 2 toward black, 2 toward white +// +// A change that holds for lag steps or more is driven lag steps, so a bigger lag drives harder. +// When frames stop changing, lag more steps finish the drive. Each frame is rendered once into +// the ring and the planes are sent from there. +// +// The ring has lag slots, N-1 to N-lag, and N replaces N-lag in its slot. The box to send must +// be known before that, so it comes from the areas that were rendered, not from comparing frames. +// Frames N and N-lag can only differ inside the areas of the last lag steps. Sending the areas of +// the last lag + 1 steps also covers what the previous step left unequal, so the planes are +// equal outside the box: a byte there is either outside the last box too, so equal by induction, +// or was last written with frames that have not changed there since. + +void epaperdisplay_lag_construct(epaperdisplay_epaperdisplay_obj_t *self, uint8_t lag_frames) { + self->lag.stride = (self->core.area.x2 + 7) / 8; + self->lag.rows = self->core.area.y2; + self->lag.slot_size = self->lag.stride * self->lag.rows; + self->lag.frames = lag_frames; + self->lag.head = 0; + self->lag.box_index = 0; + self->lag.settle = 0; + // The panel's RAM is unknown, so the first step writes everything. + self->lag.force_full = true; + size_t meta_size = lag_frames * sizeof(uint8_t *) + (lag_frames + 1) * sizeof(displayio_area_t); + self->lag.slots = port_malloc(meta_size, false); + bool ok = self->lag.slots != NULL; + if (ok) { + memset(self->lag.slots, 0, meta_size); + self->lag.boxes = (displayio_area_t *)(self->lag.slots + lag_frames); + } + for (uint8_t i = 0; ok && i < lag_frames; i++) { + self->lag.slots[i] = port_malloc(self->lag.slot_size, true); + ok = self->lag.slots[i] != NULL; + } + if (!ok) { + epaperdisplay_lag_free(self); + self->base.type = &mp_type_NoneType; + m_malloc_fail(self->lag.slot_size); + } + // History starts white, the state a cleared panel is in. + for (uint8_t i = 0; i < lag_frames; i++) { + memset(self->lag.slots[i], self->black_bits_inverted ? 0x00 : 0xff, self->lag.slot_size); + } +} + +void epaperdisplay_lag_free(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.slots != NULL) { + for (uint8_t i = 0; i < self->lag.frames; i++) { + if (self->lag.slots[i] != NULL) { + port_free(self->lag.slots[i]); + } + } + port_free(self->lag.slots); + self->lag.slots = NULL; + } + self->lag.frames = 0; +} + +static void lag_render_area(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *area, uint8_t *frame, bool white) { + displayio_area_t clipped; + if (!displayio_display_core_clip_area(&self->core, area, &clipped)) { + return; + } + if (white) { + for (int16_t row = clipped.y1; row < clipped.y2; row++) { + memset(frame + row * self->lag.stride + clipped.x1 / 8, self->black_bits_inverted ? 0x00 : 0xff, + displayio_area_width(&clipped) / 8); + } + return; + } + uint16_t width = displayio_area_width(&clipped); + uint16_t bytes_per_row = width / 8; + uint16_t rows_per_buffer = MAX(1, 128 * 32 / width); + size_t words = (rows_per_buffer * width + 31) / 32; + uint32_t buffer[words]; + uint32_t mask[words]; + + self->core.colorspace.grayscale = true; + self->core.colorspace.grayscale_bit = 7; + for (int16_t y = clipped.y1; y < clipped.y2; y += rows_per_buffer) { + displayio_area_t subrectangle = { + .x1 = clipped.x1, + .y1 = y, + .x2 = clipped.x2, + .y2 = MIN(y + rows_per_buffer, clipped.y2), + }; + memset(mask, 0, sizeof(mask)); + memset(buffer, 0, sizeof(buffer)); + displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + if (self->black_bits_inverted) { + for (size_t k = 0; k < words; k++) { + buffer[k] = ~buffer[k]; + } + } + const uint8_t *in = (const uint8_t *)buffer; + for (int16_t row = subrectangle.y1; row < subrectangle.y2; row++) { + memcpy(frame + row * self->lag.stride + clipped.x1 / 8, in, bytes_per_row); + in += bytes_per_row; + } + // Run background tasks like a regular refresh does. + RUN_BACKGROUND_TASKS; + } +} + +static bool lag_send_ram_commit(epaperdisplay_epaperdisplay_obj_t *self) { + const uint8_t command = LAG_RAM_COMMIT_COMMAND; + return epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1); +} + +// Rows are gathered so a narrow box doesn't cost one SPI transfer per row. +static bool lag_send_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, const uint8_t *frame, const displayio_area_t *box) { + if (!epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1)) { + return false; + } + uint16_t bytes_per_row = displayio_area_width(box) / 8; + const uint8_t *in = frame + box->y1 * self->lag.stride + box->x1 / 8; + if (bytes_per_row == self->lag.stride) { + return epaperdisplay_lag_send(self, DISPLAY_DATA, in, displayio_area_height(box) * bytes_per_row) && lag_send_ram_commit(self); + } + uint8_t chunk[512]; + size_t used = 0; + for (int16_t y = box->y1; y < box->y2; y++) { + if (used + bytes_per_row > sizeof(chunk)) { + if (!epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, used)) { + return false; + } + used = 0; + } + memcpy(chunk + used, in, bytes_per_row); + used += bytes_per_row; + in += self->lag.stride; + } + return epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, used) && lag_send_ram_commit(self); +} + +// A send stopped by a held interrupt leaves the RAM pointer partway, so start over from the region. +static bool lag_write_plane(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, const uint8_t *frame, displayio_area_t *box) { + for (uint8_t attempt = 0; attempt < 3; attempt++) { + if (!lag_set_region(self, box) || !displayio_display_bus_begin_transaction(&self->bus)) { + return false; + } + self->lag.bus_held = true; + const bool sent = lag_send_plane(self, command, frame, box); + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + if (sent) { + return true; + } + } + return false; +} + +// Finish a step whose frame is already in the ring: frame N to its plane, then the update. Each +// part stays owed until it has gone out, so a pixel is only counted as driven if it was. Interrupts +// wait, since a send they stop can't be told from one that arrived. False if the bus can't be had. +static bool lag_commit_step(epaperdisplay_epaperdisplay_obj_t *self) { + const bool was_holding = self->lag.holding_interrupts; + lag_hold_interrupts(self); + bool ok = true; + if (self->lag.commit == LAG_COMMIT_PLANE) { + ok = lag_write_plane(self, self->write_black_ram_command, self->lag.slots[self->lag.head], &self->lag.commit_box); + if (ok) { + self->lag.commit = LAG_COMMIT_ACTIVATE; + } + } + if (ok && self->lag.commit == LAG_COMMIT_ACTIVATE) { + // Nothing can differ when the box is empty, so an update would only take time. + if (self->lag.commit_drive) { + ok = epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len); + if (ok) { + epaperdisplay_epaperdisplay_wait_for_update_start(self); + supervisor_enable_tick(); + self->refreshing = true; + self->lag.update_start = supervisor_ticks_ms64(); + } + } + if (ok) { + self->lag.commit = LAG_COMMIT_NONE; + } + } + if (!was_holding) { + epaperdisplay_lag_release_interrupts(self); + } + return ok; +} + +// Send one step: frame N-lag to the previous plane, then frame N, rendered from areas (may be +// NULL) over its slot, or white in those areas without rendering, to the other. The history +// doesn't advance if the bus can't be had before the slot is touched, so a retry does the same +// step. Once it has, the step is owed and true is returned, whatever happens to the rest. +static bool lag_step(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas, bool white) { + uint8_t lag = self->lag.frames; + uint8_t next = (self->lag.head + 1) % lag; + uint8_t *frame = self->lag.slots[next]; + displayio_area_t *boxes = self->lag.boxes; + uint8_t index = self->lag.box_index; + bool changed = areas != NULL; + + for (const displayio_area_t *area = areas; area != NULL; area = area->next) { + displayio_area_t clipped; + if (displayio_display_core_clip_area(&self->core, area, &clipped)) { + displayio_area_union(&boxes[index], &clipped, &boxes[index]); + } + } + // The oldest entry is lag steps back: only needed to make the planes equal again. + uint8_t oldest = (index + 1) % (lag + 1); + displayio_area_t drive_box = {0}; + for (uint8_t i = 0; i <= lag; i++) { + if (i != oldest) { + displayio_area_union(&drive_box, &boxes[i], &drive_box); + } + } + displayio_area_t write_box; + if (self->lag.force_full) { + displayio_display_core_clip_area(&self->core, &self->core.area, &write_box); + } else { + displayio_area_union(&drive_box, &boxes[oldest], &write_box); + } + bool write = !displayio_area_empty(&write_box); + + if (write && !lag_write_plane(self, self->write_previous_ram_command, frame, &write_box)) { + return false; + } + // N-1 and N-lag only differ inside the box. + self->lag.slot_overwriting = true; + if (write && next != self->lag.head) { + const uint8_t *in = self->lag.slots[self->lag.head] + write_box.y1 * self->lag.stride + write_box.x1 / 8; + uint8_t *out = frame + write_box.y1 * self->lag.stride + write_box.x1 / 8; + uint16_t bytes_per_row = displayio_area_width(&write_box) / 8; + for (int16_t y = write_box.y1; y < write_box.y2; y++) { + memcpy(out, in, bytes_per_row); + in += self->lag.stride; + out += self->lag.stride; + } + } + for (const displayio_area_t *area = areas; area != NULL; area = area->next) { + lag_render_area(self, area, frame, white); + } + // The slot no longer holds N-lag, so from here the step has to finish or that drive is lost. + self->lag.slot_overwriting = false; + self->lag.head = next; + self->lag.box_index = oldest; + boxes[oldest] = (displayio_area_t) {0}; + if (changed) { + self->lag.settle = lag; + } else if (self->lag.settle > 0) { + self->lag.settle--; + } + self->lag.force_full = false; + self->lag.commit_box = write_box; + self->lag.commit_drive = !displayio_area_empty(&drive_box); + self->lag.commit = write ? LAG_COMMIT_PLANE : LAG_COMMIT_ACTIVATE; + lag_commit_step(self); + return true; +} + +static bool lag_refresh_unguarded(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { + if (!self->awake || self->lag.state_lost) { + // The panel may still hold anything, including ghosts from a run that lost power. + if (!lag_clean(self, 0, true) || mp_hal_is_interrupted()) { + return false; + } + areas = &self->core.area; + } + if (self->lag.commit != LAG_COMMIT_NONE) { + // The areas stay marked, so the next refresh draws them. + return lag_commit_step(self); + } + if (areas == NULL && self->lag.settle == 0) { + return true; + } + if (self->needs_start) { + lag_hold_interrupts(self); + epaperdisplay_epaperdisplay_start_refresh(self); + epaperdisplay_lag_release_interrupts(self); + } else { + epaperdisplay_epaperdisplay_start_refresh(self); + } + bool ok = !self->needs_start && lag_step(self, areas, false); + if (ok) { + displayio_display_core_finish_refresh(&self->core); + } else { + self->core.refresh_in_progress = false; + } + return ok; +} + +// An update takes far less than the limit, so a panel still BUSY is stuck, and only a reset can +// help. That has to wait for the next refresh or the shutdown, since the bus may be in use here. +// Until then a script polling busy can go on. +void epaperdisplay_lag_give_up_if_stuck(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames > 0 && !self->lag.holding_interrupts && + supervisor_ticks_ms64() - self->lag.update_start > EPAPER_WAIT_TIMEOUT_MS) { + supervisor_disable_tick(); + self->refreshing = false; + self->awake = false; + self->lag.state_lost = true; + } +} + +// False if the update is still running: interrupted, or the panel is stuck. RAM writes and a reset +// would land on that update, so nothing more can be sent. +static bool lag_wait_idle(epaperdisplay_epaperdisplay_obj_t *self) { + epaperdisplay_epaperdisplay_wait_for_busy(self); + return common_hal_digitalio_digitalinout_get_value(&self->busy) != self->busy_state; +} + +// The deadline bounds each piece of work, not the whole drive, which can be 32 updates and a clean. +// Only called after progress, so a bus that is never free still runs out. +static void lag_renew_deadline(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.busy_deadline != 0 && !self->lag.last_try) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + } +} + +// Give every changed pixel the rest of its drive so the panel is left DC balanced. +void epaperdisplay_lag_flush(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames == 0 || !self->awake) { + return; + } + while (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE) { + if (!lag_wait_idle(self) || epaperdisplay_epaperdisplay_interrupted(self)) { + return; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + if (self->lag.commit != LAG_COMMIT_NONE ? !lag_commit_step(self) : !lag_step(self, NULL, false)) { + return; + } + lag_renew_deadline(self); + } +} + +bool epaperdisplay_lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + bool ok = lag_refresh_unguarded(self, areas); + nlr_pop(); + return ok; + } + epaperdisplay_lag_raise(self, nlr.ret_val); +} + +// Fill a whole RAM plane with one color from a small buffer. +static bool lag_send_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, bool white) { + if (!epaperdisplay_lag_send(self, DISPLAY_COMMAND, &command, 1)) { + return false; + } + uint8_t chunk[512]; + memset(chunk, white != self->black_bits_inverted ? 0xff : 0x00, sizeof(chunk)); + size_t remaining = self->lag.slot_size; + while (remaining > 0) { + size_t count = MIN(remaining, sizeof(chunk)); + if (!epaperdisplay_lag_send(self, DISPLAY_DATA, chunk, count)) { + return false; + } + remaining -= count; + } + return lag_send_ram_commit(self); +} + +static bool lag_write_solid(epaperdisplay_epaperdisplay_obj_t *self, uint8_t command, bool white) { + displayio_area_t box; + displayio_display_core_clip_area(&self->core, &self->core.area, &box); + for (uint8_t attempt = 0; attempt < 3; attempt++) { + if (!lag_set_region(self, &box) || !displayio_display_bus_begin_transaction(&self->bus)) { + return false; + } + self->lag.bus_held = true; + const bool sent = lag_send_solid(self, command, white); + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + if (sent) { + return true; + } + } + return false; +} + +// Updates with the planes as they are. Background tasks run, so a video keeps decoding. Returns how +// many went out. +static uint8_t lag_wipe_phase(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames) { + for (uint8_t i = 0; i < frames; i++) { + lag_renew_deadline(self); + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, false, self->refresh_sequence, self->refresh_sequence_len)) { + return i; + } + epaperdisplay_epaperdisplay_wait_for_update_start(self); + if (!lag_wait_idle(self)) { + return i + 1; // A stuck panel: writes would be dropped, so nothing more can help. + } + } + return frames; +} + +// Both planes must be white. Every pixel gets frames toward black, then the same number toward white, +// and both planes are white again at the end. The caller holds interrupts, so the phases stay +// together. If that can't be done the drive is unknown, and only a stored-waveform refresh is safe. +static void lag_wipe(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames) { + bool ok = lag_write_solid(self, self->write_black_ram_command, false); + if (ok) { + const uint8_t toward_black = lag_wipe_phase(self, frames); + lag_renew_deadline(self); + ok = toward_black == frames && lag_wait_idle(self) && + lag_write_solid(self, self->write_previous_ram_command, false) && + lag_write_solid(self, self->write_black_ram_command, true) && + lag_wipe_phase(self, frames) == frames && + lag_write_solid(self, self->write_previous_ram_command, true); + } + if (!ok) { + self->lag.state_lost = true; + } +} + +// A normal step to white, then the owed steps. Everything changed so far gets all of its drive, +// and the ring and both planes end up white with nothing owed. False if interrupted first, with +// the drive still owed. +static bool lag_to_white(epaperdisplay_epaperdisplay_obj_t *self) { + while (true) { + if (!lag_wait_idle(self) || epaperdisplay_epaperdisplay_interrupted(self)) { + return false; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + if (self->lag.commit == LAG_COMMIT_NONE) { + break; + } + if (!lag_commit_step(self)) { + return false; + } + } + displayio_area_t whole = self->core.area; + whole.next = NULL; + if (!lag_step(self, &whole, true)) { + return false; + } + lag_renew_deadline(self); + epaperdisplay_lag_flush(self); + if (self->lag.settle > 0 || self->lag.commit != LAG_COMMIT_NONE || !lag_wait_idle(self)) { + return false; + } + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + return true; +} + +// An exception came out of a bus send (Ctrl-C during a long transfer raises) or a render. Without +// this the bus stays locked and chip select low, and everything after waits for it. The next +// step writes all of both planes, since this one may have stopped partway through either. +static void lag_abandon_step(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.bus_held) { + displayio_display_bus_end_transaction(&self->bus); + self->lag.bus_held = false; + } + self->core.refresh_in_progress = false; + self->lag.force_full = true; + if (self->lag.slot_overwriting) { + // The drive it held can't be sent any more. + self->lag.slot_overwriting = false; + self->lag.state_lost = true; + } +} + +static void lag_after_raise(epaperdisplay_epaperdisplay_obj_t *self) { + lag_abandon_step(self); + epaperdisplay_lag_release_interrupts(self); +} + +// Raise once: what was held, if anything, instead of both that and this. +MP_NORETURN void epaperdisplay_lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised) { + if (self->lag.held_exception != MP_OBJ_NULL) { + raised = self->lag.held_exception; + self->lag.held_exception = MP_OBJ_NULL; + } + lag_after_raise(self); + nlr_jump(raised); +} + +static bool lag_wake(epaperdisplay_epaperdisplay_obj_t *self) { + self->awake = false; + self->bus.bus_reset(self->bus.bus); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); + if (!epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len)) { + return false; + } + self->awake = true; + self->needs_start = false; + return true; +} + +// What the panel received is unknown, so paying back could drive pixels the wrong way. Forget the +// drive and let the stored waveform, which is balanced by itself, bring the panel to white. +static bool lag_recover(epaperdisplay_epaperdisplay_obj_t *self) { + // An update that may have started shouldn't be cut. A stuck panel has already been waited for. + if (self->awake) { + epaperdisplay_epaperdisplay_wait_for_busy(self); + } + if (!lag_wake(self) || !lag_wait_idle(self)) { + return false; + } + lag_renew_deadline(self); + if (!lag_write_solid(self, self->write_previous_ram_command, true) || + !lag_write_solid(self, self->write_black_ram_command, true) || + !epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) || + !lag_wait_idle(self)) { + return false; + } + lag_renew_deadline(self); + if (!lag_wake(self)) { + return false; + } + for (uint8_t i = 0; i < self->lag.frames; i++) { + memset(self->lag.slots[i], self->black_bits_inverted ? 0x00 : 0xff, self->lag.slot_size); + } + memset(self->lag.boxes, 0, (self->lag.frames + 1) * sizeof(displayio_area_t)); + self->lag.settle = 0; + self->lag.commit = LAG_COMMIT_NONE; + self->lag.force_full = false; + self->lag.state_lost = false; + return true; +} + +// A clean can't stop partway, so an interrupt raises when it ends. False if the bus can't be had. +static bool lag_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full) { + if (!displayio_display_bus_is_free(&self->bus)) { + return false; + } + lag_hold_interrupts(self); + self->core.full_refresh = true; + bool ok; + if (self->lag.state_lost) { + ok = lag_recover(self); + } else { + ok = self->awake || lag_wake(self); + ok = ok && lag_to_white(self); + if (ok && full) { + lag_renew_deadline(self); + self->lag.force_full = true; + ok = epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len) && lag_wait_idle(self); + if (ok) { + lag_renew_deadline(self); + ok = lag_wake(self); + lag_renew_deadline(self); + } + } + } + if (ok && frames > 0) { + lag_wipe(self, frames); + } + epaperdisplay_lag_release_interrupts(self); + return ok; +} + +bool common_hal_epaperdisplay_epaperdisplay_clean(epaperdisplay_epaperdisplay_obj_t *self, uint8_t frames, bool full) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + bool ok = lag_clean(self, frames, full); + nlr_pop(); + return ok; + } + epaperdisplay_lag_raise(self, nlr.ret_val); +} + +// Leave the panel white and balanced when the display goes away: release, or the end of the VM, +// where nothing can catch an exception. Interrupts stay held until release_epaperdisplay() has sent +// the stop sequence. All of it is bounded so a stuck panel can't hang shutdown. Doesn't allocate. +void epaperdisplay_lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self) { + if (self->lag.frames == 0 || (!self->awake && !self->lag.state_lost)) { + return; // Nothing was drawn. + } + lag_after_raise(self); // Anything still held from before is ours to end. + lag_hold_interrupts(self); + volatile bool done = false; + // Each raise abandons the step and the loop starts it again, up to 4 times. + volatile uint8_t raises = 0; + while (!done && !lag_deadline_passed(self) && raises < 4) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + if (self->lag.state_lost) { + done = lag_recover(self); + } else if (lag_to_white(self)) { + lag_renew_deadline(self); + done = epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->clean_sequence, self->clean_sequence_len); + } + nlr_pop(); + RUN_BACKGROUND_TASKS; // Whatever holds the bus may let it go. + } else { + lag_abandon_step(self); + raises++; + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; + } + } + } + epaperdisplay_epaperdisplay_wait_for_busy(self); + if (self->refreshing) { + supervisor_disable_tick(); + self->refreshing = false; + } + // BUSY outlasted the wait. One reset may free the panel for the stored waveform. If BUSY stays + // high after it, nothing more is sent, and the deadline isn't renewed, so this is the last wait. + if (!done && raises < 4 && common_hal_digitalio_digitalinout_get_value(&self->busy) == self->busy_state) { + self->lag.busy_deadline = supervisor_ticks_ms64() + EPAPER_WAIT_TIMEOUT_MS; + self->lag.last_try = true; + self->awake = false; + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + self->bus.bus_reset(self->bus.bus); + epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); + if (lag_wait_idle(self)) { + lag_recover(self); + } + nlr_pop(); + } else { + lag_abandon_step(self); + if (self->lag.held_exception == MP_OBJ_NULL) { + self->lag.held_exception = nlr.ret_val; + } + } + self->lag.last_try = false; + } +} + +#endif // CIRCUITPY_EPAPERDISPLAY_FAST diff --git a/shared-module/epaperdisplay/lag_drive.h b/shared-module/epaperdisplay/lag_drive.h new file mode 100644 index 00000000000..0d39d5471b2 --- /dev/null +++ b/shared-module/epaperdisplay/lag_drive.h @@ -0,0 +1,60 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/epaperdisplay/EPaperDisplay.h" + +// Shared by EPaperDisplay.c and lag_drive.c only. + +// Sends a start, stop or refresh sequence. True if every command went out. +bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, + bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len); +// Runs the start sequence. A fast display that is already awake skips it unless the start +// sequence changed. +void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self); +// Waits for BUSY to clear or an interrupt. On a lag display the wait is bounded. +void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self); + +#if CIRCUITPY_EPAPERDISPLAY_FAST +// Longer than one update or stored-waveform refresh takes. This bounds each wait for an update on +// a lag display, and the wait before writing the previous RAM. A stock display's waits have no bound. +#define EPAPER_WAIT_TIMEOUT_MS 30000 + +// What the newest lag step still has to send: nothing, its plane, or the update that shows it. +enum { LAG_COMMIT_NONE, LAG_COMMIT_PLANE, LAG_COMMIT_ACTIVATE }; + +// Delays, but while interrupts are held an interrupt doesn't cut it short. +void epaperdisplay_epaperdisplay_delay_ms(epaperdisplay_epaperdisplay_obj_t *self, uint32_t delay_ms); +// True if interrupted. While interrupts are held it saves the interrupt for the release and +// returns false. +bool epaperdisplay_epaperdisplay_interrupted(epaperdisplay_epaperdisplay_obj_t *self); +// True once BUSY rises after an update command. False if it doesn't within 2 ms. +bool epaperdisplay_epaperdisplay_wait_for_update_start(epaperdisplay_epaperdisplay_obj_t *self); + +// Allocates the frame ring outside the VM heap. Raises MemoryError if it can't. +void epaperdisplay_lag_construct(epaperdisplay_epaperdisplay_obj_t *self, uint8_t lag_frames); +// Sends the rest of the drive owed. Returns early if interrupted or the panel is stuck. +void epaperdisplay_lag_flush(epaperdisplay_epaperdisplay_obj_t *self); +// Frees the frame ring. +void epaperdisplay_lag_free(epaperdisplay_epaperdisplay_obj_t *self); +// Marks a panel BUSY for longer than EPAPER_WAIT_TIMEOUT_MS as lost, for the next refresh to reset. +void epaperdisplay_lag_give_up_if_stuck(epaperdisplay_epaperdisplay_obj_t *self); +// Abandons the step in progress, ends any hold on interrupts, then raises. An interrupt saved +// during the hold is raised instead of raised. +MP_NORETURN void epaperdisplay_lag_raise(epaperdisplay_epaperdisplay_obj_t *self, mp_obj_t raised); +// Sends one lag step for areas, or only the owed drive when areas is NULL. False if the step +// couldn't be sent: interrupted, the panel is stuck, or the start sequence changed. The areas +// stay marked and the drive stays owed. +bool epaperdisplay_lag_refresh(epaperdisplay_epaperdisplay_obj_t *self, const displayio_area_t *areas); +// Ends a hold on interrupts and schedules any interrupt saved during it. +void epaperdisplay_lag_release_interrupts(epaperdisplay_epaperdisplay_obj_t *self); +// Sends bytes on the bus. False only while interrupts are held and the transfer was interrupted. +bool epaperdisplay_lag_send(epaperdisplay_epaperdisplay_obj_t *self, display_byte_type_t type, + const uint8_t *data, uint32_t length); +// Leaves the panel white and balanced when the display goes away. Doesn't raise or allocate. +void epaperdisplay_lag_shutdown(epaperdisplay_epaperdisplay_obj_t *self); +#endif // CIRCUITPY_EPAPERDISPLAY_FAST From e37704aa62eaa057d4cfa35a959c58c337a1b439 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 10 Nov 2026 17:24:38 -0800 Subject: [PATCH 23/24] epaperdisplay: no size cost when fast is off Compile the fast keyword arguments and clean() only when CIRCUITPY_EPAPERDISPLAY_FAST is on, and keep the shared helpers static otherwise, so boards without fast build the same as before. Co-Authored-By: Claude Opus 5.5 --- shared-bindings/epaperdisplay/EPaperDisplay.c | 56 +++++++++---------- shared-module/epaperdisplay/EPaperDisplay.c | 24 ++++++-- shared-module/epaperdisplay/lag_drive.h | 2 +- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/shared-bindings/epaperdisplay/EPaperDisplay.c b/shared-bindings/epaperdisplay/EPaperDisplay.c index dd6a4be158c..ee0d624573f 100644 --- a/shared-bindings/epaperdisplay/EPaperDisplay.c +++ b/shared-bindings/epaperdisplay/EPaperDisplay.c @@ -108,7 +108,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. -//| Raises `NotImplementedError` on boards without it. +//| Not accepted on boards without fast e-paper support. //| :param int highlight_color: RGB888 of source color to highlight with third ePaper color. //| :param int highlight_color2: RGB888 of source color to highlight with fourth ePaper color. //| :param int refresh_display_command: Command used to start a display refresh. Single int or byte-packed command sequence @@ -131,7 +131,7 @@ //| before the next one. If the image stops changing, call ``refresh()`` ``lag_frames`` more times to finish //| the drive. Needs ``write_previous_ram_command``, ``busy_pin``, ``clean_sequence``, the window commands and //| a monochrome display, and allocates ``lag_frames * width * height / 8`` bytes outside the VM heap until -//| the display is released. Raises `NotImplementedError` on boards without it. +//| the display is released. Not accepted on boards without fast e-paper support. //| :param int lag_frames: How many refreshes each changed pixel is driven for, 1 to 32, when ``fast`` is true. //| More gives darker motion with more smear and uses more memory. Ignored when ``fast`` is false. //| :param ~circuitpython_typing.ReadableBuffer clean_sequence: Byte-packed commands for a full refresh, such as @@ -151,12 +151,14 @@ 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, 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_fast, ARG_lag_frames, ARG_clean_sequence }; + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_write_previous_ram_command, ARG_fast, ARG_lag_frames, ARG_clean_sequence, + #endif + }; static const mp_arg_t allowed_args[] = { { MP_QSTR_display_bus, MP_ARG_REQUIRED | MP_ARG_OBJ }, { MP_QSTR_start_sequence, MP_ARG_REQUIRED | MP_ARG_OBJ }, @@ -176,7 +178,6 @@ 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 }, @@ -191,22 +192,16 @@ static mp_obj_t epaperdisplay_epaperdisplay_make_new(const mp_obj_type_t *type, { MP_QSTR_two_byte_sequence_length, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_start_up_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NEW_SMALL_INT(0)} }, { MP_QSTR_address_little_endian, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + #if CIRCUITPY_EPAPERDISPLAY_FAST + { MP_QSTR_write_previous_ram_command, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, { MP_QSTR_fast, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, { MP_QSTR_lag_frames, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 4} }, { MP_QSTR_clean_sequence, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, + #endif }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - #if !CIRCUITPY_EPAPERDISPLAY_FAST - if (args[ARG_write_previous_ram_command].u_obj != mp_const_none) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_write_previous_ram_command); - } - if (args[ARG_fast].u_bool) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_fast); - } - #endif - mp_obj_t display_bus = args[ARG_display_bus].u_obj; mp_buffer_info_t start_bufinfo; @@ -387,15 +382,21 @@ 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. Raises `NotImplementedError` -//| on boards without it.""" +//| When it is None, the current refresh sequence is kept. Not accepted on boards +//| without fast e-paper support.""" //| static mp_obj_t epaperdisplay_epaperdisplay_update_refresh_mode(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_start_sequence, ARG_seconds_per_frame, ARG_refresh_sequence }; + enum { ARG_start_sequence, ARG_seconds_per_frame, + #if CIRCUITPY_EPAPERDISPLAY_FAST + ARG_refresh_sequence, + #endif + }; static const mp_arg_t allowed_args[] = { { MP_QSTR_start_sequence, MP_ARG_REQUIRED | MP_ARG_OBJ }, { MP_QSTR_seconds_per_frame, MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(180)} }, + #if CIRCUITPY_EPAPERDISPLAY_FAST { MP_QSTR_refresh_sequence, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + #endif }; epaperdisplay_epaperdisplay_obj_t *self = native_display(pos_args[0]); mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -406,15 +407,14 @@ 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 CIRCUITPY_EPAPERDISPLAY_FAST + mp_buffer_info_t refresh_sequence; if (args[ARG_refresh_sequence].u_obj != mp_const_none) { - #if !CIRCUITPY_EPAPERDISPLAY_FAST - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_refresh_sequence); - #endif 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); @@ -448,9 +448,10 @@ 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); +#if CIRCUITPY_EPAPERDISPLAY_FAST //| def clean(self, frames: int = 8, *, full: bool = False) -> None: -//| """Remove ghosting from a display using ``fast``, with balanced drive. Raises -//| `NotImplementedError` on boards without ``fast``. +//| """Remove ghosting from a display using ``fast``, with balanced drive. Not available +//| on boards without fast e-paper support. //| //| All pixels go to white, including the drive they are still owed. If ``full`` is True, //| the ``clean_sequence`` is sent, followed by the ``start_sequence``. Then every pixel is @@ -462,9 +463,6 @@ MP_DEFINE_CONST_FUN_OBJ_1(epaperdisplay_epaperdisplay_refresh_obj, epaperdisplay //| ... //| static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - #if !CIRCUITPY_EPAPERDISPLAY_FAST - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_clean); - #else enum { ARG_frames, ARG_full }; static const mp_arg_t allowed_args[] = { { MP_QSTR_frames, MP_ARG_INT, {.u_int = 8} }, @@ -483,9 +481,9 @@ static mp_obj_t epaperdisplay_epaperdisplay_clean(size_t n_args, const mp_obj_t mp_raise_RuntimeError(MP_ERROR_TEXT("Refresh too soon")); } return mp_const_none; - #endif } MP_DEFINE_CONST_FUN_OBJ_KW(epaperdisplay_epaperdisplay_clean_obj, 1, epaperdisplay_epaperdisplay_clean); +#endif // CIRCUITPY_EPAPERDISPLAY_FAST //| time_to_refresh: float //| """Time, in fractional seconds, until the ePaper display can be refreshed.""" @@ -602,7 +600,9 @@ static const mp_rom_map_elem_t epaperdisplay_epaperdisplay_locals_dict_table[] = { MP_ROM_QSTR(MP_QSTR_show), MP_ROM_PTR(&epaperdisplay_epaperdisplay_show_obj) }, { MP_ROM_QSTR(MP_QSTR_update_refresh_mode), MP_ROM_PTR(&epaperdisplay_epaperdisplay_update_refresh_mode_obj) }, { MP_ROM_QSTR(MP_QSTR_refresh), MP_ROM_PTR(&epaperdisplay_epaperdisplay_refresh_obj) }, + #if CIRCUITPY_EPAPERDISPLAY_FAST { MP_ROM_QSTR(MP_QSTR_clean), MP_ROM_PTR(&epaperdisplay_epaperdisplay_clean_obj) }, + #endif { MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&epaperdisplay_epaperdisplay_width_obj) }, { MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&epaperdisplay_epaperdisplay_height_obj) }, diff --git a/shared-module/epaperdisplay/EPaperDisplay.c b/shared-module/epaperdisplay/EPaperDisplay.c index 56c9c7b3995..0a0e40bd0a4 100644 --- a/shared-module/epaperdisplay/EPaperDisplay.c +++ b/shared-module/epaperdisplay/EPaperDisplay.c @@ -30,6 +30,13 @@ #define DELAY 0x80 +// lag_drive.c calls these only when it is built. Otherwise they stay static, as before it. +#if CIRCUITPY_EPAPERDISPLAY_FAST +#define LAG_SHARED +#else +#define LAG_SHARED static +#endif + void common_hal_epaperdisplay_epaperdisplay_construct(epaperdisplay_epaperdisplay_obj_t *self, const epaperdisplay_construct_args_t *args) { uint16_t color_depth = 1; @@ -255,7 +262,7 @@ void epaperdisplay_epaperdisplay_delay_ms(epaperdisplay_epaperdisplay_obj_t *sel #define epaperdisplay_epaperdisplay_delay_ms(self, delay_ms) common_hal_time_delay_ms(delay_ms) #endif // CIRCUITPY_EPAPERDISPLAY_FAST -void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { +LAG_SHARED void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self) { if (self->busy.base.type == &mp_type_NoneType) { return; } @@ -270,7 +277,7 @@ void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t } // True if every command went out. Lag drive counts an update only if its last command did. -bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, +LAG_SHARED bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len) { uint32_t i = 0; while (i < sequence_len) { @@ -320,13 +327,20 @@ bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdispl #endif epaperdisplay_epaperdisplay_wait_for_busy(self); } + #if !CIRCUITPY_EPAPERDISPLAY_FAST + if (mp_hal_is_interrupted()) { + return true; + } + #endif i += 2 + data_size; if (self->two_byte_sequence_length) { i++; } + #if CIRCUITPY_EPAPERDISPLAY_FAST if (epaperdisplay_epaperdisplay_interrupted(self)) { return i >= sequence_len; } + #endif } return true; } @@ -386,7 +400,7 @@ static void send_stop_sequence_guarded(epaperdisplay_epaperdisplay_obj_t *self) } #endif // CIRCUITPY_EPAPERDISPLAY_FAST -void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { +LAG_SHARED void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t *self) { if (!displayio_display_bus_is_free(&self->bus)) { // Can't acquire display bus; skip updating this display. Try next display. return; @@ -415,13 +429,15 @@ void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t epaperdisplay_epaperdisplay_delay_ms(self, self->start_up_time_ms); + #if CIRCUITPY_EPAPERDISPLAY_FAST if (!epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len) || epaperdisplay_epaperdisplay_interrupted(self)) { return; } - #if CIRCUITPY_EPAPERDISPLAY_FAST self->awake = true; self->needs_start = false; + #else + epaperdisplay_epaperdisplay_send_command_sequence(self, true, self->start_sequence, self->start_sequence_len); #endif } diff --git a/shared-module/epaperdisplay/lag_drive.h b/shared-module/epaperdisplay/lag_drive.h index 0d39d5471b2..f3062593dce 100644 --- a/shared-module/epaperdisplay/lag_drive.h +++ b/shared-module/epaperdisplay/lag_drive.h @@ -10,6 +10,7 @@ // Shared by EPaperDisplay.c and lag_drive.c only. +#if CIRCUITPY_EPAPERDISPLAY_FAST // Sends a start, stop or refresh sequence. True if every command went out. bool epaperdisplay_epaperdisplay_send_command_sequence(epaperdisplay_epaperdisplay_obj_t *self, bool should_wait_for_busy, const uint8_t *sequence, uint32_t sequence_len); @@ -19,7 +20,6 @@ void epaperdisplay_epaperdisplay_start_refresh(epaperdisplay_epaperdisplay_obj_t // Waits for BUSY to clear or an interrupt. On a lag display the wait is bounded. void epaperdisplay_epaperdisplay_wait_for_busy(epaperdisplay_epaperdisplay_obj_t *self); -#if CIRCUITPY_EPAPERDISPLAY_FAST // Longer than one update or stored-waveform refresh takes. This bounds each wait for an update on // a lag display, and the wait before writing the previous RAM. A stock display's waits have no bound. #define EPAPER_WAIT_TIMEOUT_MS 30000 From 8766b30facbfa3c0cfe72a5ca1ac105859f254c7 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 10 Nov 2026 18:08:59 -0800 Subject: [PATCH 24/24] Turn off fast e-paper on boards short on flash or RAM Off for ESP32-C3, C5 and C6, STM32F401 and F411, the WIZnet Picos and TG-Watch, which would have under 64 KB of flash free with it on. Also off for atmegazero_esp32s2 and microdev_micro_s2, which run out of internal RAM. Co-Authored-By: Claude Opus 5.5 --- ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk | 3 +++ ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk | 3 +++ ports/espressif/mpconfigport.mk | 5 +++++ ports/nordic/boards/TG-Watch/mpconfigboard.mk | 2 ++ .../boards/wiznet_w5100s_evb_pico/mpconfigboard.mk | 1 + .../boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk | 1 + .../boards/wiznet_w5500_evb_pico/mpconfigboard.mk | 1 + .../boards/wiznet_w5500_evb_pico2/mpconfigboard.mk | 1 + .../boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk | 1 + ports/stm/mpconfigport.mk | 5 +++++ 10 files changed, 23 insertions(+) diff --git a/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk b/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk index d0628afa6b6..7fc08c903e8 100644 --- a/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk +++ b/ports/espressif/boards/atmegazero_esp32s2/mpconfigboard.mk @@ -12,3 +12,6 @@ CIRCUITPY_ESP_FLASH_SIZE = 16MB CIRCUITPY_ESP_PSRAM_SIZE = 8MB CIRCUITPY_ESP_PSRAM_MODE = qio CIRCUITPY_ESP_PSRAM_FREQ = 80m + +# Fast e-paper doesn't fit in internal RAM. +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk b/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk index c090c69aac7..fe6d11aa579 100644 --- a/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk +++ b/ports/espressif/boards/microdev_micro_s2/mpconfigboard.mk @@ -12,3 +12,6 @@ CIRCUITPY_ESP_FLASH_FREQ = 80m CIRCUITPY_ESP_PSRAM_SIZE = 8MB CIRCUITPY_ESP_PSRAM_MODE = qio CIRCUITPY_ESP_PSRAM_FREQ = 80m + +# Fast e-paper doesn't fit in internal RAM. +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index a6db0fadf08..928010add07 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -34,6 +34,11 @@ IDF_TARGET_ARCH = xtensa CROSS_COMPILE = xtensa-$(IDF_TARGET)-elf- endif +# Fast e-paper doesn't fit in flash on the C3, C5 and C6. +ifneq ($(filter $(IDF_TARGET),esp32c3 esp32c5 esp32c6),) +CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif + # Use "internal flash" for CIRCUITPY drive. # Even thought it's an external flash chip, it appears as internal. INTERNAL_FLASH_FILESYSTEM = 1 diff --git a/ports/nordic/boards/TG-Watch/mpconfigboard.mk b/ports/nordic/boards/TG-Watch/mpconfigboard.mk index 212ab7edc20..011890898ac 100644 --- a/ports/nordic/boards/TG-Watch/mpconfigboard.mk +++ b/ports/nordic/boards/TG-Watch/mpconfigboard.mk @@ -20,3 +20,5 @@ FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_LC709203F FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_DRV2605 FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BLE_Apple_Notification_Center + +CIRCUITPY_EPAPERDISPLAY_FAST = 0 diff --git a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk index 3ab04a09833..95d228a8642 100644 --- a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_FLOPPYIO = 0 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 # The default is -O3. Change to -O2 because the build was overflowing. diff --git a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk index d47571e7950..c2e26d9caaf 100644 --- a/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5100s_evb_pico2/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q32JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 # GPIO12-19 needed for picodvi, but GPIO16 is used for Wiznet. CIRCUITPY_PICODVI = 0 diff --git a/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk index 4a8343963bc..96f36b4a0c2 100644 --- a/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5500_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_FLOPPYIO = 0 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 # The default is -O3. Change to -O2 because the build was overflowing. diff --git a/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk index 68b621fdf97..9aa01792824 100644 --- a/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w5500_evb_pico2/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q32JVxQ" CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 # GPIO12-19 needed for picodvi, but GPIO16 is used for Wiznet. CIRCUITPY_PICODVI = 0 diff --git a/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk b/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk index d43195c0f2a..edd434fe119 100755 --- a/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk +++ b/ports/raspberrypi/boards/wiznet_w55rp20_evb_pico/mpconfigboard.mk @@ -11,6 +11,7 @@ EXTERNAL_FLASH_DEVICES = "PY25Q16HB" CIRCUITPY_WIZNET = 1 CIRCUITPY__EVE = 1 CIRCUITPY_SSL = 1 +CIRCUITPY_EPAPERDISPLAY_FAST = 0 CIRCUITPY_USB_HOST = 0 OPTIMIZATION_FLAGS = -O2 diff --git a/ports/stm/mpconfigport.mk b/ports/stm/mpconfigport.mk index aa5038964c5..5d73bc9ee3e 100644 --- a/ports/stm/mpconfigport.mk +++ b/ports/stm/mpconfigport.mk @@ -19,6 +19,11 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) OPTIMIZATION_FLAGS ?= -Os endif +# Fast e-paper doesn't fit in flash on the F401 and F411. +ifeq ($(MCU_VARIANT),$(filter $(MCU_VARIANT),STM32F401xE STM32F411xE)) + CIRCUITPY_EPAPERDISPLAY_FAST ?= 0 +endif + ifeq ($(MCU_VARIANT),STM32F407xx) UF2_FAMILY_ID ?= 0x6d0922fa endif