From ad0b05850850028407a273611777927fab93358d Mon Sep 17 00:00:00 2001 From: gcormier Date: Mon, 26 Oct 2026 00:56:35 +0000 Subject: [PATCH 01/17] feat: original M5Stack M5Paper support (panel-IC 3005, FastEPD IT8951 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 Claude-Session: https://claude.ai/code/session_01KZwG6PyfGku5uKoVTyB9MC --- include/opendisplay_structs.h | 10 ++++++++-- src/config_parser.cpp | 9 +++++++++ src/display_fastepd.cpp | 23 ++++++++++++++++++++++- src/display_service.cpp | 5 +++-- src/main.cpp | 23 +++++++++++++++++++++++ src/main.h | 1 + 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index eac6917..33dcc7d 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -21,7 +21,7 @@ * * OD_STRUCTS_VERSION 2.1 (MAJOR.MINOR spec marker for THIS file; see * VERSIONING POLICY below -- NOT sent on the wire) - * LAST CHANGED 2026-08-30 + * LAST CHANGED 2026-10-26 * * The two version schemes carried here are DISTINCT (see §6 Q2 of the plan): * - OD_STRUCTS_VERSION_* : documents this spec file, like protocol.h's @@ -88,6 +88,11 @@ * - SensorType 6 is nPM1300 (already used by another OpenDisplay firmware * family); SensorType 7 is AHT20, publishing the same 3-byte * temperature/humidity MSD value as SHT40. GPIO40 gates its supply. + * - PanelIC 3005: OD_PANEL_IC_M5PAPER_960X540 (original M5Stack M5Paper, + * ED047TC1 960x540 behind an IT8951 over SPI; FastEPD IT8951 path). + * Backward-compatible enum addition. Firmware treats the panel type as + * identifying the board (as with 3002-3004) and drives the M5Paper's + * main-power hold and external-power enable itself. * - Add each payload-layout change here as it lands. On the next version bump, * move these under a new "MAJOR.MINOR (YYYY-MM-DD)" heading. * @@ -704,7 +709,8 @@ enum PanelIC { OD_PANEL_IC_ED103TC2_1872X1404_4GRAY = 3001, /**< @doc "same panel as 3000; 4bpp (16-level gray via FastEPD)" */ OD_PANEL_IC_INKPLATE5V2_1280X720 = 3002, /**< @doc "Soldered Inkplate 5 V2 (ED050WROW, 1280x720, 1bpp; FastEPD native parallel path)" */ OD_PANEL_IC_INKPLATE10_1200X825 = 3003, /**< @doc "Soldered Inkplate 10 (ED097TC2, 1200x825, 1bpp; FastEPD native parallel path)" */ - OD_PANEL_IC_M5PAPERS3_960X540 = 3004 /**< @doc "M5Stack PaperS3 (ED047TC1-class, 960x540, 16-gray capable; FastEPD native parallel path)" @since 1.5 */ + OD_PANEL_IC_M5PAPERS3_960X540 = 3004, /**< @doc "M5Stack PaperS3 (ED047TC1-class, 960x540, 16-gray capable; FastEPD native parallel path)" @since 1.5 */ + OD_PANEL_IC_M5PAPER_960X540 = 3005 /**< @doc "original M5Stack M5Paper (ED047TC1 960x540 + IT8951 over SPI, 16-gray capable; FastEPD IT8951 path)" */ }; /* DisplayConfig.transmission_modes @bits TransmissionModes (bits 5-6 reserved -- diff --git a/src/config_parser.cpp b/src/config_parser.cpp index a587609..b9a6482 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -55,6 +55,7 @@ void xiaoinit(); void powerDownExternalFlashFromConfig(void); void ws_pp_init(); void nm420_init(); +void m5paper_init(); extern bool encryptionInitialized; // Defined in main.h (the single-inclusion globals header), so it needs an extern @@ -958,6 +959,14 @@ void full_config_init() { nm420_init(); od_log_info("nm420_init() completed"); } + // The M5Paper panel type identifies the board. Before powerLatchBegin(): + // that releases the deep-sleep pad hold on pwr_pin_2, and on the M5Paper + // that pin (GPIO2) must already be driven HIGH or the board loses power + // on battery. + if (globalConfig.loaded && globalConfig.display_count > 0 && + globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_M5PAPER_960X540) { + m5paper_init(); + } // Must run after config load: latch pins/flag come from globalConfig. powerLatchBegin(); } else { diff --git a/src/display_fastepd.cpp b/src/display_fastepd.cpp index 7c8965e..8118863 100644 --- a/src/display_fastepd.cpp +++ b/src/display_fastepd.cpp @@ -82,6 +82,20 @@ void opendisplay_fastepd_load_pins_from_display(const struct DisplayConfig* d, c s_ite_en = fastepd_aux_pin(sys->pwr_pin_3, 21); } break; + case OD_PANEL_IC_M5PAPER_960X540: + // M5Paper: IT8951 reset is not wired to the ESP32; GPIO23 gates the + // IT8951 supply, so reset_pin doubles as RST/EN/ITE_EN (same as + // FastEPD's own BB_PANEL_M5PAPER definition). pwr_pin_2/pwr_pin_3 + // are the board's power latch here, never panel enables. + s_sclk = (d->clk_pin != 0xFF) ? (int8_t)d->clk_pin : 14; + s_mosi = (d->data_pin != 0xFF) ? (int8_t)d->data_pin : 12; + s_miso = (d->dc_pin != 0xFF) ? (int8_t)d->dc_pin : 13; + s_cs = (d->cs_pin != 0xFF) ? (int8_t)d->cs_pin : 15; + s_busy = (d->busy_pin != 0xFF) ? (int8_t)d->busy_pin : 27; + s_rst = (d->reset_pin != 0xFF) ? (int8_t)d->reset_pin : 23; + s_en = s_rst; + s_ite_en = s_rst; + break; default: break; } @@ -260,7 +274,14 @@ void fastepd_epaper_begin(void) { } if (!g_epd.currentBuffer()) { - rc = g_epd.setPanelSize(BBEP_DISPLAY_ED103TC2); + // M5Paper: ED047TC1 960x540, unmirrored. FastEPD 2.2.0 has no + // BBEP_DISPLAY_ED047TC1, so size it explicitly. The IT8951 runs its own + // waveforms; the gray matrix that setPanelSize(id) would add is unused. + if (globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_M5PAPER_960X540) { + rc = g_epd.setPanelSize(960, 540, BB_PANEL_FLAG_NONE, -1600); + } else { + rc = g_epd.setPanelSize(BBEP_DISPLAY_ED103TC2); + } if (rc != BBEP_SUCCESS || !g_epd.currentBuffer()) { s_init_failed = true; s_hw_initialized = false; diff --git a/src/display_service.cpp b/src/display_service.cpp index 74d92f6..249d21f 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -864,9 +864,10 @@ bool fastepd_driver_used(void) { #else if (globalConfig.display_count < 1) return false; const struct DisplayConfig& d = globalConfig.displays[0]; - // FastEPD IT8951 (SPI) path: E Ink ED103TC2 (Seeed reTerminal). + // FastEPD IT8951 (SPI) path: E Ink ED103TC2 (Seeed reTerminal), M5Stack M5Paper. const bool it8951 = (d.panel_ic_type == OD_PANEL_IC_ED103TC2_1872X1404 || - d.panel_ic_type == OD_PANEL_IC_ED103TC2_1872X1404_4GRAY); + d.panel_ic_type == OD_PANEL_IC_ED103TC2_1872X1404_4GRAY || + d.panel_ic_type == OD_PANEL_IC_M5PAPER_960X540); // FastEPD native parallel path: Soldered Inkplate 5V2 / 10, M5Stack PaperS3. const bool parallel = (d.panel_ic_type == OD_PANEL_IC_INKPLATE5V2_1280X720 || d.panel_ic_type == OD_PANEL_IC_INKPLATE10_1200X825 || diff --git a/src/main.cpp b/src/main.cpp index 7b9e148..74b78d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1509,6 +1509,29 @@ void nm420_init(){ #endif } +// Original M5Stack M5Paper (classic ESP32 + IT8951), selected by panel IC +// OD_PANEL_IC_M5PAPER_960X540 (the panel only ships on this board). The power +// button only switches the board on while it is held; GPIO2 (main-power hold) +// must be driven HIGH to keep it on, and GPIO5 enables the external-power rail. GPIO23 (IT8951 power) is driven by +// FastEPD as the panel's EN pin. Deep-sleep hold and power-off of GPIO2 go +// through the MOSFET latch (DEVICE_FLAG_BATTERY_LATCH, pwr_pin_2 = 2), so the +// preset sets both flags. +void m5paper_init(){ +#if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) + static constexpr uint8_t kM5PaperOnPins[] = {2, 5}; + for (uint8_t pin : kM5PaperOnPins) { + // Level before direction, then drop the sleep hold: the pad never + // glitches LOW on a wake from deep sleep. + gpio_set_level((gpio_num_t)pin, 1); + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + gpio_hold_dis((gpio_num_t)pin); + pinMode(pin, OUTPUT); + digitalWrite(pin, HIGH); + } + od_log_info("M5Paper init: main-power hold and EXT power HIGH"); +#endif +} + #ifdef TARGET_ESP32 void nm420HoldForSleep() { #if defined(CONFIG_IDF_TARGET_ESP32S3) diff --git a/src/main.h b/src/main.h index 2c59cef..49808bf 100644 --- a/src/main.h +++ b/src/main.h @@ -193,6 +193,7 @@ void powerDownExternalFlashFromConfig(void); void xiaoinit(); void ws_pp_init(); void nm420_init(); +void m5paper_init(); #ifdef TARGET_ESP32 void nm420HoldForSleep(); void fullSetupAfterConnection(); From 12fa848edfa3701f28a453c5a3dd1badc428c4f3 Mon Sep 17 00:00:00 2001 From: gcormier Date: Mon, 26 Oct 2026 01:56:07 +0000 Subject: [PATCH 02/17] feat: RTC power-off sleep via PCF8563/BM8563 + MOSFET latch (M5Paper) 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 Claude-Session: https://claude.ai/code/session_01KZwG6PyfGku5uKoVTyB9MC --- include/opendisplay_structs.h | 9 ++- src/display_service.cpp | 3 + src/main.cpp | 31 +++++++- src/power_latch.cpp | 12 ++++ src/power_latch.h | 4 ++ src/rtc_pcf8563.cpp | 128 ++++++++++++++++++++++++++++++++++ src/rtc_pcf8563.h | 27 +++++++ tools/od-device-cli.py | 2 +- 8 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/rtc_pcf8563.cpp create mode 100644 src/rtc_pcf8563.h diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index 33dcc7d..f94c673 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -93,6 +93,10 @@ * Backward-compatible enum addition. Firmware treats the panel type as * identifying the board (as with 3002-3004) and drives the M5Paper's * main-power hold and external-power enable itself. + * - SensorType 8: OD_SENSOR_TYPE_PCF8563 (PCF8563 / BM8563 RTC, I2C 0x51). + * Together with OD_DEVICE_FLAG_PWR_LATCH, deep sleep arms the RTC and + * releases the latch; the RTC INT powers the board back on (M5Paper). + * Backward-compatible enum addition; publishes no MSD data. * - Add each payload-layout change here as it lands. On the next version bump, * move these under a new "MAJOR.MINOR (YYYY-MM-DD)" heading. * @@ -807,7 +811,8 @@ enum SensorType { OD_SENSOR_TYPE_SHT40 = 4, /**< @doc "Sensirion SHT40 (I2C); writes 3 LE bytes into MSD dynamic data (21 bits: 0.1C, 0.1% RH)" */ OD_SENSOR_TYPE_BQ27220 = 5, /**< @doc "TI BQ27220 fuel gauge (I2C 0x55); voltage in MSD bytes 14-15, one packed dynamic byte (bits 0-6 SOC%, bit7 charging)" */ OD_SENSOR_TYPE_NPM1300 = 6, /**< @doc "nPM1300 PMIC (other firmware family; not implemented by this firmware)" */ - OD_SENSOR_TYPE_AHT20 = 7 /**< @doc "AHT20 (I2C 0x38); wake-time sample in SHT40-compatible 3-byte MSD format. NM-EPD-420 power gate GPIO40 is off between samples." */ + OD_SENSOR_TYPE_AHT20 = 7, /**< @doc "AHT20 (I2C 0x38); wake-time sample in SHT40-compatible 3-byte MSD format. NM-EPD-420 power gate GPIO40 is off between samples." */ + OD_SENSOR_TYPE_PCF8563 = 8 /**< @doc "NXP PCF8563 / BM8563 RTC (I2C 0x51) as a power-off wake source; with pwr_latch, deep sleep releases the latch and the RTC INT powers the board back on. Publishes no MSD data." */ }; /** @struct SensorData @packet 0x23 @repeatable max=4 @@ -817,7 +822,7 @@ struct SensorData { uint8_t instance_number; /**< @doc "0-based sensor-block index." */ uint16_t sensor_type; /**< @enum SensorType @endian le @doc "sensor type." */ uint8_t bus_id; /**< @doc "data_bus instance this sensor lives on." */ - uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address; 0 or 0xFF = per-sensor default (SHT40 0x44, BQ27220 0x55, AHT20 0x38)." @default 0xFF */ + uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address; 0 or 0xFF = per-sensor default (SHT40 0x44, BQ27220 0x55, AHT20 0x38, PCF8563 0x51)." @default 0xFF */ uint8_t msd_data_start_byte; /**< @doc "first dynamicreturndata index this sensor publishes into. SHT40/AHT20 use 3 bytes (0/0xFF = default 7); BQ27220 uses 1 packed byte (0xFF = do not publish)." @default 0xFF */ uint8_t reserved[24]; /**< @reserved @doc "future use; must be 0." */ } __attribute__((packed)); diff --git a/src/display_service.cpp b/src/display_service.cpp index 249d21f..2e03012 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -1201,6 +1201,9 @@ void initSensors(){ else if(sensor->sensor_type == OD_SENSOR_TYPE_AHT20){ od_log_debug(" AHT20 (wake-time I2C + MSD slot)"); } + else if(sensor->sensor_type == OD_SENSOR_TYPE_PCF8563){ + od_log_debug(" PCF8563/BM8563 RTC (power-off wake source with pwr_latch)"); + } else if(sensor->sensor_type == OD_SENSOR_TYPE_BQ27220){ od_log_debug(" BQ27220 fuel gauge (MSD voltage + optional dynamic SOC/status bytes)"); } diff --git a/src/main.cpp b/src/main.cpp index 74b78d4..484bc5f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "display_service.h" #include "power_latch.h" #include "wake_button.h" +#include "rtc_pcf8563.h" #include "touch_input.h" #include "encryption.h" #include "ble_transport.h" @@ -197,6 +198,20 @@ void setup() { // reservation above depends on. One call site serves both the normal-boot and the // deep-sleep-wake path, which share this stretch of setup(). odLanReserveRxBuffer(); +#endif +#ifdef TARGET_ESP32 + // A cold boot raised by the RTC (power-off sleep, see enterDeepSleep()) is a + // timer wake in all but the ESP32's own wake cause. Always called when an RTC + // is configured: it also clears the flag that holds INT (and the rail) on. + // RTC memory died with the rail, so displayed_etag is 0 (the host sends a + // full image) and rebootFlag is cleared here -- this is not a reboot. + if (rtcConsumeWake() && !is_deep_sleep_wake) { + is_deep_sleep_wake = true; + woke_from_deep_sleep = true; + deep_sleep_count++; + rebootFlag = 0; + od_log_info("=== WOKE FROM RTC POWER-OFF ==="); + } #endif if (is_deep_sleep_wake) { od_log_info("[wake] << full_config_init >> initio"); od_log_flush(); } #if defined(TARGET_NRF) && defined(OPENDISPLAY_BOOT_DIAG) @@ -1242,12 +1257,22 @@ void enterDeepSleep(bool force, uint16_t overrideSleepSeconds) { // manipulation then cannot disturb freshly configured RTC pulls, and its // gpio_hold_en() touches only the latch pin, never the wake pads. armButtonWakeSources(); - od_log_info("Entering deep sleep for %u seconds%s", sleepSeconds, - overrideSleepSeconds ? " (host override, one cycle)" : " (config)"); + // RTC power-off (PCF8563/BM8563 + MOSFET latch): the RTC switches the board + // back on, so the latch is released instead of held. On an I2C failure the + // rail stays held and this is a plain timer deep sleep. The ESP32 timer + // above stays armed either way: it is what wakes a USB-powered board. + const bool rtcPowerOff = rtcArmWake(sleepSeconds); + od_log_info("Entering deep sleep for %u seconds%s%s", sleepSeconds, + overrideSleepSeconds ? " (host override, one cycle)" : " (config)", + rtcPowerOff ? ", RTC power-off" : ""); od_log_info("Heap: free=%u min=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMinFreeHeap()); od_log_flush(); // drain UART/Serial prior to deep sleep delay(100); // Brief delay to ensure serial output is sent - powerLatchHoldForSleep(); + if (rtcPowerOff) { + powerLatchReleaseForRtcSleep(); + } else { + powerLatchHoldForSleep(); + } touchPrepareForDeepSleep(); nm420HoldForSleep(); esp_deep_sleep_start(); diff --git a/src/power_latch.cpp b/src/power_latch.cpp index 9c787f2..d3166f7 100644 --- a/src/power_latch.cpp +++ b/src/power_latch.cpp @@ -241,6 +241,18 @@ void powerLatchPowerOff() { dffLatchRelease(); } +void powerLatchReleaseForRtcSleep() { + if (dffLatchEnabled() || !latchEnabled()) { + return; + } + // padHold, not padIsolateAndHold: the wake pads armed before this keep + // their pulls for the USB-powered deep-sleep fallback. + padHoldDisable(latchPin()); + pinMode(latchPin(), OUTPUT); + digitalWrite(latchPin(), LOW); + padHold(latchPin()); +} + bool powerLatchMosfetConfigured(void) { return latchEnabled(); } diff --git a/src/power_latch.h b/src/power_latch.h index 687fe29..db555a8 100644 --- a/src/power_latch.h +++ b/src/power_latch.h @@ -23,5 +23,9 @@ void powerLatchHoldForSleep(); // Release 74AHC1G79 D-FF latch (pwr_latch_dff only). Used by command 0x0052. bool powerLatchDffConfigured(void); bool powerLatchMosfetConfigured(void); +// RTC power-off sleep (MOSFET latch only): drive the hold pin LOW and keep it +// LOW through sleep. On battery the rail drops inside this call; on USB it +// stays up and the caller's esp_deep_sleep_start() is the fallback. +void powerLatchReleaseForRtcSleep(); void powerLatchPowerOff(); void powerLatchTriggerOff(); diff --git a/src/rtc_pcf8563.cpp b/src/rtc_pcf8563.cpp new file mode 100644 index 0000000..d8f2632 --- /dev/null +++ b/src/rtc_pcf8563.cpp @@ -0,0 +1,128 @@ +#include "rtc_pcf8563.h" + +#ifdef TARGET_ESP32 +#include "display_service.h" +#include "power_latch.h" +#include "structs.h" +#include "od_log.h" + +#include +#include + +extern struct GlobalConfig globalConfig; + +// PCF8563 / BM8563 register map (the two are register-compatible). +#define RTC_DEFAULT_ADDR 0x51 +#define RTC_REG_CTRL1 0x00 // bit5 STOP +#define RTC_REG_CTRL2 0x01 // bit3 AF, bit2 TF, bit1 AIE, bit0 TIE +#define RTC_REG_SECONDS 0x02 // bit7 VL +#define RTC_REG_ALARM_MIN 0x09 // 0x09..0x0C: minute/hour/day/weekday, bit7 AE = disabled +#define RTC_REG_TIMER_CTRL 0x0E // bit7 TE, bits1:0 TD +#define RTC_REG_TIMER 0x0F + +#define RTC_CTRL2_FLAGS 0x0C +#define RTC_CTRL2_AIE 0x02 +#define RTC_CTRL2_TIE 0x01 +#define RTC_TIMER_ENABLE 0x80 +#define RTC_TIMER_1HZ 0x02 +#define RTC_TIMER_1_60HZ 0x03 // also the lowest-power setting while disabled +#define RTC_ALARM_OFF 0x80 + +static const SensorData* rtcSensor(void) { + for (uint8_t i = 0; i < globalConfig.sensor_count; i++) { + if (globalConfig.sensors[i].sensor_type == OD_SENSOR_TYPE_PCF8563) { + return &globalConfig.sensors[i]; + } + } + return nullptr; +} + +// Selects the sensor's bus and returns its address, or 0 on failure. +static uint8_t rtcBegin(void) { + const SensorData* s = rtcSensor(); + if (!s) return 0; + const uint8_t busId = (s->bus_id == 0xFF) ? 0 : s->bus_id; + if (!initOrRestoreWireForBus(busId)) return 0; + return (s->i2c_addr_7bit == 0 || s->i2c_addr_7bit == 0xFF) ? RTC_DEFAULT_ADDR : s->i2c_addr_7bit; +} + +static bool rtcWrite(uint8_t addr, uint8_t reg, const uint8_t* data, uint8_t len) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(data, len); + return Wire.endTransmission() == 0; +} + +static bool rtcWrite1(uint8_t addr, uint8_t reg, uint8_t value) { + return rtcWrite(addr, reg, &value, 1); +} + +static bool rtcRead(uint8_t addr, uint8_t reg, uint8_t* data, uint8_t len) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return false; + if (Wire.requestFrom(addr, (size_t)len, true) != len) return false; + for (uint8_t i = 0; i < len; i++) data[i] = Wire.read(); + return true; +} + +static uint8_t bcdToBin(uint8_t v) { return (uint8_t)((v >> 4) * 10 + (v & 0x0F)); } +static uint8_t binToBcd(uint8_t v) { return (uint8_t)(((v / 10) << 4) | (v % 10)); } + +// Interrupts off, flags cleared, timer stopped, every alarm field disabled. +static bool rtcQuiesce(uint8_t addr) { + static const uint8_t alarmsOff[4] = {RTC_ALARM_OFF, RTC_ALARM_OFF, RTC_ALARM_OFF, RTC_ALARM_OFF}; + return rtcWrite1(addr, RTC_REG_CTRL2, 0x00) && + rtcWrite1(addr, RTC_REG_TIMER_CTRL, RTC_TIMER_1_60HZ) && + rtcWrite(addr, RTC_REG_ALARM_MIN, alarmsOff, sizeof(alarmsOff)); +} + +bool rtcWakeConfigured(void) { + return rtcSensor() != nullptr && powerLatchMosfetConfigured(); +} + +bool rtcConsumeWake(void) { + if (!rtcWakeConfigured()) return false; + const uint8_t addr = rtcBegin(); + uint8_t ctrl2 = 0; + if (addr == 0 || !rtcRead(addr, RTC_REG_CTRL2, &ctrl2, 1)) { + od_log_warn("RTC: not reachable at boot"); + return false; + } + // INT is level-mode (TI_TP = 0): it holds the board on until this clears. + if (!rtcQuiesce(addr)) od_log_warn("RTC: failed to clear wake flags"); + return (ctrl2 & RTC_CTRL2_FLAGS) != 0; +} + +bool rtcArmWake(uint32_t seconds) { + if (seconds == 0 || !rtcWakeConfigured()) return false; + const uint8_t addr = rtcBegin(); + // Clock running (STOP = 0) or neither the countdown nor the alarm advances. + if (addr == 0 || !rtcWrite1(addr, RTC_REG_CTRL1, 0x00) || !rtcQuiesce(addr)) { + od_log_warn("RTC: arm failed (I2C)"); + return false; + } + if (seconds <= 255) { + return rtcWrite1(addr, RTC_REG_TIMER, (uint8_t)seconds) && + rtcWrite1(addr, RTC_REG_TIMER_CTRL, RTC_TIMER_ENABLE | RTC_TIMER_1HZ) && + rtcWrite1(addr, RTC_REG_CTRL2, RTC_CTRL2_TIE); + } + const uint32_t minutes = (seconds + 59) / 60; + if (minutes <= 255) { + return rtcWrite1(addr, RTC_REG_TIMER, (uint8_t)minutes) && + rtcWrite1(addr, RTC_REG_TIMER_CTRL, RTC_TIMER_ENABLE | RTC_TIMER_1_60HZ) && + rtcWrite1(addr, RTC_REG_CTRL2, RTC_CTRL2_TIE); + } + // Beyond 255 min (deep_sleep_time_seconds tops out near 18 h): minute+hour + // alarm, which matches once within the next 24 h. + uint8_t now[3]; + if (!rtcRead(addr, RTC_REG_SECONDS, now, sizeof(now))) return false; + const uint32_t nowMin = (uint32_t)bcdToBin(now[2] & 0x3F) * 60u + bcdToBin(now[1] & 0x7F); + const uint32_t target = (nowMin + minutes) % (24u * 60u); + const uint8_t alarm[4] = {binToBcd((uint8_t)(target % 60)), binToBcd((uint8_t)(target / 60)), + RTC_ALARM_OFF, RTC_ALARM_OFF}; + return rtcWrite(addr, RTC_REG_ALARM_MIN, alarm, sizeof(alarm)) && + rtcWrite1(addr, RTC_REG_CTRL2, RTC_CTRL2_AIE); +} + +#endif // TARGET_ESP32 diff --git a/src/rtc_pcf8563.h b/src/rtc_pcf8563.h new file mode 100644 index 0000000..d46527a --- /dev/null +++ b/src/rtc_pcf8563.h @@ -0,0 +1,27 @@ +#ifndef RTC_PCF8563_H +#define RTC_PCF8563_H + +#include + +// NXP PCF8563 / M5Stack BM8563 RTC as a power-off wake source +// (SensorType OD_SENSOR_TYPE_PCF8563). Used only together with the MOSFET +// power latch (DEVICE_FLAG_BATTERY_LATCH): instead of holding the rail through +// ESP32 deep sleep, the sleep path arms the RTC and releases the latch; the +// RTC's open-drain INT line switches the board back on (M5Paper wiring). + +// True when an RTC sensor block is configured AND the MOSFET latch is enabled. +bool rtcWakeConfigured(void); + +// Call once per boot after full_config_init() (the board's power hold is +// already asserted by then). Clears the timer/alarm flags and disables both +// interrupts so INT releases, and returns true if one of them had fired, i.e. +// this cold boot is an RTC wake. No I2C traffic unless rtcWakeConfigured(). +bool rtcConsumeWake(void); + +// Arm a wake `seconds` from now. <= 255 s: 1 Hz countdown timer; <= 255 min: +// 1/60 Hz countdown (rounded up to whole minutes, first tick may come up to a +// minute early); longer: minute/hour alarm. Returns false on any I2C failure, +// in which case the caller must keep the rail held and use ESP32 deep sleep. +bool rtcArmWake(uint32_t seconds); + +#endif diff --git a/tools/od-device-cli.py b/tools/od-device-cli.py index 57f4036..d0fdebb 100755 --- a/tools/od-device-cli.py +++ b/tools/od-device-cli.py @@ -538,7 +538,7 @@ class Block(NamedTuple): # sets only trigger a warning (not an error) on encode - the firmware may define values # this tool doesn't know about yet. VALID_ENUMS: dict[tuple[str, str], set[int]] = { - ("sensors", "sensor_type"): {1, 2, 3, 4, 5, 6, 7}, # SENSOR_TYPE_TEMPERATURE..AHT20 + ("sensors", "sensor_type"): {1, 2, 3, 4, 5, 6, 7, 8}, # SENSOR_TYPE_TEMPERATURE..PCF8563 ("displays", "color_scheme"): {0, 1, 2, 3, 4, 5, 6, 7, 8, 100, 101, 102}, # COLOR_SCHEME_MONO..BWGBRY_SPLIT, RGB565/RGB888/RGB16BPC ("touch_controllers", "touch_ic_type"): {0, 1}, # TOUCH_IC_NONE, TOUCH_IC_GT911 ("power_option", "capacity_estimator"): {1, 2, 3, 4, 5}, # CAPACITY_EST_LI_ION..SEEED_LI_ION From f271dc5600b0df4902cb683afabbd829b2165429 Mon Sep 17 00:00:00 2001 From: gcormier Date: Mon, 26 Oct 2026 18:11:42 -0400 Subject: [PATCH 03/17] fix(m5paper): load unmirrored IT8951 frames B_ENDIAN; never drive input-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. --- src/display_fastepd.cpp | 61 +++++++++++++++++++++++++++++++++++++++-- src/touch_input.cpp | 21 ++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/display_fastepd.cpp b/src/display_fastepd.cpp index 8118863..340b4d9 100644 --- a/src/display_fastepd.cpp +++ b/src/display_fastepd.cpp @@ -19,6 +19,9 @@ extern void it8951LoadImgAreaStart(FASTEPDSTATE* pState, uint16_t endian, uint16 extern void it8951WriteCmdCode(FASTEPDSTATE* pState, uint16_t cmd); extern void it8951DisplayArea1Bit(FASTEPDSTATE* pState, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t mode, uint8_t bg_gray, uint8_t fg_gray); +extern void it8951DisplayArea(FASTEPDSTATE* pState, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t mode); +extern uint16_t it8951ReadReg(FASTEPDSTATE* pState, uint16_t addr); +extern void it8951WriteReg(FASTEPDSTATE* pState, uint16_t addr, uint16_t val); // FastEPD's cached PCAL6416A write extern void bbepPCALDigitalWrite(uint8_t pin, uint8_t value); static void fastepd_inkplate_wakeup_off(void); @@ -192,8 +195,9 @@ static bool it8951_fullscreen_du(void) { it8951WaitForLUTReady(st); it8951SetImgBufBaseAddr(st); - const uint16_t endian = mirror_x ? IT8951_LDIMG_B_ENDIAN : IT8951_LDIMG_L_ENDIAN; - it8951LoadImgAreaStart(st, endian, IT8951_8BPP, IT8951_ROTATE_0, 0, 0, pitch, h); + // B_ENDIAN either way: L_ENDIAN swaps the bytes of each 16-bit word (see + // it8951_full_write_unmirrored()). + it8951LoadImgAreaStart(st, IT8951_LDIMG_B_ENDIAN, IT8951_8BPP, IT8951_ROTATE_0, 0, 0, pitch, h); uint8_t* rowbuf = mirror_x ? (uint8_t*)malloc(pitch) : nullptr; if (mirror_x && !rowbuf) return false; @@ -298,9 +302,62 @@ void fastepd_epaper_begin(void) { s_hw_initialized = true; } +// FastEPD 2.2.0 loads an unmirrored IT8951 frame as L_ENDIAN, which swaps the +// two bytes of every 16-bit word: 4bpp pixel pairs land 2 px off, 1bpp bytes +// 8 px off (verified on an M5Paper). Same frame and GC16 waveform as +// it8951WriteFramebuffer4Bit/1Bit, loaded B_ENDIAN as M5EPD does. Mirrored +// panels (ED103TC2) already get B_ENDIAN from FastEPD. +static void it8951_full_write_unmirrored(void) { + FASTEPDSTATE* st = g_epd.state(); + const uint8_t* s = g_epd.currentBuffer(); + if (!st || !s) return; + const bool gray = (g_epd.getMode() == BB_MODE_4BPP); + const uint16_t w = (uint16_t)st->native_width; + const uint16_t h = (uint16_t)st->native_height; + const unsigned pitch = gray ? (unsigned)((w + 1) / 2) : (unsigned)((w + 7) / 8); + + g_epd.einkPower(1); + it8951WaitForLUTReady(st); + it8951SetImgBufBaseAddr(st); + if (gray) { + // Leave 1bpp mode (it8951DisplayArea1Bit() sets it). + it8951WriteReg(st, IT8951_REG_UP1SR + 2, it8951ReadReg(st, IT8951_REG_UP1SR + 2) & ~(1 << 2)); + it8951LoadImgAreaStart(st, IT8951_LDIMG_B_ENDIAN, IT8951_4BPP, IT8951_ROTATE_0, 0, 0, w, h); + } else { + it8951LoadImgAreaStart(st, IT8951_LDIMG_B_ENDIAN, IT8951_8BPP, IT8951_ROTATE_0, 0, 0, pitch, h); + } + + gpio_set_level((gpio_num_t)st->u8CS, LOW); + SPI.beginTransaction(SPISettings(st->spi_frequency, MSBFIRST, SPI_MODE0)); + it8951WaitForReady(st); + SPI.transfer16(0x0000); + it8951WaitForReady(st); + for (uint16_t row = 0; row < h; row++) { + SPI.writeBytes(s, pitch); + s += pitch; + if ((row & 7) == 0) yield(); + } + SPI.endTransaction(); + gpio_set_level((gpio_num_t)st->u8CS, HIGH); + + it8951WriteCmdCode(st, IT8951_TCON_LD_IMG_END); + if (gray) { + it8951DisplayArea(st, 0, 0, w, h, IT8951_MODE_2); + } else { + it8951DisplayArea1Bit(st, 0, 0, w, h, IT8951_MODE_2, 0x00, 0xff); + } + it8951WaitForReady(st); + g_epd.einkPower(0); +} + // REFRESH_FULL: parallel panels flash-clear to flush ghosting; IT8951 clears // internally (ignores the mode) so it stays CLEAR_NONE. static void fastepd_full_refresh_impl(void) { + FASTEPDSTATE* st = g_epd.state(); + if (!fastepd_is_parallel() && !(st->iFlags & BB_PANEL_FLAG_MIRROR_X)) { + it8951_full_write_unmirrored(); + return; + } int clear = CLEAR_NONE; if (fastepd_is_parallel()) { // The PaperS3 needs the library-default 10-pass clear: CLEAR_FAST's diff --git a/src/touch_input.cpp b/src/touch_input.cpp index 6be6799..1ec2d64 100644 --- a/src/touch_input.cpp +++ b/src/touch_input.cpp @@ -152,8 +152,23 @@ static void touch_detach_all_configured_ints(void) { } } +// ESP32 GPIO34-39 are input-only with no internal pull-up (M5Paper GT911 INT is +// GPIO36, pulled up on the board): never drive them, read them as plain INPUT. +static bool touch_pin_can_output(uint8_t pin) { +#if defined(TARGET_ESP32) + return GPIO_IS_VALID_OUTPUT_GPIO(pin); +#else + (void)pin; + return true; +#endif +} + +static uint8_t touch_int_input_mode(uint8_t pin) { + return touch_pin_can_output(pin) ? INPUT_PULLUP : INPUT; +} + static void gt911_int_wake_before_irq(const TouchController* t) { - if (t->int_pin == 0xFF) { + if (t->int_pin == 0xFF || !touch_pin_can_output(t->int_pin)) { return; } pinMode(t->int_pin, OUTPUT); @@ -171,7 +186,7 @@ static void attach_touch_int(uint8_t idx, uint8_t pin) { od_log_warn("Touch[%u]: digitalPinToInterrupt failed for GPIO %u — using poll only", idx, pin); return; } - pinMode(pin, INPUT_PULLUP); + pinMode(pin, touch_int_input_mode(pin)); attachInterrupt(irq_num, s_touch_isrs[idx], FALLING); s_touch_rt[idx].int_irq_attached = 1; } @@ -244,7 +259,7 @@ static void gt911_hw_reset(const TouchController* t, bool int_low_for_addr_5d) { if (t->rst_pin == 0xFF) { return; } - if (t->int_pin == 0xFF) { + if (t->int_pin == 0xFF || !touch_pin_can_output(t->int_pin)) { pinMode(t->rst_pin, OUTPUT); digitalWrite(t->rst_pin, LOW); delay(10); From c6f98699f9ff7cd18c69ac9122d839e27667249b Mon Sep 17 00:00:00 2001 From: gcormier Date: Mon, 26 Oct 2026 22:03:18 -0400 Subject: [PATCH 04/17] docs: M5Paper hardware bring-up findings (2026-10-26) 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 194 ++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md new file mode 100644 index 0000000..f9f76fe --- /dev/null +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -0,0 +1,194 @@ +# M5Paper (original, ESP32) hardware bring-up — 2026-10-26 + +Hardware validation of the two M5Paper branches on a real device: + +- `greg/awesome-dijkstra-5pq3ha` (PR 1): panel IC 3005 on the FastEPD IT8951 + path, `m5paper_init()`. +- `greg/awesome-dijkstra-5pq3ha-rtc` (PR 2, stacked): RTC power-off sleep, + sensor type 8 (PCF8563/BM8563) and the MOSFET latch. + +**Device:** original M5Stack M5Paper. ESP32-D0WDQ6-V3, MAC 3c:61:05:09:20:54, +16 MB flash, **CP2104** USB-UART (not CH9102, so it needs the Silicon Labs +CP210x driver). The panel is rigid, which suggests v1.0, but this is not +confirmed. Built as `esp32-wrover-e-N4R8`, pioarduino 55.03.39, flashed from +Windows 11 on COM16. The original 16 MB flash was backed up before erasing. + +**Host-side tooling:** serial logging opened with DTR/RTS deasserted, because +opening the CP2104 normally resets the ESP32. BLE uploads used `py-opendisplay` +7.14.1. Touch and button events are only published in the BLE MSD, not on +serial at INFO level, so they were read from the advertisements or from a +`-DOD_LOG_LEVEL=OD_LOG_DEBUG` build. + +## Summary + +| Area | Result | +|---|---| +| Boot, `m5paper_init()`, FastEPD init | PASS | +| 4bpp gray image | **FAIL → fixed** (byte-swapped IT8951 load, see §1) | +| Orientation | PASS (landscape, not mirrored) | +| Contrast at FastEPD's fixed VCOM −1.5 V | Acceptable to the eye; −2.30 V patch kept unapplied (§2) | +| Repeated pushes | PASS (several full pushes, no wedge, no WDT) | +| Mono (color_scheme 0), DU fast path | **Not verified on hardware** | +| Touch (GT911) | PASS with flags **0x06**. Fixed GPIO36 errors (§3) | +| Buttons 37/38/39 | PASS (press and release on all three) | +| Battery hold (GPIO2) after unplugging USB | PASS (19 min on battery, still advertising) | +| Power-button hold time | **Not measured** (§6) | +| Battery voltage factor | **Not calibrated** (no multimeter). 0xA1 reads 3.91 V while charging, which is plausible | +| RTC sleep on USB (fallback path) | PASS | +| RTC power-off on battery, 120 s | PASS: 20 arms, 19 wakes + 1 pending, 0 missed (§4) | +| 600 s (1/60 Hz countdown) | PASS on USB: RTC timer fired (TF set) before the ESP32 timer | +| 16000 s (minute/hour alarm) | Programming verified by register readback. The fire itself was not waited out (§4) | +| Image push in a wake window | see §5 | +| Off current | **Not measured** | +| Boot screen not redrawn on battery RTC wakes | **Not observed visually.** The firmware path (`is_deep_sleep_wake`) is confirmed on USB | +| All CI targets build | PASS (12 envs in `firmware-targets.json`, not 11) | + +## 1. IT8951 byte order (FastEPD bug) — fixed on PR 1 + +**Symptom:** the 4bpp test image was readable and the right way up, but fine +detail looked "interlaced / shifted horizontally". + +**Cause:** FastEPD 2.2.0 loads IT8951 frames with `IT8951_LDIMG_L_ENDIAN` +unless the panel has `BB_PANEL_FLAG_MIRROR_X`. That swaps the two bytes of +every 16-bit word, so 4bpp pixel pairs land 2 px off and 1bpp bytes land +8 px off. M5Stack's M5EPD driver sends the same high-nibble-first bytes with +`IT8951_LDIMG_B_ENDIAN`. The reTerminal ED103TC2 is MIRROR_X, so it already +gets B_ENDIAN, and nobody had seen the bug. The old note in +`fastepd_partial_refresh()` about "IT8951 region DU had persistent edge +alignment issues" may be the same bug. + +**Fix (`f271dc5`):** for unmirrored IT8951 panels (only the M5Paper today), +full refreshes go through `it8951_full_write_unmirrored()` in +`src/display_fastepd.cpp`. It sends the same frame with the same GC16 +waveform, loaded B_ENDIAN. `it8951_fullscreen_du()` uses B_ENDIAN too. +Mirrored panels keep FastEPD's own writer, unchanged. + +**Verified:** the 4bpp image is sharp after the fix (user confirmed). +**Not verified:** 1bpp/mono and the DU path on screen. Worth reporting +upstream to FastEPD. + +## 2. VCOM + +`bbepInitIT8951()` always writes VCOM −1.5 V. The `iVCOM` passed to +`setPanelSize()` is not used on the IT8951 path, so the branch's `-1600` is a +no-op. M5EPD uses −2.30 V for this panel. Gray steps 0 (black) to 15 (white) +looked right at −1.5 V, so nothing was changed. A tested patch that writes +−2.30 V after `initIT8951()` for panel 3005 is available if a side-by-side +comparison favours it. + +## 3. Touch + +- **Mapping:** the GT911 reports portrait coordinates (540×960). With + flags 0 the firmware clamps y to 539. Corner taps logged with SWAP_XY only: + TL (63,483), TR (906,488), BR (903,46), BL (50,58), centre (479,271). So + x is correct and y is inverted, giving **flags = SWAP_XY | INVERT_Y = 0x06**, + the same as M5EPD's ROTATE_0. The opendisplay.org `m5-paper` preset is + updated (`a075d2c`). +- **GPIO36 errors (fixed, `f271dc5`):** GT911 INT is GPIO36, an input-only pad + with no internal pull-up. `gt911_int_wake_before_irq()`, `attach_touch_int()` + and `gt911_hw_reset()` drove it as an output or set INPUT_PULLUP, which + logged `GPIO can only be used as input mode` and `gpio_pullup_en` errors on + every boot and every EPD refresh. These pads are now never driven and are + read as plain INPUT. +- **I2C address varies:** because INT can't be driven, the GT911 comes up at + 0x5D on some boots and 0x14 on others. Auto-detect handles it. + +## 4. RTC power-off (PR 2) + +**Schematic (A4-SYS_PWR):** FET1 (PMOS) switches VBAT. Its gate, `PWR_EN`, +is pulled up to VBAT_IN and pulled low by FET2 (`PS_ON` = GPIO2), by +`KEY_PUSH` (the rocker pushed in) through D10, and by `RTC_ALM` (PCF8563 INT) +through D7. `KEY_PUSH_IO` (GPIO38) reads the key through D8 and appears to +share the R34 pull-up node with `RTC_ALM`, so an asserted RTC INT probably +also reads as "rocker pushed" until it is cleared. + +**USB fallback:** `Entering deep sleep for 120 seconds (config), RTC +power-off`, then a timer wake (`Reset reason: DEEPSLEEP`). The boot screen is +not redrawn, the 20 s window applies, and it re-arms. No `RTC: not reachable` +or `arm failed` errors. + +**Battery, 120 s:** BLE presence alone was misleading, because the Windows +scanner misses whole 20 s windows. A temporary NVS counter (not committed) +recorded arms and wakes instead. Over about 46 min (16 min on USB, then +30 min on battery): **arms = 20, wake boots = 19, plus 1 pending at replug** +(`TC=82 T=5E`, 94 s left). No arm went without a wake. 18 of the 19 wake +boots had TF set. The one without was a USB wake where the ESP32 timer beat +the RTC by about 1 s (`T=01` seen at that boot). + +**600 s:** boot dump at the wake shows `TC=83 T=0A C2=05`: 1/60 Hz countdown +from 10, TF set, TIE on. + +**16000 s:** armed at RTC 13:33:27. 267 min (rounded up) gives a target of +18:00. Readback `AL=00 18 80 80`, `C2=02` (AIE), `TC=03` (timer off). Correct. +It fires up to 59 s early because the alarm ignores seconds. + +**Leftover RTC state from M5Stack's factory firmware:** with the USB-only +config (no sensor type 8), the board switched itself back on about 60–75 s +after every reset-button power-off. A repeating RTC timer left armed by the +factory firmware survives a flash erase, because the RTC is on VBAT_IN. The +RTC config clears it at every boot. *Suggestion:* clear the PCF8563 interrupt +flags and enables in `m5paper_init()` whatever the sensor config, so the +USB-only preset doesn't get phantom power-ons. + +**VL flag:** the seconds register has VL (bit 7) set, meaning the RTC lost +power at some point (probably long ago). The firmware doesn't use wall-clock +time, so it's harmless. + +## 5. Image push in a wake window + +PASS on USB, pushed during a 20 s timer-wake window: `[EPD session] +acquire: COLD bring-up` (the IT8951 re-inits after deep sleep), then +`DW complete: 71 chunks, 259200/259200 bytes … 11.92 s` and `EPD refresh: +FULL`, `refreshSuccess=1`. It was a full image transfer, as expected. Not +repeated during a battery RTC-wake window, where the boot path differs only in +how the wake is detected. + +## 6. Power-on, power-off and buttons + +- **Power button = rocker pushed in** (KEY_PUSH, GPIO38). M5Stack documents a + 2 s hold. `m5paper_init()` drives GPIO2 at about 0.17 s after boot (about + 0.3–0.5 s after power-on, counting the ROM and bootloader). The hold time was + not measured, because tests were confused by the leftover RTC timer and by + reset presses that didn't register. +- **Reset button (back)** cuts power on battery (GPIO2 released), unless an + RTC interrupt is holding `PWR_EN`. +- **`CMD_POWER_OFF` (0x0052)** NACKs on the MOSFET latch: only the D-FF latch + is implemented. The M5Paper could support it by driving GPIO2 low. + Follow-up. +- **Deep-sleep button wake:** a classic ESP32 has ext0 only, so just GPIO37 + (rocker up) is armed. The log warns `pin 37 has no internal pull`. 38 and 39 + are timer-only. On battery with RTC power-off the ESP32 has no power at all, + so only the rocker push (hardware) or the RTC can switch it on. Rocker + up/down can't. +- **Cold boot stays awake 120 s** before the first sleep + (`DEFAULT_MIN_WAKE_TIME_SECONDS`). This is by design. Timer wakes use the + 20 s window. + +## 7. Battery voltage + +The schematic shows R83 3 kΩ / R86 11 kΩ (VBAT_S = 0.79 × VBAT). M5EPD +assumes 2:1 (`SCALE_INV 2`). With factor 0xA1 the firmware reports 3.91 V +while charging, which fits roughly 2:1, so this board may not match that +schematic revision. Calibrate with a multimeter: new = 0xA1 × actual / reported. + +## Changes + +| Commit | Branch | What | +|---|---|---| +| `f271dc5` | Firmware `greg/awesome-dijkstra-5pq3ha` | IT8951 B_ENDIAN for unmirrored panels; never drive input-only touch INT | +| `207ac66` | Firmware `-rtc` | merge of the above | +| `a075d2c` | opendisplay.org `greg/awesome-dijkstra-5pq3ha` | `m5-paper` touch flags 0x6 | +| `05de81b` | opendisplay.org `-rtc` | merge of the above | + +No changes to the RTC code were needed. + +## Open items + +- Mono (color_scheme 0) and the DU fast path on screen. +- Battery voltage calibration (multimeter). +- Off-current measurement. +- Power-button hold time. +- Watching the screen during battery RTC wakes for a boot-screen redraw. +- The actual 16000 s alarm fire. +- Follow-ups: 0x0052 on the MOSFET latch; clearing the RTC in `m5paper_init()`; + upstream FastEPD endian bug report. From 0b7bf40c8b4b3597967cdfee992ab3f644fde4d2 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 08:14:03 -0400 Subject: [PATCH 05/17] fix(m5paper): clear the on-board RTC's interrupts in m5paper_init() 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). --- src/main.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 74b78d4..e49b579 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1516,6 +1516,39 @@ void nm420_init(){ // FastEPD as the panel's EN pin. Deep-sleep hold and power-off of GPIO2 go // through the MOSFET latch (DEVICE_FLAG_BATTERY_LATCH, pwr_pin_2 = 2), so the // preset sets both flags. +// +// The on-board BM8563 RTC (0x51 on I2C bus 0) runs from VBAT_IN, and its INT +// line also pulls the power-switch gate low: an alarm or timer left armed (the +// M5Stack factory firmware leaves a repeating timer) switches the board back +// on after every power-off, and it survives a flash erase. m5paper_init() +// therefore turns both interrupts off, clears the flags, stops the timer and +// disables the alarm, whatever the sensor config says. +#if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) +static bool m5paperRtcWrite(const uint8_t* regAndData, size_t len) { + Wire.beginTransmission(0x51); + Wire.write(regAndData, len); + return Wire.endTransmission() == 0; +} + +static void m5paperRtcQuiesce() { + static const uint8_t kCtrl2[] = {0x01, 0x00}; // AIE/TIE off, AF/TF cleared + static const uint8_t kTimer[] = {0x0E, 0x03}; // timer off, 1/60 Hz + static const uint8_t kAlarm[] = {0x09, 0x80, 0x80, 0x80, 0x80}; // every alarm field disabled + if (!initOrRestoreWireForBus(0)) { + od_log_warn("M5Paper init: no I2C bus 0, RTC left as is"); + return; + } + const bool ok = m5paperRtcWrite(kCtrl2, sizeof(kCtrl2)) && + m5paperRtcWrite(kTimer, sizeof(kTimer)) && + m5paperRtcWrite(kAlarm, sizeof(kAlarm)); + if (ok) { + od_log_info("M5Paper init: RTC interrupts, timer and alarm cleared"); + } else { + od_log_warn("M5Paper init: RTC not reachable at 0x51"); + } +} +#endif + void m5paper_init(){ #if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) static constexpr uint8_t kM5PaperOnPins[] = {2, 5}; @@ -1529,6 +1562,7 @@ void m5paper_init(){ digitalWrite(pin, HIGH); } od_log_info("M5Paper init: main-power hold and EXT power HIGH"); + m5paperRtcQuiesce(); #endif } From d0072afcbd55ca35171f8a9a14226e4563455060 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 08:28:28 -0400 Subject: [PATCH 06/17] feat: CMD_POWER_OFF (0x0052) on the MOSFET latch; keep RTC wake flags 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 27 ++++++++++++++------- src/device_control.cpp | 12 +++++---- src/main.cpp | 4 ++- src/power_latch.h | 3 ++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index f9f76fe..8a2f663 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -126,9 +126,12 @@ It fires up to 59 s early because the alarm ignores seconds. config (no sensor type 8), the board switched itself back on about 60–75 s after every reset-button power-off. A repeating RTC timer left armed by the factory firmware survives a flash erase, because the RTC is on VBAT_IN. The -RTC config clears it at every boot. *Suggestion:* clear the PCF8563 interrupt -flags and enables in `m5paper_init()` whatever the sensor config, so the -USB-only preset doesn't get phantom power-ons. +RTC config clears it at every boot. **Fixed (`0b7bf40`, PR 1):** +`m5paper_init()` now clears the RTC interrupts, timer and alarm on every +boot. On -rtc it skips that when RTC power-off is configured, because +`rtcConsumeWake()` does the clear itself and needs the flags. Verified with a +temporary readback: armed `C2=01 TC=82` before, `C2=00 TC=03` after. Not +verified: that the phantom power-on is gone after a battery power-off. **VL flag:** the seconds register has VL (bit 7) set, meaning the RTC lost power at some point (probably long ago). The firmware doesn't use wall-clock @@ -152,9 +155,11 @@ how the wake is detected. reset presses that didn't register. - **Reset button (back)** cuts power on battery (GPIO2 released), unless an RTC interrupt is holding `PWR_EN`. -- **`CMD_POWER_OFF` (0x0052)** NACKs on the MOSFET latch: only the D-FF latch - is implemented. The M5Paper could support it by driving GPIO2 low. - Follow-up. +- **`CMD_POWER_OFF` (0x0052)** used to NACK on the MOSFET latch. It now + drives GPIO2 low via `powerLatchTriggerOff()` (on -rtc). On USB the board + went silent on serial and BLE for 60 s and the reset button brought it + back. Not verified on battery. The vendored protocol header still says + "D-FF only" and needs an upstream update; no wire bytes change. - **Deep-sleep button wake:** a classic ESP32 has ext0 only, so just GPIO37 (rocker up) is armed. The log warns `pin 37 has no internal pull`. 38 and 39 are timer-only. On battery with RTC power-off the ESP32 has no power at all, @@ -179,8 +184,10 @@ schematic revision. Calibrate with a multimeter: new = 0xA1 × actual / reported | `207ac66` | Firmware `-rtc` | merge of the above | | `a075d2c` | opendisplay.org `greg/awesome-dijkstra-5pq3ha` | `m5-paper` touch flags 0x6 | | `05de81b` | opendisplay.org `-rtc` | merge of the above | +| `0b7bf40` | Firmware `greg/awesome-dijkstra-5pq3ha` | `m5paper_init()` clears the RTC interrupts, timer and alarm | +| (this commit) | Firmware `-rtc` | merge of the above; 0x0052 on the MOSFET latch; skip the board-init RTC clear when RTC power-off is configured | -No changes to the RTC code were needed. +No changes to the RTC driver itself were needed. ## Open items @@ -190,5 +197,7 @@ No changes to the RTC code were needed. - Power-button hold time. - Watching the screen during battery RTC wakes for a boot-screen redraw. - The actual 16000 s alarm fire. -- Follow-ups: 0x0052 on the MOSFET latch; clearing the RTC in `m5paper_init()`; - upstream FastEPD endian bug report. +- 0x0052 power-off on battery, and no phantom power-on after it. +- Follow-ups: update the 0x0052 `@targets`/`@errors` text in + opendisplay-protocol (MOSFET latch now supported); upstream FastEPD endian + bug report. diff --git a/src/device_control.cpp b/src/device_control.cpp index f5867a8..9e18dbb 100644 --- a/src/device_control.cpp +++ b/src/device_control.cpp @@ -1039,14 +1039,16 @@ void handlePowerOffCommand(const uint8_t* payload, uint16_t payloadLen) { (void)payloadLen; // powerLatch* are defined on every target (no-op stubs off ESP32; the header // guarantees callers need no guards), so no #ifdef TARGET_ESP32 is required here: - // powerLatchDffConfigured() returns false off-ESP32 -> falls through to the NACK. - if (powerLatchDffConfigured()) { - // Fire-and-forget hard rail-cut: queue the ACK, then release the D-FF latch. - // On latch HW the rail usually drops before the ACK is actually transmitted. + // Both predicates return false off-ESP32 -> falls through to the NACK. + if (powerLatchDffConfigured() || powerLatchMosfetConfigured()) { + // Fire-and-forget hard rail-cut: queue the ACK, then release the latch (D-FF: + // clock D low; MOSFET: drive pwr_pin_2 low, e.g. the M5Paper's GPIO2). On latch + // HW the rail usually drops before the ACK is actually transmitted. On USB the + // MOSFET path parks in deep sleep until reset (or the pwr_pin_3 button). uint8_t ok[] = {RESP_ACK, RESP_POWER_OFF, 0x00, 0x00}; sendResponse(ok, sizeof(ok)); delay(100); - powerLatchPowerOff(); + powerLatchTriggerOff(); return; } // ANCHOR(power-off-no-latch-fallback): FUTURE WORK for a later agent/implementer. diff --git a/src/main.cpp b/src/main.cpp index 45beae6..c03d1fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1587,7 +1587,9 @@ void m5paper_init(){ digitalWrite(pin, HIGH); } od_log_info("M5Paper init: main-power hold and EXT power HIGH"); - m5paperRtcQuiesce(); + // With RTC power-off configured, rtcConsumeWake() clears it later in + // setup() and needs the flags to recognise an RTC wake. + if (!rtcWakeConfigured()) m5paperRtcQuiesce(); #endif } diff --git a/src/power_latch.h b/src/power_latch.h index db555a8..c404264 100644 --- a/src/power_latch.h +++ b/src/power_latch.h @@ -20,7 +20,8 @@ void powerButtonPoll(); // esp_deep_sleep_start(). void powerLatchHoldForSleep(); -// Release 74AHC1G79 D-FF latch (pwr_latch_dff only). Used by command 0x0052. +// Release 74AHC1G79 D-FF latch (pwr_latch_dff only). Command 0x0052 uses +// powerLatchTriggerOff(), which also cuts the MOSFET latch. bool powerLatchDffConfigured(void); bool powerLatchMosfetConfigured(void); // RTC power-off sleep (MOSFET latch only): drive the hold pin LOW and keep it From 27016a436cbd49046e398f5fd243ac18db94dd3a Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 17:04:33 -0400 Subject: [PATCH 07/17] fix(fastepd): IT8951 fast refresh in 4bpp falls back to full instead 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). --- src/display_fastepd.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/display_fastepd.cpp b/src/display_fastepd.cpp index 340b4d9..5e6aed4 100644 --- a/src/display_fastepd.cpp +++ b/src/display_fastepd.cpp @@ -431,13 +431,11 @@ void fastepd_direct_refresh(int refresh_mode) { if (!g_epd.currentBuffer()) return; if (refresh_mode == 1) { // FAST: parallel 1bpp uses native diff; IT8951 uses its DU waveform. - // 4bpp has no fast path (fastUpdate is 1bpp-only) -> fall back to full. - if (fastepd_is_parallel()) { - if (fastepd_panel_is_4gray()) fastepd_full_refresh_impl(); - else g_epd.fastUpdate(true); - } else { - it8951_fullscreen_du(); - } + // 4bpp has no fast path (fastUpdate and it8951_fullscreen_du() are + // 1bpp-only) -> fall back to full. + if (fastepd_panel_is_4gray()) fastepd_full_refresh_impl(); + else if (fastepd_is_parallel()) g_epd.fastUpdate(true); + else it8951_fullscreen_du(); } else { fastepd_full_refresh_impl(); } @@ -521,10 +519,11 @@ bool fastepd_partial_refresh(int refresh_mode) { if (refresh_mode == 0) { fastepd_full_refresh_impl(); - } else if (fastepd_is_parallel()) { + } else if (fastepd_panel_is_4gray()) { // Rect already blitted into the framebuffer; 4bpp has no fast path. - if (fastepd_panel_is_4gray()) fastepd_full_refresh_impl(); - else g_epd.fastUpdate(true); + fastepd_full_refresh_impl(); + } else if (fastepd_is_parallel()) { + g_epd.fastUpdate(true); } else { // Region rect is still applied to the framebuffer via blit; refresh is // full-screen DU (IT8951 region DU had persistent edge alignment issues). From d672a6bd30bd9e3b7964da8f2dc875d49a569d1f Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 17:08:35 -0400 Subject: [PATCH 08/17] docs: M5Paper findings: IT8951 readback, 16000 s alarm fire, battery 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 67 +++++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index 8a2f663..db7e1d2 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -28,16 +28,17 @@ serial at INFO level, so they were read from the advertisements or from a | Orientation | PASS (landscape, not mirrored) | | Contrast at FastEPD's fixed VCOM −1.5 V | Acceptable to the eye; −2.30 V patch kept unapplied (§2) | | Repeated pushes | PASS (several full pushes, no wedge, no WDT) | -| Mono (color_scheme 0), DU fast path | **Not verified on hardware** | +| Mono (color_scheme 0), DU fast path | Load verified by IT8951 memory readback (§1). Not checked on screen | +| FAST refresh in 4bpp mode | **FAIL → fixed** (gray data sent as 1bpp DU, §1) | | Touch (GT911) | PASS with flags **0x06**. Fixed GPIO36 errors (§3) | | Buttons 37/38/39 | PASS (press and release on all three) | | Battery hold (GPIO2) after unplugging USB | PASS (19 min on battery, still advertising) | | Power-button hold time | **Not measured** (§6) | -| Battery voltage factor | **Not calibrated** (no multimeter). 0xA1 reads 3.91 V while charging, which is plausible | +| Battery voltage factor | **0xAE** (2:1 divider, measured at full charge, §7) | | RTC sleep on USB (fallback path) | PASS | | RTC power-off on battery, 120 s | PASS: 20 arms, 19 wakes + 1 pending, 0 missed (§4) | | 600 s (1/60 Hz countdown) | PASS on USB: RTC timer fired (TF set) before the ESP32 timer | -| 16000 s (minute/hour alarm) | Programming verified by register readback. The fire itself was not waited out (§4) | +| 16000 s (minute/hour alarm) | PASS on USB: alarm flag set at the expected minute (§4) | | Image push in a wake window | see §5 | | Off current | **Not measured** | | Boot screen not redrawn on battery RTC wakes | **Not observed visually.** The firmware path (`is_deep_sleep_wake`) is confirmed on USB | @@ -64,8 +65,27 @@ waveform, loaded B_ENDIAN. `it8951_fullscreen_du()` uses B_ENDIAN too. Mirrored panels keep FastEPD's own writer, unchanged. **Verified:** the 4bpp image is sharp after the fix (user confirmed). -**Not verified:** 1bpp/mono and the DU path on screen. Worth reporting -upstream to FastEPD. +A temporary readback of IT8951 memory after each load (MEM_BST_RD, sampled +every 27th row, not committed) confirms the frame lands as sent: + +| Path | Sampled bytes matching the framebuffer | +|---|---| +| Full, 4bpp (GC16) | 19200/19200 | +| Full, 1bpp (mono config) | 2400/2400 (1232 with the old byte swap) | +| DU, 1bpp (mono config, FAST upload) | 2400/2400 | + +**Not verified:** the 1bpp bit order on screen (the readback only proves the +bytes arrive unswapped). Worth reporting upstream to FastEPD. + +**FAST refresh in 4bpp mode (fixed, `27016a4`, PR 1):** the readback also +showed that a FAST upload with the GRAY16 config sent the 4bpp framebuffer +through `it8951_fullscreen_du()`, which loads w/8 bytes per row as 1bpp. Every +sampled byte in IT8951 memory equalled the raw 4bpp bytes, so the top quarter +of the gray image would show as 1bpp noise. The parallel path already fell +back to a full refresh for 4bpp. The IT8951 path now does too, for direct +and partial transfers. After the fix the same upload logs `EPD refresh: FAST` +and loads through the 4bpp full path (19200/19200). The ED103TC2 (reTerminal) +takes the same branch but was not tested. ## 2. VCOM @@ -122,6 +142,17 @@ from 10, TF set, TIE on. 18:00. Readback `AL=00 18 80 80`, `C2=02` (AIE), `TC=03` (timer off). Correct. It fires up to 59 s early because the alarm ignores seconds. +**16000 s fire (USB, second run):** armed at RTC 04:11:40 with `AL=38 08 80 80` +(08:38). For the test only, the ESP32 backstop timer was extended by 600 s so +the RTC would act first. At the next boot (RTC 08:42:38) the dump showed +`C2=0A`: **AF set**, AIE on, so the alarm matched. The ESP32 itself woke on its +timer (`Wake-up reason: timer`): on USB the RTC INT only pulls `PWR_EN`, and the +rail is already up, so nothing wakes the chip. That is expected. On battery the +same INT switches the rail on. The ESP32 timer (16600 s nominal) expired after +about 16260 s of RTC time, so the ESP32 slow clock ran about 2% fast. The wake +cycle after that re-armed the 120 s countdown and fired normally (`C2=05`, TF). +Not verified: the 16000 s wake on battery. + **Leftover RTC state from M5Stack's factory firmware:** with the USB-only config (no sensor type 8), the board switched itself back on about 60–75 s after every reset-button power-off. A repeating RTC timer left armed by the @@ -172,9 +203,14 @@ how the wake is detected. ## 7. Battery voltage The schematic shows R83 3 kΩ / R86 11 kΩ (VBAT_S = 0.79 × VBAT). M5EPD -assumes 2:1 (`SCALE_INV 2`). With factor 0xA1 the firmware reports 3.91 V -while charging, which fits roughly 2:1, so this board may not match that -schematic revision. Calibrate with a multimeter: new = 0xA1 × actual / reported. +assumes 2:1 (`SCALE_INV 2`). After a night on USB (battery full, about 4.20 V), +GPIO35 read raw 2416 and the eFuse-calibrated `analogReadMilliVolts()` read +2101 mV, which is exactly 2:1. So this board does not match that schematic +revision. The firmware computes V = raw × factor / 100000, so 4.20 V needs +factor 174 = **0xAE**. The `m5-paper` preset is updated on both opendisplay.org +branches (`5479e3f`, merged as `4d6e092`). The board now reports 4.20 V. Not +checked against a multimeter, or at a lower charge, where ADC non-linearity +may add error. ## Changes @@ -185,18 +221,25 @@ schematic revision. Calibrate with a multimeter: new = 0xA1 × actual / reported | `a075d2c` | opendisplay.org `greg/awesome-dijkstra-5pq3ha` | `m5-paper` touch flags 0x6 | | `05de81b` | opendisplay.org `-rtc` | merge of the above | | `0b7bf40` | Firmware `greg/awesome-dijkstra-5pq3ha` | `m5paper_init()` clears the RTC interrupts, timer and alarm | -| (this commit) | Firmware `-rtc` | merge of the above; 0x0052 on the MOSFET latch; skip the board-init RTC clear when RTC power-off is configured | +| `d0072af` | Firmware `-rtc` | merge of the above; 0x0052 on the MOSFET latch; skip the board-init RTC clear when RTC power-off is configured | + +| `5479e3f` | opendisplay.org `greg/awesome-dijkstra-5pq3ha` | `m5-paper` voltage_scaling_factor 0xAE | +| `4d6e092` | opendisplay.org `-rtc` | merge of the above | +| `27016a4` | Firmware `greg/awesome-dijkstra-5pq3ha` | IT8951 FAST refresh in 4bpp falls back to full | +| `2d9a8f8` | Firmware `-rtc` | merge of the above | +| (this commit) | Firmware `-rtc` | readback, alarm-fire and battery results in this report | No changes to the RTC driver itself were needed. ## Open items -- Mono (color_scheme 0) and the DU fast path on screen. -- Battery voltage calibration (multimeter). +- Mono (color_scheme 0) and the DU fast path on screen (bit order). +- Battery voltage against a multimeter, and at lower charge. - Off-current measurement. - Power-button hold time. - Watching the screen during battery RTC wakes for a boot-screen redraw. -- The actual 16000 s alarm fire. +- The 16000 s alarm wake on battery (the fire is verified on USB). +- FAST refresh in 4bpp on the ED103TC2 (reTerminal). - 0x0052 power-off on battery, and no phantom power-on after it. - Follow-ups: update the 0x0052 `@targets`/`@errors` text in opendisplay-protocol (MOSFET latch now supported); upstream FastEPD endian From 34548b0a7e8e00277ca75441248ae0370cac518b Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 17:33:10 -0400 Subject: [PATCH 09/17] fix(fastepd): bit-reverse 1bpp rows for unmirrored IT8951 panels 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. --- src/display_fastepd.cpp | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/display_fastepd.cpp b/src/display_fastepd.cpp index 5e6aed4..03beb10 100644 --- a/src/display_fastepd.cpp +++ b/src/display_fastepd.cpp @@ -179,6 +179,26 @@ static void blit_1bpp_rect(uint8_t* dst, const uint8_t* src, uint32_t src_len, *io_offset = off; } +// The IT8951 1bpp mode takes the leftmost pixel from bit 0; FastEPD packs it +// in bit 7. Mirrored panels get away with reversing the byte order of each +// row (that, plus LSB-first, mirrors the whole row). Unmirrored panels need +// each byte bit-reversed (verified on an M5Paper). +static void it8951_write_1bpp_row_lsb_first(const uint8_t* src, unsigned n) { + uint8_t chunk[64]; + while (n) { + const unsigned k = n < sizeof(chunk) ? n : (unsigned)sizeof(chunk); + for (unsigned i = 0; i < k; i++) { + uint8_t b = src[i]; + b = (uint8_t)((b >> 4) | (b << 4)); + b = (uint8_t)(((b & 0xCC) >> 2) | ((b & 0x33) << 2)); + chunk[i] = (uint8_t)(((b & 0xAA) >> 1) | ((b & 0x55) << 1)); + } + SPI.writeBytes(chunk, k); + src += k; + n -= k; + } +} + /** Full-screen 1bpp DU (mode 1) — non-flashing. Matches FastEPD full-frame * packing (MIRROR_X → B_ENDIAN + byte-reverse) with IT8951 DU waveform. */ static bool it8951_fullscreen_du(void) { @@ -216,7 +236,7 @@ static bool it8951_fullscreen_du(void) { } SPI.writeBytes(rowbuf, pitch); } else { - SPI.writeBytes(src, pitch); + it8951_write_1bpp_row_lsb_first(src, pitch); } if ((row & 7) == 0) yield(); } @@ -305,8 +325,9 @@ void fastepd_epaper_begin(void) { // FastEPD 2.2.0 loads an unmirrored IT8951 frame as L_ENDIAN, which swaps the // two bytes of every 16-bit word: 4bpp pixel pairs land 2 px off, 1bpp bytes // 8 px off (verified on an M5Paper). Same frame and GC16 waveform as -// it8951WriteFramebuffer4Bit/1Bit, loaded B_ENDIAN as M5EPD does. Mirrored -// panels (ED103TC2) already get B_ENDIAN from FastEPD. +// it8951WriteFramebuffer4Bit/1Bit, loaded B_ENDIAN as M5EPD does; 1bpp rows +// are also bit-reversed (it8951_write_1bpp_row_lsb_first()). Mirrored panels +// (ED103TC2) already get B_ENDIAN from FastEPD. static void it8951_full_write_unmirrored(void) { FASTEPDSTATE* st = g_epd.state(); const uint8_t* s = g_epd.currentBuffer(); @@ -333,7 +354,8 @@ static void it8951_full_write_unmirrored(void) { SPI.transfer16(0x0000); it8951WaitForReady(st); for (uint16_t row = 0; row < h; row++) { - SPI.writeBytes(s, pitch); + if (gray) SPI.writeBytes(s, pitch); + else it8951_write_1bpp_row_lsb_first(s, pitch); s += pitch; if ((row & 7) == 0) yield(); } From 35ec236a503b018a2528f454a421ea073c89232d Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 17:45:38 -0400 Subject: [PATCH 10/17] docs: M5Paper findings: 1bpp bit order fixed, mono verified on screen 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 25 ++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index db7e1d2..78b1927 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -28,7 +28,7 @@ serial at INFO level, so they were read from the advertisements or from a | Orientation | PASS (landscape, not mirrored) | | Contrast at FastEPD's fixed VCOM −1.5 V | Acceptable to the eye; −2.30 V patch kept unapplied (§2) | | Repeated pushes | PASS (several full pushes, no wedge, no WDT) | -| Mono (color_scheme 0), DU fast path | Load verified by IT8951 memory readback (§1). Not checked on screen | +| Mono (color_scheme 0), DU fast path | **FAIL → fixed** (1bpp bit order, §1). Full clean; DU readable with normal DU ghosting | | FAST refresh in 4bpp mode | **FAIL → fixed** (gray data sent as 1bpp DU, §1) | | Touch (GT911) | PASS with flags **0x06**. Fixed GPIO36 errors (§3) | | Buttons 37/38/39 | PASS (press and release on all three) | @@ -74,8 +74,20 @@ every 27th row, not committed) confirms the frame lands as sent: | Full, 1bpp (mono config) | 2400/2400 (1232 with the old byte swap) | | DU, 1bpp (mono config, FAST upload) | 2400/2400 | -**Not verified:** the 1bpp bit order on screen (the readback only proves the -bytes arrive unswapped). Worth reporting upstream to FastEPD. +Worth reporting upstream to FastEPD. + +**1bpp bit order (fixed, `34548b0`, PR 1):** with the bytes loading correctly, +a full mono refresh was still unreadable ("vertical interlacing"). A test image +with four bands, each pre-scrambled on the host (none, bits reversed within +each byte, bytes swapped, both), showed only the bit-reversed band cleanly: +the IT8951 1bpp mode takes the leftmost pixel from bit 0, while FastEPD packs +it in bit 7. Mirrored panels never showed it, because FastEPD reverses the +byte order of each row for them, and that plus LSB-first mirrors the whole row. +Unmirrored panels now send each 1bpp byte bit-reversed (full writes and DU). +After the fix a full refresh of a text, ruler and 1 px line test image is +clean (user confirmed). A FAST (DU) refresh is readable but shows faint ghosts +of the previous image and some white specks in large black areas, which is +normal for the DU waveform. **FAST refresh in 4bpp mode (fixed, `27016a4`, PR 1):** the readback also showed that a FAST upload with the GRAY16 config sent the 4bpp framebuffer @@ -227,13 +239,16 @@ may add error. | `4d6e092` | opendisplay.org `-rtc` | merge of the above | | `27016a4` | Firmware `greg/awesome-dijkstra-5pq3ha` | IT8951 FAST refresh in 4bpp falls back to full | | `2d9a8f8` | Firmware `-rtc` | merge of the above | -| (this commit) | Firmware `-rtc` | readback, alarm-fire and battery results in this report | +| `d672a6b` | Firmware `-rtc` | readback, alarm-fire and battery results in this report | +| `34548b0` | Firmware `greg/awesome-dijkstra-5pq3ha` | bit-reverse 1bpp rows for unmirrored IT8951 panels | +| `314e411` | Firmware `-rtc` | merge of the above | +| (this commit) | Firmware `-rtc` | 1bpp bit-order result in this report | No changes to the RTC driver itself were needed. ## Open items -- Mono (color_scheme 0) and the DU fast path on screen (bit order). +- DU fast-refresh quality (ghosting, fill); a different fast waveform may do better. - Battery voltage against a multimeter, and at lower charge. - Off-current measurement. - Power-button hold time. From 1a94f60b09dcd508ec857c647c60d5725997aa5f Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 18:09:35 -0400 Subject: [PATCH 11/17] docs: M5Paper findings: 0x0052 power-off verified on battery 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). --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index 78b1927..b490318 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -201,7 +201,11 @@ how the wake is detected. - **`CMD_POWER_OFF` (0x0052)** used to NACK on the MOSFET latch. It now drives GPIO2 low via `powerLatchTriggerOff()` (on -rtc). On USB the board went silent on serial and BLE for 60 s and the reset button brought it - back. Not verified on battery. The vendored protocol header still says + back. **On battery: PASS.** Sent at 17:47:07, advertising stopped by 17:47:16 +and stayed off for 20 min (one RSSI −127 cached report from the Windows +scanner at 17:48, not a real power-on), so no phantom power-on. A 2 s rocker +push brought it back (advertising and answering at 4.13 V). The ACK was not +received (fire-and-forget). The vendored protocol header still says "D-FF only" and needs an upstream update; no wire bytes change. - **Deep-sleep button wake:** a classic ESP32 has ext0 only, so just GPIO37 (rocker up) is armed. The log warns `pin 37 has no internal pull`. 38 and 39 @@ -255,7 +259,6 @@ No changes to the RTC driver itself were needed. - Watching the screen during battery RTC wakes for a boot-screen redraw. - The 16000 s alarm wake on battery (the fire is verified on USB). - FAST refresh in 4bpp on the ED103TC2 (reTerminal). -- 0x0052 power-off on battery, and no phantom power-on after it. - Follow-ups: update the 0x0052 `@targets`/`@errors` text in opendisplay-protocol (MOSFET latch now supported); upstream FastEPD endian bug report. From 0cb3e8e63908d48f726eb96cced639be2f68be00 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 18:26:29 -0400 Subject: [PATCH 12/17] docs: M5Paper findings: power-button hold time about 1.1 s 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index b490318..e213e4b 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -33,7 +33,7 @@ serial at INFO level, so they were read from the advertisements or from a | Touch (GT911) | PASS with flags **0x06**. Fixed GPIO36 errors (§3) | | Buttons 37/38/39 | PASS (press and release on all three) | | Battery hold (GPIO2) after unplugging USB | PASS (19 min on battery, still advertising) | -| Power-button hold time | **Not measured** (§6) | +| Power-button hold time | About **1.1 s** minimum; hold 2–3 s (§6) | | Battery voltage factor | **0xAE** (2:1 divider, measured at full charge, §7) | | RTC sleep on USB (fallback path) | PASS | | RTC power-off on battery, 120 s | PASS: 20 arms, 19 wakes + 1 pending, 0 missed (§4) | @@ -191,11 +191,18 @@ how the wake is detected. ## 6. Power-on, power-off and buttons -- **Power button = rocker pushed in** (KEY_PUSH, GPIO38). M5Stack documents a - 2 s hold. `m5paper_init()` drives GPIO2 at about 0.17 s after boot (about - 0.3–0.5 s after power-on, counting the ROM and bootloader). The hold time was - not measured, because tests were confused by the leftover RTC timer and by - reset presses that didn't register. +- **Power button = rocker pushed in** (KEY_PUSH, GPIO38). The rocker only + powers the rail while it is held; the board stays on once `m5paper_init()` + drives GPIO2. Timed on the PC over serial (USB, EN reset, 3 runs): first app + log line at 1058 ms, `M5Paper init` at 1073 ms, so **about 1.1 s minimum + hold**. Roughly 0.9 s of that is before the app starts: the prebuilt + bootloader validates the whole app image on every power-on + (`BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON` off) and PSRAM is set up. On battery, + after 0x0052: 0.3 s and 1 s holds did not power on, a hand-timed 2 s hold + did once and not once, and 5 s worked every time, including 15 s after a + power-off (so it is hold time, not leftover charge in the rail). M5Stack's + "hold 2 s" advice fits; 3 s is reliable. Driving GPIO2 from an earlier hook + (a global constructor) might save the app-side part; not tried. - **Reset button (back)** cuts power on battery (GPIO2 released), unless an RTC interrupt is holding `PWR_EN`. - **`CMD_POWER_OFF` (0x0052)** used to NACK on the MOSFET latch. It now @@ -255,7 +262,6 @@ No changes to the RTC driver itself were needed. - DU fast-refresh quality (ghosting, fill); a different fast waveform may do better. - Battery voltage against a multimeter, and at lower charge. - Off-current measurement. -- Power-button hold time. - Watching the screen during battery RTC wakes for a boot-screen redraw. - The 16000 s alarm wake on battery (the fire is verified on USB). - FAST refresh in 4bpp on the ED103TC2 (reTerminal). From 321f3be700d62ae8bb7ae26502b91feab2978511 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 20:36:10 -0400 Subject: [PATCH 13/17] feat(m5paper): run at 80 MHz and leave the Grove 5 V boost off 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). --- src/main.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e49b579..b37051f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1551,17 +1551,23 @@ static void m5paperRtcQuiesce() { void m5paper_init(){ #if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) - static constexpr uint8_t kM5PaperOnPins[] = {2, 5}; - for (uint8_t pin : kM5PaperOnPins) { - // Level before direction, then drop the sleep hold: the pad never - // glitches LOW on a wake from deep sleep. - gpio_set_level((gpio_num_t)pin, 1); - gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); - gpio_hold_dis((gpio_num_t)pin); - pinMode(pin, OUTPUT); - digitalWrite(pin, HIGH); - } - od_log_info("M5Paper init: main-power hold and EXT power HIGH"); + // GPIO2 holds main power. Level before direction, then drop the sleep + // hold: the pad never glitches LOW on a wake from deep sleep. + gpio_set_level(GPIO_NUM_2, 1); + gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT); + gpio_hold_dis(GPIO_NUM_2); + pinMode(2, OUTPUT); + digitalWrite(2, HIGH); + // GPIO5 enables the 5 V boost for the Grove ports, which nothing here + // uses; M5EPD turns it on by default. Keep it off to save power. + gpio_hold_dis(GPIO_NUM_5); + pinMode(5, OUTPUT); + digitalWrite(5, LOW); + // BLE needs no more than 80 MHz, and the wake window is mostly idle + // polling. Switched after the GPIO2 hold so the power-button hold time + // does not grow. + setCpuFrequencyMhz(80); + od_log_info("M5Paper init: main-power hold, EXT power off, CPU %u MHz", (unsigned)getCpuFrequencyMhz()); m5paperRtcQuiesce(); #endif } From 54ea4faf2a46936c71341760aaffec1fa8ea77c1 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 20:45:01 -0400 Subject: [PATCH 14/17] docs: M5Paper findings: battery RTC wakes, 80 MHz and Grove power off 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. --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 42 +++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index e213e4b..56435af 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -41,7 +41,8 @@ serial at INFO level, so they were read from the advertisements or from a | 16000 s (minute/hour alarm) | PASS on USB: alarm flag set at the expected minute (§4) | | Image push in a wake window | see §5 | | Off current | **Not measured** | -| Boot screen not redrawn on battery RTC wakes | **Not observed visually.** The firmware path (`is_deep_sleep_wake`) is confirmed on USB | +| Boot screen not redrawn on battery RTC wakes | PASS: a gray image survived about 1.5 h (about 38 wakes) on battery | +| Wake-window power | 80 MHz CPU, Grove 5 V off (§8). Saving not measured | | All CI targets build | PASS (12 envs in `firmware-targets.json`, not 11) | ## 1. IT8951 byte order (FastEPD bug) — fixed on PR 1 @@ -235,6 +236,36 @@ branches (`5479e3f`, merged as `4d6e092`). The board now reports 4.20 V. Not checked against a multimeter, or at a lower charge, where ADC non-linearity may add error. +## 8. Power during the wake window + +Between wakes the RTC power-off cuts the whole board, so the 20 s advertising +window is where the battery goes. The M5Paper build has no WiFi +(`OPENDISPLAY_ENABLE_WIFI` is not set for `esp32-wrover-e-N4R8`) and no +ESP-IDF power management (`CONFIG_PM_ENABLE` off), so the CPU polls through +the window. Two changes (`321f3be`, PR 1): + +- **80 MHz CPU**, set in `m5paper_init()` right after the GPIO2 hold, so the + power-button hold time is unchanged (reset to hold 1068 ms vs 1073 ms). + A 259200-byte gray upload took 21.72 s on the device vs 21.76 s at 240 MHz + (BLE-bound), and its refresh 1.49 s vs 1.47 s. The cold-boot QR boot screen + now takes 8.2 s to render instead of 4.2 s. RTC wakes don't draw it. +- **Grove 5 V boost off:** GPIO5 (EXT power) is now driven LOW. M5EPD turns it + on by default; nothing here uses the Grove ports. Touch still works + (a corner tap read x=18, y=51). + +Verified on USB with the RTC config: cold boot, RTC power-off sleep, timer +wake at 80 MHz with the 20 s window, back to sleep. Not measured: the current +saved (no meter). The battery wake test (§4) is still the 240 MHz build. +Turning the Grove ports back on needs a config option, for example a new +DeviceFlags bit, which is a protocol change upstream. + +**Wake-interval settings** (all config, no firmware change): +`deep_sleep_time_seconds` (time asleep, up to about 18 h; the RTC picks the +seconds countdown, the minutes countdown or the alarm), `sleep_timeout_ms` (the +advertising window, 20 s in the preset), `min_wake_time_seconds` (the cold-boot +hold, 120 s by default), and command 0x0053 (host override for one cycle, 60 s +minimum). A longer sleep and a shorter window are the biggest saving. + ## Changes | Commit | Branch | What | @@ -253,7 +284,11 @@ may add error. | `d672a6b` | Firmware `-rtc` | readback, alarm-fire and battery results in this report | | `34548b0` | Firmware `greg/awesome-dijkstra-5pq3ha` | bit-reverse 1bpp rows for unmirrored IT8951 panels | | `314e411` | Firmware `-rtc` | merge of the above | -| (this commit) | Firmware `-rtc` | 1bpp bit-order result in this report | +| `35ec236` | Firmware `-rtc` | 1bpp bit-order result in this report | +| `1a94f60`, `0cb3e8e` | Firmware `-rtc` | 0x0052 battery result, power-button hold time | +| `321f3be` | Firmware `greg/awesome-dijkstra-5pq3ha` | M5Paper at 80 MHz, Grove 5 V boost off | +| `f4f73cc` | Firmware `-rtc` | merge of the above | +| (this commit) | Firmware `-rtc` | battery RTC wake and wake-window power results in this report | No changes to the RTC driver itself were needed. @@ -262,7 +297,8 @@ No changes to the RTC driver itself were needed. - DU fast-refresh quality (ghosting, fill); a different fast waveform may do better. - Battery voltage against a multimeter, and at lower charge. - Off-current measurement. -- Watching the screen during battery RTC wakes for a boot-screen redraw. +- A config option to turn the Grove 5 V back on (needs a protocol flag). +- Wake-window current at 80 MHz vs 240 MHz (needs a meter). - The 16000 s alarm wake on battery (the fire is verified on USB). - FAST refresh in 4bpp on the ED103TC2 (reTerminal). - Follow-ups: update the 0x0052 `@targets`/`@errors` text in From 32a60fdc1b7ec578ec56467fa45efd05fd1d7866 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 20:54:56 -0400 Subject: [PATCH 15/17] docs(m5paper): note how to re-enable the Grove 5 V boost Comment only, no code change. Not verified on hardware (nothing to verify). --- src/main.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b37051f..db79351 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1558,8 +1558,11 @@ void m5paper_init(){ gpio_hold_dis(GPIO_NUM_2); pinMode(2, OUTPUT); digitalWrite(2, HIGH); - // GPIO5 enables the 5 V boost for the Grove ports, which nothing here - // uses; M5EPD turns it on by default. Keep it off to save power. + // GPIO5 enables the 5 V boost for the Grove ports (A/B/C), which nothing + // here uses; M5EPD turns it on by default. Off by default to save power. + // Anything plugged into a Grove port is unpowered. To power it, drive + // GPIO5 HIGH here instead; no config option exists yet (it would need a + // new DeviceFlags bit in opendisplay-protocol). gpio_hold_dis(GPIO_NUM_5); pinMode(5, OUTPUT); digitalWrite(5, LOW); From 16689b209b07f8c00ae235eb62f51a5a76fdea36 Mon Sep 17 00:00:00 2001 From: gcormier Date: Tue, 27 Oct 2026 22:08:16 -0400 Subject: [PATCH 16/17] docs: M5Paper findings: 80 MHz RTC wakes and upload on battery 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/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index 56435af..8a595a2 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -42,7 +42,7 @@ serial at INFO level, so they were read from the advertisements or from a | Image push in a wake window | see §5 | | Off current | **Not measured** | | Boot screen not redrawn on battery RTC wakes | PASS: a gray image survived about 1.5 h (about 38 wakes) on battery | -| Wake-window power | 80 MHz CPU, Grove 5 V off (§8). Saving not measured | +| Wake-window power | 80 MHz CPU, Grove 5 V off (§8): 28 wakes and an upload on battery, PASS. Saving not measured | | All CI targets build | PASS (12 envs in `firmware-targets.json`, not 11) | ## 1. IT8951 byte order (FastEPD bug) — fixed on PR 1 @@ -255,7 +255,14 @@ the window. Two changes (`321f3be`, PR 1): Verified on USB with the RTC config: cold boot, RTC power-off sleep, timer wake at 80 MHz with the 20 s window, back to sleep. Not measured: the current -saved (no meter). The battery wake test (§4) is still the 240 MHz build. +saved (no meter). + +**On battery at 80 MHz** (`496255d`, Grove off, 120 s sleep, 20 s window): +a 1bpp full upload in a wake window took 18 s and displayed correctly. A BLE +watch from 20:56 to 22:06 saw 21 distinct windows. Every gap was one cycle +(127–159 s) or two (272–303 s, the Windows scanner missing a window, as seen +on USB). There were no longer gaps. That is 28 cycles in +67 min. The reported battery stayed at 4.15–4.17 V throughout (ADC noise). Turning the Grove ports back on needs a config option, for example a new DeviceFlags bit, which is a protocol change upstream. @@ -288,7 +295,9 @@ minimum). A longer sleep and a shorter window are the biggest saving. | `1a94f60`, `0cb3e8e` | Firmware `-rtc` | 0x0052 battery result, power-button hold time | | `321f3be` | Firmware `greg/awesome-dijkstra-5pq3ha` | M5Paper at 80 MHz, Grove 5 V boost off | | `f4f73cc` | Firmware `-rtc` | merge of the above | -| (this commit) | Firmware `-rtc` | battery RTC wake and wake-window power results in this report | +| `54ea4fa` | Firmware `-rtc` | battery RTC wake and wake-window power results in this report | +| `32a60fd`, `496255d` | Firmware PR 1, merged into `-rtc` | comment on re-enabling the Grove 5 V | +| (this commit) | Firmware `-rtc` | 80 MHz battery wake results in this report | No changes to the RTC driver itself were needed. From 2c32ae8d1b65cc9ee4227b7c0b97513bbb3e936b Mon Sep 17 00:00:00 2001 From: gcormier Date: Wed, 28 Oct 2026 03:08:09 -0400 Subject: [PATCH 17/17] docs: M5Paper findings: 16000 s RTC alarm wake on battery 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). --- docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md index 8a595a2..12d54a8 100644 --- a/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md +++ b/docs/FINDINGS_M5PAPER_BRINGUP_2026-10-26.md @@ -38,7 +38,7 @@ serial at INFO level, so they were read from the advertisements or from a | RTC sleep on USB (fallback path) | PASS | | RTC power-off on battery, 120 s | PASS: 20 arms, 19 wakes + 1 pending, 0 missed (§4) | | 600 s (1/60 Hz countdown) | PASS on USB: RTC timer fired (TF set) before the ESP32 timer | -| 16000 s (minute/hour alarm) | PASS on USB: alarm flag set at the expected minute (§4) | +| 16000 s (minute/hour alarm) | PASS on USB (alarm flag at the expected minute) and on battery (woke after 4 h 27 min) (§4) | | Image push in a wake window | see §5 | | Off current | **Not measured** | | Boot screen not redrawn on battery RTC wakes | PASS: a gray image survived about 1.5 h (about 38 wakes) on battery | @@ -164,7 +164,11 @@ rail is already up, so nothing wakes the chip. That is expected. On battery the same INT switches the rail on. The ESP32 timer (16600 s nominal) expired after about 16260 s of RTC time, so the ESP32 slow clock ran about 2% fast. The wake cycle after that re-armed the 120 s countdown and fired normally (`C2=05`, TF). -Not verified: the 16000 s wake on battery. +**16000 s wake on battery** (80 MHz build `496255d`): a one-time 0x0053 +sleep of 16000 s was sent at 22:07:39. The device did not advertise again until +02:34:10, 15991 s later. That is within the alarm's one-minute resolution. +It then went back to the configured 120 s cycle (gaps of 141–146 s). The +reported battery was 4.14–4.15 V, down from 4.15–4.17 V the evening before. **Leftover RTC state from M5Stack's factory firmware:** with the USB-only config (no sensor type 8), the board switched itself back on about 60–75 s @@ -297,7 +301,8 @@ minimum). A longer sleep and a shorter window are the biggest saving. | `f4f73cc` | Firmware `-rtc` | merge of the above | | `54ea4fa` | Firmware `-rtc` | battery RTC wake and wake-window power results in this report | | `32a60fd`, `496255d` | Firmware PR 1, merged into `-rtc` | comment on re-enabling the Grove 5 V | -| (this commit) | Firmware `-rtc` | 80 MHz battery wake results in this report | +| `16689b2` | Firmware `-rtc` | 80 MHz battery wake results in this report | +| (this commit) | Firmware `-rtc` | 16000 s battery wake result in this report | No changes to the RTC driver itself were needed. @@ -308,7 +313,6 @@ No changes to the RTC driver itself were needed. - Off-current measurement. - A config option to turn the Grove 5 V back on (needs a protocol flag). - Wake-window current at 80 MHz vs 240 MHz (needs a meter). -- The 16000 s alarm wake on battery (the fire is verified on USB). - FAST refresh in 4bpp on the ED103TC2 (reTerminal). - Follow-ups: update the 0x0052 `@targets`/`@errors` text in opendisplay-protocol (MOSFET latch now supported); upstream FastEPD endian