diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 3691e66f..7f90a27d 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -56,5 +56,7 @@ build_flags = ${nrf52_base.build_flags} -D DUAL_SERIAL=1 -D ENABLE_SCREENSHOT -D KEEP_DISPLAY_ON_USB + -D EINK_FAST_WRITES=1 + -D EINK_SPI_HZ=8000000 -Os extra_scripts = post:create-uf2.py diff --git a/src/helpers/ui/GxEPD2_FastWrite.h b/src/helpers/ui/GxEPD2_FastWrite.h new file mode 100644 index 00000000..a4bdf703 --- /dev/null +++ b/src/helpers/ui/GxEPD2_FastWrite.h @@ -0,0 +1,108 @@ +#pragma once +// Faster image writes for large GxEPD2 panels. +// +// GxEPD2 sends the framebuffer to the panel one byte at a time: every byte is +// its own SPI.transfer(uint8_t) call, and on nRF52 each of those is a complete +// EasyDMA transaction with several microseconds of fixed setup overhead -- +// independent of the SPI clock. A partial refresh here writes the full screen +// five times (GxEPDDisplay::endFrame's inverse-redrive pass, then +// GxEPD2_BW::display()), so on an 800x480 panel (48 KB a screen) that overhead +// alone blocks the main loop for well over a second per refresh. +// +// These subclasses override only the image writes, sending each row as one +// bulk SPI.transfer(buf, nullptr, n) -- the same bytes, in the same order, to +// the same RAM window -- so the cost becomes just the wire time. Everything +// else (init, refresh, power) is the stock driver. The first write (panel init +// and the initial screen clear) and the rarely used mirror/PGM variants go +// through the stock path unchanged. +// +// Use by naming the subclass as the panel: -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82_Fast + +#include + +#ifdef _GxEPD2_426_GDEQ0426T82_H_ +class GxEPD2_426_GDEQ0426T82_Fast : public GxEPD2_426_GDEQ0426T82 { + typedef GxEPD2_426_GDEQ0426T82 Base; + uint8_t _row[WIDTH / 8]; // one panel row, in RAM (EasyDMA can't read flash) + + bool canFast(bool mirror_y, bool pgm) const { + return _init_display_done && !_initial_write && !mirror_y && !pgm; + } + + // Same RAM window as the stock driver's (private) _setPartialRamArea(): + // the gates are reversed on this panel, so y is mirrored and entered decreasing. + void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + y = HEIGHT - y - h; + _writeCommand(0x11); + _writeData(0x01); + _writeCommand(0x44); + _writeData(x % 256); + _writeData(x / 256); + _writeData((x + w - 1) % 256); + _writeData((x + w - 1) / 256); + _writeCommand(0x45); + _writeData((y + h - 1) % 256); + _writeData((y + h - 1) / 256); + _writeData(y % 256); + _writeData(y / 256); + _writeCommand(0x4e); + _writeData(x % 256); + _writeData(x / 256); + _writeCommand(0x4f); + _writeData((y + h - 1) % 256); + _writeData((y + h - 1) / 256); + } + + // The stock _writeImage(), with the per-byte loop replaced by one bulk + // transfer per row (clipping and indexing unchanged). + void fastWrite(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert) { + delay(1); // as the stock driver: yield + int16_t wb = (w + 7) / 8; + x -= x % 8; + w = wb * 8; + int16_t x1 = x < 0 ? 0 : x; + int16_t y1 = y < 0 ? 0 : y; + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) return; + setRamArea(x1, y1, w1, h1); + _writeCommand(command); + _startTransfer(); + const int16_t nb = w1 / 8; + for (int16_t i = 0; i < h1; i++) { + const uint8_t* src = &bitmap[dx / 8 + int32_t(i + dy) * wb]; + if (invert) { for (int16_t j = 0; j < nb; j++) _row[j] = ~src[j]; } + else { memcpy(_row, src, nb); } + _pSPIx->transfer(_row, nullptr, nb); + } + _endTransfer(); + delay(1); + } + +public: + GxEPD2_426_GDEQ0426T82_Fast(int16_t cs, int16_t dc, int16_t rst, int16_t busy) : Base(cs, dc, rst, busy) {} + using Base::writeImage; // keep the other overloads visible + + void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x24, bitmap, x, y, w, h, invert); + } + void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImageForFullRefresh(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x26, bitmap, x, y, w, h, invert); // set previous + fastWrite(0x24, bitmap, x, y, w, h, invert); // set current + } + void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImageAgain(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x26, bitmap, x, y, w, h, invert); // set previous + fastWrite(0x24, bitmap, x, y, w, h, invert); // set current + } +}; +#endif diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 7548a69e..510dfd84 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,8 +14,16 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +// SPI clock for the panel. 4 MHz is the conservative default every board has +// always used; a short FPC run can go faster (SSD16xx-family controllers are +// rated ~20 MHz for writes). On nRF52840 the Adafruit core puts SPI1 on SPIM2, +// which caps at 8 MHz -- higher requests are silently clamped to 8 MHz there. +#ifndef EINK_SPI_HZ + #define EINK_SPI_HZ 4000000 +#endif + bool GxEPDDisplay::begin() { - display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + display.epd2.selectSPI(SPI1, SPISettings(EINK_SPI_HZ, MSBFIRST, SPI_MODE0)); #ifdef ESP32 SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS); #else diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index ffb5e96b..89f58610 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -18,6 +18,18 @@ #include #include "EinkGfxDisplay.h" +#include "GxEPD2_FastWrite.h" + +// EINK_FAST_WRITES=1: drive the panel through its _Fast subclass +// (GxEPD2_FastWrite.h) -- the same bytes, sent a row at a time in bulk instead +// of one SPI call per byte. Only for panels that have one. +#define EINK_CAT_(a, b) a##b +#define EINK_CAT(a, b) EINK_CAT_(a, b) +#if defined(EINK_DISPLAY_MODEL) && defined(EINK_FAST_WRITES) && EINK_FAST_WRITES + #define EINK_PANEL_TYPE EINK_CAT(EINK_DISPLAY_MODEL, _Fast) +#elif defined(EINK_DISPLAY_MODEL) + #define EINK_PANEL_TYPE EINK_DISPLAY_MODEL +#endif // This driver calls callBusyPump() during its BUSY-pin waits; app code keys // its setBusyPumpFn() wiring off this so it isn't compiled for displays (or @@ -53,8 +65,8 @@ // The panel object, as a base so it's built before EinkGfxDisplay draws on it. struct GxEPDPanel { #if defined(EINK_DISPLAY_MODEL) - GxEPD2_BW display; - GxEPDPanel() : display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} + GxEPD2_BW display; + GxEPDPanel() : display(EINK_PANEL_TYPE(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} #else GxEPD2_BW display; GxEPDPanel() : display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}