Files
mattandClaude Opus 5 d2e4c974dd Implement rotary thermostat firmware from PRD
ESP-IDF v5.3.2 firmware for the CrowPanel 2.1" rotary display. Builds clean;
nothing has been run on hardware yet.

Hardware verified against Elecrow's own source for this board and
cross-checked against their ESPHome config, per PRD 8.1. Three corrections to
the PRD's pinout, all documented in docs/HARDWARE.md:

  - touch is CST826 at 0x15, not CST816 (chip ID reads 0x11, not 0xB4)
  - the ST7701S needs a 3-wire SPI bus on GPIO 16/2/1 that the PRD omitted;
    without it the init sequence cannot be sent and the panel stays black
  - the backlight is GPIO6

Two further departures from the PRD, both forced by facts on the ground:

  - Transport is MQTT, not HTTP. zigbee2mqtt has no REST endpoint for live
    sensor state. Subscribing to the broker also gets retained topics, so
    every room's last reading arrives on connect rather than after a poll.
  - The furnace is 2-wire R/W with no C, not 3-wire. The board needs its own
    5V supply. The thermostat's 5 CPH setting is enforced as a minimum
    12-minute period between heat-call starts, which a minimum off-time alone
    does not achieve.

All of PRD section 5 is implemented in the relay layer itself: fail-open,
watchdog-gated output, max on-time, min off-time, cycle-rate limit, startup
lockout, and the stale-data fallback ladder down to open-loop freeze
protection. The one gap that cannot be closed in software — the PCF8574 not
resetting when the ESP32 does — is documented rather than papered over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:47:00 -07:00

222 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hardware reference — CrowPanel 2.1" HMI Rotary Display
Everything below was taken from Elecrow's own source for **this** board and
cross-checked against a second independent source, as PRD section 8.1 requires.
Nothing here is from wiki prose or a search summary.
**Sources**
1. `Elecrow-RD/CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen`
— `example/RotaryScreen_2_1/RotaryScreen_2_1.ino`, the vendor's shipping
Arduino example, plus the bundled `GFX_Library_for_Arduino`.
2. The same repo's `example/esphome/esphome2.1.yaml` — an ESP-IDF-framework
config for the same board, written independently of the Arduino example.
The two agree on every pin. Where they disagree on anything else, it is called
out below.
**Not yet verified on hardware.** The board was not connected when this was
written. Section "First bring-up" at the end is the order to check things in.
---
## Corrections to the PRD
The PRD's section 2.2 was close but incomplete in three ways that matter:
| PRD said | Actually | Why it matters |
|---|---|---|
| Touch is **CST816** | Touch is **CST826**, I2C `0x15` | Different chip ID register value (`0xAA` reads `0x11`, not `0xB4`). A CST816 driver's probe fails and the PRD's own phase-1 sanity check would look like a hardware fault. |
| `CS 16` listed alone | The panel needs a **3-wire SPI** bus: **CS 16, SCK 2, MOSI/SDA 1** | The ~30-command ST7701S init sequence goes over this bus. Without SCK and MOSI there is no way to send it, and the panel stays black — exactly the failure mode section 8.1 warns about. |
| No backlight mentioned | **Backlight is GPIO6**, PWM | Panel can be fully initialised and still appear dead with the backlight off. |
Two smaller additions: there is a **breathing LED on GPIO43**, and expander
**P3 ("LCD power") is driven HIGH during init and LOW at the end** of the
vendor's setup — see the note under PCF8574 below.
The PRD's claim that expander **P1, P6 and P7 are free is correct** — the
vendor example configures P0, P2, P3, P4, P5 and never touches the other
three. P1 is what this project uses for the relay.
---
## Pinout
### RGB panel (matches the PRD exactly)
| Signal | GPIO |
|---|---|
| DE | 40 |
| HSYNC | 15 |
| VSYNC | 7 |
| PCLK | 41 |
| R0–R4 | 46, 3, 8, 18, 17 |
| G0–G5 | 14, 13, 12, 11, 10, 9 |
| B0–B4 | 5, 45, 48, 47, 21 |
### Panel control SPI (missing from the PRD)
| Signal | GPIO |
|---|---|
| CS | 16 |
| SCK | 2 |
| SDA (MOSI) | 1 |
9-bit writes, MSB first, **SPI mode 0**. Arduino_GFX's `Arduino_SWSPI::begin()`
drives SCK low at init and shifts on the rising edge, which is mode 0.
Elecrow's ESPHome config says `spi_mode: MODE3` — that config also carries the
comment that its colours are wrong, so mode 0 is the one to trust. If the panel
is black, `ST7701_SPI_MODE` in `drivers/display/display.c` is a one-line change.
### Other
| Signal | GPIO |
|---|---|
| I2C SDA / SCL | 38 / 39 |
| Backlight (LEDC PWM) | 6 |
| Encoder A / B | 42 / 4 |
| Breathing LED | 43 |
PCLK **18 MHz**, and `pclk_inverted` (negative edge) per the ESPHome config.
### Panel timings
The two sources disagree, which is normal — these have slack.
| | hsync fp / pw / bp | vsync fp / pw / bp |
|---|---|---|
| Arduino example (**used here**) | 10 / 4 / 20 | 10 / 4 / 20 |
| ESPHome example | 20 / 10 / 10 | 8 / 10 / 10 |
The Arduino numbers are from the vendor's shipping firmware, so those are the
defaults in `display.c`. Either set should work; swap if the image rolls or
shears.
---
## I2C bus (SDA 38 / SCL 39, 400 kHz)
| Address | Device |
|---|---|
| `0x21` | PCF8574 I/O expander |
| `0x15` | CST826 capacitive touch |
Finding both in the boot scan proves the board is alive and the I2C pins are
right, independently of the display. That is the cheapest first check.
### PCF8574 allocation
| Pin | Use | Direction |
|---|---|---|
| P0 | Touch reset | out |
| P1 | **Relay — heat call** (this project) | out, active low |
| P2 | Touch interrupt | out (driven high by vendor init) |
| P3 | LCD power | out |
| P4 | LCD reset | out |
| P5 | Encoder button | in, pull-up |
| P6 | free | — |
| P7 | free | — |
Two things about this part that are easy to get wrong:
**Power-on state is all-high.** A PCF8574's quasi-bidirectional pins come up
weakly pulled high. With an active-low relay module that means the relay is
**off** at power-on, which is what we want. The driver keeps a shadow byte
initialised to `0xFF` so a read-modify-write on any other pin can never
accidentally pull P1 low.
**The expander does not reset when the ESP32 does.** A software reset, panic
reboot or `esp_restart()` leaves the PCF8574's output latch exactly as it was.
If P1 was low (heat on) when the firmware crashed, the relay stays closed until
firmware comes back and clears it. `app_main()` therefore de-asserts P1 before
doing anything else — before WiFi, before the display — which bounds that
window to roughly a second. This is documented in `hvac/relay.h` too. A true
hardware fail-safe would need an external watchdog or an RC on the relay coil;
this firmware does not have one, and the honest statement is that a crash gives
you about one second of unwanted heat call, not zero.
**P3 is driven HIGH before the LCD reset pulse and LOW at the end of setup**
in the vendor example, after the backlight PWM is running. Despite the "LCD
power" label the sequence only makes sense as a blanking control — it hides
init garbage. `display.c` reproduces the vendor's ordering exactly rather than
reasoning about what the net is really called.
---
## ST7701S initialisation
`drivers/display/st7701_init.h` carries the sequence verbatim from
`st7701_type5_init_operations` in the bundled `Arduino_GFX`
(`display/Arduino_ST7701_RGBPanel.h`), which is what the vendor example passes
to the panel constructor.
The ESPHome config's `init_sequence` is the same sequence with three commands
missing from bank `0x10` — `0xC0 3B 00`, `0xC1 0B 02` (VBP) and `0xC2 00 02` —
and an extra leading `0x01` software reset. Since that same file admits its
colours are wrong, the Arduino/type5 version is the one reproduced here, with
the software reset added at the front because it is harmless and makes the
state deterministic.
Pixel format is set to **RGB666** (`0x3A 0x60`) by the sequence, with `0x36 0x00`.
The Arduino example additionally passes `bgr = true` to the panel constructor;
that is reflected here by setting the RGB panel's BGR flag. If red and blue are
swapped on first light, that flag is the thing to toggle.
---
## Furnace side
**Confirmed by the user, and it differs from the PRD:** the system is
**2-wire, R and W only**. Existing thermostat is set to Gas/Oil Warm Air,
**5 CPH**.
Consequences:
- **There is no C wire and no third wire to repurpose.** The PRD's section 2.4
discussion of moving `G` to `C` does not apply. The board must run from its
own **5V USB supply**. Do not attempt power stealing — this board needs ~5W
and power stealing supplies roughly a tenth of that.
- **5 CPH is a real constraint, not a display preference.** Five cycles per
hour means at most one heat-call *start* every 12 minutes. A minimum
off-time alone does not enforce that, because a short call followed by a
5-minute gap would give 8 CPH. `hvac/relay.c` enforces a minimum period
between successive heat-call starts (720 s) in addition to the minimum
off-time.
Wiring:
```
PCF8574 P1 ──► relay module IN (active low)
R (24VAC) ──► relay COM
W (heat) ◄── relay NO closes to call for heat
```
Opto-isolated relay module with a separate JD-VCC jumper, normally open.
The PCF8574 sinks ~25 mA but barely sources, which is why the module must be
active-low triggered.
**Never connect R to C** — dead short across the transformer, blows the fuse on
the furnace control board. On this 2-wire system there is no C at the
thermostat anyway.
---
## First bring-up
In this order, because each step's failure mode is unambiguous only if the
previous one passed:
1. **I2C scan.** Expect `0x21` and `0x15`. If the bus looks empty, stop and
re-check SDA 38 / SCL 39 before touching anything else.
2. **Backlight.** GPIO6 PWM up. Screen should glow even with no image.
3. **Panel.** Colour bars. If black: check SPI mode, then the init sequence,
then re-verify the pinout — in that order, and re-verify the pinout before
writing any new software hypothesis.
4. **Touch.** Chip ID register `0xAA` should read `0x11`.
5. **Encoder.** Detents count in both directions; button on P5 reads low when
pressed.
6. **Relay — on the bench, with an LED or a spare relay module, not the
furnace.** Exercise every interlock in PRD section 5 before the unit goes
anywhere near 24VAC.