Skip to content

Commit e1ad929

Browse files
committed
displayio: send one area buffer while composing the next
A display bus can now have send_async next to send, and flush waits for it. FourWire provides both when the SPI port has an async write. BusDisplay then uses two area buffers: it composes the next one while the previous one is sent. Background tasks still run only while the bus is released, so another device on the same SPI bus is not locked out. Full-screen repaint: PicoPad (RP2040) 76.7 -> 57.2 ms, PyBadge (SAMD51) 26.5 -> 15.5 ms, Feather ESP32-S3 TFT 29.9 -> 19.6 ms. Ports without the async write build the same code as before.
1 parent 838cbd7 commit e1ad929

5 files changed

Lines changed: 91 additions & 12 deletions

File tree

‎shared-bindings/fourwire/FourWire.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ void common_hal_fourwire_fourwire_send(mp_obj_t self, display_byte_type_t byte_t
3232

3333
void common_hal_fourwire_fourwire_end_transaction(mp_obj_t self);
3434

35+
#if CIRCUITPY_BUSIO_SPI_ASYNC
36+
void common_hal_fourwire_fourwire_send_async(mp_obj_t self, display_byte_type_t byte_type,
37+
display_chip_select_behavior_t chip_select, const uint8_t *data, uint32_t data_length);
38+
void common_hal_fourwire_fourwire_flush(mp_obj_t self);
39+
#endif
40+
3541
// The FourWire object always lives off the MP heap. So, code must collect any pointers
3642
// back to the MP heap manually. Otherwise they'll get freed.
3743
void common_hal_fourwire_fourwire_collect_ptrs(mp_obj_t obj);

‎shared-module/busdisplay/BusDisplay.c‎

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,30 @@ static void _send_pixels(busdisplay_busdisplay_obj_t *self, uint8_t *pixels, uin
224224
self->bus.send(self->bus.bus, DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels, length);
225225
}
226226

227+
// Whether the bus sends in the background. Always false without async SPI, so that code is
228+
// left out. Not a local variable: GCC would then compile the refresh loop twice.
229+
#define SEND_ASYNC(self) (CIRCUITPY_BUSIO_SPI_ASYNC && (self)->bus.send_async != NULL)
230+
231+
// The data half of _send_pixels, handed to the bus without waiting for it to be sent.
232+
static void _send_pixels_async(busdisplay_busdisplay_obj_t *self, uint8_t *pixels, uint32_t length) {
233+
if (!self->bus.data_as_commands) {
234+
self->bus.send(self->bus.bus, DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, &self->write_ram_command, 1);
235+
}
236+
self->bus.send_async(self->bus.bus, DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels, length);
237+
}
238+
239+
// Call only with the bus transaction closed: a background task may use the same bus.
240+
static void _run_background_tasks(void) {
241+
// Run background tasks so they can run during an explicit refresh.
242+
// Auto-refresh won't run background tasks here because it is a background task itself.
243+
RUN_BACKGROUND_TASKS;
244+
245+
// Run USB background tasks so they can run during an implicit refresh.
246+
#if CIRCUITPY_TINYUSB
247+
usb_background();
248+
#endif
249+
}
250+
227251
static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_area_t *area) {
228252
uint16_t buffer_size = CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE / sizeof(uint32_t); // In uint32_ts
229253

@@ -272,8 +296,11 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are
272296
// Allocated and shared as a uint32_t array so the compiler knows the
273297
// alignment everywhere.
274298
uint32_t mask_length = (pixels_per_buffer / 32) + 1;
275-
uint32_t buffer[buffer_size];
299+
// With an asynchronous bus, one buffer is composited while the other is being sent.
300+
uint32_t buffers[SEND_ASYNC(self) ? 2 : 1][buffer_size];
276301
uint32_t mask[mask_length];
302+
uint8_t fill_index = 0;
303+
bool sending = false;
277304

278305
displayio_area_t subrectangle = clipped;
279306
subrectangle.next = NULL;
@@ -294,33 +321,45 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are
294321
subrectangle_size_bytes = displayio_area_size(&subrectangle) / (8 / self->core.colorspace.depth);
295322
}
296323

324+
uint32_t *buffer = buffers[fill_index];
297325
memset(mask, 0, mask_length * sizeof(mask[0]));
298326
memset(buffer, 0, buffer_size * sizeof(buffer[0]));
299327

300328
displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer);
301329

330+
// The previous strip must be sent before the region commands for this one.
331+
if (sending) {
332+
displayio_display_bus_flush(&self->bus);
333+
displayio_display_bus_end_transaction(&self->bus);
334+
sending = false;
335+
_run_background_tasks();
336+
}
337+
302338
displayio_display_bus_set_region_to_update(&self->bus, &self->core, &subrectangle);
303339

304340
// Can't acquire display bus; skip the rest of the data.
305341
if (!displayio_display_bus_begin_transaction(&self->bus)) {
306342
return false;
307343
}
308-
_send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes);
309-
displayio_display_bus_end_transaction(&self->bus);
344+
if (SEND_ASYNC(self)) {
345+
_send_pixels_async(self, (uint8_t *)buffer, subrectangle_size_bytes);
346+
sending = true;
347+
fill_index ^= 1;
348+
} else {
349+
_send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes);
350+
displayio_display_bus_end_transaction(&self->bus);
351+
}
310352
subrectangle.y1 = subrectangle.y2;
311-
312-
// Run background tasks so they can run during an explicit refresh.
313-
// Auto-refresh won't run background tasks here because it is a background task itself.
314-
RUN_BACKGROUND_TASKS;
315-
316-
// Run USB background tasks so they can run during an implicit refresh.
317-
#if CIRCUITPY_TINYUSB
318-
usb_background();
319-
#endif
353+
if (!sending) {
354+
_run_background_tasks();
355+
}
320356
}
321357

322358
// Drain any remaining asynchronous transfers.
323359
displayio_display_bus_flush(&self->bus);
360+
if (sending) {
361+
displayio_display_bus_end_transaction(&self->bus);
362+
}
324363

325364
return true;
326365
}

‎shared-module/displayio/bus_core.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ void displayio_display_bus_construct(displayio_display_bus_t *self,
5353
self->address_little_endian = address_little_endian;
5454

5555
self->flush = NULL;
56+
#if CIRCUITPY_BUSIO_SPI_ASYNC
57+
self->send_async = NULL;
58+
#endif
5659

5760
#if CIRCUITPY_PARALLELDISPLAYBUS
5861
if (mp_obj_is_type(bus, &paralleldisplaybus_parallelbus_type)) {
@@ -72,6 +75,10 @@ void displayio_display_bus_construct(displayio_display_bus_t *self,
7275
self->send = common_hal_fourwire_fourwire_send;
7376
self->end_transaction = common_hal_fourwire_fourwire_end_transaction;
7477
self->collect_ptrs = common_hal_fourwire_fourwire_collect_ptrs;
78+
#if CIRCUITPY_BUSIO_SPI_ASYNC
79+
self->send_async = common_hal_fourwire_fourwire_send_async;
80+
self->flush = common_hal_fourwire_fourwire_flush;
81+
#endif
7582
} else
7683
#endif
7784
#if CIRCUITPY_I2CDISPLAYBUS

‎shared-module/displayio/bus_core.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ typedef struct {
2020
display_bus_bus_free bus_free;
2121
display_bus_begin_transaction begin_transaction;
2222
display_bus_send send;
23+
// Optional. Like send, but may return before the data is sent: the data must not change and
24+
// the bus must not be used until flush returns. NULL when the bus has no such mode; only set
25+
// and read when CIRCUITPY_BUSIO_SPI_ASYNC.
26+
display_bus_send send_async;
2327
display_bus_end_transaction end_transaction;
2428
display_bus_flush flush;
2529
display_bus_collect_ptrs collect_ptrs;

‎shared-module/fourwire/FourWire.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,29 @@ void common_hal_fourwire_fourwire_send(mp_obj_t obj, display_byte_type_t data_ty
186186
}
187187
}
188188

189+
#if CIRCUITPY_BUSIO_SPI_ASYNC
190+
void common_hal_fourwire_fourwire_send_async(mp_obj_t obj, display_byte_type_t data_type,
191+
display_chip_select_behavior_t chip_select, const uint8_t *data, uint32_t data_length) {
192+
fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj);
193+
// Without a DC pin every byte carries a DC bit, and the toggling mode pulses CS per byte:
194+
// both are sent byte by byte, so they stay synchronous.
195+
if (self->command == mp_const_none || chip_select == CHIP_SELECT_TOGGLE_EVERY_BYTE) {
196+
common_hal_fourwire_fourwire_send(obj, data_type, chip_select, data, data_length);
197+
return;
198+
}
199+
if (data_length == 0) {
200+
return;
201+
}
202+
digitalinout_protocol_set_value(self->command, data_type == DISPLAY_DATA);
203+
common_hal_busio_spi_write_async(self->bus, data, data_length);
204+
}
205+
206+
void common_hal_fourwire_fourwire_flush(mp_obj_t obj) {
207+
fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj);
208+
common_hal_busio_spi_wait(self->bus);
209+
}
210+
#endif
211+
189212
void common_hal_fourwire_fourwire_end_transaction(mp_obj_t obj) {
190213
fourwire_fourwire_obj_t *self = MP_OBJ_TO_PTR(obj);
191214
if (self->chip_select != mp_const_none) {

0 commit comments

Comments
 (0)