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..03beb10 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); @@ -82,6 +85,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; } @@ -162,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) { @@ -178,8 +215,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; @@ -198,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(); } @@ -260,7 +298,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; @@ -277,9 +322,64 @@ 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; 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(); + 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++) { + if (gray) SPI.writeBytes(s, pitch); + else it8951_write_1bpp_row_lsb_first(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 @@ -353,13 +453,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(); } @@ -443,10 +541,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). 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..db79351 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1509,6 +1509,72 @@ 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. +// +// 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) + // 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 (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); + // 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 +} + #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(); 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);