From 829bb2ebdb64d2516407bd142487c35a2947ec6a Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Wed, 7 Oct 2026 05:38:10 +0000 Subject: [PATCH] Power: PaperMono: do not touch the charger when it is in LED mode The IP2316 latches its mode when VIN is applied: I2C mode only if SCL and SDA are both High at that moment, otherwise LED mode until VIN is removed. On PaperMono the lines are only pulled High while the level-shifter gate (IOE1 GPIO11) is open, so plugging USB in with the gate closed, which is always the case while powered off, gives LED mode. In LED mode the charger drives LED1/LED2 (= SCL/SDA) at about 500 Hz. getChargeState() and setBatteryCharge() opened the gate and polled 0x75 up to 64 times on that disturbed bus, which stalled the board until the power key was pressed; writes in that state can also land corrupted. The gate is now opened with the IOE1 output byte saved first, and the bus is checked with plain GPIO reads (own outputs disabled) for 4.5 ms before any I2C traffic; a watch with a long gap between reads is repeated, and a bus that could not be watched is an io_error. If a line goes Low, the charger is in LED mode: the gate is closed again between the charger pulses and the whole byte is confirmed by reading it back (up to 20 tries). A gate that cannot be confirmed closed is remembered with the byte to restore, and both APIs retry closing it before any other bus access (io_error / false until it succeeds). LED mode is remembered, only on a clean judgement with VIN present, until the PM1 reports no VIN; both getChargeState() and setBatteryCharge() read the power source for this. In LED mode getChargeState() returns undetermined and setBatteryCharge() returns false. The sequence assumes no other task uses the internal bus meanwhile, like the rest of the power functions. Limits: VIN removed and reapplied between two calls is not seen, so LED mode stays remembered; only these two APIs retry an unconfirmed close. --- src/utility/Power_Class.inl | 175 +++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 20 deletions(-) diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index 58c19f6..2ab311c 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -23,6 +23,7 @@ #include #include +#include // ESP-IDF v4 defines only the generic SOC_PM_SUPPORT_EXT_WAKEUP; the split into // SOC_PM_SUPPORT_EXT0_WAKEUP / SOC_PM_SUPPORT_EXT1_WAKEUP came later. @@ -86,11 +87,6 @@ namespace m5 ioe1.digitalWrite(M5IOE1_Class::gpio11, false); } - static bool set_papermono_ip2315_enabled(bool enable) - { - return M5.getIOExpander(0).digitalWrite(M5IOE1_Class::gpio11, enable); - } - static bool wait_papermono_ip2315_ready(void) { m5gfx::delay(2); @@ -101,6 +97,130 @@ namespace m5 return false; } + /// The IP2316 latches its mode when VIN is applied: I2C mode only if SCL and SDA + /// are both High at that moment, otherwise LED mode until VIN is removed. On this + /// board the lines are only pulled High while the gate (IOE1 GPIO11, the level + /// shifter OE) is open, so plugging USB in with the gate closed (always the case + /// while powered off) gives LED mode. In LED mode the charger drives LED1/LED2 + /// (= SCL/SDA) at about 500 Hz, and opening the gate disturbs the internal I2C bus: + /// I2C traffic in that state stalls the board, and writes can land corrupted. + /// So the bus is checked with plain GPIO reads right after opening, and in LED mode + /// the gate is closed again without touching the charger. The result is kept until + /// VIN is seen removed. + static constexpr std::uint8_t papermono_ioe1_out_reg = 0x06; // M5IOE1 GPIO_OUT_H (GPIO9..) + static constexpr std::uint8_t papermono_gate_bit = 1 << 2; // GPIO11 + /// LED mode is remembered until VIN is seen removed. A gate that could not be + /// confirmed closed is remembered too, with the byte to restore, and closing is + /// retried before anything else. Like the rest of this class, the sequence + /// assumes no other task uses the internal I2C bus meanwhile. + /// Limits: VIN removed and reapplied between two calls is not seen, so LED mode + /// stays remembered (the charger stays untouched); only the two charge APIs retry + /// an unconfirmed close, other users of the internal bus do not. + static bool papermono_ip2315_led_mode = false; + static bool papermono_gate_unclosed = false; + static std::uint8_t papermono_gate_saved = 0; + + enum class papermono_gate_t : std::uint8_t { opened, led_mode, error }; + + static constexpr std::uint32_t papermono_bus_mask = (1u << (GPIO_NUM_47 - 32)) | (1u << (GPIO_NUM_48 - 32)); + + enum class papermono_bus_t : std::uint8_t { quiet, disturbed, unobserved }; + + /// quiet when neither SCL (G48) nor SDA (G47) goes Low for 4.5 ms after the gate + /// write. The ESP32's own outputs are disabled meanwhile so that only the bus is + /// read. IOE1 applies the write to its pin about 1.1 ms later; in LED mode SDA is + /// Low half of every 2 ms period. A watch with a gap between two reads longer + /// than 200 us (preemption) proves nothing and is repeated; unobserved when every + /// try had such a gap. + static papermono_bus_t papermono_bus_watch(void) + { + for (int retry = 0; retry < 4; ++retry) + { + std::uint32_t en = REG_READ(GPIO_ENABLE1_REG) & papermono_bus_mask; + REG_WRITE(GPIO_ENABLE1_W1TC_REG, en); + bool low = false; + std::uint32_t gap_max = 0; + auto t0 = m5gfx::micros(); + auto prev = t0; + for (;;) + { + auto now = m5gfx::micros(); + if (now - prev > gap_max) { gap_max = now - prev; } + prev = now; + if (now - t0 >= 4500) { break; } + if ((REG_READ(GPIO_IN1_REG) & papermono_bus_mask) != papermono_bus_mask) { low = true; break; } + } + REG_WRITE(GPIO_ENABLE1_W1TS_REG, en); + if (low) { return papermono_bus_t::disturbed; } + if (gap_max <= 200) { return papermono_bus_t::quiet; } + } + return papermono_bus_t::unobserved; + } + + /// Close the gate, writing back the output byte saved before opening, and confirm + /// the whole byte by reading it back. When the bus is disturbed, each write is + /// placed right after both lines return High (the gap between the charger's + /// pulses), waiting at most 3 ms for each edge. + static bool papermono_gate_close(std::uint8_t saved, bool disturbed) + { + auto& ioe1 = M5.getIOExpander(0); + const std::uint8_t closed = saved & ~papermono_gate_bit; + for (int i = 0; i < 20; ++i) + { + if (disturbed) + { + std::uint32_t en = REG_READ(GPIO_ENABLE1_REG) & papermono_bus_mask; + REG_WRITE(GPIO_ENABLE1_W1TC_REG, en); + auto t = m5gfx::micros(); + while (m5gfx::micros() - t < 3000 && (REG_READ(GPIO_IN1_REG) & papermono_bus_mask) == papermono_bus_mask) {} + t = m5gfx::micros(); + while (m5gfx::micros() - t < 3000 && (REG_READ(GPIO_IN1_REG) & papermono_bus_mask) != papermono_bus_mask) {} + REG_WRITE(GPIO_ENABLE1_W1TS_REG, en); + } + std::uint8_t rb; + if (ioe1.writeRegister8(papermono_ioe1_out_reg, closed) + && ioe1.readRegister(papermono_ioe1_out_reg, &rb, 1) && rb == closed) + { + papermono_gate_unclosed = false; + return true; + } + } + papermono_gate_unclosed = true; + papermono_gate_saved = saved; + return false; + } + + /// Retry closing a gate left unconfirmed by an earlier call. Call this before any + /// other access to the internal bus. false: still unconfirmed. + static bool papermono_gate_recover(void) + { + return !papermono_gate_unclosed || papermono_gate_close(papermono_gate_saved, true); + } + + /// Open the gate. Call papermono_gate_recover() first. vin_present: the caller's + /// PM1 power-source reading. + /// opened: the bus is quiet, the charger can be accessed, close afterwards with the + /// saved byte; led_mode: the charger is in LED mode and the gate is closed; + /// error: the bus could not be judged, or the gate state is not confirmed (a failed + /// close is retried by the next papermono_gate_recover()). + static papermono_gate_t papermono_gate_open(bool vin_present, std::uint8_t* saved) + { + if (!vin_present) { papermono_ip2315_led_mode = false; } + if (papermono_ip2315_led_mode) { return papermono_gate_t::led_mode; } + auto& ioe1 = M5.getIOExpander(0); + if (!ioe1.readRegister(papermono_ioe1_out_reg, saved, 1)) { return papermono_gate_t::error; } + /// a write reported as failed may still have been applied: close in any case. + bool opened = ioe1.writeRegister8(papermono_ioe1_out_reg, *saved | papermono_gate_bit); + auto bus = papermono_bus_watch(); + if (opened && bus == papermono_bus_t::quiet) { return papermono_gate_t::opened; } + bool closed = papermono_gate_close(*saved, bus != papermono_bus_t::quiet); + /// LED mode is remembered only on a clean judgement: the gate was opened, a line + /// went Low, the gate is closed again, and the charger is powered. + if (!closed || !opened || bus != papermono_bus_t::disturbed || !vin_present) { return papermono_gate_t::error; } + papermono_ip2315_led_mode = true; + return papermono_gate_t::led_mode; + } + #elif defined (CONFIG_IDF_TARGET_ESP32C6) static constexpr int M5NanoC6_LED_PIN = GPIO_NUM_7; @@ -2996,16 +3116,18 @@ namespace m5 // M5PaperMono: charging is controlled by the IP2316 charger, not PM1. if (M5.getBoard() == board_t::board_M5PaperMono) { /// every write of the sequence counts: a gate left in the wrong - /// state after a failed close is not a success. - bool res = set_papermono_ip2315_enabled(true); - if (res && wait_papermono_ip2315_ready()) { - res = enable ? M5.In_I2C.bitOn (ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq) - : M5.In_I2C.bitOff(ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq); - } else { - res = false; - } - res = set_papermono_ip2315_enabled(false) && res; - return res; + /// state after a failed close is not a success. A charger in LED mode + /// cannot be controlled until VIN is reapplied. + if (!papermono_gate_recover()) { return false; } + M5PM1_Class::pwr_src_t sources; + if (!M5pm1.getPowerSource(&sources)) { return false; } + std::uint8_t saved; + if (papermono_gate_open((sources & (M5PM1_Class::vin | M5PM1_Class::vinout)) != 0, &saved) + != papermono_gate_t::opened) { return false; } + bool res = wait_papermono_ip2315_ready() + && (enable ? M5.In_I2C.bitOn (ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq) + : M5.In_I2C.bitOff(ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq)); + return papermono_gate_close(saved, false) && res; } #endif #if defined (CONFIG_IDF_TARGET_ESP32C5) @@ -3612,10 +3734,16 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PaperMono: { + if (!papermono_gate_recover()) { return charge_state_t::io_error; } // No external power -> not charging. PWR_SRC is a bitmap, and the battery bit may coexist with VIN. M5PM1_Class::pwr_src_t sources; if (!M5pm1.getPowerSource(&sources)) { return charge_state_t::io_error; } - if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return charge_state_t::not_charging; } + // VIN removed: the charger mode is decided again on the next plug-in. + if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) + { + papermono_ip2315_led_mode = false; + return charge_state_t::not_charging; + } // External power present. The IP2316 charger reports // its state in REG_CHG_STAT(0xC7): bit7 = charging in progress (measured: // 0x82 charging / 0x45 charge-complete / 0x00 charge-disabled). @@ -3624,16 +3752,23 @@ namespace m5 /// a charger that does not answer through the gate is an unfinished /// procedure, not missing evidence: io_error, not undetermined. The /// gate writes themselves are part of the procedure, so a failed open - /// or close is io_error as well. + /// or close is io_error as well. A charger in LED mode has no readable + /// state until VIN is reapplied: undetermined. + std::uint8_t saved; + switch (papermono_gate_open(true, &saved)) + { + case papermono_gate_t::led_mode: return charge_state_t::undetermined; + case papermono_gate_t::error: return charge_state_t::io_error; + default: break; + } charge_state_t res = charge_state_t::io_error; uint8_t chg_stat; - if (set_papermono_ip2315_enabled(true) - && wait_papermono_ip2315_ready() + if (wait_papermono_ip2315_ready() && M5.In_I2C.readRegister(ip2315_i2c_addr, 0xC7, &chg_stat, 1, i2c_freq)) { res = (chg_stat & (1 << 7)) ? charge_state_t::charging : charge_state_t::not_charging; } - if (!set_papermono_ip2315_enabled(false)) { res = charge_state_t::io_error; } + if (!papermono_gate_close(saved, false)) { res = charge_state_t::io_error; } return res; }