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

8.6 KiB
Raw Permalink Blame History

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.