mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
## 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**_ ---------
116 lines
5.1 KiB
C++
116 lines
5.1 KiB
C++
#pragma once
|
|
#include <Arduino.h>
|
|
#include <EInkDisplay.h>
|
|
|
|
class HalDisplay {
|
|
public:
|
|
// Constructor with pin configuration
|
|
HalDisplay();
|
|
|
|
// Destructor
|
|
~HalDisplay();
|
|
|
|
// Refresh modes
|
|
enum RefreshMode {
|
|
FULL_REFRESH, // Full refresh with complete waveform
|
|
HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed
|
|
FAST_REFRESH // Fast refresh using custom LUT
|
|
};
|
|
|
|
// Pass seamless=true on any path where the panel already shows the
|
|
// content it should after begin() returns (silent reboot's popup,
|
|
// sleep-wake with a restored buffer). Skips the wakeup-gated
|
|
// requestResync() and defuses the SDK's X3 _x3InitialFullSyncsRemaining
|
|
// counter; otherwise the first two paints get promoted to FULL
|
|
// (~770ms each on X3).
|
|
void begin(bool seamless = false);
|
|
|
|
// Display dimensions
|
|
static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH;
|
|
static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT;
|
|
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
|
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
|
|
|
// Frame buffer operations
|
|
void clearScreen(uint8_t color = 0xFF) const;
|
|
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
|
bool fromProgmem = false) const;
|
|
void drawImageTransparent(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
|
bool fromProgmem = false) const;
|
|
|
|
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
|
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
|
|
// while the panel refreshes on its own. The framebuffer must stay untouched
|
|
// until waitRefreshComplete(), and the caller must rebuild the differential
|
|
// baseline before the next differential update (the tiled grayscale cleanup
|
|
// does). Panels without deferral fall back to a blocking refresh.
|
|
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
|
// Block until a pending deferred refresh completes (no-op when none is).
|
|
void waitRefreshComplete();
|
|
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
|
|
// false where it falls back to a blocking refresh.
|
|
bool supportsAsyncRefresh() const;
|
|
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
|
|
|
// Output polarity. The framebuffer remains in normal polarity; inversion is
|
|
// applied by the display driver while sending it to the panel.
|
|
void setInverted(bool inverted);
|
|
bool toggleInverted();
|
|
bool isInverted() const;
|
|
|
|
// Power management
|
|
void deepSleep();
|
|
|
|
// Install the slice hook that replaces the BUSY poll delay on proven-long
|
|
// waits (see EpdBus::setBusyWaitSliceHook for the contract)
|
|
void setBusyWaitSliceHook(bool (*sliceHook)(int8_t busyPin, uint8_t busyLevel));
|
|
|
|
// Access to frame buffer
|
|
uint8_t* getFrameBuffer() const;
|
|
|
|
// Lend the framebuffer's ~48 KB STORAGE to a memory-hungry phase (chapter
|
|
// builds) without freeing it: the allocation never moves, so repeated loans
|
|
// cannot fragment the heap (free+realloc measurably did). No display calls
|
|
// between lend and return; the panel keeps its last refreshed image. The
|
|
// buffer comes back white — redraw fully. Returns nullptr if already lent.
|
|
uint8_t* lendFrameBufferStorage(uint32_t* sizeOut);
|
|
void returnFrameBufferStorage();
|
|
|
|
// X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed
|
|
// to the gray region in physical panel coordinates (no-arg = full frame).
|
|
// Call after the BW base frame is displayed and before the grayscale planes
|
|
// are written; no-op on X4. See EInkDisplay::preconditionGrayscale.
|
|
void preconditionGrayscale();
|
|
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
|
|
|
// Display the framebuffer as the base frame for a grayscale overlay that
|
|
// follows. On X3, HALF fallback first requests a resync to match
|
|
// displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform
|
|
// ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode.
|
|
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
|
|
|
|
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
|
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
|
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
|
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
|
|
|
|
void displayGrayBuffer(bool turnOffScreen = false);
|
|
|
|
// Tiled grayscale: stream one band of a plane (lsbPlane selects LSB/MSB RAM)
|
|
// straight to the controller; supportsStripGrayscale() gates the path. See
|
|
// EInkDisplay::writeGrayscalePlaneStrip.
|
|
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows);
|
|
bool supportsStripGrayscale() const;
|
|
|
|
// Runtime geometry passthrough
|
|
uint16_t getDisplayWidth() const;
|
|
uint16_t getDisplayHeight() const;
|
|
uint16_t getDisplayWidthBytes() const;
|
|
uint32_t getBufferSize() const;
|
|
|
|
private:
|
|
EInkDisplay einkDisplay;
|
|
};
|
|
|
|
extern HalDisplay display;
|