diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 1b9935fc..37b41fae 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -1561,10 +1561,13 @@ constexpr BoardProfile XTEINK_X4_PRO = { // Frontlight: dual warm/cold LEDC PWM with color temperature (NVS lightWarmValue/ // lightColdValue/lightCT/lightBri/lightOn). Recovered from the OEM LEDC init (IROM // 0x420a2130 → helper 0x420a20c0): two channels — GPIO8 on LEDC ch4 and GPIO9 on ch5 — - // The original bring-up dump used 10 kHz; stock 7.0.8 passes 25 kHz / 10-bit to - // the frontlight initializer on the same pins. Use that directly recovered value. - // Both channels are active-HIGH (init drives the pin LOW = off, brightness raises - // duty). + // The FREEINK_FRONTLIGHT_LS path no longer clocks the LEDC timer from RC_FAST (that + // retention path is dropped: this fork only deep-sleeps, and RC_FAST (~17.5 MHz) could + // not reach 25 kHz at 10-bit — ledc_timer_config failed -> both channels unconfigured + // -> light stays dark — which is what forced this profile down to 10 kHz). The timer + // now runs on the default AUTO clock (≥40 MHz), so the OEM 25 kHz / 10-bit is valid + // again and is restored here. + // Both channels are active-HIGH (init drives the pin LOW = off, brightness raises duty). // GPIO8 is the hardware-confirmed cool channel and GPIO9 the warm channel; // FrontlightManager mixes them for color-temperature control. {8, 25000, 10, true, 9}, diff --git a/libs/hardware/FrontlightManager/include/FrontlightManager.h b/libs/hardware/FrontlightManager/include/FrontlightManager.h index bdcb0e0f..632a7cc0 100644 --- a/libs/hardware/FrontlightManager/include/FrontlightManager.h +++ b/libs/hardware/FrontlightManager/include/FrontlightManager.h @@ -36,9 +36,9 @@ class FrontlightManager { void on(); // Cut frontlight leakage through deep sleep: drive the LED pads LOW and hold - // them (so the level survives deep sleep via gpio_deep_sleep_hold_en), and - // release the LEDC KEEP_ALIVE clock. Call from the consumer's sleep path just - // before deep sleep. Only meaningful on LEDC frontlights (no-op otherwise). + // them (so the level survives deep sleep via gpio_deep_sleep_hold_en). Call + // from the consumer's sleep path just before deep sleep. Only meaningful on + // LEDC frontlights (no-op otherwise). // releaseOnWake() must be called at boot before begin() re-attaches the // channels. // Implemented only under FREEINK_FRONTLIGHT_LS, so guard the declarations to @@ -109,16 +109,7 @@ class FrontlightManager { #endif #endif #ifdef FREEINK_FRONTLIGHT_LS - // Keep RC_FAST powered through light sleep only while the light is actually - // lit. The LEDC driver's KEEP_ALIVE config pins RC_FAST (and the digital - // domain at its higher sleep bias) for every light-sleep window from boot; - // begin() cancels that via the refcounted sleep sub-mode API and apply() - // re-arms it on 0<->nonzero total-duty transitions, so dark idle sleeps at - // full depth. - void updateLsKeepAlive(bool lit); - bool _lsAttachOk = false; // both channel attaches succeeded (refcount is balanced) - bool _lsKeepAliveArmed = false; // our own +1 on the RC_FAST sleep sub-mode is active - bool _lsParked = false; // park() has driven + held the frontlight pads LOW + bool _lsParked = false; // park() has driven + held the frontlight pads LOW #endif bool _begun = false; diff --git a/libs/hardware/FrontlightManager/src/FrontlightManager.cpp b/libs/hardware/FrontlightManager/src/FrontlightManager.cpp index 1ee59e94..4706c0f7 100644 --- a/libs/hardware/FrontlightManager/src/FrontlightManager.cpp +++ b/libs/hardware/FrontlightManager/src/FrontlightManager.cpp @@ -8,11 +8,6 @@ #ifdef FREEINK_FRONTLIGHT_LS #include #include -// esp_sleep_sub_mode_config lives in a private IDF header (no public API exists -// for balancing the refcounted RC_FAST keep-on the LEDC driver takes for -// KEEP_ALIVE channels — the driver manages it through this same header). Pinned -// IDF 5.5; re-check on IDF bumps. -#include #endif // Logging: use the firmware's Logging.h LOG_INF facility, NOT esp_log. The @@ -79,30 +74,31 @@ uint32_t physicalDuty(uint32_t logicalDuty, uint32_t full, bool activeHigh) { } #ifdef FREEINK_FRONTLIGHT_LS -// Light-sleep-surviving LEDC: clock the timer from RC_FAST (~17.5 MHz on the -// S3 — the practical LEDC source that keeps running through light sleep at -// near-zero extra sleep power; XTAL can also be kept up but costs far more in -// sleep current), mark the -// channels KEEP_ALIVE, and disable the GPIO sleep-isolation override on the -// output pins (a documented gotcha: sleep entry reconfigures the pad and kills -// the PWM even when the clock survives). RC_FAST at 10 kHz supports up to -// 10-bit resolution (17.5 MHz / 10 kHz = 1750 >= 1024), so the board profiles' -// full duty range — including setBrightnessLevel's level-1 minimum step — stays -// expressible. Uses the IDF driver directly (fixed LEDC_TIMER_0 + the channel -// ids below) because the Arduino helpers don't expose sleep_mode; safe here -// because frontlight boards using this flag have no other LEDC consumer. +// LEDC attach for light-sleep-flagged boards, with the RC_FAST retention path +// dropped: this fork only deep-sleeps (it never light-sleeps — setPowerSaving +// only scales the CPU), so LEDC_SLEEP_MODE_KEEP_ALIVE + RC_FAST bought nothing, +// and RC_FAST (~17.5 MHz on the S3) cannot reach the board profile's 25 kHz at +// 10-bit — ledc_timer_config() returned ESP_FAIL, which is what forced the X4 +// Pro profile down to 10 kHz. Leaving clk_cfg unset selects LEDC_AUTO_CLK: the +// driver picks a clock that meets the requested freq × resolution (XTAL/APB, +// both ≥ 40 MHz on the S3), so 25 kHz / 10-bit (div ≈ 1.56 at 40 MHz) is valid +// again and the OEM profile value works unchanged. Channels use the default +// sleep mode: the pad is isolated at deep sleep by +// esp_sleep_config_gpio_isolate(), and park()'s pad-hold cuts the light anyway. +// Still uses the IDF driver directly (fixed LEDC_TIMER_0 + the channel ids +// below); safe here because frontlight boards using this flag have no other +// LEDC consumer. bool attachChannel(int8_t gpio, uint8_t ch, uint32_t freq, uint8_t bits) { ledc_timer_config_t timer = {}; timer.speed_mode = LEDC_LOW_SPEED_MODE; timer.duty_resolution = static_cast(bits); timer.timer_num = LEDC_TIMER_0; timer.freq_hz = freq; - timer.clk_cfg = LEDC_USE_RC_FAST_CLK; - if (ledc_timer_config(&timer) != ESP_OK) { - // freq/bits exceed RC_FAST — leave the light unconfigured rather than - // silently falling back to a clock that freezes in light sleep. - return false; - } + // No clk_cfg -> LEDC_AUTO_CLK: the driver picks a ≥40 MHz source (XTAL/APB on + // the S3), which supports the board's 25 kHz / 10-bit (div ≈ 1.56 at 40 MHz). + // RC_FAST (~17.5 MHz) cannot reach 25 kHz at 10-bit, which is what forced the + // profile down to 10 kHz under the old RC_FAST path. + if (ledc_timer_config(&timer) != ESP_OK) return false; ledc_channel_config_t chan = {}; chan.gpio_num = gpio; chan.speed_mode = LEDC_LOW_SPEED_MODE; @@ -111,9 +107,8 @@ bool attachChannel(int8_t gpio, uint8_t ch, uint32_t freq, uint8_t bits) { chan.timer_sel = LEDC_TIMER_0; chan.duty = 0; chan.hpoint = 0; - chan.sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE; - // ledc_channel_config() disables the pad's sleep-isolation override itself - // for KEEP_ALIVE channels (IDF 5.5), so no explicit gpio_sleep_sel_dis here. + // Default sleep_mode (NOT KEEP_ALIVE): the pad is isolated at deep sleep by + // esp_sleep_config_gpio_isolate(); the frontlight is cut by park()'s pad-hold. return ledc_channel_config(&chan) == ESP_OK; } void writeChannel(int8_t /*gpio*/, uint8_t ch, uint32_t duty) { @@ -187,20 +182,9 @@ void FrontlightManager::begin() { } _lsParked = false; LOG_INF("FrontlightMgr", "begin: cleared any stale held pads"); - // The FIRST successful KEEP_ALIVE channel config takes a single refcounted +1 - // on the RC_FAST sleep sub-mode (esp_sleep_sub_mode_config; the driver's - // global-clock latch means later configs don't take another), which would - // keep RC_FAST — and the digital domain at its higher sleep bias — powered - // through every light-sleep window from boot, even with the light off. - // Balance it here and let apply() re-arm only while the light is actually - // lit. attachOk is true when ANY channel config succeeded (exactly the - // condition under which the driver's +1 was taken); the !_begun guard keeps a - // hypothetical second begin() from decrementing twice. - _lsAttachOk = attachOk; - _lsKeepAliveArmed = false; - if (attachOk && !_begun) { - esp_sleep_sub_mode_config(ESP_SLEEP_DIG_USE_RC_FAST_MODE, false); - } + // No RC_FAST sleep sub-mode bookkeeping any more: attachChannel() now runs on + // the default AUTO clock with the default channel sleep mode, so there is no + // refcounted RC_FAST keep-alive to balance (see attachChannel() above). #else (void)attachOk; #endif @@ -329,9 +313,6 @@ void FrontlightManager::apply() { warmDuty = (totalDuty * _warmPercent + 50u) / 100u; coolDuty = totalDuty - warmDuty; } -#ifdef FREEINK_FRONTLIGHT_LS - updateLsKeepAlive(totalDuty != 0); -#endif writeChannel(fl.gpio, LEDC_CH_COOL, physicalDuty(coolDuty, full, fl.activeHigh)); if (dual) { @@ -342,36 +323,20 @@ void FrontlightManager::apply() { } #ifdef FREEINK_FRONTLIGHT_LS -void FrontlightManager::updateLsKeepAlive(const bool lit) { - // Refcounted, so strictly transition-edged: one +1 while lit, returned at 0. - // Skipped when the attach failed (see begin()) — the driver never took its - // +1 there, and RC_FAST keep-alive is moot without a working LS channel. - if (!_lsAttachOk || lit == _lsKeepAliveArmed) return; - esp_sleep_sub_mode_config(ESP_SLEEP_DIG_USE_RC_FAST_MODE, lit); - _lsKeepAliveArmed = lit; -} - void FrontlightManager::park() { // Frontlight leakage through deep sleep (Xteink X4 Pro — Mark31415, - // crosspoint-reader#3215). The channels are configured LEDC_SLEEP_MODE_KEEP_ALIVE - // so the PWM keeps driving GPIO8/9 (cool/warm) through light sleep; at deep - // sleep the panel rail is held up (PR #3215 holds power.latch0 / GPIO1 HIGH for - // fast-wake), so the frontlight driver IC stays powered and the KEEP_ALIVE pad - // keeps drawing quiescent + leakage current. Cut it at the source: drive both - // pads LOW (active-high frontlight -> LED off, no booster bias) and hold them - // LOW so the level survives deep sleep via gpio_deep_sleep_hold_en() (called by - // PowerManager::deepSleep()). The LEDC peripheral clock (RC_FAST) is also - // released so the driver's refcounted +1 is dropped and the clock can fully - // stop in deep sleep. releaseOnWake() must undo this before begin() re-attaches - // the LEDC channels on boot. + // crosspoint-reader#3215). At deep sleep the panel rail is held up (PR #3215 + // holds power.latch0 / GPIO1 HIGH for fast-wake), so the frontlight driver IC + // stays powered and a driven pad keeps drawing quiescent + leakage current. + // Cut it at the source: drive both pads LOW (active-high frontlight -> LED + // off, no booster bias) and hold them LOW so the level survives deep sleep via + // gpio_deep_sleep_hold_en() (called by PowerManager::deepSleep()). + // releaseOnWake() must undo this before begin() re-attaches the LEDC channels + // on boot. const auto& fl = BoardConfig::ACTIVE.frontlight; if (!_begun) return; LOG_INF("FrontlightMgr", "park: begun, driving pads LOW + hold"); - // Return the LEDC driver's refcounted RC_FAST keep-alive it took at attach, if - // it is still armed (apply() re-arms only while lit; off()/setBrightness(0) - // returns it, but be safe if the light was parked while lit). - updateLsKeepAlive(false); - // Tear down the KEEP_ALIVE LEDC channels so the pads no longer answer to the + // Tear down the LEDC channels so the pads no longer answer to the // peripheral; the explicit GPIO hold below then owns the pad level. ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); if (fl.gpioWarm != BoardConfig::PIN_UNASSIGNED) { diff --git a/libs/hardware/PowerManager/include/PowerManager.h b/libs/hardware/PowerManager/include/PowerManager.h index 56224371..1a1adf2a 100644 --- a/libs/hardware/PowerManager/include/PowerManager.h +++ b/libs/hardware/PowerManager/include/PowerManager.h @@ -55,6 +55,27 @@ class PowerManager { // Convenience: wait for release, arm the power-button wakeup, then deep sleep. [[noreturn]] static void deepSleepUntilPowerButton(); + + // Stock-parity shutdown-reason marker (evidence: xteink_app v7.4.4 RE, + // ghidra_poweroff_report.md — stock writes a magic + reason byte to RTC slow + // RAM before its power-off transaction and reads/clears it at boot). + enum ShutdownReason : uint8_t { + ShutdownNone = 0, + ShutdownUser = 1, // power-button long press / short-press off + ShutdownAutoOff = 2, // auto power off dwell timer elapsed + ShutdownLowBattery = 3, // reserved + }; + + // Record WHY the device is powering off. Writes a magic word to RTC slow RAM + // (survives deep sleep and most resets) + the reason byte. Call BEFORE the + // deep-sleep sink. No-op on SoCs without RTC slow memory. + static void setShutdownReason(ShutdownReason reason); + + // Read + clear the marker at boot. Returns (reason << 8) | 1 when a recorded + // shutdown is present (stock's convention), 0 when there is none (cold boot, + // previous wake was a normal sleep, or unsupported SoC). Idempotent: the + // cells are zeroed on read, so a second call returns 0. + static uint16_t takeShutdownReason(); }; } // namespace freeink diff --git a/libs/hardware/PowerManager/src/PowerManager.cpp b/libs/hardware/PowerManager/src/PowerManager.cpp index f3f8601d..bcda0563 100644 --- a/libs/hardware/PowerManager/src/PowerManager.cpp +++ b/libs/hardware/PowerManager/src/PowerManager.cpp @@ -104,4 +104,41 @@ void PowerManager::deepSleepUntilPowerButton() { deepSleep(); } +// --- Stock-parity shutdown-reason RTC marker ------------------------------- +// xteink_app v7.4.4 writes magic 0x58435253 ("SRCX") at RTC slow RAM 0x50000004 +// and a u8 reason at 0x50000000 before its power-off commit; the wake boot reads +// the magic, reports (reason << 8) | 1, and zeroes both cells (RE evidence: +// ghidra_poweroff_report.md checkpoints 3-5, FINAL CONCLUSION). RTC slow RAM +// survives deep sleep and most resets, which is exactly why stock uses it. +#if SOC_RTC_SLOW_MEM_SUPPORTED +namespace { +constexpr uintptr_t kShutdownMagicAddr = 0x50000004; // RTC slow RAM + 4 +constexpr uintptr_t kShutdownReasonAddr = 0x50000000; // RTC slow RAM + 0 +constexpr uint32_t kShutdownMagic = 0x58435253u; // "SRCX" little-endian +} // namespace + +void PowerManager::setShutdownReason(ShutdownReason reason) { + auto* magicCell = reinterpret_cast(kShutdownMagicAddr); + auto* reasonCell = reinterpret_cast(kShutdownReasonAddr); + // Reason first, magic last: a torn write (power cut mid-sequence) leaves NO + // magic, so the boot side never reports a half-written marker. + *reasonCell = static_cast(reason) & 0xffu; + *magicCell = kShutdownMagic; +} + +uint16_t PowerManager::takeShutdownReason() { + auto* magicCell = reinterpret_cast(kShutdownMagicAddr); + auto* reasonCell = reinterpret_cast(kShutdownReasonAddr); + if (*magicCell != kShutdownMagic) return 0; + const uint16_t result = static_cast(((*reasonCell & 0xffu) << 8) | 1u); + // Clear both cells: the marker is consumed exactly once (stock behavior). + *reasonCell = 0; + *magicCell = 0; + return result; +} +#else // !SOC_RTC_SLOW_MEM_SUPPORTED +void PowerManager::setShutdownReason(ShutdownReason) {} +uint16_t PowerManager::takeShutdownReason() { return 0; } +#endif // SOC_RTC_SLOW_MEM_SUPPORTED + } // namespace freeink