diff --git a/libs/display/EInkDisplay/README.md b/libs/display/EInkDisplay/README.md new file mode 100644 index 0000000..f3e16ed --- /dev/null +++ b/libs/display/EInkDisplay/README.md @@ -0,0 +1,78 @@ +# EInkDisplay library + +This is a focused low-level interaction library for the X4 display. + +It's best paried with a higher-level GFX library to help with rendering shapes, text, and images. +Along with dealing with display orientation. + +See the [SSD1677 guide](./doc/SSD1677_GUIDE.md) for details on the implemention. + +## Usage + +### Setup + +```cpp +// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) +#define EPD_SCLK 8 // SPI Clock +#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) +#define EPD_CS 21 // Chip Select +#define EPD_DC 4 // Data/Command +#define EPD_RST 5 // Reset +#define EPD_BUSY 6 // Busy + +EInkDisplay display(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); +display.begin(); +``` + +### Rendering black and white frames + +```cpp +// First frame +display.clearScreen(); +uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); +// ... your drawing code here, writing to frameBuffer, a 0 bit is black, a 1 bit is white ... +display.displayBuffer(FAST_REFRESH); + +// Next frame +display.clearScreen(); +uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); +// ... your drawing code here, writing to frameBuffer, a 0 bit is black, a 1 bit is white ... +// Using fash refresh for fast updates +display.displayBuffer(FAST_REFRESH); +``` + +### Rendering greyscale frames + +```cpp +display.clearScreen(); +uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); +// ... regular drawing code from before, all gray pixels should also be marked as black ... +display.displayBuffer(FAST_REFRESH); + + +// Grayscale rendering +// Refetch the frame buffer to ensure it's up to date +frameBuffer = einkDisplay.getFrameBuffer(); + +// Dark gray rendering +// Clear the screen with 0, all 0 bits are considered out of bounds for grayscale rendering +// Only mark the bits you want to be gray as 1 +display.clearScreen(0x00); +// ... exact same screen content as before, but only mark the **dark** grays pixels with `1`, rest leave as `0` +display.copyGrayscaleLsbBuffers(frameBuffer); + +display.clearScreen(0x00); +// ... exact same screen content as before, but mark the **light and dark** grays pixels with `1`, rest leave as `0` +display.copyGrayscaleMsbBuffers(frameBuffer); +display.displayGrayBuffer(); + +// All done :) +``` + +### Power off + +To ensure the display locks the image in, it's important to power off the display before exiting the program. + +```cpp +display.deepSleep(); +``` diff --git a/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md b/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md index 81a053d..b60746c 100644 --- a/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md +++ b/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md @@ -11,6 +11,7 @@ Based on the GxEPD2_426_GDEQ0426T82 driver implementation. # Table of Contents - [Hardware Configuration](#hardware-configuration) +- [Buffer Modes](#buffer-modes) - [SPI Communication](#spi-communication) - [Initialization](#initialization) - [RAM Operations](#ram-operations) @@ -31,6 +32,111 @@ Based on the GxEPD2_426_GDEQ0426T82 driver implementation. --- +# Buffer Modes + +The EInkDisplay driver supports two buffering modes, selectable at compile time: + +## Dual Buffer Mode (Default) + +**Memory usage:** 96KB (two 48KB framebuffers) + +- Two framebuffers in ESP32 RAM: `frameBuffer0` and `frameBuffer1` +- Buffers alternate roles as "current" and "previous" using `swapBuffers()` +- On each `displayBuffer()`: + - Current buffer → BW RAM (0x24) + - Previous buffer → RED RAM (0x26) + - Display controller compares them for differential fast refresh + - Buffers swap roles +- **Advantage:** Fast buffer switching with no overhead +- **Disadvantage:** Uses 96KB of precious RAM + +## Single Buffer Mode (Memory Optimized) + +**Memory usage:** 48KB (one 48KB framebuffer) + +Enable by defining `EINK_DISPLAY_SINGLE_BUFFER_MODE` before including EInkDisplay.h: + +```cpp +#define EINK_DISPLAY_SINGLE_BUFFER_MODE +#include +``` + +Or in your build system (e.g., platformio.ini): + +```ini +build_flags = + -D EINK_DISPLAY_SINGLE_BUFFER_MODE +``` + +- Single framebuffer in ESP32 RAM: `frameBuffer0` +- Display's internal RED RAM acts as "previous frame" storage +- On each `displayBuffer()`: + - **FAST_REFRESH:** + - New frame → BW RAM (0x24) + - Display refresh (compares BW vs existing RED RAM) + - New frame → RED RAM (0x26) - syncs for next refresh + - **HALF/FULL_REFRESH:** + - New frame → both BW RAM and RED RAM (0x24 and 0x26) + - Display refresh + - Extra RED RAM sync (already contains correct frame) +- **Advantages:** + - Saves 48KB RAM (critical for ESP32-C3 with ~380KB usable) + - Fast refresh still works via differential updates +- **Disadvantages:** + - Extra RED RAM write after each refresh (~100ms overhead) + - Grayscale rendering requires temporary buffer allocation + - `swapBuffers()` not available (not needed) + +## Choosing a Mode + +**Use Dual Buffer Mode if:** +- RAM is plentiful +- You want absolute minimum latency +- You don't need every KB of RAM + +**Use Single Buffer Mode if:** +- Running on memory-constrained hardware (like ESP32-C3) +- 48KB RAM savings is critical for your application +- ~100ms extra per refresh is acceptable + +## API Differences Between Modes + +Most of the API is identical between modes, but there are a few differences: + +| Feature | Dual Buffer | Single Buffer | +|---------|-------------|---------------| +| `getFrameBuffer()` | ✓ Available | ✓ Available | +| `clearScreen()` | ✓ Available | ✓ Available | +| `displayBuffer()` | ✓ Available | ✓ Available | +| `swapBuffers()` | ✓ Available | ✗ Not available | +| `cleanupGrayscaleBuffers()` | ✗ Not needed | ✓ Required after grayscale | +| Memory overhead | 96KB always | 48KB + temp 48KB during grayscale | + +**Code that works in both modes:** +```cpp +display.begin(); +display.clearScreen(); +display.displayBuffer(FAST_REFRESH); +``` + +**Code specific to single buffer mode:** + +(This is not required in dual buffer mode) + +```cpp +// Before grayscale +const auto bwBuffer = static_cast(malloc(EInkDisplay::BUFFER_SIZE)); +memcpy(bwBuffer, display.getFrameBuffer(), EInkDisplay::BUFFER_SIZE); + +// ... grayscale rendering ... + +// After grayscale +display.cleanupGrayscaleBuffers(bwBuffer); +free(bwBuffer); +``` + +--- + # SPI Communication ## Low-Level Protocol @@ -512,9 +618,13 @@ ssd1677_init(); ssd1677_display_frame(bw_image, red_image); ``` -## Complete Example: Fast Refresh with Double Buffering +## Complete Example: Fast Refresh with Buffering -The driver implements double buffering to enable fast partial updates: +The driver supports two buffering modes for fast partial updates: + +### Dual Buffer Mode (Default) + +**Memory usage:** 96KB (two 48KB buffers) ```cpp // Initialize display @@ -538,7 +648,71 @@ display.displayBuffer(FAST_REFRESH); 1. Two internal buffers (`frameBuffer0` and `frameBuffer1`) alternate as current/previous 2. On `displayBuffer()`, current buffer written to BW RAM (0x24), previous to RED RAM (0x26) 3. Controller compares buffers and only updates changed pixels -4. Buffers swap roles after each display +4. Buffers swap roles after each display using `swapBuffers()` + +### Single Buffer Mode (Memory Optimized) + +**Memory usage:** 48KB (one 48KB buffer) - saves 48KB RAM + +Enable by defining `EINK_DISPLAY_SINGLE_BUFFER_MODE` before including EInkDisplay.h. + +```cpp +// Initialize display (same as dual buffer) +display.begin(); +display.clearScreen(0xFF); +display.displayBuffer(FULL_REFRESH); + +// Draw content to framebuffer +uint8_t* fb = display.getFrameBuffer(); +// ... draw into fb ... + +// Fast refresh (compares with previous frame in display's RED RAM) +display.displayBuffer(FAST_REFRESH); +``` + +**How it works:** +1. Only one internal buffer (`frameBuffer0`) +2. On `displayBuffer()`: + - **FAST_REFRESH:** Write new frame to BW RAM (0x24), RED RAM already contains previous frame from last refresh + - **HALF/FULL_REFRESH:** Write new frame to both BW and RED RAM (0x24 and 0x26) + - After refresh, always sync RED RAM with current frame for next differential update +3. Controller compares BW RAM (new) vs RED RAM (old) for differential updates +4. RED RAM acts as the "previous frame buffer" for fast refresh + +**Trade-offs:** +- **Pro:** Saves 48KB of RAM (critical for ESP32-C3 with only ~380KB usable) +- **Con:** Extra RED RAM write after each refresh (~100ms overhead) +- **Con:** Cannot preserve screen content during grayscale rendering without external buffer + +### Grayscale Rendering in Single Buffer Mode + +Single buffer mode requires special handling for grayscale rendering since the BW framebuffer is overwritten: + +```cpp +// Store BW buffer after the BW render but before grayscale render +const auto bwBuffer = static_cast(malloc(EInkDisplay::BUFFER_SIZE)); +const auto frameBuffer = display.getFrameBuffer(); +memcpy(bwBuffer, frameBuffer, EInkDisplay::BUFFER_SIZE); + +// Perform grayscale rendering (overwrites BW and RED RAM) +display.clearScreen(0x00); +// ... render grayscale LSB to frameBuffer ... +display.copyGrayscaleMsbBuffers(frameBuffer); + +display.clearScreen(0x00); +// ... render grayscale MSB to frameBuffer ... +display.copyGrayscaleLsbBuffers(frameBuffer); + +// Display grays +display.displayGrayBuffer(); + +// After grayscale render +display.cleanupGrayscaleBuffers(bwBuffer); +free(bwBuffer); +``` + +The `cleanupGrayscaleBuffers()` method restores the BW buffer to both the framebuffer and RED RAM, ensuring proper state +for subsequent fast refreshes ## Auto-Write Commands for Fast Clear @@ -638,6 +812,11 @@ This is much faster than writing 48,000 bytes manually during initialization. - All X coordinates and widths must be multiples of 8 (byte boundaries) - Y coordinates are reversed in hardware (gates bottom-to-top) - RAM auto-increments after each byte transfer -- Total RAM size: 48,000 bytes (800×480 ÷ 8) -- Dual-buffer system enables differential partial updates +- Display controller internal RAM: 96,000 bytes (800×480 ÷ 8 * 2 buffers: BW + RED) +- ESP32 buffer memory usage: + - **Dual buffer mode:** 96KB (two 48KB buffers for fast buffer swapping) + - **Single buffer mode:** 48KB (one 48KB buffer, uses display's RED RAM for differential) +- Differential partial updates require RED RAM to contain the previous frame - First write after init should be full refresh to clear ghost images +- Single buffer mode adds ~100ms overhead per refresh (extra RED RAM sync) +- Grayscale rendering in single buffer mode requires temporary 48KB allocation