From 56796aa6d30ba5bed33459c1c9f2e0b2e382298b Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 16 Dec 2025 22:12:56 +1100 Subject: [PATCH] Add SSD1677 E-Ink Display Driver Guide Copied verbatium from https://github.com/CidVonHighwind/microreader/blob/main/doc/SSD1677_GUIDE.md --- libs/display/EInkDisplay/doc/SSD1677_GUIDE.md | 643 ++++++++++++++++++ 1 file changed, 643 insertions(+) create mode 100644 libs/display/EInkDisplay/doc/SSD1677_GUIDE.md diff --git a/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md b/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md new file mode 100644 index 0000000..81a053d --- /dev/null +++ b/libs/display/EInkDisplay/doc/SSD1677_GUIDE.md @@ -0,0 +1,643 @@ +# 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) +- [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 + +--- + +# 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 Double Buffering + +The driver implements double buffering to enable fast partial updates: + +```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 + +## 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 +- Total RAM size: 48,000 bytes (800×480 ÷ 8) +- Dual-buffer system enables differential partial updates +- First write after init should be full refresh to clear ghost images