[EInkDisplay] Single buffer mode (#7)
* Cleanup EInkDisplay * Add EINK_DISPLAY_SINGLE_BUFFER_MODE build flag and allow for single buffer rendering * Add SSD1677 E-Ink Display Driver Guide Copied verbatium from https://github.com/CidVonHighwind/microreader/blob/main/doc/SSD1677_GUIDE.md * Add a few details in the readme and update the SSD1677_GUIDE
This commit is contained in:
parent
4d0dcd5ff8
commit
af965a074b
78
libs/display/EInkDisplay/README.md
Normal file
78
libs/display/EInkDisplay/README.md
Normal file
@ -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();
|
||||||
|
```
|
||||||
822
libs/display/EInkDisplay/doc/SSD1677_GUIDE.md
Normal file
822
libs/display/EInkDisplay/doc/SSD1677_GUIDE.md
Normal file
@ -0,0 +1,822 @@
|
|||||||
|
# SSD1677 E-Ink Display Driver Guide
|
||||||
|
## GDEQ0426T82 (4.26" 800×480) on Xteink X4
|
||||||
|
|
||||||
|
> **Note:** This document was AI-generated by consolidating multiple reference documents. Please verify critical details against the official SSD1677 datasheet.
|
||||||
|
|
||||||
|
Complete reference for programming the SSD1677 e-paper display controller, including initialization, image updates, custom LUT creation, and low-level protocol details.
|
||||||
|
|
||||||
|
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)
|
||||||
|
- [Writing Image Data](#writing-image-data)
|
||||||
|
- [Display Updates](#display-updates)
|
||||||
|
- [Custom LUT Guide](#custom-lut-guide)
|
||||||
|
- [Complete Workflows](#complete-workflows)
|
||||||
|
- [Command Reference](#command-reference)
|
||||||
|
- [Timing & Power Management](#timing--power-management)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hardware Configuration
|
||||||
|
- **Controller**: SSD1677
|
||||||
|
- **Display**: 800×480 pixels (100×60 bytes = 48,000 bytes)
|
||||||
|
- **SPI Pins**: SCLK=8, MOSI=10, CS=21, DC=4, RST=5, BUSY=6
|
||||||
|
- **SPI Settings**: 40MHz (spec: 20MHz, but 40MHz works), MSB First, SPI Mode 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 <EInkDisplay.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
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<uint8_t *>(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
|
||||||
|
|
||||||
|
### Command Format
|
||||||
|
```
|
||||||
|
DC=LOW, CS=LOW → Transfer(command_byte) → CS=HIGH, DC=HIGH
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Format
|
||||||
|
```
|
||||||
|
DC=HIGH, CS=LOW → Transfer(data_byte) → CS=HIGH
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bulk Transfer Format
|
||||||
|
```
|
||||||
|
CS=LOW → Transfer(byte1) → Transfer(byte2) → ... → CS=HIGH
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reset Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
RST=HIGH → delay(20ms) → RST=LOW → delay(2ms) → RST=HIGH → delay(20ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Initialization
|
||||||
|
|
||||||
|
## Minimal Initialization Example
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ssd1677_init() {
|
||||||
|
// 1. Software Reset
|
||||||
|
epd_cmd(0x12); // SWRESET
|
||||||
|
epd_wait_busy(); // Wait for busy to clear
|
||||||
|
|
||||||
|
// 2. Driver Output Control
|
||||||
|
epd_cmd(0x01);
|
||||||
|
epd_data(0xA7); // 680 rows -> 0x2A7, low byte
|
||||||
|
epd_data(0x02); // high byte
|
||||||
|
epd_data(0x00); // GD=0, SM=0, TB=0
|
||||||
|
|
||||||
|
// 3. Data Entry Mode
|
||||||
|
epd_cmd(0x11);
|
||||||
|
epd_data(0x03); // X+, Y+
|
||||||
|
|
||||||
|
// 4. RAM X Start/End
|
||||||
|
epd_cmd(0x44);
|
||||||
|
epd_data(0x00); // X start = 0
|
||||||
|
epd_data(0x3B); // X end = 959 / 8 = 0x3B
|
||||||
|
|
||||||
|
// 5. RAM Y Start/End
|
||||||
|
epd_cmd(0x45);
|
||||||
|
epd_data(0x00); // Y start (low byte)
|
||||||
|
epd_data(0x00); // Y start (high byte)
|
||||||
|
epd_data(0xA7); // Y end (low byte)
|
||||||
|
epd_data(0x02); // Y end (high byte)
|
||||||
|
|
||||||
|
// 6. Border Control
|
||||||
|
epd_cmd(0x3C);
|
||||||
|
epd_data(0xC0); // Border = Hi-Z
|
||||||
|
|
||||||
|
// 7. Temperature Sensor (internal)
|
||||||
|
epd_cmd(0x18);
|
||||||
|
epd_data(0x80);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Initialization Sequence
|
||||||
|
|
||||||
|
After reset, then:
|
||||||
|
|
||||||
|
| Step | Command | Data | Description |
|
||||||
|
|------|---------|------|-------------|
|
||||||
|
| 1 | `0x12` | - | Software Reset (SWRESET) |
|
||||||
|
| - | Wait BUSY | - | Wait for reset to complete |
|
||||||
|
| 2 | `0x18` | `0x80` | Temperature sensor control (internal) |
|
||||||
|
| 3 | `0x0C` | `0xAE`, `0xC7`, `0xC3`, `0xC0`, `0x40` | Booster soft start configuration |
|
||||||
|
| 4 | `0x01` | `0xDF`, `0x01`, `0x02` | Driver output control (479 gates = HEIGHT-1) |
|
||||||
|
| 5 | `0x3C` | `0x01` | Border waveform control |
|
||||||
|
| 6 | Set RAM area | See below | Configure full screen area |
|
||||||
|
| 7 | `0x46` | `0xF7` | Auto write BW RAM (clear to white) |
|
||||||
|
| 8 | Wait BUSY | - | Wait for auto-write to complete |
|
||||||
|
| 9 | `0x47` | `0xF7` | Auto write RED RAM (clear to white) |
|
||||||
|
| 10 | Wait BUSY | - | Wait for auto-write to complete |
|
||||||
|
|
||||||
|
### Command Explanations
|
||||||
|
|
||||||
|
**Software Reset (0x12)**
|
||||||
|
Resets the internal registers (except deep sleep). Mandatory after power-up.
|
||||||
|
|
||||||
|
**Driver Output Control (0x01)**
|
||||||
|
Sets display height and scan direction.
|
||||||
|
- Byte 1: `(HEIGHT - 1) % 256` = `0xDF` (479 & 0xFF)
|
||||||
|
- Byte 2: `(HEIGHT - 1) / 256` = `0x01` (479 >> 8)
|
||||||
|
- Byte 3: `0x02` (interlaced/SM mode)
|
||||||
|
|
||||||
|
**Data Entry Mode (0x11)**
|
||||||
|
Controls RAM addressing: `0x03 = X increment, Y increment`.
|
||||||
|
|
||||||
|
**Set RAM Window (0x44 & 0x45)**
|
||||||
|
Defines the region written during RAM writes.
|
||||||
|
For full 960×680 screen, X=0..0x3B, Y=0..0x2A7.
|
||||||
|
|
||||||
|
**Border Waveform (0x3C)**
|
||||||
|
Controls VBD (border pixel behavior). `0xC0 = Hi-Z`, common default.
|
||||||
|
|
||||||
|
**Temperature Sensor (0x18)**
|
||||||
|
`0x80 = use internal sensor`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# RAM Operations
|
||||||
|
|
||||||
|
## RAM Area Configuration
|
||||||
|
|
||||||
|
Sets the window for subsequent RAM writes. Y-coordinates are reversed due to hardware gates orientation.
|
||||||
|
|
||||||
|
**Important:** X addresses are specified in **pixels**, not bytes. The controller handles the byte conversion internally.
|
||||||
|
|
||||||
|
**For coordinates (x, y, w, h):**
|
||||||
|
|
||||||
|
1. Calculate reversed Y: `y_rev = HEIGHT - y - h`
|
||||||
|
|
||||||
|
2. **Set RAM Entry Mode** (Command `0x11`):
|
||||||
|
- Data: `0x01` (X increment, Y decrement - Y reversed)
|
||||||
|
|
||||||
|
3. **Set RAM X Address** (Command `0x44`) - **In pixels**:
|
||||||
|
- Data[0]: `x % 256` (X start LSB)
|
||||||
|
- Data[1]: `x / 256` (X start MSB)
|
||||||
|
- Data[2]: `(x + w - 1) % 256` (X end LSB)
|
||||||
|
- Data[3]: `(x + w - 1) / 256` (X end MSB)
|
||||||
|
|
||||||
|
4. **Set RAM Y Address** (Command `0x45`):
|
||||||
|
- Data[0]: `(y_rev + h - 1) % 256` (Y start LSB)
|
||||||
|
- Data[1]: `(y_rev + h - 1) / 256` (Y start MSB)
|
||||||
|
- Data[2]: `y_rev % 256` (Y end LSB)
|
||||||
|
- Data[3]: `y_rev / 256` (Y end MSB)
|
||||||
|
|
||||||
|
5. **Set RAM X Counter** (Command `0x4E`):
|
||||||
|
- Data[0]: `x % 256` (Initial X LSB)
|
||||||
|
- Data[1]: `x / 256` (Initial X MSB)
|
||||||
|
|
||||||
|
6. **Set RAM Y Counter** (Command `0x4F`):
|
||||||
|
- Data[0]: `(y_rev + h - 1) % 256` (Initial Y LSB)
|
||||||
|
- Data[1]: `(y_rev + h - 1) / 256` (Initial Y MSB)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Writing Image Data
|
||||||
|
|
||||||
|
## Write to Current Buffer (Command 0x24)
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ssd1677_write_bw(uint8_t *buffer, uint32_t size) {
|
||||||
|
// Set RAM Address Counters
|
||||||
|
epd_cmd(0x4E); // X counter
|
||||||
|
epd_data(0x00);
|
||||||
|
epd_cmd(0x4F); // Y counter
|
||||||
|
epd_data(0x00);
|
||||||
|
epd_data(0x00);
|
||||||
|
|
||||||
|
// Write BW RAM
|
||||||
|
epd_cmd(0x24);
|
||||||
|
for (uint32_t i = 0; i < size; i++)
|
||||||
|
epd_data(buffer[i]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Configure RAM area with `_setPartialRamArea(x, y, w, h)`
|
||||||
|
2. Send command `0x24`
|
||||||
|
3. Start bulk transfer (CS=LOW)
|
||||||
|
4. Transfer image data bytes (one bit per pixel, MSB first)
|
||||||
|
- Total bytes = `(w * h) / 8`
|
||||||
|
- `0xFF` = white, `0x00` = black
|
||||||
|
5. End transfer (CS=HIGH)
|
||||||
|
|
||||||
|
**Explanation:**
|
||||||
|
- **0x4E / 0x4F** set starting address for RAM.
|
||||||
|
- **0x24** selects the BW image buffer.
|
||||||
|
|
||||||
|
## Write to Previous Buffer (Command 0x26)
|
||||||
|
|
||||||
|
Same as above but use command `0x26` instead of `0x24`. Used for differential updates.
|
||||||
|
|
||||||
|
## Full Screen Clear
|
||||||
|
|
||||||
|
1. Write to previous buffer: `_setPartialRamArea(0, 0, 800, 480)` → Command `0x26` → 48000 bytes of `0xFF`
|
||||||
|
2. Write to current buffer: `_setPartialRamArea(0, 0, 800, 480)` → Command `0x24` → 48000 bytes of `0xFF`
|
||||||
|
3. Perform full refresh
|
||||||
|
|
||||||
|
## Full Frame Example
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ssd1677_display_frame(uint8_t *bw, uint8_t *red) {
|
||||||
|
ssd1677_write_bw(bw, BW_BUFFER_SIZE);
|
||||||
|
|
||||||
|
epd_cmd(0x26); // Write RED RAM
|
||||||
|
for (int i = 0; i < RED_BUFFER_SIZE; i++)
|
||||||
|
epd_data(red[i]);
|
||||||
|
|
||||||
|
ssd1677_update();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Display Updates
|
||||||
|
|
||||||
|
## Power On
|
||||||
|
|
||||||
|
| Command | Data | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `0x22` | `0xE0` | Display update control sequence |
|
||||||
|
| `0x20` | - | Master activation (trigger update) |
|
||||||
|
| Wait | ~100ms | Wait while BUSY pin is HIGH |
|
||||||
|
|
||||||
|
## Full Refresh
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ssd1677_update() {
|
||||||
|
epd_cmd(0x22);
|
||||||
|
epd_data(0xC7); // Display mode: load LUT + refresh + power off
|
||||||
|
|
||||||
|
epd_cmd(0x20); // Master activation
|
||||||
|
epd_wait_busy(); // Wait for driving waves to complete
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed Sequence:**
|
||||||
|
|
||||||
|
| Step | Command | Data | Description |
|
||||||
|
|------|---------|------|-------------|
|
||||||
|
| 1 | `0x21` | `0x40`, `0x00` | Display update control (bypass RED as 0, single chip) |
|
||||||
|
| 2a | `0x1A` | `0x5A` | Temperature register (fast mode only) |
|
||||||
|
| 2b | `0x22` | `0xD7` | Update sequence (fast mode) |
|
||||||
|
| **OR** | | | |
|
||||||
|
| 2b | `0x22` | `0xF7` | Update sequence (normal mode, extended temp) |
|
||||||
|
| 3 | `0x20` | - | Master activation |
|
||||||
|
| Wait | ~1600ms | Wait while BUSY pin is HIGH |
|
||||||
|
|
||||||
|
**Explanation:**
|
||||||
|
- **0x22 / 0xC7** tells SSD1677 which tasks to run (enable analog, load LUT, drive display).
|
||||||
|
- **0x20** starts the entire update cycle.
|
||||||
|
- **epd_wait_busy()** waits until the driver finishes waveform driving.
|
||||||
|
|
||||||
|
**Fast vs Normal Mode**: `useFastFullUpdate=true` uses faster refresh but limited temperature range.
|
||||||
|
|
||||||
|
### Display Update Control 2 (0x22) Bit Documentation
|
||||||
|
|
||||||
|
Based on driver implementation analysis:
|
||||||
|
|
||||||
|
| Bit | Hex | Name | Effect |
|
||||||
|
|-----|-----|------|--------|
|
||||||
|
| 7 | 0x80 | CLOCK_ON | Start internal oscillator |
|
||||||
|
| 6 | 0x40 | ANALOG_ON | Enable analog power rails (VGH/VGL drivers) |
|
||||||
|
| 5 | 0x20 | TEMP_LOAD | Load temperature (internal or external) |
|
||||||
|
| 4 | 0x10 | LUT_LOAD | Load waveform LUT |
|
||||||
|
| 3 | 0x08 | MODE_SELECT | Mode 1/2 selection |
|
||||||
|
| 2 | 0x04 | DISPLAY_START | Run display update |
|
||||||
|
| 1 | 0x02 | ANALOG_OFF | Analog shutdown phase |
|
||||||
|
| 0 | 0x01 | CLOCK_OFF | Disable internal oscillator |
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
- Full refresh (first power on): `0xC0 | 0x34 = 0xF4` (CLOCK+ANALOG+TEMP+LUT+DISPLAY)
|
||||||
|
- Full refresh (already on): `0x34` (TEMP+LUT+DISPLAY)
|
||||||
|
- Half refresh with high temp: `0xD4` (CLOCK+ANALOG+LUT+DISPLAY)
|
||||||
|
- Fast refresh with custom LUT: `0x0C` (MODE+DISPLAY)
|
||||||
|
- Fast refresh without custom LUT: `0x1C` (LUT+MODE+DISPLAY)
|
||||||
|
|
||||||
|
## Partial Refresh
|
||||||
|
|
||||||
|
| Command | Data | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `0x21` | `0x00`, `0x00` | Display update control (RED normal, single chip) |
|
||||||
|
| `0x22` | `0xFC` | Partial update sequence |
|
||||||
|
| `0x20` | - | Master activation |
|
||||||
|
| Wait | ~600ms | Wait while BUSY pin is HIGH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Custom LUT Guide
|
||||||
|
|
||||||
|
## What is a LUT?
|
||||||
|
|
||||||
|
The SSD1677 uses a **Look-Up Table (LUT)** to control **pixel waveform driving** during updates.
|
||||||
|
Each pixel (BW/RED) needs a sequence of voltage phases to switch states correctly.
|
||||||
|
|
||||||
|
A LUT controls:
|
||||||
|
- Voltage level per phase (VSH1, VSH2, VSL, Hi‑Z)
|
||||||
|
- VCOM toggling pattern
|
||||||
|
- Duration of each phase (TP0–TP7)
|
||||||
|
- Phase repetitions
|
||||||
|
- Additional red-pixel handling
|
||||||
|
|
||||||
|
## LUT Structure (111 bytes)
|
||||||
|
|
||||||
|
Used in the driver implementation for grayscale support:
|
||||||
|
|
||||||
|
| Byte Range | Size | Purpose |
|
||||||
|
|------------|------|---------||
|
||||||
|
| 0–49 | 50 | VS waveforms (5 groups × 10 bytes) |
|
||||||
|
| 50–99 | 50 | TP/RP timing groups (10 groups × 5 bytes) |
|
||||||
|
| 100–104 | 5 | Frame rate control |
|
||||||
|
| 105 | 1 | VGH (Gate voltage) - sent via 0x03 |
|
||||||
|
| 106 | 1 | VSH1 (Source voltage 1) - sent via 0x04 |
|
||||||
|
| 107 | 1 | VSH2 (Source voltage 2) - sent via 0x04 |
|
||||||
|
| 108 | 1 | VSL (Source voltage low) - sent via 0x04 |
|
||||||
|
| 109 | 1 | VCOM voltage - sent via 0x2C |
|
||||||
|
| 110 | 1 | Reserved |
|
||||||
|
|
||||||
|
**Note:** Bytes 105-109 are sent using separate voltage control commands after loading the main LUT.
|
||||||
|
|
||||||
|
## How to Build a Custom LUT
|
||||||
|
|
||||||
|
### Step 1 — Define Source Voltage Waveform (WS0–WS7)
|
||||||
|
You choose for each phase:
|
||||||
|
- VSH1 (medium positive)
|
||||||
|
- VSH2 (strong positive — drives white)
|
||||||
|
- VSL (strong negative — drives black)
|
||||||
|
- Hi‑Z (float)
|
||||||
|
|
||||||
|
These define **pixel movement direction** and strength.
|
||||||
|
|
||||||
|
### Step 2 — Define VCOM Waveform (WS8–WS14)
|
||||||
|
VCOM biases the entire display.
|
||||||
|
These bytes define:
|
||||||
|
- On/off toggling per phase
|
||||||
|
- Matching with source driver phases
|
||||||
|
- Ghost reduction
|
||||||
|
|
||||||
|
### Step 3 — Phase Timing TP0–TP7 (WS15–WS23)
|
||||||
|
Each TPx sets duration of a phase.
|
||||||
|
Longer = cleaner image, slower refresh.
|
||||||
|
Shorter = faster, but potential ghosting.
|
||||||
|
|
||||||
|
### Step 4 — Repeat Counts & Finalization (WS24–WS33)
|
||||||
|
These adjust:
|
||||||
|
- How many times each phase repeats
|
||||||
|
- Red pigment extra driving
|
||||||
|
- Cleanup phases
|
||||||
|
|
||||||
|
## How to Load a Custom LUT
|
||||||
|
|
||||||
|
A custom LUT is written using **Command 0x32**:
|
||||||
|
|
||||||
|
```
|
||||||
|
CMD 0x32
|
||||||
|
DATA WS0
|
||||||
|
DATA WS1
|
||||||
|
...
|
||||||
|
DATA WS33
|
||||||
|
```
|
||||||
|
|
||||||
|
The first **105 bytes** are written to the LUT register (0x32), followed by separate voltage control commands.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Load LUT (111-byte format with voltage controls)
|
||||||
|
void ssd1677_load_lut_extended(const uint8_t* lut) {
|
||||||
|
// Load main LUT (105 bytes: VS + TP/RP + frame rate)
|
||||||
|
epd_cmd(0x32);
|
||||||
|
for (int i = 0; i < 105; i++)
|
||||||
|
epd_data(lut[i]);
|
||||||
|
|
||||||
|
// Set voltage values from bytes 105-109
|
||||||
|
epd_cmd(0x03); // Gate voltage (VGH)
|
||||||
|
epd_data(lut[105]);
|
||||||
|
|
||||||
|
epd_cmd(0x04); // Source voltages (VSH1, VSH2, VSL)
|
||||||
|
epd_data(lut[106]); // VSH1
|
||||||
|
epd_data(lut[107]); // VSH2
|
||||||
|
epd_data(lut[108]); // VSL
|
||||||
|
|
||||||
|
epd_cmd(0x2C); // VCOM voltage
|
||||||
|
epd_data(lut[109]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Apply (Use) the Custom LUT
|
||||||
|
|
||||||
|
After loading the LUT, tell the display to **use it**.
|
||||||
|
|
||||||
|
### 1. Configure Display Update Mode (0x22)
|
||||||
|
Typical value enabling LUT usage:
|
||||||
|
```
|
||||||
|
CMD 0x22
|
||||||
|
DATA 0xF7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start Master Activation (0x20)
|
||||||
|
```
|
||||||
|
CMD 0x20
|
||||||
|
WAIT BUSY = LOW
|
||||||
|
```
|
||||||
|
|
||||||
|
While BUSY is high, the LUT waveform is driving the display.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Apply LUT
|
||||||
|
void ssd1677_apply_lut() {
|
||||||
|
epd_cmd(0x22);
|
||||||
|
epd_data(0xF7); // Use LUT
|
||||||
|
epd_cmd(0x20); // Master Activation
|
||||||
|
epd_wait_busy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## LUT Summary
|
||||||
|
|
||||||
|
**Build a custom LUT**
|
||||||
|
- Create 111 bytes: 105 for LUT register + 5 voltage values + 1 reserved
|
||||||
|
|
||||||
|
**Use a custom LUT**
|
||||||
|
1. Write with **0x32**
|
||||||
|
2. Enable with **0x22**
|
||||||
|
3. Trigger with **0x20**
|
||||||
|
|
||||||
|
**Optional**
|
||||||
|
- Burn to OTP with **0x36**
|
||||||
|
|
||||||
|
## Grayscale Rendering with Custom LUTs
|
||||||
|
|
||||||
|
The driver implements 4-level grayscale using a multi-pass technique with custom LUTs.
|
||||||
|
|
||||||
|
### Grayscale Principle
|
||||||
|
|
||||||
|
1. **First pass (Black/White):** Write BW framebuffer to both RAM buffers, perform standard refresh
|
||||||
|
2. **Second pass (Grayscale):** Write LSB and MSB grayscale buffers, apply custom grayscale LUT, perform fast refresh
|
||||||
|
3. The custom LUT creates intermediate gray levels by controlling pixel voltage phases
|
||||||
|
|
||||||
|
### Grayscale LUT Structure
|
||||||
|
|
||||||
|
The driver includes two grayscale LUTs (111 bytes each):
|
||||||
|
- `lut_grayscale`: Forward grayscale rendering
|
||||||
|
- `lut_grayscale_revert`: Cleans up grayscale artifacts back to pure BW
|
||||||
|
|
||||||
|
Key characteristics:
|
||||||
|
- Uses different voltage sequences for 4 gray levels (00, 01, 10, 11)
|
||||||
|
- Frame timing optimized for fast refresh (~500ms)
|
||||||
|
- VS waveforms: 50 bytes (5 groups × 10 bytes)
|
||||||
|
- TP/RP timing: 50 bytes (10 groups × 5 bytes)
|
||||||
|
- Voltages: VGH=0x17, VSH1=0x41, VSH2=0xA8, VSL=0x32, VCOM=0x30
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Complete Workflows
|
||||||
|
|
||||||
|
## Full Screen Update (Initial or Complete Refresh)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. _InitDisplay() [if not initialized]
|
||||||
|
2. _setPartialRamArea(0, 0, 800, 480)
|
||||||
|
3. Write to previous buffer: Command 0x26 + 48000 bytes
|
||||||
|
4. Write to current buffer: Command 0x24 + 48000 bytes
|
||||||
|
5. _Update_Full()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Partial Update (Fast Refresh)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. _InitDisplay() [if not initialized]
|
||||||
|
2. [First time only] Clear screen buffers with full refresh
|
||||||
|
3. _setPartialRamArea(x, y, w, h)
|
||||||
|
4. Write image: Command 0x24 + image bytes
|
||||||
|
5. _Update_Part()
|
||||||
|
6. Write image again: Command 0x24 + image bytes
|
||||||
|
7. Write to previous: Command 0x26 + same image bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why write twice?** Partial updates compare current vs previous buffer. Writing to both buffers after refresh prevents ghosting on next update.
|
||||||
|
|
||||||
|
## Minimal Usage Example
|
||||||
|
|
||||||
|
```c
|
||||||
|
ssd1677_init();
|
||||||
|
|
||||||
|
ssd1677_display_frame(bw_image, red_image);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example: Fast Refresh with Buffering
|
||||||
|
|
||||||
|
The driver supports two buffering modes for fast partial updates:
|
||||||
|
|
||||||
|
### Dual Buffer Mode (Default)
|
||||||
|
|
||||||
|
**Memory usage:** 96KB (two 48KB buffers)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Initialize display
|
||||||
|
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)
|
||||||
|
display.displayBuffer(FAST_REFRESH);
|
||||||
|
|
||||||
|
// Next frame
|
||||||
|
// ... modify fb ...
|
||||||
|
display.displayBuffer(FAST_REFRESH);
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
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 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<uint8_t *>(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
|
||||||
|
|
||||||
|
Commands `0x46` and `0x47` allow rapid buffer clearing:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Clear BW RAM to white pattern
|
||||||
|
sendCommand(0x46); // Auto write BW RAM
|
||||||
|
sendData(0xF7); // Fill pattern
|
||||||
|
waitWhileBusy();
|
||||||
|
|
||||||
|
// Clear RED RAM to white pattern
|
||||||
|
sendCommand(0x47); // Auto write RED RAM
|
||||||
|
sendData(0xF7); // Fill pattern
|
||||||
|
waitWhileBusy();
|
||||||
|
```
|
||||||
|
|
||||||
|
This is much faster than writing 48,000 bytes manually during initialization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command Reference
|
||||||
|
|
||||||
|
| Command | Name | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `0x01` | Driver Output Control | Set gate scanning (HEIGHT) |
|
||||||
|
| `0x0C` | Booster Soft Start | Configure boost converter |
|
||||||
|
| `0x10` | Deep Sleep Mode | Enter low power mode |
|
||||||
|
| `0x11` | Data Entry Mode | Set X/Y increment direction |
|
||||||
|
| `0x12` | Software Reset | Reset controller |
|
||||||
|
| `0x18` | Temperature Sensor | Control temp sensor |
|
||||||
|
| `0x1A` | Temperature Register | Set temp value (fast mode) |
|
||||||
|
| `0x20` | Master Activation | Trigger display update |
|
||||||
|
| `0x21` | Display Update Control | Configure update mode |
|
||||||
|
| `0x22` | Display Update Sequence | Set update waveform |
|
||||||
|
| `0x24` | Write RAM (BW) | Write to current buffer |
|
||||||
|
| `0x26` | Write RAM (RED/OLD) | Write to previous buffer |
|
||||||
|
| `0x03` | Gate Voltage | Set VGH voltage level |
|
||||||
|
| `0x04` | Source Voltage | Set VSH1, VSH2, VSL voltages |
|
||||||
|
| `0x2C` | Write VCOM | Set VCOM voltage |
|
||||||
|
| `0x32` | Write LUT Register | Load custom 105-byte LUT (part of 111-byte structure) |
|
||||||
|
| `0x36` | Write OTP | Burn LUT to one-time-programmable memory |
|
||||||
|
| `0x3C` | Border Waveform | Configure border behavior |
|
||||||
|
| `0x44` | Set RAM X Address | Define X window (in pixels) |
|
||||||
|
| `0x45` | Set RAM Y Address | Define Y window (in pixels) |
|
||||||
|
| `0x46` | Auto Write BW RAM | Fast fill BW RAM with pattern |
|
||||||
|
| `0x47` | Auto Write RED RAM | Fast fill RED RAM with pattern |
|
||||||
|
| `0x4E` | Set RAM X Counter | Set initial X position (in pixels) |
|
||||||
|
| `0x4F` | Set RAM Y Counter | Set initial Y position (in pixels) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Timing & Power Management
|
||||||
|
|
||||||
|
## Timing Specifications
|
||||||
|
|
||||||
|
| Operation | Duration | Notes |
|
||||||
|
|-----------|----------|-------|
|
||||||
|
| Reset pulse | 10ms | Low duration |
|
||||||
|
| Power on | ~100ms | BUSY signal duration |
|
||||||
|
| Power off | ~200ms | BUSY signal duration |
|
||||||
|
| Full refresh | ~1600ms | Normal mode, wait for BUSY |
|
||||||
|
| Partial refresh | ~600ms | Wait for BUSY |
|
||||||
|
| Software reset delay | 10ms | After command 0x12 |
|
||||||
|
|
||||||
|
## BUSY Signal Monitoring
|
||||||
|
|
||||||
|
- **Pin**: GPIO6 (INPUT)
|
||||||
|
- **Active level**: HIGH
|
||||||
|
- **Polling**: Read pin until LOW, with timeout protection
|
||||||
|
- **Timeout**: 10000ms (10 seconds)
|
||||||
|
- **Usage**: Wait after commands `0x20` (master activation)
|
||||||
|
|
||||||
|
## Power Off
|
||||||
|
|
||||||
|
| Command | Data | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `0x22` | `0x83` | Power off sequence |
|
||||||
|
| `0x20` | - | Master activation |
|
||||||
|
| Wait | ~200ms | Wait while BUSY pin is HIGH |
|
||||||
|
|
||||||
|
## Hibernate (Deep Sleep)
|
||||||
|
|
||||||
|
1. Execute Power Off sequence
|
||||||
|
2. Send command `0x10` (Deep Sleep Mode)
|
||||||
|
3. Send data `0x01` (Enter deep sleep)
|
||||||
|
|
||||||
|
**Wake from Hibernate**: Requires hardware reset via RST pin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Important Notes
|
||||||
|
|
||||||
|
- BUSY pin *must* be polled after reset and update
|
||||||
|
- All RAM writes auto-increment based on data entry mode
|
||||||
|
- SSD1677 can display BW-only or RED-only if desired
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
@ -8,7 +8,7 @@ class EInkDisplay {
|
|||||||
EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy);
|
EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy);
|
||||||
|
|
||||||
// Destructor
|
// Destructor
|
||||||
~EInkDisplay();
|
~EInkDisplay() = default;
|
||||||
|
|
||||||
// Refresh modes (guarded to avoid redefinition in test builds)
|
// Refresh modes (guarded to avoid redefinition in test builds)
|
||||||
enum RefreshMode {
|
enum RefreshMode {
|
||||||
@ -21,21 +21,26 @@ class EInkDisplay {
|
|||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
// Display dimensions
|
// Display dimensions
|
||||||
static const uint16_t DISPLAY_WIDTH = 800;
|
static constexpr uint16_t DISPLAY_WIDTH = 800;
|
||||||
static const uint16_t DISPLAY_HEIGHT = 480;
|
static constexpr uint16_t DISPLAY_HEIGHT = 480;
|
||||||
static const uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
||||||
static const uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
||||||
|
|
||||||
// Frame buffer operations
|
// Frame buffer operations
|
||||||
void clearScreen(uint8_t color = 0xFF);
|
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);
|
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const;
|
||||||
|
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
void swapBuffers();
|
void swapBuffers();
|
||||||
void setFramebuffer(const uint8_t* bwBuffer);
|
#endif
|
||||||
|
void setFramebuffer(const uint8_t* bwBuffer) const;
|
||||||
|
|
||||||
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
||||||
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
||||||
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
||||||
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
|
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
|
||||||
|
#endif
|
||||||
|
|
||||||
void displayBuffer(RefreshMode mode = FAST_REFRESH);
|
void displayBuffer(RefreshMode mode = FAST_REFRESH);
|
||||||
void displayGrayBuffer(bool turnOffScreen = false);
|
void displayGrayBuffer(bool turnOffScreen = false);
|
||||||
@ -52,7 +57,7 @@ class EInkDisplay {
|
|||||||
void deepSleep();
|
void deepSleep();
|
||||||
|
|
||||||
// Access to frame buffer
|
// Access to frame buffer
|
||||||
uint8_t* getFrameBuffer() {
|
uint8_t* getFrameBuffer() const {
|
||||||
return frameBuffer;
|
return frameBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,10 +70,11 @@ class EInkDisplay {
|
|||||||
|
|
||||||
// Frame buffer (statically allocated)
|
// Frame buffer (statically allocated)
|
||||||
uint8_t frameBuffer0[BUFFER_SIZE];
|
uint8_t frameBuffer0[BUFFER_SIZE];
|
||||||
uint8_t frameBuffer1[BUFFER_SIZE];
|
|
||||||
|
|
||||||
uint8_t* frameBuffer;
|
uint8_t* frameBuffer;
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
|
uint8_t frameBuffer1[BUFFER_SIZE];
|
||||||
uint8_t* frameBufferActive;
|
uint8_t* frameBufferActive;
|
||||||
|
#endif
|
||||||
|
|
||||||
// SPI settings
|
// SPI settings
|
||||||
SPISettings spiSettings;
|
SPISettings spiSettings;
|
||||||
|
|||||||
@ -115,27 +115,31 @@ EInkDisplay::EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t
|
|||||||
_rst(rst),
|
_rst(rst),
|
||||||
_busy(busy),
|
_busy(busy),
|
||||||
frameBuffer(nullptr),
|
frameBuffer(nullptr),
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
frameBufferActive(nullptr),
|
frameBufferActive(nullptr),
|
||||||
|
#endif
|
||||||
customLutActive(false) {
|
customLutActive(false) {
|
||||||
Serial.printf("[%lu] EInkDisplay: Constructor called\n", millis());
|
Serial.printf("[%lu] EInkDisplay: Constructor called\n", millis());
|
||||||
Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", millis(), sclk, mosi, cs, dc, rst, busy);
|
Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", millis(), sclk, mosi, cs, dc, rst, busy);
|
||||||
}
|
}
|
||||||
|
|
||||||
EInkDisplay::~EInkDisplay() {
|
|
||||||
// No dynamic memory to clean up (buffers are statically allocated)
|
|
||||||
}
|
|
||||||
|
|
||||||
void EInkDisplay::begin() {
|
void EInkDisplay::begin() {
|
||||||
Serial.printf("[%lu] EInkDisplay: begin() called\n", millis());
|
Serial.printf("[%lu] EInkDisplay: begin() called\n", millis());
|
||||||
|
|
||||||
frameBuffer = frameBuffer0;
|
frameBuffer = frameBuffer0;
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
frameBufferActive = frameBuffer1;
|
frameBufferActive = frameBuffer1;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Initialize to white
|
// Initialize to white
|
||||||
memset(frameBuffer0, 0xFF, BUFFER_SIZE);
|
memset(frameBuffer0, 0xFF, BUFFER_SIZE);
|
||||||
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
|
Serial.printf("[%lu] Static frame buffer (%lu bytes = 48KB)\n", millis(), BUFFER_SIZE);
|
||||||
|
#else
|
||||||
memset(frameBuffer1, 0xFF, BUFFER_SIZE);
|
memset(frameBuffer1, 0xFF, BUFFER_SIZE);
|
||||||
|
|
||||||
Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE);
|
Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE);
|
||||||
|
#endif
|
||||||
|
|
||||||
Serial.printf("[%lu] Initializing e-ink display driver...\n", millis());
|
Serial.printf("[%lu] Initializing e-ink display driver...\n", millis());
|
||||||
|
|
||||||
// Initialize SPI with custom pins
|
// Initialize SPI with custom pins
|
||||||
@ -266,13 +270,11 @@ void EInkDisplay::initDisplayController() {
|
|||||||
Serial.printf("[%lu] SSD1677 controller initialized\n", millis());
|
Serial.printf("[%lu] SSD1677 controller initialized\n", millis());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||||
const uint16_t WIDTH = 800;
|
constexpr uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01;
|
||||||
const uint16_t HEIGHT = 480;
|
|
||||||
const uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01;
|
|
||||||
|
|
||||||
// Reverse Y coordinate (gates are reversed on this display)
|
// Reverse Y coordinate (gates are reversed on this display)
|
||||||
y = HEIGHT - y - h;
|
y = DISPLAY_HEIGHT - y - h;
|
||||||
|
|
||||||
// Set data entry mode (X increment, Y decrement for reversed gates)
|
// Set data entry mode (X increment, Y decrement for reversed gates)
|
||||||
sendCommand(CMD_DATA_ENTRY_MODE);
|
sendCommand(CMD_DATA_ENTRY_MODE);
|
||||||
@ -303,28 +305,28 @@ void EInkDisplay::setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
|||||||
sendData((y + h - 1) / 256); // high byte
|
sendData((y + h - 1) / 256); // high byte
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::clearScreen(uint8_t color) {
|
void EInkDisplay::clearScreen(const uint8_t color) const {
|
||||||
memset(frameBuffer, color, BUFFER_SIZE);
|
memset(frameBuffer, color, BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
void EInkDisplay::drawImage(const uint8_t* imageData, const uint16_t x, const uint16_t y, const uint16_t w, const uint16_t h,
|
||||||
bool fromProgmem) {
|
const bool fromProgmem) const {
|
||||||
if (!frameBuffer) {
|
if (!frameBuffer) {
|
||||||
Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis());
|
Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bytes per line for the image
|
// Calculate bytes per line for the image
|
||||||
uint16_t imageWidthBytes = w / 8;
|
const uint16_t imageWidthBytes = w / 8;
|
||||||
|
|
||||||
// Copy image data to frame buffer
|
// Copy image data to frame buffer
|
||||||
for (uint16_t row = 0; row < h; row++) {
|
for (uint16_t row = 0; row < h; row++) {
|
||||||
uint16_t destY = y + row;
|
const uint16_t destY = y + row;
|
||||||
if (destY >= DISPLAY_HEIGHT)
|
if (destY >= DISPLAY_HEIGHT)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
|
const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
|
||||||
uint16_t srcOffset = row * imageWidthBytes;
|
const uint16_t srcOffset = row * imageWidthBytes;
|
||||||
|
|
||||||
for (uint16_t col = 0; col < imageWidthBytes; col++) {
|
for (uint16_t col = 0; col < imageWidthBytes; col++) {
|
||||||
if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES)
|
if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES)
|
||||||
@ -343,25 +345,27 @@ void EInkDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, ui
|
|||||||
|
|
||||||
void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size) {
|
void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size) {
|
||||||
const char* bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED";
|
const char* bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED";
|
||||||
unsigned long startTime = millis();
|
const unsigned long startTime = millis();
|
||||||
Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size);
|
Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size);
|
||||||
|
|
||||||
sendCommand(ramBuffer);
|
sendCommand(ramBuffer);
|
||||||
sendData(data, size);
|
sendData(data, size);
|
||||||
|
|
||||||
unsigned long duration = millis() - startTime;
|
const unsigned long duration = millis() - startTime;
|
||||||
Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), bufferName, duration);
|
Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), bufferName, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::setFramebuffer(const uint8_t* bwBuffer) {
|
void EInkDisplay::setFramebuffer(const uint8_t* bwBuffer) const {
|
||||||
memcpy(frameBuffer, bwBuffer, BUFFER_SIZE);
|
memcpy(frameBuffer, bwBuffer, BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
void EInkDisplay::swapBuffers() {
|
void EInkDisplay::swapBuffers() {
|
||||||
uint8_t* temp = frameBuffer;
|
uint8_t* temp = frameBuffer;
|
||||||
frameBuffer = frameBufferActive;
|
frameBuffer = frameBufferActive;
|
||||||
frameBufferActive = temp;
|
frameBufferActive = temp;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void EInkDisplay::grayscaleRevert() {
|
void EInkDisplay::grayscaleRevert() {
|
||||||
if (!inGrayscaleMode) {
|
if (!inGrayscaleMode) {
|
||||||
@ -372,7 +376,7 @@ void EInkDisplay::grayscaleRevert() {
|
|||||||
|
|
||||||
// Load the revert LUT
|
// Load the revert LUT
|
||||||
setCustomLUT(true, lut_grayscale_revert);
|
setCustomLUT(true, lut_grayscale_revert);
|
||||||
refreshDisplay(FAST_REFRESH, false);
|
refreshDisplay(FAST_REFRESH);
|
||||||
setCustomLUT(false);
|
setCustomLUT(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,6 +396,18 @@ void EInkDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t*
|
|||||||
writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE);
|
writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
|
/**
|
||||||
|
* In single buffer mode, this should be called with the previously written BW buffer
|
||||||
|
* to reconstruct the RED buffer for proper differential fast refreshes following a
|
||||||
|
* grayscale display.
|
||||||
|
*/
|
||||||
|
void EInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) {
|
||||||
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||||
|
writeRamBuffer(CMD_WRITE_RAM_RED, bwBuffer, BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void EInkDisplay::displayBuffer(RefreshMode mode) {
|
void EInkDisplay::displayBuffer(RefreshMode mode) {
|
||||||
if (!isScreenOn) {
|
if (!isScreenOn) {
|
||||||
// Force half refresh if screen is off
|
// Force half refresh if screen is off
|
||||||
@ -414,17 +430,25 @@ void EInkDisplay::displayBuffer(RefreshMode mode) {
|
|||||||
} else {
|
} else {
|
||||||
// For fast refresh, write to BW buffer only
|
// For fast refresh, write to BW buffer only
|
||||||
writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE);
|
writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE);
|
||||||
|
// In single buffer mode, the RED RAM should already contain the previous frame
|
||||||
|
// In dual buffer mode, we write back frameBufferActive which is the last frame
|
||||||
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE);
|
writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap active buffer for next time
|
|
||||||
swapBuffers();
|
|
||||||
|
|
||||||
// Refresh the display
|
// Refresh the display
|
||||||
refreshDisplay(mode, false);
|
refreshDisplay(mode);
|
||||||
|
|
||||||
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
||||||
|
// In single buffer mode always sync RED RAM after refresh to prepare for next fast refresh
|
||||||
|
// This ensures RED contains the currently displayed frame for differential comparison
|
||||||
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||||
|
writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, BUFFER_SIZE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::displayGrayBuffer(bool turnOffScreen) {
|
void EInkDisplay::displayGrayBuffer(const bool turnOffScreen) {
|
||||||
drawGrayscale = false;
|
drawGrayscale = false;
|
||||||
inGrayscaleMode = true;
|
inGrayscaleMode = true;
|
||||||
|
|
||||||
@ -434,7 +458,7 @@ void EInkDisplay::displayGrayBuffer(bool turnOffScreen) {
|
|||||||
setCustomLUT(false);
|
setCustomLUT(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) {
|
void EInkDisplay::refreshDisplay(const RefreshMode mode, const bool turnOffScreen) {
|
||||||
// Configure Display Update Control 1
|
// Configure Display Update Control 1
|
||||||
sendCommand(CMD_DISPLAY_UPDATE_CTRL1);
|
sendCommand(CMD_DISPLAY_UPDATE_CTRL1);
|
||||||
sendData((mode == FAST_REFRESH) ? CTRL1_NORMAL : CTRL1_BYPASS_RED); // Configure buffer comparison mode
|
sendData((mode == FAST_REFRESH) ? CTRL1_NORMAL : CTRL1_BYPASS_RED); // Configure buffer comparison mode
|
||||||
@ -490,7 +514,7 @@ void EInkDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) {
|
|||||||
waitWhileBusy(refreshType);
|
waitWhileBusy(refreshType);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EInkDisplay::setCustomLUT(bool enabled, const unsigned char* lutData) {
|
void EInkDisplay::setCustomLUT(const bool enabled, const unsigned char* lutData) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
Serial.printf("[%lu] Loading custom LUT...\n", millis());
|
Serial.printf("[%lu] Loading custom LUT...\n", millis());
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user