Files
waveshare-panel/CLAUDE.md
T

4.0 KiB
Raw Blame History

Waveshare 2.9" E-Paper LVGL Project

Goal Summary

  • Waveshare 2.9" Touch ePaper HAT driving LVGL UI via ESPHome on ESP32
  • 2-bit grayscale display support implemented via custom ESPHome component
  • Touchscreen integration in progress

Hardware

  • ESP32-WROOM-32 (nodemcu-32s board)
  • Waveshare 2.9inch Touch ePaper HAT (296x128, SSD1680 display, GT1151 touchscreen)

Pin Mapping

Function ESP Pin HAT Pin
Display SCLK GPIO18 Pin23/GPIO11/SPI0_SCLK
Display MOSI GPIO23 Pin19/GPIO10/SPI0_MOSI
Display CS GPIO19 Pin24/GPIO8/SPI0_CE0
Display DC GPIO17 Pin22/GPIO25
Display BUSY GPIO4 Pin24/GPIO24
Display RST GPIO5 Pin11/GPIO17/SPI1_CE1
Touch SDA GPIO21 Pin3/GPIO2/I2C1_SDA
Touch SCL GPIO22 Pin5/GPIO3/I2C1_SCL
Touch RST GPIO15 Pin13/GPIO27
Touch INT GPIO16 Pin15/GPIO22

Note: GPIO15 (Touch RST) is an ESP32 strapping pin, but only affects boot log output (LOW silences it). No impact on UART download mode or OTA flashing.

Software

  • ESPHome 2026.1.0
  • Framework: esp-idf
  • Project directory: ~/Projects/waveshare-panel

Current Status

  • 2-bit grayscale fully working via custom component waveshare_epaper_2bit
  • LVGL rendering working, 4-shade colorspace confirmed
  • Bayer ordered dithering implemented for gradient smoothing
  • Touchscreen working: polling mode, mirror_x + mirror_y transforms confirmed correct
  • LVGL touch input and button state working end-to-end
  • Active: LVGL layout work

Display Refresh Pattern

Two-tier refresh wired to lvgl.on_draw_end via two mode: restart scripts:

  • Partial (refresh_display, 60ms): calls display_partial() — fast 1-bit, non-blocking, immediate interaction feedback
  • Full (full_refresh_display, 10s): calls component.update: display0display() — full 2-bit grayscale quality restore after idle

60ms was empirically tuned: 80ms felt sluggish, 40ms intermittently raced. component.update: display0 only pushes the frame buffer to hardware and does not re-trigger on_draw_end.

Grayscale Implementation

Custom component model name: 2.90inv2-r2-2bpp

Key implementation details:

  • Gray4 waveform LUT loaded via register 0x32 on every display() call
  • Activation uses 0xC7 (custom LUT), not 0xF7 (which reloads OTP LUT)
  • display() = full 2-bit grayscale refresh; display_partial() = fast 1-bit partial refresh
  • Second bitplane buffer (buffer2_) allocated in initialize()

Confirmed bitplane table for this panel with Gray4 LUT:

0x24 0x26 Result
0 0 White
1 0 Light grey
0 1 Dark grey
1 1 Black

Color convention: 0xFFFFFF=white, 0x000000=black (no inversion). LVGL 4-shade palette: 0xFFFFFF, 0xAAAAAA, 0x555555, 0x000000

Dithering: 4×4 Bayer ordered dither, bias = bayer*2 - 15 (±15 range). Amplitude chosen to keep 0xAAAAAA and 0x555555 as solid fills.

Touchscreen

  • Controller: ICNT86X (I2C address 0x48; 0x30 also ACKs, likely DFU/bootloader interface)
  • Custom component: custom_components/icnt86x/ (ESPHome has no native ICNT86X driver)
  • Touch count at register 0x1001, touch data at 0x1002 (7 bytes/point), clear by writing 0x00 to 0x1001
  • Waveshare's own driver inverts both axes: X = 295 - raw_x, Y = 127 - raw_y
  • INT pulse is very brief (sub-ms); component works in polling mode regardless
  • Coordinate transform: mirror_x + mirror_y confirmed correct; swap_xy not needed

Relevant Files

  • Custom component: custom_components/waveshare_epaper_2bit/
    • __init__.py — CODEOWNERS only
    • display.py — component schema and model registration
    • waveshare_epaper_2bit.cpp — driver implementation
    • waveshare_epaper_2bit.h — header
  • Main config: waveshare-test.yaml (pin table here is source of truth)
  • Build cache: .esphome/build/waveshare-epaper-test/src/esphome/components/
  • ESPHome source (venv): ~/Code/esphome-2026.1.0/