Skip to content

feat: RTC power-off sleep via PCF8563/BM8563 and the MOSFET latch (M5Paper) - #176

Open
gcormier wants to merge 23 commits into
OpenDisplay:mainfrom
gcormier:greg/awesome-dijkstra-5pq3ha-rtc
Open

gcormier wants to merge 23 commits into
OpenDisplay:mainfrom
gcormier:greg/awesome-dijkstra-5pq3ha-rtc

Conversation

@gcormier

@gcormier gcormier commented Oct 28, 2026 •

Copy link
Copy Markdown

Stacked on #175. This branch includes #175's commits. The changes specific to this PR are in git diff greg/awesome-dijkstra-5pq3ha...greg/awesome-dijkstra-5pq3ha-rtc (10 files). I'll rebase or retarget once #175 merges.

Adds RTC power-off sleep for boards whose MOSFET power latch can be re-closed by an RTC interrupt, as on the M5Paper. Instead of keeping the ESP32 in deep sleep, the firmware arms the RTC, drops the latch, and the whole board is unpowered until the RTC INT turns it back on.

Changes

  • src/rtc_pcf8563.{h,cpp}: a new PCF8563/BM8563 driver (I2C 0x51). Sleep lengths up to 255 s use the 1 Hz countdown, up to 255 min the 1/60 Hz countdown, and anything longer the minute/hour alarm (up to about 18 h). rtcConsumeWake() reads and clears the wake flags on boot.
  • include/opendisplay_structs.h, tools/od-device-cli.py: add OD_SENSOR_TYPE_PCF8563 = 8. This is a backward-compatible enum addition with no MSD data, but it still needs to land in opendisplay-protocol.
  • src/main.cpp, src/power_latch.{h,cpp}: on deep sleep with a PCF8563 sensor and the MOSFET latch configured, arm the RTC and release the latch (powerLatchReleaseForRtcSleep()). On USB the rail stays up, so this falls back to ESP32 deep sleep with the same timer. m5paper_init() leaves the RTC flags alone when the RTC wake is configured, so rtcConsumeWake() still sees them.
  • src/device_control.cpp: CMD_POWER_OFF (0x0052) now also works on the MOSFET latch, not just the D-FF latch. The 0x0052 @targets/@errors text in opendisplay-protocol should be updated to match.
  • docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md: the full hardware bring-up report for both PRs. Drop it if you'd rather not keep it in the repo.

Testing

Hardware: an original M5Paper on battery, configured with the RTC preset from the matching opendisplay.org PR (120 s sleep, 20 s advertising window).

  • RTC power-off on battery at 240 MHz: 20 sleeps, 19 wakes plus 1 pending, none missed. The image stays on screen; RTC wakes don't redraw the boot screen (a gray image survived about 1.5 h, about 38 wakes).
  • On battery at 80 MHz (after feat: original M5Stack M5Paper support (panel IC 3005, FastEPD IT8951) #175's change): 28 wake cycles in 67 min, plus an upload in a wake window.
  • 600 s on USB: the RTC 1/60 Hz countdown fired before the ESP32 timer.
  • 16000 s (alarm path): fired at the expected minute on USB. On battery, a one-time 0x0053 sleep of 16000 s woke the board 15991 s later. It then resumed the 120 s cycle.
  • CMD_POWER_OFF (0x0052) on battery cuts power. A press of at least about 1.1 s on the power button turns the board back on; most of that is ESP32 boot before m5paper_init() can take the GPIO2 hold.
  • All 12 CI targets build.

Not tested:

  • Off-current or wake-window current (no meter).
  • Battery voltage against a multimeter. The preset uses factor 0xAE, calibrated at full charge only.
  • Any board other than the M5Paper.

Toolbox preset: OpenDisplay/opendisplay.org#188

gcormier and others added 23 commits October 26, 2026 00:56
… path)

The M5Paper (classic ESP32, 16 MB flash, 8 MB PSRAM) drives its 4.7"
ED047TC1 960x540 panel through an IT8951 over SPI, the same FastEPD path
the ED103TC2 (reTerminal E1003) already uses. No new PlatformIO env: it
runs the existing esp32-wrover-e-N4R8 build.

- vendored structs header: PanelIC 3005 OD_PANEL_IC_M5PAPER_960X540
  (Unreleased changelog entry; canonical opendisplay-protocol change to
  follow -- the vendored copy is already ahead of canonical)
- fastepd_driver_used(): include 3005 in the IT8951 set
- IT8951 pins for 3005: SPI from the display block (M5Paper defaults
  12/13/14/15, busy 27); reset_pin (GPIO23, the IT8951 supply gate --
  reset is not wired) doubles as RST/EN/ITE_EN, matching FastEPD's own
  BB_PANEL_M5PAPER definition. pwr_pin_2/3 stay the power latch.
- panel size: explicit 960x540 unmirrored for 3005 (FastEPD 2.2.0 has no
  BBEP_DISPLAY_ED047TC1)
- m5paper_init(), keyed off panel IC 3005 rather than a device_flags bit
  (bit 5 is already used differently across firmware families): drives
  GPIO2 (main-power hold) and GPIO5 (EXT power) HIGH before
  powerLatchBegin(). Deep-sleep hold and power-off of GPIO2 use the
  existing MOSFET latch (pwr_latch, pwr_pin_2 = 2).

Not yet validated on hardware. Without an RTC wake the board holds its
rail through ESP32 deep sleep; RTC power-off follows separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZwG6PyfGku5uKoVTyB9MC
On boards whose RTC INT can switch the rail back on (M5Paper: BM8563 on
I2C 0x51, GPIO2 main-power hold), deep sleep now arms the RTC and
releases the power latch instead of holding the rail through ESP32 deep
sleep, so the off-state draw is just the RTC.

- SensorType 8 OD_SENSOR_TYPE_PCF8563 (vendored structs header,
  od-device-cli VALID_ENUMS). Active only together with the MOSFET latch
  (pwr_latch, pwr_pin_2). Publishes no MSD data.
- src/rtc_pcf8563.*: arm a wake (<=255 s: 1 Hz countdown; <=255 min:
  1/60 Hz countdown, rounded up; longer, up to the 18 h max of
  deep_sleep_time_seconds: minute+hour alarm) and consume it at boot.
  Fixed register I/O, no heap.
- enterDeepSleep(): rtcArmWake() after the ESP32 timer arm; on success
  powerLatchReleaseForRtcSleep() drops the hold (rail falls on battery),
  else the existing hold-and-deep-sleep path. The ESP32 timer stays
  armed, so a USB-powered board (rail stays up) still wakes on time.
- setup(): after full_config_init() (power hold already asserted), always
  clear the RTC flags so INT releases; if the timer/alarm had fired, treat
  the cold boot as a timer wake (no boot-screen redraw, wake advertising
  window, rebootFlag cleared). displayed_etag is lost with the rail, so
  the first update after a power-off is a full image (partial NACKs with
  ETAG_MISMATCH).

Not yet validated on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZwG6PyfGku5uKoVTyB9MC
…ut-only touch INT

Display: FastEPD 2.2.0 loads an IT8951 frame L_ENDIAN unless the panel is
MIRROR_X, which swaps the two bytes of every 16-bit word. On the M5Paper (the
only unmirrored IT8951 panel) 4bpp pixel pairs landed 2 px off, so the image
looked interlaced / shifted horizontally. Full refreshes on unmirrored IT8951
panels now go through it8951_full_write_unmirrored() (same frame and GC16
waveform as FastEPD's writer, loaded B_ENDIAN as M5Stack's M5EPD driver does);
the full-screen DU path uses B_ENDIAN too. Mirrored panels (ED103TC2) keep
FastEPD's own path, unchanged.

Touch: the M5Paper GT911 INT is GPIO36, an input-only pad with no internal
pull-up. The INT wake pulse and address-select reset drove it as an output and
INPUT_PULLUP failed, logging GPIO errors on every boot and EPD refresh. Skip
driving input-only pads and read them as plain INPUT.

Verified on an original M5Paper (ESP32-D0WDQ6-V3, USB config): 960x540 4bpp
test image sharp (shifted before the fix), orientation correct, gray steps
0 (black) to 15 (white) look right; no GPIO errors at boot; GT911 taps at all
four corners and the centre map correctly with touch flags SWAP_XY|INVERT_Y
(0x06); rocker 37/38/39 press and release. Not verified on hardware: 1bpp
(mono) and the DU fast path. The mirrored (reTerminal) path is unchanged.
Results of validating PR 1 (panel 3005 / m5paper_init) and PR 2 (RTC
power-off) on an original M5Paper (ESP32-D0WDQ6-V3, CP2104).

Verified on hardware: boot and FastEPD init; 4bpp image after the IT8951
endian fix; touch (flags 0x06) and rocker buttons; battery hold on GPIO2;
RTC sleep on USB (fallback path) and RTC power-off on battery at 120 s
(20 arms, 19 wakes plus 1 pending, 0 missed); 600 s countdown fire; 16000 s
alarm programming by register readback; full image push in a wake window.

Not verified: mono and the DU path on screen, battery voltage calibration,
off current, power-button hold time, absence of a boot-screen redraw on
battery wakes (observed only on USB), and the actual 16000 s alarm fire.
The BM8563 at 0x51 runs from VBAT_IN and its INT line pulls the M5Paper's
power-switch gate low. A timer or alarm left armed (M5Stack's factory
firmware leaves a repeating timer, and it survives a flash erase) switched
the board back on 60-75 s after every reset-button power-off on battery.
m5paper_init() now turns AIE/TIE off, clears AF/TF, stops the timer and
disables the alarm on every boot, whatever the sensor config says.

Verified on hardware (M5Paper, USB config): with the RTC armed by the
previous firmware (C2=01, TC=82, 18 s left), boot logged the clear and a
readback (temporary, not committed) showed C2=00, TC=03, alarm disabled.

Not verified: absence of the phantom power-on after a battery power-off
with this build (needs someone at the device).
… for rtcConsumeWake

0x0052 used to NACK unless the board had the D-FF latch. With
DEVICE_FLAG_BATTERY_LATCH it now queues the ACK and calls
powerLatchTriggerOff(), the same rail cut as the long-press power button:
pwr_pin_2 (GPIO2 on the M5Paper) goes LOW and the ESP32 parks in deep
sleep, which only matters when USB keeps the rail up.

m5paper_init() (merged from the PR 1 branch) clears the on-board RTC on
every boot. When RTC power-off is configured it now leaves that to
rtcConsumeWake(), which needs the flags to recognise an RTC wake.

The vendored opendisplay_protocol.h still says 0x0052 is D-FF only. No wire
bytes change; the contract text needs updating upstream.

Verified on hardware (M5Paper, RTC config, USB power): boot did not run
the m5paper_init() RTC clear; 0x0052 over BLE was logged, then serial went
silent and BLE advertising stopped for 60 s (the scanner sees the running
board every ~6 s); the reset button brought it back to a normal boot.

Not verified: 0x0052 on battery (the rail drop itself), the ACK reaching
the host (fire-and-forget, none received), and an RTC wake on battery with
this build.
…of sending gray data as 1bpp

it8951_fullscreen_du() always loads w/8 bytes per row from the framebuffer
and shows them with the 1bpp DU waveform. In 4bpp mode (GRAY16, or the
ED103TC2 panel) that framebuffer holds two pixels per byte, so a FAST
refresh put the top quarter of the gray buffer on screen as 1bpp noise.
The parallel path already falls back to a full refresh for 4bpp; the
IT8951 path now does the same, for both direct and partial transfers.

Verified on hardware (M5Paper, GRAY16 config): with a temporary IT8951
memory readback, a FAST upload before this fix left the raw 4bpp bytes in
the 1bpp image area (2400/2400 sampled bytes equal to the 4bpp buffer).
With this fix the same FAST upload logs "EPD refresh: FAST" and then takes
the full 4bpp path: 19200/19200 sampled pixels in IT8951 memory match the
framebuffer, and nothing is loaded through the 1bpp DU path.

Also verified by readback (no code change needed): 4bpp full, 1bpp full and
1bpp DU on the M5Paper load the frame byte-for-byte as sent (B_ENDIAN).
Not verified: the ED103TC2 (reTerminal), which takes the same branch; the
on-screen 1bpp bit order (needs eyes); partial transfers in 4bpp mode, which
still blit the rect as 1bpp (the M5Paper preset has partial updates off).
…factor 0xAE

Adds the results of the second round of hardware tests to the bring-up report:
IT8951 memory readback for 4bpp full, 1bpp full and 1bpp DU loads; the 4bpp
FAST-refresh bug and its fix (27016a4); the 16000 s RTC alarm fire on USB
(AF set at the expected minute, the ESP32 woke on its own timer as expected on
USB); and the 2:1 battery divider measured at full charge (factor 0xAE).

Documentation only. Not verified on hardware: the 16000 s wake on battery,
the 1bpp bit order on screen, and the battery reading against a multimeter.
The IT8951 1bpp mode takes the leftmost pixel from bit 0 of each byte;
FastEPD packs it in bit 7. Mirrored panels (ED103TC2) never showed this,
because FastEPD reverses the byte order of each row for them, which together
with LSB-first mirrors the whole row. On the unmirrored M5Paper every 8 px
group came out mirrored, so mono images were unreadable. Full 1bpp writes and
the 1bpp DU path now send each byte bit-reversed, through a 64-byte stack
buffer. 4bpp and mirrored panels are unchanged.

Verified on hardware (M5Paper, mono config): before the fix a full mono
image was unreadable. A test image with four bands pre-scrambled on the host
(none, bits reversed in each byte, bytes swapped, both) showed only the
bit-reversed band cleanly. After the fix a full refresh of a text/ruler/1 px
line test image is clean, and a FAST (DU) refresh is readable with the usual
DU ghosting and incomplete fill of large black areas.

Not verified: the ED103TC2 (reTerminal), whose mirrored path is unchanged;
1bpp partial (region) refresh.
Records the 1bpp bit-order bug (IT8951 takes the leftmost pixel from bit 0)
found with a four-band pre-scrambled test image, and the fix in 34548b0.

Documentation only. Verified on hardware: full mono refresh clean, FAST (DU)
refresh readable with normal DU ghosting. Not verified: the ED103TC2.
Documentation only. Verified on hardware (M5Paper, RTC config, battery): 0x0052
over BLE stopped advertising within 9 s, the board stayed off for 20 min with
no phantom power-on, and a 2 s rocker push powered it back on. Not verified:
the ACK reaching the host, and the 2 s hold as a minimum (shorter holds untested).
Documentation only. Verified on hardware: reset-to-GPIO2-latch timed over
serial at 1.07 s (3 runs, USB, EN reset); on battery 0.3 s and 1 s rocker holds
did not power on, 5 s did, including 15 s after 0x0052. Not verified: the
exact threshold on battery (hand-timed holds), and a true power-on reset as
opposed to an EN reset.
m5paper_init() drove GPIO5 HIGH, which turns on the 5 V boost for the Grove
ports (M5EPD's default). Nothing here uses the ports, so GPIO5 is now driven
LOW. The CPU also drops to 80 MHz right after the GPIO2 power hold: BLE needs
no more, and a battery wake window is mostly idle polling. Switching after the
hold keeps the power-button hold time unchanged.

Verified on hardware (M5Paper, USB): reset to GPIO2 hold 1068 ms (1073 ms
before), boot log "CPU 80 MHz". A 259200-byte gray upload took 21.72 s on the
device (21.76 s at 240 MHz, BLE-bound) and its refresh 1.49 s (1.47 s). Touch
still works with EXT power off (a corner tap read x=18, y=51). Cost: the
cold-boot QR boot screen renders in 8.2 s instead of 4.2 s; RTC wakes do not
draw it.

Not verified: the current saving itself (no meter), anything plugged into a
Grove port (now unpowered; enabling it needs a config option that does not
exist yet).
Documentation only. Verified on hardware: a gray image survived about 1.5 h
of battery RTC wakes (no boot-screen redraw); the 80 MHz / EXT-power-off build
boots, sleeps and wakes with the RTC config on USB, uploads at the same speed
and keeps touch. Not verified: the current saving (no meter), and the 80 MHz
build on battery.
Comment only, no code change. Not verified on hardware (nothing to verify).
Docs only. Verified on hardware (M5Paper on battery, 496255d): an
upload in a wake window, and 28 RTC wake cycles in 67 min with no gap
longer than two cycles. Not verified: wake-window current (no meter),
whether the two-cycle gaps were scanner misses or skipped wakes.
Docs only. Verified on hardware (M5Paper on battery, 496255d): after a
one-time 16000 s sleep via 0x0053 the device advertised again 15991 s
later, then resumed the 120 s cycle. Not verified: current draw (no
meter).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant