E1004 / dual-controller panels: pin bb_epaper upstream, replace the fork shim with a runtime-gated bufferless module - #147
Merged
Conversation
…04 env bb_epaper has no releases, so the unpinned `[env]` URL meant CI and local builds each got whatever HEAD was at install time. Locally that was c651b2a while the source already carried a workaround for 71f6e70 (see epdAlignCustomPartialRamMode) -- a commit that was not in the installed library. Pinning closes that divergence rather than creating one. 5dccfbb is "Added support for the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel", which replaces the limengdu fork pin the E1004 env carried for PR bitbank2#32. The esp32-s3-E1004 env goes with it: it was the same hardware as esp32-s3-N32R8-extuart, and the panel is selected at runtime by panel_ic_type, not at build time. CI matrix drops 12 -> 11 targets. NOTE: E1004 hardware built from esp32-s3-N32R8-extuart now runs a 9-bit (512 B) zlib window instead of 15-bit. That is a hard reject at the zlib header for any stream declaring a larger window (od_zlib_stream.c:641), not a slower decode -- deliberate, but worth knowing if a client ever compressed with wbits=15 for this target.
Replaces the E1004 shim in display_service.cpp with src/split_panel.{h,cpp}.
The shim was guarded by `#ifdef BBEP_T133A01` -- a *bb_epaper* define from
the limengdu fork, not a firmware one. Repinning to upstream removed the
define, so all 12 guarded regions compiled to nothing while
`pio run -e esp32-s3-E1004` still reported SUCCESS: firmware that could not
drive the panel, with no error and no log line. The new module is gated at
runtime on `bbep.iFlags & BBEP_SPLIT_BUFFER`, so panel support cannot
disappear with a library change, and static_asserts turn a future rename
into a compile error on every target.
Still bufferless: upstream's own dual-CS writer (bbepWriteImage4bppDual, via
bbepWritePlane) needs a 960 KB ucScreen, so the data phase is driven from the
library's own primitives instead -- cs_mode = CMD_CS_NONE, manual chip-select,
one DTM per controller, chunked bbepWriteData. Same idiom the library uses
internally; only the byte source differs. The other three phases need no help:
the init sequence carries CMD_CS1/CMD_CS1_CS2 opcodes, and bbepRefresh/
bbepSleep broadcast DRF/POFF off the SPLIT_BUFFER flag.
Keying on the flag rather than a panel id also covers EP81_SPECTRA_1024x576
(wire 0x2B), which previously took the generic path and drove only its left
controller.
Fixed rather than carried over from the shim:
- run length computed in uint16_t, so a >=65536-byte chunk truncated to 0
and spun forever in a loop with no watchdog feed
- truncated/overrun frames dropped silently and still refreshed as success;
splitPanelCloseFrame now returns a verdict and both refresh sites honour it
- no bound in the raw write path, and no busy gate before streaming
- waitforrefresh's E1004 shortcut assumed bbepRefresh waited. True of the
fork, false upstream -- it could report success before BUSY even asserted
and let the rail be cut mid-refresh
- no POFF after refresh: the C bbepRefresh issues DRF and returns, and only
the unused C++ BBEPAPER::refresh wrapper powers down
Also drops three branches that are now dead: the `|| splitPanelUsed()` in both
partial-update NACKs (bwgbry_split reports 4 bpp, so the bpp test already
excludes them) and the forced REFRESH_FULL (the Spectra6 table rows carry a
NULL pInitFast, so bbepRefresh falls back to pInitFull and both modes emit an
identical sequence).
boot_screen keeps its two half-passes -- it already emits in the sink's wire
order, so it just feeds it, and painting half a frame at a time still avoids
rasterising 1200x1600 twice.
All 11 CI targets build (incl. esp32-wrover-e-N4R8, absent from default_envs).
Cost ~1.6 KB flash per target, no RAM.
UNVERIFIED ON HARDWARE. A clean build proves linkage, not function -- that is
exactly what the old gate got wrong. Open bench questions: whether the init
sequence bbepRefresh re-sends leaves streamed RAM intact (it has no EPD_RESET,
so it should), whether holding CS across the boot screen's rasterisation is
tolerated, and the 8.1" behaviour change above.
Debug-level logging on the E1004's hardware. Mirrors esp32-s3-N16R8-extuart-debug: extends the parent env and adds -DOD_LOG_LEVEL=OD_LOG_DEBUG, nothing else. Deliberately absent from default_envs and .github/firmware-targets.json, as both existing -debug envs are -- this is a bench build, not a shipped target, so CI stays at 11 and the release matrix is unchanged. Worth having on this env specifically: -extuart routes logging to GPIO43/44 and prints no panic backtrace over USB, so debug logs are the only running commentary during dual-controller panel bring-up. +12,116 B flash, +16 B RAM over the parent (in line with the N16R8 pair's +12,492). All 16 envs build.
configureDisplayPinsLowPower() drove cs_pin, clk, data, dc and reset low on every pwrmgm(false) but left cs_pin_2 out, so on dual-controller panels the second chip select 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. Guarded separately rather than added to the pins[] array: that loop skips only 0xFF, and an unconfigured cs_pin_2 reads 0 on most devices, which would drive GPIO0 -- a strapping pin -- low on the way down. The 0/0xFF guard matches what the auxPins loop below already does. Keyed on the pin being configured, not on splitPanelUsed(): main.cpp has no business knowing the panel class, and a configured cs_pin_2 means a second controller regardless. Scope: this closes the pin state at rail cut, not through deep sleep. Nothing calls gpio_hold_en on the panel bus (only the power-latch pin is held), so every one of these pads still reverts at sleep entry. That is a separate, board-wide question needing the schematic or an ammeter.
CI builds 11 environments, not 12, and default_envs lists 10, not 11 -- esp32-s3-E1004 is gone (same hardware as esp32-s3-N32R8-extuart; the panel is selected at runtime by panel_ic_type). The esp32-wrover-e-N4R8 caveat is unchanged: it still ships and is still absent from default_envs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
esp32-s3-E1004pinned a fork of bb_epaper (limengdu, PR bitbank2#32) for T133A01 support. That support has since landed upstream in5dccfbb, so the fork pin can go.Repinning on its own is not safe, and this is the important part: the E1004 code was guarded by
#ifdef BBEP_T133A01— a bb_epaper library define, not a firmware one. Upstream does not define it, so moving the pin compiled every E1004 path out of the binary whilepio run -e esp32-s3-E1004still reported SUCCESS. Firmware that cannot drive the panel, with no build error and no log line.This PR repins, deletes the fork shim, and replaces it with a module that cannot fail that way.
What changed
Pin + env — all envs pin
bitbank2/bb_epaper.git#5dccfbb. The[env]URL was previously unpinned, so CI and local builds each got whatever HEAD was at install time; locally that wasc651b2a, while the source already carried a workaround for71f6e70(seeepdAlignCustomPartialRamMode) — a commit not in the installed library. Pinning closes that divergence rather than creating one.The
esp32-s3-E1004env is deleted. It was the same hardware asesp32-s3-N32R8-extuart, and the panel is selected at runtime bypanel_ic_type. CI matrix 12 → 11.New
src/split_panel.{h,cpp}— gated at runtime onbbep.iFlags & BBEP_SPLIT_BUFFER, withstatic_asserts that turn a future library rename into a compile error on every target instead of a silent no-op.Still bufferless. Upstream's own dual-CS writer (
bbepWriteImage4bppDual, viabbepWritePlane) requires a 960 KBucScreen; the data phase is driven from the library's own primitives instead —cs_mode = CMD_CS_NONE, manual chip-select, one DTM per controller, chunkedbbepWriteData. That is the same idiombbepWriteImage4bppDualuses internally; only the byte source differs. The other three phases need no help: the init sequence carriesCMD_CS1/CMD_CS1_CS2opcodes, andbbepRefresh/bbepSleepbroadcast DRF/POFF off theSPLIT_BUFFERflag.Keying on the flag rather than a panel id also covers
EP81_SPECTRA_1024x576(wire0x2B), which previously took the generic path and drove only its left controller.Bugs fixed rather than carried over
uint16_t, so a chunk of ≥65536 bytes truncated to 0 and spun forever in a loop with no watchdog feedsplitPanelCloseFramenow returns a verdict and both refresh sites honour itwaitforrefreshhad an E1004 shortcut assumingbbepRefreshwaited. True of the fork, false upstream — it could report success before BUSY even asserted and let the rail be cut mid-refreshbbepRefreshissues DRF and returns; only the unused C++BBEPAPER::refreshwrapper powers downcs_pin_2was absent fromconfigureDisplayPinsLowPower(), leaving the second chip select driven HIGH into an unpowered controller at rail cutAlso drops three branches that are now dead: the
|| splitPanelUsed()in both partial-update NACKs (bwgbry_split reports 4 bpp, so the bpp test already excludes them) and the forcedREFRESH_FULL(the Spectra6 table rows carry a NULLpInitFast, sobbepRefreshfalls back topInitFulland both modes emit an identical sequence).Also
esp32-s3-N32R8-extuart-debugbench env, mirroring the N16R8 pair. Not indefault_envsorfirmware-targets.json, like the other-debugenvs.CLAUDE.mdenv counts corrected (12 → 11, 11 → 10).Cost
~1.6 KB flash per target, no RAM. All 11 CI targets build, plus all 16 local envs.
Hardware verification
Full image push cycle verified on an E1004. That exercises the parts that could not be settled by inspection:
bbepRefreshre-sends after the data write. It contains noEPD_RESETand ends with PON + CCSET, so it acts as the pre-DRF preamble rather than a reset — confirmed not to disturb the streamed RAM. Worth noting the fork solved the same problem the opposite way, suppressing re-init, because its init table began with a reset; the two libraries are not interchangeable here by inspection.waitforrefreshshortcutNot verified: the 8.1"
EP81_SPECTRA_1024x576(wire0x2B). It now takes this path off theBBEP_SPLIT_BUFFERflag rather than the generic single-CS path, where it drove only its left controller. That is a fix on paper, but it is an untested behaviour change and no such unit was available. A reviewer with one should exercise it before this merges, or say so and I will gate it back to the E1004 panel id only.