* 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
823 lines
25 KiB
Markdown
823 lines
25 KiB
Markdown
# 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
|