feat: optional hooks around long e-ink BUSY waits - #8
Merged
Conversation
A refresh takes ~0.3-2 s during which EpdBus::waitBusy() only polls the BUSY pin at full CPU clock. Add a pair of optional plain-function-pointer hooks (setBusyWaitHooks, forwarded through FreeInkDisplay) fired when a wait exceeds 20 ms and when it completes, so host firmware can apply its own power policy (e.g. reduce the CPU clock) for the wait window. Hooking the bus covers every driver that polls through waitBusy (SSD1677/X4, UC8253/X3, ED2208/M5). No behavior change unless hooks are installed; short command waits never trigger them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A state change only commits after two consecutive matching samples (DEBOUNCE_DELAY stability window). A host that polls slowly — e.g. an idle loop pacing 50 ms light-sleep slices — can therefore drop a tap shorter than its poll period: the press lands in one sample and the next sample already reads released, so it never commits. Expose the pending-debounce state so such hosts can re-poll quickly until the change commits. Also sync lastState in applyStateChange() so the accessor stays false on the hold-style input paths, which bypass the debounce sampling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Once a BUSY wait has proven long (same 20 ms threshold as the begin/end hooks), each poll-delay iteration is offered to a host- installed slice hook, which can e.g. light-sleep until the BUSY pin leaves its active level. The hook returns false (or is absent) to fall back to the plain delay. Gated by a longWait flag independent of the begin hook's presence, so installing only the slice hook works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
If you've still got an x3 open and a way to scope it... Can you put a logic analyzer on the panel SPI while stock firmware renders a sleep wallpaper. I need to know the actual CDI value, the exact plane bytes for a known image, and the timing. I want to diff our command stream against stock's byte-for-byte. |
Contributor
Author
itsthisjustin
pushed a commit
to crosspoint-reader/crosspoint-reader
that referenced
this pull request
Aug 13, 2026
…e (X3-measured) (#2525) ## Summary Reduces active-reading power draw ~3.2x, measured end-to-end on an X3 with a [Nordic PPK2](https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2) at the battery terminals (slim build, USB cable removed, 3.8v). Three independent changes: 1. **Light-sleep between input polls when idle** (the big one): after 1 s of inactivity the main loop replaces its busy `delay(50)` with 50 ms timer-paced `esp_light_sleep_start()` slices. Poll cadence and input latency are unchanged — the CPU sleeps between polls instead of spinning at 10 MHz. Guards: never sleeps while a `HalPowerManager::Lock` is held (render in flight), WiFi is up, or USB is connected (light sleep kills the CDC link). 2. **Two-stage idle backoff**: downclock at 500 ms, light sleep at 1 s, replacing the flat 3 s full-speed window. Renders re-raise the clock via `Lock` the moment they start, so the full-speed tail only ever served loop bookkeeping. (The 3 s value in #852 was a placeholder; responsiveness verified on device against stock.) 3. **Refresh BUSY-wait downclock**: drops the CPU to 10 MHz while `pollBusy()` polls the BUSY pin (0.3–2 s per refresh), via new optional hooks in the SDK — **depends on Free-Ink/freeink-sdk/pull/8**. Plus a 1 Hz throttle on the X3's I2C-based USB detection (was polled every loop). (1+2) reduced average static-page-current from 9.68mA to 3.45mA. Reducing the battery-monitor polling frequency further reduced it to 2.78mA. Before merging, we'll need to update the submodule reference. ## Measured results (PPK2, X3) | Metric | Before | After | |---|---|---| | Idle on a static page | 9.68 mA | **2.78 mA** | | Fast page turn (incl. post-turn behavior) | ~100 mC | **28.98 mC** | | Post-turn 160 MHz tail | 21.2 mA x 3 s | eliminated | | Session average @ 30 s/page | ~11.6 mA | **~3.6 mA (~3.2x reading time)** | | Deep sleep | 12.8 µA | unchanged | ## Typical-day profiles Assuming someone spends 30s per page. - **Light**: 1 h reading, 10 lock/wake cycles, remaining ~23 h deep sleep - **Heavy**: 2 h reading, 20 lock/wake cycles, remaining ~22 h deep sleep - Per-day cost components: reading 11.6 → 3.6 mA (session avg), lock+wake cycle 0.113 → 0.099 mAh each (sleep-screen mode), deep sleep 12.8 µA (unchanged, ~0.29 mAh/day) - Excluded: WiFi sessions, cold chapter indexing, self-discharge ### Days per charge (650mAh) | Profile | Before | After | Ratio | |---|---|---|---| | Light (1 h/day, 10 locks) | 13.0 mAh/day → ~50 days | 4.9 mAh/day → ~133 days | 2.7× | | Heavy (2 h/day, 20 locks) | 25.7 mAh/day → ~25 days | 9.5 mAh/day → ~68 days | 2.7× | | Pure active reading | 11.6 mA → ~56 h | 3.6 mA → ~180 h | 3.2× | ### Caveats - "After" saturates in reality: Li-ion self-discharge (~2–3%/month) is not modeled and becomes the limiter once firmware draw is this low. - Faster page-turners trend toward ~2.8×, slower readers slightly above 3.2×. - Frequent locking dilutes the ratio (wake = full cold boot, barely touched by the power branch): at 10–20 locks/day, cycles are ~20% of the "after" budget. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ ---------
szetszho
pushed a commit
to szetszho/crosspoint-reader
that referenced
this pull request
Aug 14, 2026
…e (X3-measured) (crosspoint-reader#2525) ## Summary Reduces active-reading power draw ~3.2x, measured end-to-end on an X3 with a [Nordic PPK2](https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2) at the battery terminals (slim build, USB cable removed, 3.8v). Three independent changes: 1. **Light-sleep between input polls when idle** (the big one): after 1 s of inactivity the main loop replaces its busy `delay(50)` with 50 ms timer-paced `esp_light_sleep_start()` slices. Poll cadence and input latency are unchanged — the CPU sleeps between polls instead of spinning at 10 MHz. Guards: never sleeps while a `HalPowerManager::Lock` is held (render in flight), WiFi is up, or USB is connected (light sleep kills the CDC link). 2. **Two-stage idle backoff**: downclock at 500 ms, light sleep at 1 s, replacing the flat 3 s full-speed window. Renders re-raise the clock via `Lock` the moment they start, so the full-speed tail only ever served loop bookkeeping. (The 3 s value in crosspoint-reader#852 was a placeholder; responsiveness verified on device against stock.) 3. **Refresh BUSY-wait downclock**: drops the CPU to 10 MHz while `pollBusy()` polls the BUSY pin (0.3–2 s per refresh), via new optional hooks in the SDK — **depends on Free-Ink/freeink-sdk/pull/8**. Plus a 1 Hz throttle on the X3's I2C-based USB detection (was polled every loop). (1+2) reduced average static-page-current from 9.68mA to 3.45mA. Reducing the battery-monitor polling frequency further reduced it to 2.78mA. Before merging, we'll need to update the submodule reference. ## Measured results (PPK2, X3) | Metric | Before | After | |---|---|---| | Idle on a static page | 9.68 mA | **2.78 mA** | | Fast page turn (incl. post-turn behavior) | ~100 mC | **28.98 mC** | | Post-turn 160 MHz tail | 21.2 mA x 3 s | eliminated | | Session average @ 30 s/page | ~11.6 mA | **~3.6 mA (~3.2x reading time)** | | Deep sleep | 12.8 µA | unchanged | ## Typical-day profiles Assuming someone spends 30s per page. - **Light**: 1 h reading, 10 lock/wake cycles, remaining ~23 h deep sleep - **Heavy**: 2 h reading, 20 lock/wake cycles, remaining ~22 h deep sleep - Per-day cost components: reading 11.6 → 3.6 mA (session avg), lock+wake cycle 0.113 → 0.099 mAh each (sleep-screen mode), deep sleep 12.8 µA (unchanged, ~0.29 mAh/day) - Excluded: WiFi sessions, cold chapter indexing, self-discharge ### Days per charge (650mAh) | Profile | Before | After | Ratio | |---|---|---|---| | Light (1 h/day, 10 locks) | 13.0 mAh/day → ~50 days | 4.9 mAh/day → ~133 days | 2.7× | | Heavy (2 h/day, 20 locks) | 25.7 mAh/day → ~25 days | 9.5 mAh/day → ~68 days | 2.7× | | Pure active reading | 11.6 mA → ~56 h | 3.6 mA → ~180 h | 3.2× | ### Caveats - "After" saturates in reality: Li-ion self-discharge (~2–3%/month) is not modeled and becomes the limiter once firmware draw is this low. - Faster page-turners trend toward ~2.8×, slower readers slightly above 3.2×. - Frequent locking dilutes the ratio (wake = full cold boot, barely touched by the power branch): at 10–20 locks/day, cycles are ~20% of the "after" budget. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ ---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Equivalent of crosspoint-reader/community-sdk#21; copying/pasting body below:
An e-ink refresh takes ~0.3–2 s, during which
pollBusy()spins on the BUSY pin at whatever clock the host is running. This PR addssetBusyWaitHooks(begin, end): plain function pointers fired when a wait exceeds 20 ms and again when it completes, letting host firmware apply its own power policy for the wait window (CrossPoint uses it to drop the ESP32-C3 to 10 MHz there).nullptr; behavior is unchanged unless installed. The only added cost is a null-check inside the 1 ms poll loop.Measured on an X3 with a Nordic PPK2 at the battery terminals at 3.8v: with CrossPoint installing a 10 MHz downclock hook, a fast page turn drops from ~38 mC to ~29 mC (−24%), with fast/full/grayscale refreshes visually unchanged on device.
Supports crosspoint-reader/crosspoint-reader#2525
🤖 Generated with Claude Code