# Thermostat HMI ESP-IDF firmware for the **Elecrow CrowPanel 7" Advance** (ESP32-S3-WROOM-1-N16R8, 16MB flash + 8MB octal PSRAM, 800×480 IPS, GT911 capacitive touch). > **Board variant matters.** Elecrow sells a *Basic* CrowPanel 7" and an > *Advance* CrowPanel 7". They share a form factor and almost nothing else: > every RGB data pin, every sync pin, the pixel clock, the porches, the sync > polarities and the touch I2C pins are different, and the Advance drives its > backlight over I2C instead of from a GPIO. This firmware targets the > **Advance**. Building it against Basic documentation yields a black screen > and a silent I2C bus. Currently displays outdoor conditions from an Ambient Weather personal weather station. Thermostat control (setpoint, HVAC relays, scheduling) is not implemented yet. ## Components | Path | Purpose | |------|---------| | `main/drivers/display` | RGB parallel panel via `esp_lcd`, double-buffered in PSRAM | | `main/drivers/touch` | GT911 capacitive touch over I2C, registered as an LVGL input device | | `main/ui/lvgl_port.c` | LVGL tick source, service task, and locking | | `main/ui/ui_manager.c` | Screen layout and update entry points | | `main/wifi` | WiFi station bring-up | | `main/api` | Ambient Weather REST client | | `main/utils` | Small helpers | ## Setup ```bash cp main/app_config.h.example main/app_config.h # edit main/app_config.h: WiFi credentials + Ambient Weather keys ``` `main/app_config.h` is gitignored — it holds your WiFi password and API keys. ## Build & Flash ```bash idf.py set-target esp32s3 idf.py build flash monitor ``` Requires ESP-IDF **v5.3 or newer** (the code uses the `i2c_master` driver API and `dma_burst_size`). ## Hardware (Advance) ### RGB panel (16-bit RGB565) | Signal | GPIO | |--------|------| | DE | 42 | | VSYNC | 41 | | HSYNC | 40 | | PCLK | 39 | | R0–R4 | 7, 17, 18, 3, 46 | | G0–G5 | 9, 10, 11, 12, 13, 14 | | B0–B4 | 21, 47, 48, 45, 38 | Timings: 18 MHz pixel clock, HFP 8 / HPW 4 / HBP 8, VFP 8 / VPW 4 / VBP 8. Both syncs idle high; PCLK is `pclk_active_neg`. ### I2C bus — GPIO 15 (SDA) / 16 (SCL) | Address | Device | |---------|--------| | 0x30 | STC8H1K28 board controller — backlight **and GT911 reset** | | 0x51 | RTC | | 0x5D | GT911 touch | The GT911 has **no ESP32 reset or interrupt GPIO** on this board. The STC8H1K28 holds it in reset, so `backlight_init()` must run before `touch_init()` or the touch controller NACKs every address. STC8H1K28 protocol is single raw bytes, no register addressing: `0x19` wake, then `0x10` and `0x18` to configure and release the GT911. Brightness is `0x00`–`0xF5` **inverted** (`0x00` = full) on v1.3; v1.2 uses a narrower `0x05`–`0x10` range instead. ## Display architecture Two 800×480×2 framebuffers in PSRAM, LVGL in `RENDER_MODE_DIRECT`. LVGL draws into whichever buffer the LCD is *not* scanning; the last flush of a refresh hands that buffer to the panel, which switches to it. Nothing ever writes into the buffer being displayed — copying into the live framebuffer races the LCD's own reads, and the resulting corrupt pixels persist indefinitely because LVGL does not know they are wrong and never reinvalidates them. A 16-scanline bounce buffer stages each chunk in internal RAM. 20 scanlines would be better but needs 64KB of internal RAM and fails to allocate once WiFi is up. ### Known limitation: occasional left-edge artefacts Short stale lines sometimes appear at the left edge, most often right after a weather fetch. The panel needs ~34 MB/s sustained from PSRAM at the 18MHz pixel clock, and 80MHz octal PSRAM is marginal for that. When the bounce refill ISR runs late — WiFi interrupt activity is enough — the panel scans out whatever was left in the buffer. The pixel clock is not a usable lever: below roughly 40Hz refresh (about 15MHz) the panel stops locking and the image rolls vertically instead. The real fix is 120MHz PSRAM, which is what the vendor's Arduino build uses (`-DCONFIG_SPIRAM_SPEED_120M=1`). On ESP-IDF 5.3 that is not safely reachable: selecting it raises the shared MSPI core clock, and IDF only ships flash timing tables pairing 120MHz PSRAM with 120MHz flash. Flash above 80MHz requires High Performance Mode and a DC-aware bootloader. ## Gotchas - **LVGL does not bubble events to parents.** An event handler on the screen object never sees taps that land on a child widget, which looks exactly like dead touch zones. The touch readout polls the input device from an `lv_timer` instead. - **`lv_label_set_text()` invalidates unconditionally**, even when the text is identical — so setting an unchanged value still forces a repaint. All label updates go through `label_set_if_changed()`. - **WiFi must be brought up before `display_init()`.** The bounce refill ISR reads PSRAM, which shares the cache with flash; an NVS write while the panel is running faults it with *"Cache disabled but cached memory region accessed"*. ## Memory notes Two 800×480×2 framebuffers (750 KB each) live in PSRAM. The 8 MB **octal** PSRAM at 80 MHz is required — QSPI cannot sustain the RGB refresh. Do **not** enable `CONFIG_SPIRAM_FETCH_INSTRUCTIONS` / `CONFIG_SPIRAM_RODATA`: they contend with the panel for PSRAM bandwidth. The bounce buffer **is** enabled (the panel cannot stream straight from PSRAM), which is exactly why flash writes are dangerous while the panel runs: the refill ISR reads PSRAM, which shares the cache with flash on the S3, so an NVS write faults it with *"Cache disabled but cached memory region accessed"*. All flash writes happen before `display_init()` and `CONFIG_ESP_WIFI_NVS_ENABLED` is off to keep the WiFi stack from writing later.