From 4e54363168ae4307bf87c1ab962c33bc78f6fe8e Mon Sep 17 00:00:00 2001 From: David Koh Date: Wed, 3 Jun 2026 22:28:17 +0800 Subject: [PATCH] feat(amoled-18): auto-detect CO5300/CST816 panel revision Waveshare ships two hardware revisions under "ESP32-S3-Touch-AMOLED-1.8": the original (SH8601 display + FT3168 touch @0x38) and a later one (CO5300 display + CST816 touch @0x15). On the later board the SH8601 driver left the panel blank and the FT3168 probe spammed ESP_ERR_INVALID_STATE because nothing answers at 0x38. Detect the revision at boot by probing the touch address and pick the matching drivers, so one binary drives both boards: - board_rev.{h}/board_init.cpp: probe 0x15 (CST816) vs 0x38 (FT3168) - display.cpp: instantiate Arduino_CO5300 or Arduino_SH8601 behind a shared Arduino_OLED*; CO5300 gets a 16px column offset to center the 368-wide image (verified on hardware) - touch.cpp: one FocalTech-style reader (regs 0x02..0x06) for both chips, address chosen by revision - board.h: add CST816_ADDR 0x15 - CLAUDE.md: document the dual revision Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 7 +-- .../src/boards/waveshare_amoled_18/board.h | 7 ++- .../boards/waveshare_amoled_18/board_init.cpp | 22 +++++++++ .../boards/waveshare_amoled_18/board_rev.h | 18 +++++++ .../boards/waveshare_amoled_18/display.cpp | 25 ++++++++-- .../src/boards/waveshare_amoled_18/touch.cpp | 48 +++++++++++-------- 6 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 firmware/src/boards/waveshare_amoled_18/board_rev.h diff --git a/CLAUDE.md b/CLAUDE.md index 66d0431..d8ef351 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ never see board-specific code. See [`docs/porting/adding-a-board.md`](docs/porti Two reference ports today: - `boards/waveshare_amoled_216/` — original Waveshare ESP32-S3-Touch-AMOLED-2.16 (CO5300, 480×480 square, CST9220 touch, IMU rotation). Build env: `waveshare_amoled_216`. -- `boards/waveshare_amoled_18/` — Waveshare ESP32-S3-Touch-AMOLED-1.8 (SH8601, 368×448 portrait, FT3168 touch, XCA9554 IO expander). Build env: `waveshare_amoled_18`. +- `boards/waveshare_amoled_18/` — Waveshare ESP32-S3-Touch-AMOLED-1.8 (368×448 portrait, XCA9554 IO expander). Build env: `waveshare_amoled_18`. **Two panel revisions are auto-detected at boot** (`board_rev()` in `board_init.cpp`, enum in `board_rev.h`): original = SH8601 display + FT3168 touch (0x38); later = CO5300 display + CST816 touch (0x15). One binary drives both. The shared code calls a small HAL (`firmware/src/hal/`) that each board implements: display, touch, input, power, IMU. Optional features are guarded by `BoardCaps` (runtime) and `BOARD_HAS_*` (compile-time) rather than `#ifdef BOARD_*`. @@ -25,8 +25,9 @@ Connects to a host daemon over BLE; daemon polls Anthropic API for usage data. T - Buttons: GPIO 0 (left → Space/voice-mode), GPIO 18 (right → Shift+Tab/mode-toggle), AXP PKEY (middle → cycle screens; on splash → cycle animations) ### AMOLED-1.8 (newer port) -- Display: **SH8601** AMOLED via QSPI (CS=12, **SCLK=11** ← different!, SDIO0..3=4..7, RST routed via XCA9554 EXIO1) -- Touch: **FT3168** via I2C (SDA=15, SCL=14, INT=21, addr=0x38). Driven by minimal inline reader in `main.cpp` (FocalTech standard register layout — avoids vendoring the GPLv3 `Arduino_DriveBus` library). +**Two hardware revisions ship under this name; the firmware probes I2C at boot and picks drivers automatically (`board_rev()`):** +- Display: **SH8601** (original) or **CO5300** (later rev) AMOLED via QSPI (CS=12, **SCLK=11** ← different!, SDIO0..3=4..7, RST routed via XCA9554 EXIO1). Both are `Arduino_OLED` subclasses held behind one base pointer in `display.cpp`. The CO5300's 368-wide active area starts at GRAM column 16, so it gets `CO5300_COL_OFFSET 16` to center; SH8601 needs none. +- Touch: **FT3168** @ 0x38 (original) or **CST816** @ 0x15 (later rev), via I2C (SDA=15, SCL=14, INT=21). Both expose the same FocalTech-style data layout at regs 0x02..0x06, so one inline reader in `touch.cpp` serves both — only the address differs. Avoids vendoring the GPLv3 `Arduino_DriveBus` library. Revision is detected by which touch address ACKs (CST816 present ⇒ CO5300 panel). - PMU: AXP2101 @ 0x34 (same chip as 2.16 — `XPowersLib` reused; battery is an optional kit add-on but PMU + charging circuitry are populated) - IMU: QMI8658 @ 0x6B (same chip — initialized for I2C bus health, rotation logic disabled) - IO expander: **XCA9554 / PCA9554** @ I2C 0x20. Gates LCD_RST, TP_RST, audio amp enable, and reads the PWR button. **`io_expander_init()` MUST run before `gfx->begin()` or `ft3168_init()`** — otherwise display/touch stay in reset and silently fail. PWR button is on EXIO4, active HIGH (verified empirically with the deleted `iox` serial debug command). diff --git a/firmware/src/boards/waveshare_amoled_18/board.h b/firmware/src/boards/waveshare_amoled_18/board.h index bacb53e..6efe20e 100644 --- a/firmware/src/boards/waveshare_amoled_18/board.h +++ b/firmware/src/boards/waveshare_amoled_18/board.h @@ -26,9 +26,12 @@ #define IIC_SDA 15 #define IIC_SCL 14 -// ---- Touch (FT3168 via vendored minimal I2C reader) ---- +// ---- Touch (minimal inline I2C reader; chip varies by board revision) ---- +// Both controllers share the FocalTech-style data layout at regs 0x02..0x06, +// so only the I2C address differs. Revision is detected in board_init(). #define TP_INT 21 -#define FT3168_ADDR 0x38 +#define FT3168_ADDR 0x38 // original revision +#define CST816_ADDR 0x15 // later (CO5300) revision // ---- PMU ---- #define AXP2101_ADDR 0x34 diff --git a/firmware/src/boards/waveshare_amoled_18/board_init.cpp b/firmware/src/boards/waveshare_amoled_18/board_init.cpp index a435dff..f2a9724 100644 --- a/firmware/src/boards/waveshare_amoled_18/board_init.cpp +++ b/firmware/src/boards/waveshare_amoled_18/board_init.cpp @@ -1,11 +1,33 @@ #include "board.h" +#include "board_rev.h" #include "io_expander.h" #include #include // AMOLED-1.8 also needs the XCA9554 IO expander up first — the display // and touch controllers stay in reset until EXIO0..1 go HIGH. + +static BoardRev g_rev = REV_SH8601_FT3168; + +BoardRev board_rev(void) { return g_rev; } + +static bool i2c_present(uint8_t addr) { + Wire.beginTransmission(addr); + return Wire.endTransmission() == 0; +} + extern "C" void board_init(void) { Wire.begin(IIC_SDA, IIC_SCL); io_expander_init(); + delay(10); // let the touch controller exit reset before probing + + // Detect the panel revision by which touch controller answers. CST816 + // (0x15) ships on the CO5300 panel; FT3168 (0x38) on the original SH8601. + if (i2c_present(CST816_ADDR)) { + g_rev = REV_CO5300_CST816; + Serial.println("Board revision: CO5300 + CST816"); + } else { + g_rev = REV_SH8601_FT3168; + Serial.println("Board revision: SH8601 + FT3168"); + } } diff --git a/firmware/src/boards/waveshare_amoled_18/board_rev.h b/firmware/src/boards/waveshare_amoled_18/board_rev.h new file mode 100644 index 0000000..4337406 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_18/board_rev.h @@ -0,0 +1,18 @@ +#pragma once + +// Two shipping hardware revisions of the Waveshare 1.8" AMOLED differ only in +// their bonded panel module: +// - original: SH8601 display (QSPI) + FT3168 touch (I2C 0x38) +// - later: CO5300 display (QSPI) + CST816 touch (I2C 0x15) +// Everything else (PMU, IMU, IO expander, pinout) is identical. We detect the +// revision at boot by probing the touch controller's I2C address — board_init() +// sets it BEFORE display_hal_init()/touch_hal_init() so both pick the right +// driver. The touch chip's presence is a reliable proxy for the panel module. + +enum BoardRev { + REV_SH8601_FT3168 = 0, // original + REV_CO5300_CST816 = 1, // later revision +}; + +// Valid only after board_init() has run. +BoardRev board_rev(void); diff --git a/firmware/src/boards/waveshare_amoled_18/display.cpp b/firmware/src/boards/waveshare_amoled_18/display.cpp index 7dd3420..67e0c81 100644 --- a/firmware/src/boards/waveshare_amoled_18/display.cpp +++ b/firmware/src/boards/waveshare_amoled_18/display.cpp @@ -1,5 +1,6 @@ #include "../../hal/display_hal.h" #include "board.h" +#include "board_rev.h" #include "io_expander.h" #include #include @@ -8,17 +9,31 @@ // Display reset is routed through the XCA9554 IO expander (EXIO1) which // must be initialized + released before gfx->begin() runs — main.cpp // arranges this by calling display_hal_init() after io_expander_init(). +// +// Two panel revisions: original SH8601 and a later CO5300 module. Both are +// Arduino_OLED subclasses, so we hold the chosen one behind a base pointer. +// The CO5300's 368-wide active area starts partway into the controller GRAM, +// so it needs a column offset to center the image (the SH8601 needs none). + +#define CO5300_COL_OFFSET 16 // centers the 368-wide image (verified on panel) static Arduino_DataBus* bus = nullptr; -static Arduino_SH8601* gfx = nullptr; +static Arduino_OLED* gfx = nullptr; void display_hal_init(void) { bus = new Arduino_ESP32QSPI( LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3); - // SH8601 constructor: (bus, rst, rotation, w, h) - gfx = new Arduino_SH8601( - bus, GFX_NOT_DEFINED /* reset via XCA9554 */, 0, - LCD_WIDTH, LCD_HEIGHT); + if (board_rev() == REV_CO5300_CST816) { + // CO5300: (bus, rst, rotation, w, h, col_off1, row_off1, col_off2, row_off2) + gfx = new Arduino_CO5300( + bus, GFX_NOT_DEFINED /* reset via XCA9554 */, 0, + LCD_WIDTH, LCD_HEIGHT, CO5300_COL_OFFSET, 0, 0, 0); + } else { + // SH8601: (bus, rst, rotation, w, h) + gfx = new Arduino_SH8601( + bus, GFX_NOT_DEFINED /* reset via XCA9554 */, 0, + LCD_WIDTH, LCD_HEIGHT); + } } void display_hal_begin(void) { diff --git a/firmware/src/boards/waveshare_amoled_18/touch.cpp b/firmware/src/boards/waveshare_amoled_18/touch.cpp index 95d2ada..80a0a8b 100644 --- a/firmware/src/boards/waveshare_amoled_18/touch.cpp +++ b/firmware/src/boards/waveshare_amoled_18/touch.cpp @@ -1,10 +1,12 @@ #include "../../hal/touch_hal.h" #include "board.h" +#include "board_rev.h" #include #include -// Minimal FT3168 reader (FocalTech standard register layout). Avoids -// vendoring Waveshare's GPLv3 Arduino_DriveBus library. +// Minimal capacitive-touch reader. Both shipping panel revisions expose the +// FocalTech-style touch-data layout, so one read path serves both — only the +// I2C address differs (FT3168 @ 0x38 vs CST816 @ 0x15), chosen from board_rev(). // reg 0x02: low nibble = active finger count // reg 0x03 / 0x04: X1 high (low nibble) + X1 low // reg 0x05 / 0x06: Y1 high (low nibble) + Y1 low @@ -13,16 +15,17 @@ static volatile bool touch_data_ready = false; static volatile bool touch_pressed = false; static volatile uint16_t touch_x = 0; static volatile uint16_t touch_y = 0; +static uint8_t touch_addr = FT3168_ADDR; static void IRAM_ATTR touch_isr(void) { touch_data_ready = true; } -static void ft3168_read_into_shared_state(void) { - Wire.beginTransmission(FT3168_ADDR); +static void touch_read_into_shared_state(void) { + Wire.beginTransmission(touch_addr); Wire.write(0x02); if (Wire.endTransmission(false) != 0) { touch_pressed = false; return; } - if (Wire.requestFrom(FT3168_ADDR, (uint8_t)5) != 5) { touch_pressed = false; return; } + if (Wire.requestFrom(touch_addr, (uint8_t)5) != 5) { touch_pressed = false; return; } uint8_t fingers = Wire.read() & 0x0F; uint8_t xH = Wire.read(); uint8_t xL = Wire.read(); @@ -38,31 +41,38 @@ static void ft3168_read_into_shared_state(void) { } void touch_hal_init(void) { - // Power-mode register 0xA5 = 0x00: active scanning. - Wire.beginTransmission(FT3168_ADDR); - Wire.write(0xA5); - Wire.write(0x00); - Wire.endTransmission(); + bool is_cst816 = (board_rev() == REV_CO5300_CST816); + touch_addr = is_cst816 ? CST816_ADDR : FT3168_ADDR; - // Verify device ID register 0xA0 (FT3168 reports 0x03 but Waveshare's - // panel sometimes returns 0x86 — log but don't fail). - Wire.beginTransmission(FT3168_ADDR); - Wire.write(0xA0); - if (Wire.endTransmission(false) == 0 && Wire.requestFrom(FT3168_ADDR, (uint8_t)1) == 1) { - Serial.printf("FT3168 ID=0x%02X\n", Wire.read()); + if (!is_cst816) { + // FT3168 power-mode register 0xA5 = 0x00: active scanning. + // (CST816 reports by default; no equivalent setup needed.) + Wire.beginTransmission(touch_addr); + Wire.write(0xA5); + Wire.write(0x00); + Wire.endTransmission(); + } + + // Verify the controller answers. FT3168 chip-id is reg 0xA0; CST816 reg 0xA7. + uint8_t id_reg = is_cst816 ? 0xA7 : 0xA0; + Wire.beginTransmission(touch_addr); + Wire.write(id_reg); + if (Wire.endTransmission(false) == 0 && Wire.requestFrom(touch_addr, (uint8_t)1) == 1) { + Serial.printf("Touch %s ID=0x%02X (addr 0x%02X)\n", + is_cst816 ? "CST816" : "FT3168", Wire.read(), touch_addr); } else { - Serial.println("FT3168 ID read failed"); + Serial.printf("Touch ID read failed (addr 0x%02X)\n", touch_addr); } pinMode(TP_INT, INPUT_PULLUP); attachInterrupt(TP_INT, touch_isr, FALLING); - Serial.println("FT3168 attached on INT pin"); + Serial.println("Touch attached on INT pin"); } void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) { if (touch_data_ready) { touch_data_ready = false; - ft3168_read_into_shared_state(); + touch_read_into_shared_state(); } *x = touch_x; *y = touch_y;