diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 637d9f7d..22638577 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -43,7 +43,12 @@ static void shotCopy(const lv_area_t* a, const uint8_t* px) { const int32_t W = lv_display_get_horizontal_resolution(NULL); const int32_t w = a->x2 - a->x1 + 1; const uint16_t* src = (const uint16_t*)px; - for (int32_t y = a->y1; y <= a->y2; y++, src += w) memcpy(s_shot + y * W + a->x1, src, w * 2); + for (int32_t y = a->y1; y <= a->y2; y++, src += w) { + memcpy(s_shot + y * W + a->x1, src, w * 2); +#ifdef UI_FLUSH_DMA // drawn big-endian for the panel + lv_draw_sw_rgb565_swap(s_shot + y * W + a->x1, w); +#endif + } } // Saved WiFi networks (Settings > WiFi), most recently saved first. Kept in RAM @@ -99,7 +104,16 @@ static const int WIFI_SCAN_MAX = 20; static lgfx::LGFX_Device* s_gfx = nullptr; static bool s_swallow = false; // ignore the touch that woke the display until it lifts -static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb +static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb (and waiting for it) +// -D UI_FLUSH_DMA: the panel fed by DMA in the background from buffers in +// internal RAM, everything drawn in its byte order. +#ifdef UI_FLUSH_DMA + #define FLUSH_DMA 1 +#else + #define FLUSH_DMA 0 +#endif +static bool s_dma = false; // the buffers are DMA-able: sent in the background +static bool s_flushing = false; // a DMA transfer under way // The CPU clock down to 80 MHz (LoRa, BLE and WiFi all run at that) with the // screen off (powerSave()), and with it on while nobody touches it and nothing @@ -121,16 +135,44 @@ static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) shotCopy(area, px_map); int w = area->x2 - area->x1 + 1; int h = area->y2 - area->y1 + 1; +#if FLUSH_DMA + if (s_dma) { + // Sent by DMA while LVGL draws the next area into the other buffer (drawn + // in the panel's byte order, RGB565_SWAPPED). flushWait() (below) ends it. + s_gfx->startWrite(); + display.panel().flushStart(area->x1, area->y1, w, h, px_map, (uint32_t)w * h * 2); + s_flushing = true; + return; // (UI_PERF_TEST: the wait is timed instead) + } +#endif s_gfx->startWrite(); s_gfx->setAddrWindow(area->x1, area->y1, w, h); - s_gfx->pushPixels((uint16_t*)px_map, (uint32_t)w * h, true /* LVGL RGB565 is little-endian */); + s_gfx->pushPixels((uint16_t*)px_map, (uint32_t)w * h, !FLUSH_DMA /* LVGL RGB565 is little-endian; _SWAPPED as sent */); s_gfx->endWrite(); + lv_display_flush_ready(disp); #ifdef UI_PERF_TEST s_flush_us += micros() - t; #endif - lv_display_flush_ready(disp); } +#if FLUSH_DMA +// The area sent last, finished: before LVGL draws into its buffer again, and +// at the end of every frame -- nothing else on the bus meets a transfer. +static void flushWait(lv_display_t* disp) { + (void)disp; + if (!s_flushing) return; +#ifdef UI_PERF_TEST + uint32_t t = micros(); +#endif + display.panel().flushWait(); + s_gfx->endWrite(); + s_flushing = false; +#ifdef UI_PERF_TEST + s_flush_us += micros() - t; +#endif +} +#endif + static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) { (void)indev; lgfx::touch_point_t tp; @@ -158,16 +200,45 @@ static bool begin() { lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height()); lv_display_set_flush_cb(disp, flushCb); - // Two half-screen buffers (75 KB each) in PSRAM: a frame renders in two - // passes instead of six (every pass walks the whole tree and lays text out - // again) -- measured ~8% faster than 40 lines; internal RAM / DMA / -O2 - // made no difference (UI_PERF_TEST). - const uint32_t buf_sz = (uint32_t)s_gfx->width() * 120 * 2; - uint8_t* buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM); - uint8_t* buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM); - if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; } + // Sent by DMA (FLUSH_DMA): two 24-line buffers (15 KB each) in internal + // RAM, ten passes a frame, each drawn while the last goes out -- 7-11% + // faster screens than 48 lines in PSRAM, the map the same. Not PSRAM: + // LovyanGFX cuts a transfer into 4092-byte DMA descriptors, and from the + // second one on PSRAM isn't 16-byte aligned as the S3's DMA needs it + // there (bands shifted, or of stale pixels). + // Otherwise two half-screen buffers (75 KB each) in PSRAM: two passes + // instead of six (every pass walks the whole tree and lays text out again) + // -- measured ~8% faster than 40 lines; internal RAM / -O2 made no + // difference (UI_PERF_TEST). + uint32_t buf_sz = 0; + uint8_t *buf1 = nullptr, *buf2 = nullptr; +#if FLUSH_DMA + buf_sz = (uint32_t)s_gfx->width() * 24 * 2; + buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + buf2 = buf1 ? (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA) : nullptr; + s_dma = buf2 != nullptr; + if (!s_dma && buf1) { heap_caps_free(buf1); buf1 = nullptr; } // no room: sent as it's drawn, as below +#endif + if (!s_dma) { + buf_sz = (uint32_t)s_gfx->width() * 120 * 2; + buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM); + buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM); + if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; } + } if (!buf1) return false; lv_display_set_buffers(disp, buf1, buf2, buf_sz, LV_DISPLAY_RENDER_MODE_PARTIAL); +#if FLUSH_DMA + // Drawn big-endian, as the panel takes it: swapping a frame's bytes + // before sending cost ~6 ms. Pictures are made so too (map tiles, the + // frozen screen): an RGB565 one is swapped pixel by pixel as it's drawn. + lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565_SWAPPED); + if (s_dma) lv_display_set_flush_wait_cb(disp, flushWait); + lv_display_add_event_cb(disp, [](lv_event_t* e) { // the frame's last area, done before anything else runs + if (!s_flushing) return; + flushWait(nullptr); + lv_display_flush_ready((lv_display_t*)lv_event_get_current_target(e)); + }, LV_EVENT_REFR_READY, NULL); +#endif lv_indev_t* indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index b2a0c72c..2ce5b14f 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -322,16 +322,26 @@ namespace freeze { s_retry = nullptr; take((lv_obj_t*)lv_timer_get_user_data(t)); } + // Darkened into the display's own byte order (UI_FLUSH_DMA: big-endian; + // lv_snapshot can't make that one itself): drawn then as a plain copy. +#ifdef UI_FLUSH_DMA + static inline uint16_t out565(uint16_t c) { return __builtin_bswap16(c); } +#else + static inline uint16_t out565(uint16_t c) { return c; } +#endif static void darken(lv_draw_buf_t* buf) { // as the dim layer over it would const uint32_t keep = 256 - s_dim; // what's left under the black for (uint32_t y = 0; y < buf->header.h; y++) { uint16_t* p = (uint16_t*)(buf->data + y * buf->header.stride); for (uint32_t x = 0; x < buf->header.w; x++) { uint32_t c = p[x]; - p[x] = (uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) | - (((c & 0x1F) * keep) >> 8)); + p[x] = out565((uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) | + (((c & 0x1F) * keep) >> 8))); } } +#ifdef UI_FLUSH_DMA + buf->header.cf = LV_COLOR_FORMAT_RGB565_SWAPPED; +#endif } static lv_draw_buf_t* snap(lv_obj_t* overlay, lv_obj_t* scr, bool top) { if (!top) lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN); diff --git a/examples/companion_radio/ui-lvgl/map/TileCache.h b/examples/companion_radio/ui-lvgl/map/TileCache.h index d9bcd5d8..f558242e 100644 --- a/examples/companion_radio/ui-lvgl/map/TileCache.h +++ b/examples/companion_radio/ui-lvgl/map/TileCache.h @@ -90,7 +90,11 @@ public: s->present = true; memset(&s->dsc, 0, sizeof(s->dsc)); s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC; +#ifdef UI_FLUSH_DMA + s->dsc.header.cf = LV_COLOR_FORMAT_RGB565_SWAPPED; +#else s->dsc.header.cf = LV_COLOR_FORMAT_RGB565; +#endif s->dsc.header.w = TILE_PX; s->dsc.header.h = TILE_PX; s->dsc.header.stride = TILE_PX * 2; @@ -151,6 +155,9 @@ private: uint32_t t0 = micros(); #endif _job.present = _spare && _job.src->renderTile(_job.z, _job.x, _job.y, _spare); +#ifdef UI_FLUSH_DMA // in the display's byte order: drawn as a plain copy (LvglPort.h) + if (_job.present) lv_draw_sw_rgb565_swap(_spare, TILE_PX * TILE_PX); +#endif #ifdef UI_PERF_TEST uint32_t dt = micros() - t0; if (_job.present) { s_perf_tile_us += dt; s_perf_tile_n++; if (dt > s_perf_tile_max) s_perf_tile_max = dt; } diff --git a/solo/wio-tracker-l2/platformio.ini b/solo/wio-tracker-l2/platformio.ini index d3088869..510771b1 100644 --- a/solo/wio-tracker-l2/platformio.ini +++ b/solo/wio-tracker-l2/platformio.ini @@ -83,6 +83,7 @@ build_flags = -D GPS_SKYVIEW ; satellites / signal / DOPs for the GPS screen (helpers/sensors/GpsSky.h) -D ESP32_LITTLEFS ; internal flash as LittleFS, not SPIFFS (helpers/esp32/InternalFS.h) -D ENABLE_SCREENSHOT ; the screen over USB for the site's tools (RGB565, UITask::captureFrame) + -DUI_FLUSH_DMA ; the panel fed by DMA while LVGL draws, in its byte order (ui-lvgl/LvglPort.h) build_src_filter = ${Wio_Tracker_L2.build_src_filter} + + @@ -111,6 +112,7 @@ lib_deps = [env:Wio_Tracker_L2_companion_solo_lvgl_arduino2] extends = env:Wio_Tracker_L2_companion_solo_lvgl platform = ${esp32_base.platform} +build_unflags = -DARDUINO_USB_MODE=1 -DUI_FLUSH_DMA ; DMA flush untried on ESP-IDF 4.4 lib_deps = ${Wio_Tracker_L2.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h index df2617f8..28081eac 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Display.h +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -88,13 +88,30 @@ public: virtual ~WioTrackerL2Backlight() = default; }; +// The panel, plus a write that doesn't wait: an area's pixels (already in the +// panel's byte order, at most 32 KB -- one SPI transaction) go out by DMA +// while the CPU draws the next one; flushWait() ends it. The library's own +// writes wait for the wire every time (the ui-lvgl frame's ~8 ms). +struct Panel_WioTrackerL2 : public lgfx::Panel_NV3031B { + void flushStart(int x, int y, int w, int h, const uint8_t* data, uint32_t len) { + setWindow(x, y, x + w - 1, y + h - 1); + start_qspi(); + _bus->writeBytes(data, len, true, true); + } + void flushWait() { + _bus->wait(); + end_qspi(); + } +}; + class LGFX_WioTrackerL2 : public lgfx::LGFX_Device { - lgfx::Panel_NV3031B _panel_instance; + Panel_WioTrackerL2 _panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::Touch_GT911 _touch_instance; WioTrackerL2Backlight _light_instance; public: + Panel_WioTrackerL2& panelL2() { return _panel_instance; } bool init_impl(bool use_reset, bool use_clear) override { // bring up backlight controller while the I2C bus is still clean _light_instance.init(_light_instance.getBrightness()); @@ -182,6 +199,7 @@ public: // direct access to the LGFX device (LVGL flush/touch glue) lgfx::LGFX_Device* lgfxDevice() { return &disp; } + Panel_WioTrackerL2& panel() { return disp.panelL2(); } // LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only // carried the boot "Loading..." -- given back once LVGL takes over. void releaseFrameBuffer() { buffer.deleteSprite(); }