diff --git a/.github/firmware-targets.json b/.github/firmware-targets.json index 618e1c5..1db52be 100644 --- a/.github/firmware-targets.json +++ b/.github/firmware-targets.json @@ -66,15 +66,6 @@ "flash_freq": "80m", "bootloader_offset": "0x0000" }, - { - "env": "esp32-s3-E1004", - "family": "esp32", - "chip": "esp32-s3", - "flash_size": "32MB", - "flash_mode": "dio", - "flash_freq": "80m", - "bootloader_offset": "0x0000" - }, { "env": "esp32-c6-N4", "family": "esp32", diff --git a/CLAUDE.md b/CLAUDE.md index 75a0bcf..b642389 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,8 +23,8 @@ pio run # build every environment Common envs: `nrf52840custom`, `esp32-s3-N16R8`, `esp32-s3-N8R8`, `esp32-c3-N16`, `esp32-c6-N4`. CI (`.github/workflows/main.yaml`) builds every -environment in `.github/firmware-targets.json` on every push — **12** of them — -keep them all green. Note `platformio.ini`'s `default_envs` lists only 11: +environment in `.github/firmware-targets.json` on every push — **11** of them — +keep them all green. Note `platformio.ini`'s `default_envs` lists only 10: `esp32-wrover-e-N4R8` ships but is NOT in it, so a bare `pio run` silently skips the target most likely to catch a broken `#ifndef OPENDISPLAY_HAS_WIFI` path. Build it explicitly (`pio run -e esp32-wrover-e-N4R8`) before claiming a diff --git a/platformio.ini b/platformio.ini index 3472b08..bb77f4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -44,7 +44,6 @@ default_envs = esp32-s3-N32R8 esp32-s3-N32R8-extuart esp32-s3-N16R8-extuart - esp32-s3-E1004 esp32-c3-N4 esp32-c3-N16 esp32-c6-N4 @@ -52,8 +51,12 @@ default_envs = [env] lib_deps = - https://github.com/bitbank2/bb_epaper.git - h2zero/NimBLE-Arduino@^2.5.0 + ; Pinned: bb_epaper has no releases, so an unpinned URL means CI and local + ; builds get whatever HEAD was at install time. 5dccfbb is "Added support for + ; the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel" -- the + ; commit that landed E1004 upstream, replacing the limengdu fork pin. + https://github.com/bitbank2/bb_epaper.git#5dccfbb + h2zero/NimBLE-Arduino@^2.5.0 extra_scripts = pre:scripts/factory_config_gen.py build_src_filter = @@ -303,16 +306,21 @@ board_upload.maximum_ram_size = 327680 board_upload.flash_size = 32MB monitor_speed = 115200 -; Seeed reTerminal E1004 — same hardware as N32R8-extuart; pinned bb_epaper for T133A01. -[env:esp32-s3-E1004] +; Debug logging on the same hardware. Not in default_envs and not in +; .github/firmware-targets.json -- like esp32-s3-N16R8-extuart-debug, this is a +; bench build, not a shipped target. Worth having here specifically because +; -extuart builds print no panic backtrace over USB (logging goes out GPIO43/44), +; so debug-level logs are the only running commentary during panel bring-up. +[env:esp32-s3-N32R8-extuart-debug] extends = env:esp32-s3-N32R8-extuart -; Pin bb_epaper to limengdu PR bitbank2#32 (T133A01) until merged upstream. -lib_deps = - https://github.com/limengdu/bb_epaper.git#95fd94afe39cd7db32bef7c70eea06d654264ff6 - h2zero/NimBLE-Arduino@^2.5.0 build_flags = - ${env:esp32-s3-N32R8-extuart.build_flags} - -DOPENDISPLAY_ZLIB_WINDOW_BITS=15 + ${env:esp32-s3-N32R8-extuart.build_flags} + -DOD_LOG_LEVEL=OD_LOG_DEBUG + +; The Seeed reTerminal E1004 has no env of its own: it is the same hardware as +; esp32-s3-N32R8-extuart, and its 13.3" Spectra6 panel is selected at runtime by +; panel_ic_type in the device config, not at build time. Flash it with +; esp32-s3-N32R8-extuart (or the -debug env above for bring-up). ; ESP targets pin pioarduino 55.03.39 (Arduino 3.3.9 / IDF 5.5.4), by exact version ; so it cannot drift CI vs local. diff --git a/src/boot_screen.cpp b/src/boot_screen.cpp index 20f974e..a78c770 100644 --- a/src/boot_screen.cpp +++ b/src/boot_screen.cpp @@ -8,6 +8,7 @@ #include #include "qr/qrcode.h" #include "display_service.h" +#include "split_panel.h" #include "od_log.h" #if __has_include("logo_bitmap.h") #include "logo_bitmap.h" @@ -905,10 +906,14 @@ bool writeBootScreenWithQr() { int textStartY = textY; const uint16_t footerInfoY = (uint16_t)(footerY0 + (footerPadTop + footerInfoH - 7 * footerInfoScale) / 2); - // Dual-controller E1004 (bwgbry_split): left half-plane then right (continuous DTM). - const bool e1004Stream = e1004_panel_used(); - const int e1004HalfPasses = e1004Stream ? 2 : 1; - const uint16_t e1004HalfPitch = (uint16_t)(pitch / 2); + // Dual-controller panels (bwgbry_split): paint and emit the left half-plane, + // then the right. That is exactly the wire order splitPanelSinkBytes() expects, + // so the halves land in the right columns of the framebuffer with no extra + // bookkeeping here -- and painting one half at a time still avoids rasterising + // the full frame twice. + const bool splitStream = splitPanelUsed(); + const int splitHalfPasses = splitStream ? 2 : 1; + const uint16_t splitHalfPitch = (uint16_t)(pitch / 2); uint8_t* row = staticRowBuffer; // bb_epaper 4-gray (scheme 5) needs the packed 2bpp image split into two @@ -925,18 +930,12 @@ bool writeBootScreenWithQr() { return false; } const int planePasses = (gray4Split || colorSwatchPlane1) ? 2 : 1; - for (int halfPass = 0; halfPass < e1004HalfPasses; halfPass++) { - if (e1004Stream) { - if (halfPass == 0) { - if (!e1004_begin_plane()) { - od_log_error("Boot screen: E1004 dual-CS plane open failed"); - return false; - } - } else if (!e1004_advance_to_cs2()) { - od_log_error("Boot screen: E1004 CS2 advance failed"); - e1004_end_plane(); - return false; - } + for (int halfPass = 0; halfPass < splitHalfPasses; halfPass++) { + // Only pass 0 opens the frame; the sink crosses to the right half on its + // own once the left half-plane's worth of bytes has arrived. + if (splitStream && halfPass == 0 && !splitPanelBeginFrame()) { + od_log_error("Boot screen: split panel frame open failed"); + return false; } for (int pass = 0; pass < planePasses; pass++) { const int bitSel = pass; // pass 0 -> LSB/PLANE_0, pass 1 -> MSB/PLANE_1 @@ -944,12 +943,12 @@ bool writeBootScreenWithQr() { : (colorSwatchPlane1 ? (pass == 0 ? PLANE_0 : PLANE_1) : (useBitplanes ? PLANE_0 : getplane())); #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) - if (!fastepd_driver_used() && !e1004Stream) { + if (!fastepd_driver_used() && !splitStream) { bbepSetAddrWindow(&bbep, 0, 0, w, h); bbepStartWrite(&bbep, targetPlane); } #else - if (!e1004Stream) { + if (!splitStream) { bbepSetAddrWindow(&bbep, 0, 0, w, h); bbepStartWrite(&bbep, targetPlane); } @@ -963,13 +962,13 @@ bool writeBootScreenWithQr() { : (uint16_t)(fwY + ls); const uint16_t k2Y = (uint16_t)(k1Y + ls); const bool colorPlanePass = colorSwatchPlane1 && pass == 1; - // E1004 half-pass: only paint the half we will stream (avoids 2× full-frame work). - const uint16_t xPaint0 = e1004Stream ? (uint16_t)(halfPass * (w / 2u)) : (uint16_t)0; - const uint16_t xPaint1 = e1004Stream ? (uint16_t)(xPaint0 + (w / 2u)) : w; + // Split half-pass: only paint the half we will emit (avoids 2× full-frame work). + const uint16_t xPaint0 = splitStream ? (uint16_t)(halfPass * (w / 2u)) : (uint16_t)0; + const uint16_t xPaint1 = splitStream ? (uint16_t)(xPaint0 + (w / 2u)) : w; for (uint16_t y_native = 0; y_native < h; y_native++) { - if (e1004Stream) { - memset(row + (size_t)halfPass * e1004HalfPitch, - colorPlanePass ? 0x00 : whiteValue, e1004HalfPitch); + if (splitStream) { + memset(row + (size_t)halfPass * splitHalfPitch, + colorPlanePass ? 0x00 : whiteValue, splitHalfPitch); } else { memset(row, colorPlanePass ? 0x00 : whiteValue, pitch); } @@ -1030,8 +1029,8 @@ bool writeBootScreenWithQr() { fastepd_boot_write_row(y_native, row, pitch); } else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); - } else if (e1004Stream) { - e1004_write_stream_bytes(row + (size_t)halfPass * e1004HalfPitch, e1004HalfPitch); + } else if (splitStream) { + splitPanelSinkBytes(row + (size_t)halfPass * splitHalfPitch, splitHalfPitch); } else { odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); bbepWriteData(&bbep, row, pitch); @@ -1039,8 +1038,8 @@ bool writeBootScreenWithQr() { #else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); - } else if (e1004Stream) { - e1004_write_stream_bytes(row + (size_t)halfPass * e1004HalfPitch, e1004HalfPitch); + } else if (splitStream) { + splitPanelSinkBytes(row + (size_t)halfPass * splitHalfPitch, splitHalfPitch); } else { odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); bbepWriteData(&bbep, row, pitch); @@ -1050,7 +1049,9 @@ bool writeBootScreenWithQr() { } } // halfPass - if (e1004Stream) e1004_end_plane(); + // No close step here: the chip selects stay held until splitPanelCloseFrame() + // releases them just before the refresh, in refreshBootScreenFull(). Closing + // early would make that call report an already-closed frame and skip the refresh. #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) if (fastepd_driver_used()) { diff --git a/src/display_service.cpp b/src/display_service.cpp index 3d9681f..0ed04bd 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -12,6 +12,7 @@ #include "communication.h" #include "encryption.h" #include "boot_screen.h" +#include "split_panel.h" #include "link_owner.h" #include "session_guard.h" #include "touch_input.h" @@ -159,13 +160,6 @@ 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); -#ifdef BBEP_T133A01 -void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs); -void bbepWriteCmdData(BBEPDISP *pBBEP, uint8_t cmd, const uint8_t *pData, int iLen); -void bbepStartDataStream(BBEPDISP *pBBEP, uint8_t cmd); -void bbepWriteDataStreamByte(BBEPDISP *pBBEP, uint8_t data); -void bbepEndDataStream(BBEPDISP *pBBEP); -#endif void flashLed(uint8_t color, uint8_t brightness); bool waitforrefresh(int timeout); @@ -202,128 +196,6 @@ static void prepareEpdRailForBoot() { #endif } -#ifdef BBEP_T133A01 -// CS2 must be set before bbepInitIO() so dual-chip init reaches both controllers. -static void e1004InitPanel(void) { - const DisplayConfig& d = globalConfig.displays[0]; - bbepSetCS2(&bbep, e1004_cs2_pin()); - odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); - // WDT-DEBUG: EPD session stage instrumentation, added alongside the hardware - // watchdog work -- safe to delete this block if it's no longer needed. - od_log_debug("[EPD session][WDT] e1004InitPanel: INIT_SEQ (dual-controller)"); - odWatchdogFeed(); // bbepInitIO sends pInitFull internally (~240 s worst case) - bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); -} - -// Half-panel DTM for bwgbry_split: CS held for left half, then right (no FB). -static bool e1004GeometryOk = false; -static bool e1004StreamOpen = false; -static bool e1004OnLeftHalf = true; -static uint32_t e1004HalfBytesWritten = 0; -static uint32_t e1004HalfPlaneBytes = 0; - -void e1004_end_plane(void); - -static uint8_t e1004_panel_byte(uint8_t packed) { - auto nibble = [](uint8_t c) -> uint8_t { - c &= 0x0f; - switch (c) { - case 0x00: case 0x01: case 0x02: case 0x03: case 0x05: case 0x06: - return c; - default: - return 0x00; - } - }; - return (uint8_t)((nibble(packed >> 4) << 4) | nibble(packed)); -} - -static void e1004_ccset_both(void) { - uint8_t data = 0x01; - digitalWrite(bbep.iCS2Pin, LOW); - bbep.iCSPin = bbep.iCS1Pin; - bbepWriteCmdData(&bbep, 0xe0, &data, 1); - digitalWrite(bbep.iCS2Pin, HIGH); - bbep.iCSPin = bbep.iCS1Pin; - bbepWaitBusy(&bbep); - delay(10); -} - -static uint32_t e1004_half_plane_bytes(void) { - return ((uint32_t)bbep.native_width / 4u) * (uint32_t)bbep.native_height; -} - -bool e1004_begin_plane(void) { - if (!e1004_panel_used() || !e1004GeometryOk) return false; - if (e1004StreamOpen) e1004_end_plane(); - e1004_ccset_both(); - e1004HalfPlaneBytes = e1004_half_plane_bytes(); - e1004HalfBytesWritten = 0; - e1004OnLeftHalf = true; - bbep.iCSPin = bbep.iCS1Pin; - bbepStartDataStream(&bbep, UC8151_DTM1); - e1004StreamOpen = true; - return true; -} - -bool e1004_advance_to_cs2(void) { - if (!e1004StreamOpen || !e1004OnLeftHalf) return false; - bbepEndDataStream(&bbep); - e1004OnLeftHalf = false; - e1004HalfBytesWritten = 0; - bbep.iCSPin = bbep.iCS2Pin; - bbepStartDataStream(&bbep, UC8151_DTM1); - return true; -} - -void e1004_end_plane(void) { - if (!e1004StreamOpen) return; - bbepEndDataStream(&bbep); - bbep.iCSPin = bbep.iCS1Pin; - e1004StreamOpen = false; - e1004OnLeftHalf = true; - e1004HalfBytesWritten = 0; -} - -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len) { - if (!e1004StreamOpen || !data || len == 0) return; - uint8_t scratch[128]; - uint16_t off = 0; - while (off < len) { - uint16_t n = (uint16_t)(len - off); - if (n > sizeof(scratch)) n = sizeof(scratch); - for (uint16_t i = 0; i < n; i++) scratch[i] = e1004_panel_byte(data[off + i]); - SPI.writeBytes(scratch, n); - off = (uint16_t)(off + n); - } - e1004HalfBytesWritten += len; -} - -static void e1004_sink_bytes(uint8_t* data, uint32_t len) { - while (len > 0 && e1004StreamOpen) { - if (e1004HalfPlaneBytes == 0) return; - uint32_t space = e1004HalfPlaneBytes - e1004HalfBytesWritten; - if (space == 0) { - if (e1004OnLeftHalf) { - if (!e1004_advance_to_cs2()) return; - continue; - } - return; - } - uint16_t take = (len < space) ? (uint16_t)len : (uint16_t)((space > 0xFFFFu) ? 0xFFFFu : space); - e1004_write_stream_bytes(data, take); - data += take; - len -= take; - if (e1004OnLeftHalf && e1004HalfBytesWritten >= e1004HalfPlaneBytes) { - if (!e1004_advance_to_cs2()) return; - } - } -} -#else -bool e1004_begin_plane(void) { return false; } -bool e1004_advance_to_cs2(void) { return false; } -void e1004_end_plane(void) {} -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len) { (void)data; (void)len; } -#endif // bb_epaper 71f6e70 replaced EP397/EP426 full-init RAM windows with SET_ORIENTATION // (flip180=0 → 0x11=0x02 on 800-wide) while part inits and our partial helpers still @@ -361,37 +233,29 @@ static void epdAlignCustomPartialRamMode(void) { } } -// Sets bbep.type/native dims/rotation and (E1004) e1004GeometryOk from globalConfig. +// Sets bbep.type/native dims/rotation from globalConfig, and re-attaches the +// split-panel framebuffer (the memset below clears bbep.ucScreen). static void configureBbepPanelGeometry(void) { memset(&bbep, 0, sizeof(BBEPDISP)); int panelType = mapEpd(globalConfig.displays[0].panel_ic_type); bbepSetPanelType(&bbep, panelType); int rotation = globalConfig.displays[0].rotation * 90; -#ifdef BBEP_T133A01 - e1004GeometryOk = false; - if (e1004_panel_used()) { - rotation = 0; // host bakes rotation into packed image - if (globalConfig.displays[0].pixel_width != bbep.native_width || - globalConfig.displays[0].pixel_height != bbep.native_height || - globalConfig.displays[0].color_scheme != OD_COLOR_SCHEME_BWGBRY_SPLIT) { - od_log_error("ERROR: E1004 requires a 1200x1600 bwgbry_split (8) display config"); - } else { - e1004GeometryOk = true; - } - } -#endif + // Dual-controller panels keep native orientation: the host bakes rotation into + // the packed image, and the framebuffer layout bbepWriteImage4bppDual() reads + // is native-order. + if (splitPanelUsed()) rotation = 0; bbepSetRotation(&bbep, rotation); + // After the memset above cleared bbep.ucScreen; re-attaches the framebuffer. + splitPanelConfigureGeometry(); } static void initBbepPanelSession() { const DisplayConfig& d = globalConfig.displays[0]; -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - e1004InitPanel(); + if (splitPanelUsed()) { + splitPanelInitIo(); delay(200); return; } -#endif odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the hardware // watchdog work -- safe to delete this block if it's no longer needed. @@ -536,13 +400,10 @@ static bool epdSessionAcquire(bool partialInit) { if (!epdSessionUsesFastepd()) { const DisplayConfig& d = globalConfig.displays[0]; configureBbepPanelGeometry(); -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - e1004InitPanel(); + if (splitPanelUsed()) { + splitPanelInitIo(); epdSessionInitWasPartial = false; - } else -#endif - { + } else { odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the // hardware watchdog work -- safe to delete this line if it's no longer needed. @@ -574,12 +435,11 @@ 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 (!epdSessionUsesFastepd()) { -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { + if (splitPanelUsed()) { + // Nothing to re-send: the controllers keep their state while the + // rail is warm, and a re-init would cost a full bring-up. epdSessionInitWasPartial = false; - } else -#endif - { + } else { odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the // hardware watchdog work -- safe to delete this line if it's no longer needed. @@ -664,9 +524,15 @@ static bool refreshBootScreenFull() { od_log_debug("[EPD session][WDT] BOOT_REFRESH: entering bbepRefresh(REFRESH_FULL)"); od_log_info("EPD refresh: FULL (boot)"); touchSuspendForEpdRefresh(); + // Dual-controller panels hold both chip selects open across the frame; release + // them before DRF. A false return means the frame was short or faulted, so the + // panel must not be refreshed with it. + if (splitPanelUsed() && !splitPanelCloseFrame()) return false; odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) bbepRefresh(&bbep, REFRESH_FULL); - return waitforrefresh(60); + const bool ok = waitforrefresh(60); + splitPanelPowerOff(); + return ok; } static void cleanup_partial_write_state(void); @@ -943,11 +809,7 @@ int mapEpd(int id){ case 0x003F: return EP31_240x320; case 0x0040: return EP75YR_800x480; case 0x0041: return EP_PANEL_UNDEFINED; -#ifdef BBEP_T133A01 - case OD_PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 -#else - case 0x0042: return EP_PANEL_UNDEFINED; -#endif + case OD_PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 case 0x0043: return EP154_200x200_4GRAY; case 0x0044: return EP42B_400x300_4GRAY; case 0x0045: return EP397_800x480; @@ -980,31 +842,10 @@ bool fastepd_driver_used(void) { #endif } -bool e1004_panel_used(void) { -#ifdef BBEP_T133A01 - if (globalConfig.display_count < 1) return false; - return globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_EP133A_SPECTRA_1200X1600; -#else - return false; -#endif -} - -// cs_pin_2; 0 or 0xFF defaults to GPIO2. -uint8_t e1004_cs2_pin(void) { - uint8_t p = globalConfig.displays[0].cs_pin_2; - if (p == 0 || p == 0xFF) return 2; - return p; -} - bool waitforrefresh(int timeout){ #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) if (fastepd_driver_used()) return fastepd_wait_refresh(timeout); #endif - if (e1004_panel_used() && !bbepIsBusy(&bbep)) { - // bbepRefresh already waited; idle here means refresh finished. - od_log_info("Refresh completed inside bb_epaper"); - return true; - } odWatchdogBreadcrumb(OD_WDT_PHASE_REFRESH_WAIT); // 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" @@ -2219,12 +2060,9 @@ static void directWriteSinkBytes(uint8_t* data, uint32_t len) { // Panel data path. Repeats are filtered inside odWatchdogBreadcrumb(), so a // per-call stamp here costs one comparison. odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - if (e1004GeometryOk) e1004_sink_bytes(data, len); - } else -#endif - { + if (splitPanelUsed()) { + splitPanelSinkBytes(data, len); + } else { bbepWriteData(&bbep, data, (int)len); } directWriteBytesWritten += len; @@ -2245,6 +2083,13 @@ void cleanupDirectWriteState(bool refreshDisplay) { directWriteTotalBytes = 0; directWriteRefreshMode = 0; directWriteStartTime = 0; + // Release the dual-controller chip selects before anything else touches the + // panel. An aborted transfer (disconnect, timeout, decompress failure) reaches + // here with cs_mode still CMD_CS_NONE, and leaving it there means every + // subsequent bb_epaper write -- including the power-down below -- silently + // reaches no controller at all. Idempotent when no frame was open; the return + // value is the completeness verdict, which only the refresh path cares about. + if (splitPanelUsed()) (void)splitPanelCloseFrame(); // Panel power acts only while a transfer/refresh is actually in flight // (PWR_ACTIVE). refreshDisplay==true is a terminal teardown (disconnect, // 15-min timeout, mid-stream error) -> power fully off. refreshDisplay==false @@ -2254,9 +2099,6 @@ void cleanupDirectWriteState(bool refreshDisplay) { if (refreshDisplay) epdSessionForceOff(); else epdSessionRelease(true); } -#ifdef BBEP_T133A01 - if (e1004_panel_used()) e1004_end_plane(); -#endif if (directWriteTouchSuspended) { touchResumeAfterEpdRefresh(); directWriteTouchSuspended = false; @@ -2312,14 +2154,11 @@ static void directWriteActivatePanel(void) { fastepd_direct_write_reset(); } else #endif -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - if (!e1004_begin_plane()) { - od_log_error("ERROR: E1004 dual-CS plane open failed"); + if (splitPanelUsed()) { + if (!splitPanelBeginFrame()) { + od_log_error("ERROR: split panel frame open failed"); } - } else -#endif - { + } else { bbepSetAddrWindow(&bbep, 0, 0, globalConfig.displays[0].pixel_width, globalConfig.displays[0].pixel_height); bbepStartWrite(&bbep, directWriteBitplanes ? PLANE_0 : getplane()); } @@ -2426,10 +2265,14 @@ 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 || e1004_panel_used()) { + if (getBitsPerPixel() != 1) { // 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. + // + // Dual-controller panels need no clause of their own: they are + // bwgbry_split, which getBitsPerPixel() reports as 4, so they are already + // excluded here on bpp alone. send_direct_write_nack(0x76, OD_ERR_PARTIAL_UNSUPPORTED, false); return; } @@ -2619,7 +2462,10 @@ 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; - if (e1004_panel_used()) refreshMode = REFRESH_FULL; // fast re-init would wipe RAM + // No dual-controller override here: the Spectra6 panel table entries carry a + // NULL pInitFast, and bbepRefresh() falls back to pInitFull when that is unset, + // so FAST and FULL emit an identical sequence for them. The forced FULL this + // replaces was guarding against fork behaviour that no longer exists. const char* modeName = (refreshMode == REFRESH_FAST) ? "FAST" : "FULL"; if (data != nullptr && len > 0) { od_log_info("EPD refresh: %s (mode=%d, end payload 0x%02X)", modeName, refreshMode, data[0]); @@ -2651,12 +2497,16 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end } else #endif { -#ifdef BBEP_T133A01 - if (e1004_panel_used()) e1004_end_plane(); -#endif - odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) - bbepRefresh(&bbep, refreshMode); - refreshSuccess = waitforrefresh(60); + // Dual-controller panels hold both chip selects open across the frame; + // release them before DRF. A false return means the frame was short or + // faulted, so skip the refresh rather than commit it to the glass. + if (splitPanelUsed() && !splitPanelCloseFrame()) refreshMode = -1; + if (refreshMode >= 0) { + odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) + bbepRefresh(&bbep, refreshMode); + refreshSuccess = waitforrefresh(60); + splitPanelPowerOff(); + } // No bbepSleep here: cleanupDirectWriteState(false) releases the session, // keeping the controller awake + rail up when keep-alive holds it warm. } @@ -2955,8 +2805,10 @@ 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: partial uses two 1bpp planes (old+new). FastEPD IT8951 accepts that stream - // and applies a row-band update; 2bpp+/E1004 remain unsupported. - if (getBitsPerPixel() != 1 || e1004_panel_used()) { + // and applies a row-band update; 2bpp+ remains unsupported. Dual-controller + // panels are bwgbry_split (4 bpp), so this excludes them without a clause of + // their own. + if (getBitsPerPixel() != 1) { displayed_etag = 0; sendPipeStartNack(OD_ERR_PIPE_START_PARTIAL_UNSUPPORTED); return; } // 6: etag gate — nonzero and must match what is currently on the panel. diff --git a/src/display_service.h b/src/display_service.h index cfb2a73..653c62a 100644 --- a/src/display_service.h +++ b/src/display_service.h @@ -23,12 +23,6 @@ void epdSessionTick(void); // millis()-poll from loop()/idleDelay(): expir bool epdSessionIsWarm(void); // true when the panel is powered-idle (PWR_WARM) bool fastepd_driver_used(void); -bool e1004_panel_used(void); -uint8_t e1004_cs2_pin(void); -bool e1004_begin_plane(void); -bool e1004_advance_to_cs2(void); -void e1004_end_plane(void); -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len); int mapEpd(int id); bool waitforrefresh(int timeout); float readBatteryVoltage(); diff --git a/src/main.cpp b/src/main.cpp index 4cfab10..77d0031 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1241,6 +1241,19 @@ static void configureDisplayPinsLowPower() { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } + // Second chip select on dual-controller panels (T133A01 / 8.1" Spectra6). + // Not in the array above: that loop only skips 0xFF, and an unconfigured + // cs_pin_2 reads 0 on most devices -- which would drive GPIO0, a strapping + // pin, LOW on the way to sleep. Guarded like the aux pins below instead. + // + // Left out until now, so CS2 stayed a driven output at whatever level the + // last frame left it (HIGH) while the rail went away -- the one panel pin + // still able to source into an unpowered controller through its protection + // diodes. LOW matches what cs_pin already does. + if (d.cs_pin_2 != 0xFF && d.cs_pin_2 != 0) { + pinMode(d.cs_pin_2, OUTPUT); + digitalWrite(d.cs_pin_2, LOW); + } if (d.busy_pin != 0xFF) { pinMode(d.busy_pin, INPUT); } diff --git a/src/split_panel.cpp b/src/split_panel.cpp new file mode 100644 index 0000000..57cb646 --- /dev/null +++ b/src/split_panel.cpp @@ -0,0 +1,224 @@ +#include "split_panel.h" + +#include +#include +#include +#include "structs.h" +#include "od_log.h" +#include "watchdog.h" + +extern struct GlobalConfig globalConfig; +extern BBEPDISP bbep; + +// bb_epaper's public header forward-declares only bbepWriteCmd/bbepWriteData/ +// bbepCMD2; everything else is declared by its callers (display_service.cpp does +// the same). These are compiled into the library from bb_ep.inl / arduino_io.inl. +void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs); +void bbepInitIO(BBEPDISP *pBBEP, uint8_t u8DC, uint8_t u8RST, uint8_t u8BUSY, uint8_t u8CS, + uint8_t u8MOSI, uint8_t u8SCK, uint32_t u32Speed); +void bbepWaitBusy(BBEPDISP *pBBEP); +bool bbepIsBusy(BBEPDISP *pBBEP); + +// Tripwires. splitPanelUsed() deliberately does NOT track a library define, so +// these assert that the pieces we build on still exist. If bb_epaper reworks the +// split-buffer model again this fails to compile on every target, rather than +// quietly producing firmware that cannot drive the panel — which is exactly how +// the BBEP_T133A01-gated predecessor of this file disappeared. +static_assert((int)EP133_SPECTRA_1200x1600 > 0, + "bb_epaper: EP133_SPECTRA_1200x1600 missing/renamed (was EP133A_* in the limengdu fork)"); +static_assert(BBEP_SPLIT_BUFFER == 0x0400, + "bb_epaper: BBEP_SPLIT_BUFFER changed; split-panel dispatch must be revisited"); +static_assert(CMD_CS_NONE == 0xf8 && CMD_CS1 == 0xfb, + "bb_epaper: cs_mode opcodes changed; the manual chip-select sequence must be revisited"); + +static bool s_geometryOk = false; +static bool s_streamOpen = false; +static bool s_onLeftHalf = true; +static uint32_t s_halfWritten = 0; // bytes into the half currently selected +static bool s_faulted = false; + +// Spectra6 accepts colour codes 0-3, 5 and 6 per nibble; 4 and 7-15 are undefined +// and a panel fed one can latch an unintended waveform. bb_epaper sanitises at +// DRAW time via u8Colors_spectra, but this firmware never draws — it forwards +// host-packed bytes — so the clamp has to happen on the way to the wire. +static uint8_t s_nibbleLut[256]; +static bool s_lutReady = false; + +static void buildNibbleLut(void) { + if (s_lutReady) return; + for (int i = 0; i < 256; i++) { + const uint8_t hi = (uint8_t)((i >> 4) & 0x0f); + const uint8_t lo = (uint8_t)(i & 0x0f); + const uint8_t hiOk = (hi <= 3 || hi == 5 || hi == 6) ? hi : 0; + const uint8_t loOk = (lo <= 3 || lo == 5 || lo == 6) ? lo : 0; + s_nibbleLut[i] = (uint8_t)((hiOk << 4) | loOk); + } + s_lutReady = true; +} + +// One controller's share of the frame: half-width rows at 4bpp, full height. +static inline uint32_t halfPlaneBytes(void) { + return ((uint32_t)bbep.native_width / 4u) * (uint32_t)bbep.native_height; +} + +bool splitPanelUsed(void) { + return (bbep.iFlags & BBEP_SPLIT_BUFFER) != 0; +} + +uint8_t splitPanelCs2Pin(void) { + uint8_t p = globalConfig.displays[0].cs_pin_2; + if (p == 0 || p == 0xFF) return 2; + return p; +} + +bool splitPanelGeometryOk(void) { + return s_geometryOk; +} + +void splitPanelConfigureGeometry(void) { + s_geometryOk = false; + s_streamOpen = false; + s_onLeftHalf = true; + s_halfWritten = 0; + s_faulted = false; + if (!splitPanelUsed()) return; + + const DisplayConfig& d = globalConfig.displays[0]; + if (d.pixel_width != bbep.native_width || d.pixel_height != bbep.native_height || + d.color_scheme != OD_COLOR_SCHEME_BWGBRY_SPLIT) { + od_log_error("ERROR: split panel requires a %dx%d bwgbry_split (8) display config", + bbep.native_width, bbep.native_height); + return; + } + s_geometryOk = true; +} + +void splitPanelInitIo(void) { + const DisplayConfig& d = globalConfig.displays[0]; + // CS2 before initIO: bbepInitIO() finishes by sending the panel's init + // sequence, whose CMD_CS1_CS2 opcodes need iCS2Pin already assigned or the + // shared registers reach only the primary controller. + bbepSetCS2(&bbep, splitPanelCs2Pin()); + odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); + odWatchdogFeed(); // bbepInitIO sends pInitFull internally (~240 s worst case) + bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); +} + +// bbepWriteCmd()/bbepWriteCmdData() open with `if (!is_awake) bbepWakeUp()`, and +// bbepWakeUp() toggles RST -- a hardware reset that would wipe the controller RAM +// we are part-way through filling. By the time any of this runs the panel really +// is awake (bbepInitIO -> bbepSendCMDSequence set the flag), so a zero here can +// only be stale bookkeeping; assert it rather than let it reset us mid-frame. +static inline void assumeAwake(void) { bbep.is_awake = 1; } + +// Select one controller, or neither. cs_mode must already be CMD_CS_NONE, which +// is what stops the library toggling CS underneath us on every write. +static inline void selectControllers(bool cs1, bool cs2) { + digitalWrite(bbep.iCSPin, cs1 ? LOW : HIGH); // upstream: iCSPin IS CS1 (iCS1Pin was deleted) + digitalWrite(bbep.iCS2Pin, cs2 ? LOW : HIGH); +} + +bool splitPanelBeginFrame(void) { + if (s_streamOpen) splitPanelCloseFrame(); + s_onLeftHalf = true; + s_halfWritten = 0; + s_faulted = false; + if (!splitPanelUsed() || !s_geometryOk) return false; + + // Never start blasting a half-plane into a controller that is still finishing + // the previous refresh. + if (bbepIsBusy(&bbep)) bbepWaitBusy(&bbep); + + assumeAwake(); + bbep.cs_mode = CMD_CS_NONE; + selectControllers(true, false); + bbepWriteCmd(&bbep, UC8151_DTM1); // 0x10; also leaves DC HIGH == data mode + s_streamOpen = true; + return true; +} + +// Cross to the second controller: releasing CS1 ends its burst, and the second +// DTM opens the right half-plane. +static bool advanceToRightHalf(void) { + if (!s_streamOpen || !s_onLeftHalf) return false; + selectControllers(false, true); + assumeAwake(); + bbepWriteCmd(&bbep, UC8151_DTM1); + s_onLeftHalf = false; + s_halfWritten = 0; + return true; +} + +void splitPanelSinkBytes(const uint8_t* data, uint32_t len) { + if (!data || len == 0) return; + if (!s_streamOpen) { s_faulted = true; return; } + buildNibbleLut(); + + const uint32_t half = halfPlaneBytes(); + // 512 B: one bbepWriteData -> SPI.transferBytes per chunk. Large enough to + // amortise the per-call overhead across a ~480 KB half-plane, and it swallows + // the boot screen's 300-byte half-rows whole. + uint8_t scratch[512]; + + while (len > 0) { + if (s_halfWritten >= half) { + if (s_onLeftHalf) { + if (!advanceToRightHalf()) { s_faulted = true; return; } + } else { + // More image than the panel has pixels. The bufferless shim this + // replaces dropped these silently and still reported success. + od_log_warn("Split panel: %lu excess image bytes dropped", (unsigned long)len); + s_faulted = true; + return; + } + } + // uint32_t throughout: the predecessor computed this run length in a + // uint16_t, so any chunk of 65536 bytes truncated to zero and spun forever. + uint32_t run = half - s_halfWritten; + if (run > len) run = len; + if (run > sizeof(scratch)) run = (uint32_t)sizeof(scratch); + + for (uint32_t i = 0; i < run; i++) scratch[i] = s_nibbleLut[data[i]]; + bbepWriteData(&bbep, scratch, (int)run); // CS_NONE: pure byte burst, no CS, no DC + + data += run; + len -= run; + s_halfWritten += run; + } +} + +bool splitPanelCloseFrame(void) { + if (!s_streamOpen) return false; + selectControllers(false, false); + // Unconditional restore, matching bbepWriteImage4bppDual's epilogue: handing + // CMD_CS_NONE back to library code that expects to drive CS itself means its + // writes reach no controller, silently. + bbep.cs_mode = CMD_CS1; + s_streamOpen = false; + + const bool complete = !s_onLeftHalf && s_halfWritten >= halfPlaneBytes(); + if (s_faulted) { + od_log_error("ERROR: split panel frame faulted; refusing to refresh"); + return false; + } + if (!complete) { + // Short frame. The caller's byte-count check catches a client that + // under-declares its total, but not a frame cut short by a geometry + // mismatch or a dropped transport. + od_log_error("ERROR: split panel frame short (half %d, %lu of %lu bytes)", + s_onLeftHalf ? 0 : 1, (unsigned long)s_halfWritten, + (unsigned long)halfPlaneBytes()); + return false; + } + return true; +} + +void splitPanelPowerOff(void) { + if (!splitPanelUsed()) return; + // POFF to both controllers, then wait for the HV rails to bleed down before + // anything considers cutting VDD. + bbep.cs_mode = CMD_CS1_CS2; + bbepCMD2(&bbep, UC8151_POFF, 0x00); + bbepWaitBusy(&bbep); + bbep.cs_mode = CMD_CS1; +} diff --git a/src/split_panel.h b/src/split_panel.h new file mode 100644 index 0000000..eb35e21 --- /dev/null +++ b/src/split_panel.h @@ -0,0 +1,92 @@ +#ifndef SPLIT_PANEL_H +#define SPLIT_PANEL_H + +#include +#include + +// Dual-controller ("split buffer") e-paper panels — a single glass driven by two +// controllers on separate chip-selects, each owning one half of every row. +// Currently the Seeed reTerminal E1004's 13.3" 1200x1600 Spectra6 (T133A01) and +// the 8.1" 1024x576 Spectra6. +// +// Selection is entirely at RUNTIME: splitPanelUsed() tests BBEP_SPLIT_BUFFER on +// bbep.iFlags, which bbepSetPanelType() fills in from the panel table for whatever +// panel_ic_type the device config names. There is no build flag — this code is +// compiled into every target, and a device that is not configured for a split +// panel simply never enters it. The predecessor of this file was gated on +// BBEP_T133A01, a *bb_epaper* define rather than a firmware one, so repinning the +// library silently compiled the whole panel away while the build still succeeded. +// +// BUFFERLESS. Image bytes go straight from the transport to SPI; nothing here +// allocates a framebuffer. bb_epaper's own dual-controller writer +// (bbepWriteImage4bppDual, reached via bbepWritePlane) requires a 960 KB +// ucScreen, so this drives the data phase directly from the library's CS +// primitives instead — cs_mode = CMD_CS_NONE, manual chip-select, one DTM per +// controller, chunked bbepWriteData. That is the same idiom bbepWriteImage4bppDual +// uses internally; only the byte source differs. +// +// The other three phases need no help: the panel's init sequence carries +// CMD_CS1/CMD_CS1_CS2 opcodes, and bbepRefresh()/bbepSleep() broadcast DRF/POFF to +// both controllers off the BBEP_SPLIT_BUFFER flag whether or not a buffer exists. +// +// The limengdu bb_epaper fork exported bbepStartDataStream/WriteDataStreamByte/ +// EndDataStream for exactly this; upstream removed them, which is what this +// module replaces. + +/** True when the configured panel is dual-controller. Valid once bbepSetPanelType() has run. */ +bool splitPanelUsed(void); + +/** CS2 GPIO from the device config (displays[0].cs_pin_2), defaulting to 2. */ +uint8_t splitPanelCs2Pin(void); + +/** + * Cold bring-up: set CS2, then bbepInitIO(), which sends the panel's init + * sequence — whose embedded CMD_CS1_CS2 opcodes need CS2 already configured, hence + * the ordering. + */ +void splitPanelInitIo(void); + +/** + * Validate config geometry against the panel. Call from + * configureBbepPanelGeometry() after bbepSetPanelType(). Sets the flag read by + * splitPanelGeometryOk(). + */ +void splitPanelConfigureGeometry(void); + +/** True when config dimensions and colour scheme match the panel. */ +bool splitPanelGeometryOk(void); + +/** + * Open the frame: wait out any live refresh, take manual control of both chip + * selects, and send the first DTM to the primary controller. Every exit path owes + * a splitPanelCloseFrame() — leaving cs_mode at CMD_CS_NONE makes every + * subsequent library write silently reach no controller at all. + */ +bool splitPanelBeginFrame(void); + +/** + * Stream image bytes in OD_COLOR_SCHEME_BWGBRY_SPLIT wire order — the entire left + * half-plane (h rows of native_width/4 bytes) followed by the entire right + * half-plane. Crossing to the second controller happens here, at the half + * boundary. Bytes past the end of the frame are dropped and the frame is flagged. + */ +void splitPanelSinkBytes(const uint8_t* data, uint32_t len); + +/** + * Release both chip selects and restore cs_mode. Idempotent, and safe to call on + * teardown paths that never opened a frame. Returns true only when a complete, + * unfaulted frame was streamed — callers refreshing the panel should treat false + * as "do not refresh", so a short or overrun frame cannot reach the glass as a + * successful update. + */ +bool splitPanelCloseFrame(void); + +/** + * Power both controllers down after a refresh has completed. bb_epaper's C + * bbepRefresh() issues DRF and returns without waiting or powering off (only the + * C++ BBEPAPER::refresh wrapper sends POFF, and this firmware does not use it), + * so without this the HV rails stay up for the whole keep-alive window. + */ +void splitPanelPowerOff(void); + +#endif