mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(eink): bulk SPI row writes for the GDEQ0426T82, at 8 MHz
GxEPD2 sends the panel RAM a byte at a time; on the nRF52's SPIM the per-call overhead dominates, and an 800x480 partial refresh writes the whole 48 KB several times. GxEPD2_426_GDEQ0426T82_Fast overrides the three image writes to send each row in one transfer() call, falling back to the stock path for the cases it doesn't cover (first write, mirror_y, PROGMEM). Byte-for-byte the same data as the stock driver. Opt-in per env: -D EINK_FAST_WRITES=1, and EINK_SPI_HZ (default 4 MHz, 8 MHz here, SPIM2's ceiling). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxAZVAbhhYr18Dd4guTQM1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <string.h>
|
||||
|
||||
#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
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
#include <GxEPD2_7C.h>
|
||||
|
||||
#include "EinkGfxDisplay.h"
|
||||
#include "GxEPD2_FastWrite.h"
|
||||
|
||||
// EINK_FAST_WRITES=1: drive the panel through its <model>_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<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
|
||||
GxEPDPanel() : display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
|
||||
GxEPD2_BW<EINK_PANEL_TYPE, EINK_PANEL_TYPE::HEIGHT> display;
|
||||
GxEPDPanel() : display(EINK_PANEL_TYPE(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
|
||||
#else
|
||||
GxEPD2_BW<GxEPD2_150_BN, 200> display;
|
||||
GxEPDPanel() : display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
|
||||
|
||||
Reference in New Issue
Block a user