diff --git a/platformio.ini b/platformio.ini index fbed6a7..08dd4a1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -134,6 +134,39 @@ board_upload.flash_size = 32MB monitor_speed = 115200 lib_ignore = Seeed_GFX +; Seeed reTerminal E1004 — ESP32-S3R8 (8 MB OPI PSRAM, 32 MB QSPI flash, CH343P +; UART console on GPIO43/44) driving the 13.3" T133A01 Spectra 6 dual-controller +; panel (1200x1600). Panel/bus pins come from runtime device config. +[env:esp32-s3-E1004] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip +framework = arduino +; bb_epaper pinned to the bitbank2/bb_epaper#32 commit; drop the pin once merged. +lib_deps = + https://github.com/limengdu/bb_epaper.git#95fd94afe39cd7db32bef7c70eea06d654264ff6 +build_flags = + -DTARGET_ESP32 + ; 32 KB inflate window: clients (py-opendisplay / HA) compress with standard zlib. + -DOPENDISPLAY_ZLIB_WINDOW_BITS=15 + -DOPENDISPLAY_ZLIB_USE_HEAP_WINDOW=1 + -DBOARD_HAS_PSRAM + -DARDUINO_USB_MODE=0 + -DARDUINO_USB_CDC_ON_BOOT=0 + -DCONFIG_FREERTOS_WATCHDOG_TIMEOUT_S=120 + -DOPENDISPLAY_LOG_UART + -DOPENDISPLAY_LOG_UART_RX=44 + -DOPENDISPLAY_LOG_UART_TX=43 +board_build.filesystem = littlefs +board = esp32-s3-devkitc-1 +board_build.partitions = default_32MB.csv +board_build.flash_mode = qio +board_build.arduino.memory_type = qio_opi +board_build.psram_type = qspi_opi +board_upload.maximum_size = 33554432 +board_upload.maximum_ram_size = 327680 +board_upload.flash_size = 32MB +monitor_speed = 115200 +lib_ignore = Seeed_GFX + [env:esp32-c3-N4] platform = espressif32 framework = arduino diff --git a/src/boot_screen.cpp b/src/boot_screen.cpp index 48b6c6f..8b7dce8 100644 --- a/src/boot_screen.cpp +++ b/src/boot_screen.cpp @@ -29,6 +29,7 @@ void writeSerial(String message, bool newLine); void bbepSetAddrWindow(BBEPDISP *pBBEP, int x, int y, int cx, int cy); void bbepStartWrite(BBEPDISP *pBBEP, int iPlane); void bbepWriteData(BBEPDISP *pBBEP, uint8_t *pData, int iLen); +int bbepWritePlane(BBEPDISP *pBBEP, int iPlane, int bInvert); typedef struct { char c; uint8_t col[5]; } BootGlyph5x7; static const BootGlyph5x7 BOOT_FONT5X7[] = { @@ -878,6 +879,14 @@ bool writeBootScreenWithQr() { int textStartY = textY; const uint16_t footerInfoY = (uint16_t)(footerY0 + (footerPadTop + footerInfoH - 7 * footerInfoScale) / 2); + // Dual-controller E1004: rows go to the framebuffer (same packed-4bpp + // pitch) and bbepWritePlane() sends them; rows cannot be streamed. + const bool e1004Framebuffered = e1004_panel_used(); + if (e1004Framebuffered && bbep.ucScreen == NULL) { + writeSerial("Boot screen: E1004 framebuffer not allocated", true); + return false; + } + uint8_t* row = staticRowBuffer; // bb_epaper 4-gray (scheme 5) needs the packed 2bpp image split into two // 1-bit controller planes, so render the frame once per plane and @@ -899,13 +908,15 @@ bool writeBootScreenWithQr() { : (colorSwatchPlane1 ? (pass == 0 ? PLANE_0 : PLANE_1) : (useBitplanes ? PLANE_0 : getplane())); #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) - if (!seeed_driver_used()) { + if (!seeed_driver_used() && !e1004Framebuffered) { bbepSetAddrWindow(&bbep, 0, 0, w, h); bbepStartWrite(&bbep, targetPlane); } #else - bbepSetAddrWindow(&bbep, 0, 0, w, h); - bbepStartWrite(&bbep, targetPlane); + if (!e1004Framebuffered) { + bbepSetAddrWindow(&bbep, 0, 0, w, h); + bbepStartWrite(&bbep, targetPlane); + } #endif const int ls = bootLineStep(middleScaleText); const uint16_t domY = (uint16_t)textStartY; @@ -975,12 +986,16 @@ bool writeBootScreenWithQr() { seeed_gfx_boot_write_row(y_native, row, pitch); } else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); + } else if (e1004Framebuffered) { + memcpy(&bbep.ucScreen[(size_t)y_native * pitch], row, pitch); } else { bbepWriteData(&bbep, row, pitch); } #else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); + } else if (e1004Framebuffered) { + memcpy(&bbep.ucScreen[(size_t)y_native * pitch], row, pitch); } else { bbepWriteData(&bbep, row, pitch); } @@ -988,6 +1003,11 @@ bool writeBootScreenWithQr() { } } + if (e1004Framebuffered) { + bbepWritePlane(&bbep, PLANE_0, 0); + writeSerial("Boot screen with QR rendered", true); + return true; + } #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) if (seeed_driver_used()) { seeed_gfx_boot_skip_planes(); diff --git a/src/display_service.cpp b/src/display_service.cpp index 5733314..265cd57 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -113,6 +113,11 @@ void bbepWriteCmd(BBEPDISP *pBBEP, uint8_t cmd); void bbepCMD2(BBEPDISP *pBBEP, uint8_t cmd1, uint8_t cmd2); void bbepWaitBusy(BBEPDISP *pBBEP); bool bbepIsBusy(BBEPDISP *pBBEP); +int bbepWritePlane(BBEPDISP *pBBEP, int iPlane, int bInvert); +int bbepAllocBuffer(BBEPDISP *pBBEP, int bDoubleSize); +#ifdef BBEP_T133A01 // bb_epaper build with T133A01 dual-controller support +void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs); +#endif void flashLed(uint8_t color, uint8_t brightness); bool waitforrefresh(int timeout); @@ -149,8 +154,25 @@ static void prepareEpdRailForBoot() { #endif } +#ifdef BBEP_T133A01 +// CS2 must be set before bbepInitIO(), which runs the full dual-chip init; +// the generic wakeup + re-init would reach only one controller. +static void e1004InitPanel(void) { + const DisplayConfig& d = globalConfig.displays[0]; + bbepSetCS2(&bbep, e1004_cs2_pin()); + bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); +} +#endif + static void initBbepPanelSession() { const DisplayConfig& d = globalConfig.displays[0]; +#ifdef BBEP_T133A01 + if (e1004_panel_used()) { + e1004InitPanel(); + delay(200); + return; + } +#endif bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); bbepWakeUp(&bbep); bbepSendCMDSequence(&bbep, bbep.pInitFull); @@ -246,12 +268,20 @@ static bool epdSessionAcquire(bool partialInit) { pwrmgm(true); // -> PWR_ACTIVE (guarded; real transition) if (!epdSessionUsesSeeed()) { const DisplayConfig& d = globalConfig.displays[0]; - bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); - bbepWakeUp(&bbep); - const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull) - : bbep.pInitFull; - bbepSendCMDSequence(&bbep, initSeq); - epdSessionInitWasPartial = partialInit; +#ifdef BBEP_T133A01 + if (e1004_panel_used()) { + e1004InitPanel(); + epdSessionInitWasPartial = false; + } else +#endif + { + bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); + bbepWakeUp(&bbep); + const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull) + : bbep.pInitFull; + bbepSendCMDSequence(&bbep, initSeq); + epdSessionInitWasPartial = partialInit; + } } cold = true; } else { @@ -263,11 +293,20 @@ static bool epdSessionAcquire(bool partialInit) { // Phase 1: full re-init on warm re-acquire (HW reset => registers identical // to cold, safest). Phase 2a will skip bbepWakeUp + resend only on change. if (!epdSessionUsesSeeed()) { - bbepWakeUp(&bbep); - const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull) - : bbep.pInitFull; - bbepSendCMDSequence(&bbep, initSeq); - epdSessionInitWasPartial = partialInit; +#ifdef BBEP_T133A01 + if (e1004_panel_used()) { + // Warm T133A01 stays fully initialized (release never sleeps it); + // every transfer resends the frame and re-opens the RAM writes. + epdSessionInitWasPartial = false; + } else +#endif + { + bbepWakeUp(&bbep); + const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull) + : bbep.pInitFull; + bbepSendCMDSequence(&bbep, initSeq); + epdSessionInitWasPartial = partialInit; + } } cold = false; } @@ -477,8 +516,9 @@ int mapEpd(int id){ case 0x003F: return EP31_240x320; case 0x0040: return EP75YR_800x480; case 0x0041: return EP_PANEL_UNDEFINED; - // bb_epaper 2.1.9 does not define the 13.3" EP133 panel yet. - case 0x0042: return EP_PANEL_UNDEFINED; +#ifdef BBEP_T133A01 + case PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 +#endif case 0x0043: return EP154_200x200_4GRAY; case 0x0044: return EP42B_400x300_4GRAY; case 0x0045: return EP397_800x480; @@ -506,10 +546,34 @@ bool seeed_driver_used(void) { #endif } +// Seeed reTerminal E1004: 13.3" T133A01 Spectra 6, one controller per panel half. +bool e1004_panel_used(void) { +#ifdef BBEP_T133A01 + if (globalConfig.display_count < 1) return false; + return globalConfig.displays[0].panel_ic_type == PANEL_IC_EP133A_SPECTRA_1200X1600; +#else + return false; +#endif +} + +// Second chip select from the display block's spare reserved_pin_2 (cs_pin_2 +// in the toolbox schema); 0 or 0xFF = board default GPIO2. +uint8_t e1004_cs2_pin(void) { + uint8_t p = globalConfig.displays[0].reserved_pin_2; + if (p == 0 || p == 0xFF) return 2; + return p; +} + bool waitforrefresh(int timeout){ #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) if (seeed_driver_used()) return seeed_gfx_wait_refresh(timeout); #endif + if (e1004_panel_used() && !bbepIsBusy(&bbep)) { + // bbepRefresh() waits for BUSY internally on the T133A01; already idle + // here means the refresh completed, not that it never started. + writeSerial("Refresh completed inside bb_epaper", true); + return true; + } // Poll at 10 ms (was 100 ms) so a ~0.5 s refresh returns up to ~90 ms sooner. // BUSY asserts within µs of MASTER_ACTIVATE, so the i==0 "never went busy" // error check stays valid at a 10 ms first poll. Loop bound scales x10 @@ -1355,10 +1419,31 @@ void initDisplay(){ #endif { prepareEpdRailForBoot(); + uint8_t* prevFramebuffer = bbep.ucScreen; // E1004 frame survives re-init memset(&bbep, 0, sizeof(BBEPDISP)); int panelType = mapEpd(globalConfig.displays[0].panel_ic_type); bbepSetPanelType(&bbep, panelType); - bbepSetRotation(&bbep, globalConfig.displays[0].rotation * 90); + int rotation = globalConfig.displays[0].rotation * 90; + if (e1004_panel_used()) { + // The framebuffer is filled in native layout (renderers and host + // apply the configured rotation), so bb_epaper must not rotate. + rotation = 0; + // 1200x1600 4bpp PSRAM frame sent by bbepWritePlane(); the row + // streaming paths cannot split rows across the two chip selects. + // The writers derive sizes from the config, so refuse a config + // that does not match the panel: ucScreen stays NULL and the + // boot screen / transfer end paths fail instead of corrupting RAM. + if (globalConfig.displays[0].pixel_width != bbep.native_width || + globalConfig.displays[0].pixel_height != bbep.native_height || + globalConfig.displays[0].color_scheme != COLOR_SCHEME_BWGBRY) { + writeSerial("ERROR: E1004 requires a 1200x1600 BWGBRY display config", true); + } else if (prevFramebuffer != NULL) { + bbep.ucScreen = prevFramebuffer; + } else if (bbepAllocBuffer(&bbep, 0) != BBEP_SUCCESS) { + writeSerial("ERROR: E1004 framebuffer allocation failed", true); + } + } + bbepSetRotation(&bbep, rotation); writeSerial(String("Height: ") + String(globalConfig.displays[0].pixel_height), true); writeSerial(String("Width: ") + String(globalConfig.displays[0].pixel_width), true); initBbepPanelSession(); @@ -1710,6 +1795,21 @@ static void streamGray4Bytes(const uint8_t* buf, uint32_t len) { } } +// Direct-write byte sink: the E1004 collects the frame in the framebuffer for +// bbepWritePlane(); other panels stream straight to panel RAM. Advances +// directWriteBytesWritten by the full length either way (callers' accounting). +static void directWriteSinkBytes(uint8_t* data, uint32_t len) { + if (e1004_panel_used()) { + if (bbep.ucScreen != NULL && directWriteBytesWritten < directWriteTotalBytes) { + uint32_t space = directWriteTotalBytes - directWriteBytesWritten; + memcpy(&bbep.ucScreen[directWriteBytesWritten], data, (len < space) ? len : space); + } + } else { + bbepWriteData(&bbep, data, (int)len); + } + directWriteBytesWritten += len; +} + static bool directWriteTouchSuspended = false; void cleanupDirectWriteState(bool refreshDisplay) { @@ -1789,7 +1889,8 @@ static void directWriteActivatePanel(void) { seeed_gfx_direct_write_reset(); } else #endif - { + // E1004 frames go to the framebuffer; bbepWritePlane() opens the RAM writes. + if (!e1004_panel_used()) { bbepSetAddrWindow(&bbep, 0, 0, globalConfig.displays[0].pixel_width, globalConfig.displays[0].pixel_height); bbepStartWrite(&bbep, directWriteBitplanes ? PLANE_0 : getplane()); } @@ -1869,7 +1970,7 @@ void handlePartialWriteStart(uint8_t* data, uint16_t len) { uint16_t dispW = globalConfig.displays[0].pixel_width; uint16_t dispH = globalConfig.displays[0].pixel_height; - if (getBitsPerPixel() != 1) { + if (getBitsPerPixel() != 1 || e1004_panel_used()) { // bb_epaper partial refresh support is effectively non-existent for // 2bpp+ panels, and physical panels may not support that mode either. // This protocol uses two 1bpp controller planes as old/new image memory. @@ -1974,8 +2075,7 @@ void handleDirectWriteData(uint8_t* data, uint16_t len) { if (directWriteIsGray4() || directWriteBitplanes) { streamGray4Bytes(data, bytesToWrite); // advances directWriteBytesWritten, splits planes } else { - bbepWriteData(&bbep, data, bytesToWrite); - directWriteBytesWritten += bytesToWrite; + directWriteSinkBytes(data, bytesToWrite); } } imageWriteLogProgress(directWriteBytesWritten, directWriteTotalBytes); @@ -2056,6 +2156,8 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end imageWriteLogFinish(directWriteBytesWritten, directWriteTotalBytes); int refreshMode = REFRESH_FULL; if (data != nullptr && len >= 1 && data[0] == 1) refreshMode = REFRESH_FAST; + // T133A01 fast refresh falls back to re-init, whose reset wipes the frame. + if (e1004_panel_used()) refreshMode = REFRESH_FULL; writeSerial("EPD refresh: ", false); writeSerial(refreshMode == REFRESH_FAST ? "FAST" : "FULL", false); writeSerial(" (mode=", false); @@ -2084,8 +2186,16 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end } else #endif { - bbepRefresh(&bbep, refreshMode); - refreshSuccess = waitforrefresh(60); + bool planeOk = true; + if (e1004_panel_used()) { + // Send the framebuffer to both controllers before the refresh; + // fails when the framebuffer is absent (bad config / alloc failure). + planeOk = (bbepWritePlane(&bbep, PLANE_0, 0) == BBEP_SUCCESS); + } + if (planeOk) { + bbepRefresh(&bbep, refreshMode); + refreshSuccess = waitforrefresh(60); + } // No bbepSleep here: cleanupDirectWriteState(false) releases the session, // keeping the controller awake + rail up when keep-alive holds it warm. } @@ -2261,8 +2371,7 @@ static bool pipeConsumePayload(uint8_t* data, uint16_t len) { if (directWriteIsGray4() || directWriteBitplanes) { streamGray4Bytes(data, toWrite); // advances directWriteBytesWritten, splits planes } else { - bbepWriteData(&bbep, data, toWrite); - directWriteBytesWritten += toWrite; + directWriteSinkBytes(data, toWrite); } } imageWriteLogProgress(directWriteBytesWritten, directWriteTotalBytes); @@ -2315,7 +2424,7 @@ void handlePipeWriteStart(uint8_t* data, uint16_t len) { uint16_t dispW = globalConfig.displays[0].pixel_width; uint16_t dispH = globalConfig.displays[0].pixel_height; // 5: two 1bpp controller planes are the partial mechanism; seeed/IT8951 has no equivalent. - if (getBitsPerPixel() != 1 || seeed_driver_used()) { + if (getBitsPerPixel() != 1 || seeed_driver_used() || e1004_panel_used()) { displayed_etag = 0; sendPipeStartNack(0x06); return; } // 6: etag gate — nonzero and must match what is currently on the panel. @@ -2774,8 +2883,7 @@ static bool zlib_stream_to_direct_write(const uint8_t* data, uint32_t len, bool } } else { - bbepWriteData(&bbep, decompressionChunk, bytesOut); - directWriteBytesWritten += (uint32_t)bytesOut; + directWriteSinkBytes(decompressionChunk, (uint32_t)bytesOut); } if (directWriteBytesWritten > directWriteDecompressedTotal) { return false; diff --git a/src/display_service.h b/src/display_service.h index b0e73c3..abdf2ff 100644 --- a/src/display_service.h +++ b/src/display_service.h @@ -23,6 +23,9 @@ void epdSessionTick(void); // millis()-poll from loop()/idleDelay(): expir bool epdSessionIsWarm(void); // true when the panel is powered-idle (PWR_WARM) bool seeed_driver_used(void); +/** True when displays[0] is the E1004 13.3" T133A01 dual-controller panel. */ +bool e1004_panel_used(void); +uint8_t e1004_cs2_pin(void); int mapEpd(int id); bool waitforrefresh(int timeout); float readBatteryVoltage(); diff --git a/src/main.cpp b/src/main.cpp index fb83965..493c4f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -541,6 +541,10 @@ static void configureDisplayPinsLowPower() { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } + if (e1004_panel_used()) { + pinMode(e1004_cs2_pin(), OUTPUT); + digitalWrite(e1004_cs2_pin(), LOW); + } if (d.busy_pin != 0xFF) { pinMode(d.busy_pin, INPUT); } @@ -621,6 +625,11 @@ void pwrmgm(bool onoff){ pinMode(disp.cs_pin, OUTPUT); digitalWrite(disp.cs_pin, HIGH); } + if (e1004_panel_used()) { + // Park the second controller's CS HIGH before any SPI traffic. + pinMode(e1004_cs2_pin(), OUTPUT); + digitalWrite(e1004_cs2_pin(), HIGH); + } if (disp.dc_pin != 0xFF) { pinMode(disp.dc_pin, OUTPUT); digitalWrite(disp.dc_pin, LOW); diff --git a/src/structs.h b/src/structs.h index a982fb9..7d22c13 100644 --- a/src/structs.h +++ b/src/structs.h @@ -78,6 +78,8 @@ struct PowerOption { // Decimal 3000–3999 = Seeed_GFX / OpenDisplay runtime epaper (add new IDs here as panels ship). #define PANEL_IC_SEEED_ED103TC2_1872X1404 3000u #define PANEL_IC_SEEED_ED103TC2_1872X1404_4GRAY 3001u +// 13.3" T133A01 Spectra 6 dual-controller panel (Seeed reTerminal E1004). +#define PANEL_IC_EP133A_SPECTRA_1200X1600 0x0042u #define BOOT_ROW_BUFFER_SIZE 960