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>
This commit is contained in:
1 parent
393592e327
commit
d2e4c974dd
35 files changed
+4685
No files matched your search
@@ -0,0 +1,326 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/ledc.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_io_additions.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "pcf8574.h"
|
||||
#include "st7701_init.h"
|
||||
|
||||
static const char *TAG = "display";
|
||||
|
||||
/* ---- Pinout: docs/HARDWARE.md, verified against Elecrow's own example ---- */
|
||||
|
||||
#define PIN_LCD_CS 16
|
||||
#define PIN_LCD_SCK 2
|
||||
#define PIN_LCD_SDA 1
|
||||
|
||||
#define PIN_LCD_DE 40
|
||||
#define PIN_LCD_HSYNC 15
|
||||
#define PIN_LCD_VSYNC 7
|
||||
#define PIN_LCD_PCLK 41
|
||||
|
||||
#define PIN_BACKLIGHT 6
|
||||
|
||||
/*
|
||||
* SPI mode 0. Arduino_GFX's Arduino_SWSPI::begin() drives SCK low at init and
|
||||
* shifts on the rising edge. Elecrow's ESPHome config says MODE3, but that
|
||||
* file also documents its own colours as wrong.
|
||||
*
|
||||
* If the panel is black on first bring-up, this is the first thing to try
|
||||
* changing — before touching the init sequence, and long before rewriting
|
||||
* anything else.
|
||||
*/
|
||||
#define ST7701_SPI_MODE 0
|
||||
|
||||
/*
|
||||
* Timings from the vendor's Arduino example. The ESPHome example uses
|
||||
* 20/10/10 and 8/10/10 instead; either set should work. Swap if the image
|
||||
* rolls or shears.
|
||||
*/
|
||||
#define LCD_PCLK_HZ (18 * 1000 * 1000)
|
||||
#define LCD_HSYNC_FRONT 10
|
||||
#define LCD_HSYNC_PULSE 4
|
||||
#define LCD_HSYNC_BACK 20
|
||||
#define LCD_VSYNC_FRONT 10
|
||||
#define LCD_VSYNC_PULSE 4
|
||||
#define LCD_VSYNC_BACK 20
|
||||
|
||||
#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_0
|
||||
#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0
|
||||
#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE
|
||||
#define BACKLIGHT_RES LEDC_TIMER_10_BIT
|
||||
|
||||
static esp_lcd_panel_handle_t s_panel;
|
||||
static void *s_fb0, *s_fb1;
|
||||
|
||||
/* ------------------------------------------------------------ backlight -- */
|
||||
|
||||
static esp_err_t backlight_init(void)
|
||||
{
|
||||
ledc_timer_config_t timer = {
|
||||
.speed_mode = BACKLIGHT_LEDC_MODE,
|
||||
.timer_num = BACKLIGHT_LEDC_TIMER,
|
||||
.duty_resolution = BACKLIGHT_RES,
|
||||
.freq_hz = 5000,
|
||||
.clk_cfg = LEDC_AUTO_CLK,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(ledc_timer_config(&timer), TAG, "ledc timer");
|
||||
|
||||
ledc_channel_config_t ch = {
|
||||
.gpio_num = PIN_BACKLIGHT,
|
||||
.speed_mode = BACKLIGHT_LEDC_MODE,
|
||||
.channel = BACKLIGHT_LEDC_CHANNEL,
|
||||
.timer_sel = BACKLIGHT_LEDC_TIMER,
|
||||
.duty = 0,
|
||||
.hpoint = 0,
|
||||
};
|
||||
return ledc_channel_config(&ch);
|
||||
}
|
||||
|
||||
void display_set_backlight(uint8_t percent)
|
||||
{
|
||||
if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
uint32_t max = (1u << BACKLIGHT_RES) - 1;
|
||||
uint32_t duty = (max * percent) / 100;
|
||||
|
||||
ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty);
|
||||
ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- reset dance --- */
|
||||
|
||||
/*
|
||||
* Reproduces the vendor example's setup() ordering exactly, including the P3
|
||||
* ("LCD power") pulse. Despite the label, P3 goes high before the reset and
|
||||
* low again once the backlight is running, which only makes sense as blanking
|
||||
* to hide init garbage. Copied rather than reasoned about — see
|
||||
* docs/HARDWARE.md.
|
||||
*
|
||||
* P1 is deliberately untouched throughout: it is the relay.
|
||||
*/
|
||||
static esp_err_t panel_power_and_reset(void)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_POWER, true), TAG, "p3");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, true), TAG, "p4 hi");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, false), TAG, "p4 lo");
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, true), TAG, "p4 hi2");
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- init ---- */
|
||||
|
||||
static esp_err_t send_init_sequence(void)
|
||||
{
|
||||
esp_lcd_panel_io_handle_t io = NULL;
|
||||
|
||||
esp_lcd_panel_io_3wire_spi_config_t io_cfg = {
|
||||
.line_config = {
|
||||
.cs_io_type = IO_TYPE_GPIO,
|
||||
.cs_gpio_num = PIN_LCD_CS,
|
||||
.scl_io_type = IO_TYPE_GPIO,
|
||||
.scl_gpio_num = PIN_LCD_SCK,
|
||||
.sda_io_type = IO_TYPE_GPIO,
|
||||
.sda_gpio_num = PIN_LCD_SDA,
|
||||
.io_expander = NULL,
|
||||
},
|
||||
.expect_clk_speed = PANEL_IO_3WIRE_SPI_CLK_MAX,
|
||||
.spi_mode = ST7701_SPI_MODE,
|
||||
.lcd_cmd_bytes = 1,
|
||||
.lcd_param_bytes = 1,
|
||||
.flags = {
|
||||
.use_dc_bit = 1, /* 9-bit frames: DC bit then 8 data bits */
|
||||
.dc_zero_on_data = 0, /* DC=0 command, DC=1 data (ST7701 order) */
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0,
|
||||
/*
|
||||
* Keep CS inactive after we delete this IO. The RGB bus takes over
|
||||
* the panel from here and must not see CS wander.
|
||||
*/
|
||||
.del_keep_cs_inactive = 1,
|
||||
},
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_3wire_spi(&io_cfg, &io),
|
||||
TAG, "3wire spi io");
|
||||
|
||||
esp_err_t err = ESP_OK;
|
||||
for (size_t i = 0; i < ST7701_INIT_SEQ_LEN; i++) {
|
||||
const st7701_cmd_t *c = &st7701_init_seq[i];
|
||||
|
||||
err = esp_lcd_panel_io_tx_param(io, c->cmd,
|
||||
c->data_len ? c->data : NULL,
|
||||
c->data_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "init cmd %u (0x%02X) failed: %s",
|
||||
(unsigned)i, c->cmd, esp_err_to_name(err));
|
||||
break;
|
||||
}
|
||||
if (c->delay_ms) {
|
||||
vTaskDelay(pdMS_TO_TICKS(c->delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
/* The init bus is single-use; the RGB peripheral owns the panel now. */
|
||||
esp_lcd_panel_io_del(io);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "ST7701S init sequence sent (%u commands)",
|
||||
(unsigned)ST7701_INIT_SEQ_LEN);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t rgb_panel_init(void)
|
||||
{
|
||||
esp_lcd_rgb_panel_config_t cfg = {
|
||||
.clk_src = LCD_CLK_SRC_PLL160M,
|
||||
.timings = {
|
||||
.pclk_hz = LCD_PCLK_HZ,
|
||||
.h_res = DISPLAY_H_RES,
|
||||
.v_res = DISPLAY_V_RES,
|
||||
.hsync_pulse_width = LCD_HSYNC_PULSE,
|
||||
.hsync_back_porch = LCD_HSYNC_BACK,
|
||||
.hsync_front_porch = LCD_HSYNC_FRONT,
|
||||
.vsync_pulse_width = LCD_VSYNC_PULSE,
|
||||
.vsync_back_porch = LCD_VSYNC_BACK,
|
||||
.vsync_front_porch = LCD_VSYNC_FRONT,
|
||||
.flags = {
|
||||
/* Elecrow's ESPHome config sets pclk_inverted, i.e. data is
|
||||
* latched on the falling edge. */
|
||||
.pclk_active_neg = 1,
|
||||
},
|
||||
},
|
||||
.data_width = 16,
|
||||
.bits_per_pixel = 16,
|
||||
.num_fbs = 2,
|
||||
/*
|
||||
* No bounce buffers — this is what makes runtime NVS writes safe.
|
||||
* See the header comment. Do not "optimise" this to a non-zero value
|
||||
* without re-reading PRD 8.2.
|
||||
*/
|
||||
.bounce_buffer_size_px = 0,
|
||||
.psram_trans_align = 64,
|
||||
.hsync_gpio_num = PIN_LCD_HSYNC,
|
||||
.vsync_gpio_num = PIN_LCD_VSYNC,
|
||||
.de_gpio_num = PIN_LCD_DE,
|
||||
.pclk_gpio_num = PIN_LCD_PCLK,
|
||||
.disp_gpio_num = -1,
|
||||
/*
|
||||
* RGB565 bit order: data[0..4] = blue, [5..10] = green, [11..15] = red.
|
||||
* If red and blue come out swapped, swap these two blocks — the panel's
|
||||
* own MADCTL is already 0x00 (RGB) from the init sequence.
|
||||
*/
|
||||
.data_gpio_nums = {
|
||||
5, 45, 48, 47, 21, /* B0..B4 */
|
||||
14, 13, 12, 11, 10, 9, /* G0..G5 */
|
||||
46, 3, 8, 18, 17, /* R0..R4 */
|
||||
},
|
||||
.flags = {
|
||||
.fb_in_psram = 1,
|
||||
},
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&cfg, &s_panel), TAG, "rgb panel");
|
||||
ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "panel reset");
|
||||
ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel init");
|
||||
|
||||
ESP_RETURN_ON_ERROR(
|
||||
esp_lcd_rgb_panel_get_frame_buffer(s_panel, 2, &s_fb0, &s_fb1),
|
||||
TAG, "get framebuffers");
|
||||
|
||||
ESP_LOGI(TAG, "RGB panel up: %dx%d @ %d MHz, fb0=%p fb1=%p",
|
||||
DISPLAY_H_RES, DISPLAY_V_RES, LCD_PCLK_HZ / 1000000, s_fb0, s_fb1);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_init(void)
|
||||
{
|
||||
if (!pcf8574_is_ready()) {
|
||||
ESP_LOGE(TAG, "expander not ready — cannot sequence panel power");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(panel_power_and_reset(), TAG, "power/reset");
|
||||
ESP_RETURN_ON_ERROR(send_init_sequence(), TAG, "init sequence");
|
||||
ESP_RETURN_ON_ERROR(rgb_panel_init(), TAG, "rgb");
|
||||
ESP_RETURN_ON_ERROR(backlight_init(), TAG, "backlight");
|
||||
|
||||
/* Clear both buffers before the backlight comes up, so the first thing on
|
||||
* screen is black rather than whatever was in PSRAM. */
|
||||
size_t fb_bytes = (size_t)DISPLAY_H_RES * DISPLAY_V_RES * 2;
|
||||
memset(s_fb0, 0, fb_bytes);
|
||||
memset(s_fb1, 0, fb_bytes);
|
||||
|
||||
display_set_backlight(80);
|
||||
|
||||
/* Vendor ordering: P3 low once the backlight is running. */
|
||||
pcf8574_write(PCF_PIN_LCD_POWER, false);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_lcd_panel_handle_t display_panel(void)
|
||||
{
|
||||
return s_panel;
|
||||
}
|
||||
|
||||
esp_err_t display_get_framebuffers(void **fb0, void **fb1)
|
||||
{
|
||||
if (!s_panel) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (fb0) {
|
||||
*fb0 = s_fb0;
|
||||
}
|
||||
if (fb1) {
|
||||
*fb1 = s_fb1;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void display_test_pattern(void)
|
||||
{
|
||||
static const uint16_t bars[] = {
|
||||
0xF800, /* red */
|
||||
0x07E0, /* green */
|
||||
0x001F, /* blue */
|
||||
0xFFFF, /* white */
|
||||
0xFFE0, /* yellow */
|
||||
0x07FF, /* cyan */
|
||||
0xF81F, /* magenta*/
|
||||
0x0000, /* black */
|
||||
};
|
||||
const int n = sizeof(bars) / sizeof(bars[0]);
|
||||
const int bar_w = DISPLAY_H_RES / n;
|
||||
|
||||
uint16_t *fb = (uint16_t *)s_fb0;
|
||||
if (!fb) {
|
||||
ESP_LOGE(TAG, "no framebuffer");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < DISPLAY_V_RES; y++) {
|
||||
for (int x = 0; x < DISPLAY_H_RES; x++) {
|
||||
int i = x / bar_w;
|
||||
fb[y * DISPLAY_H_RES + x] = bars[i < n ? i : n - 1];
|
||||
}
|
||||
}
|
||||
|
||||
esp_lcd_panel_draw_bitmap(s_panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, fb);
|
||||
ESP_LOGI(TAG, "colour bars drawn — this is the PRD phase-2 check");
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* ST7701S + RGB panel, 480x480 round.
|
||||
*
|
||||
* Two things about the configuration here are deliberate and load-bearing:
|
||||
*
|
||||
* No bounce buffers. The bounce-buffer refill ISR reads PSRAM, which shares
|
||||
* the cache with flash on the S3, so any NVS write while the panel runs faults
|
||||
* with "Cache disabled but cached memory region accessed" (PRD 8.2). This
|
||||
* project writes settings to NVS at runtime, so instead of pausing the panel
|
||||
* around every write we simply do not use bounce buffers: the LCD DMA reads
|
||||
* the framebuffer out of PSRAM directly and never touches the cache.
|
||||
*
|
||||
* Two framebuffers. Writing into the framebuffer currently being scanned
|
||||
* races the LCD's reads, and the corruption persists because LVGL does not
|
||||
* know those pixels are wrong and never reinvalidates them (PRD 8.2). With
|
||||
* num_fbs=2 the panel driver swaps whole buffers instead.
|
||||
*
|
||||
* 480x480x2 bytes x 2 buffers is ~920KB of the 8MB PSRAM.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
|
||||
#define DISPLAY_H_RES 480
|
||||
#define DISPLAY_V_RES 480
|
||||
|
||||
/*
|
||||
* Bring up the panel: expander power/reset sequencing, 3-wire SPI init,
|
||||
* RGB bus, backlight.
|
||||
*
|
||||
* Requires i2c_bus_init() and pcf8574_init() to have run. PRD 8.2 also wants
|
||||
* WiFi up before this point.
|
||||
*/
|
||||
esp_err_t display_init(void);
|
||||
|
||||
/* The esp_lcd panel handle, for the LVGL port. */
|
||||
esp_lcd_panel_handle_t display_panel(void);
|
||||
|
||||
/* Both framebuffers, for LVGL to render into directly. */
|
||||
esp_err_t display_get_framebuffers(void **fb0, void **fb1);
|
||||
|
||||
/* Backlight, 0-100. */
|
||||
void display_set_backlight(uint8_t percent);
|
||||
|
||||
/*
|
||||
* Fill the screen with vertical colour bars, bypassing LVGL.
|
||||
*
|
||||
* This is the PRD phase-2 check: it proves the init sequence, the RGB bus and
|
||||
* the timings independently of anything LVGL does.
|
||||
*/
|
||||
void display_test_pattern(void);
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* ST7701S initialisation sequence for the CrowPanel 2.1" round panel.
|
||||
*
|
||||
* Transcribed verbatim from `st7701_type5_init_operations` in the
|
||||
* Arduino_GFX bundled with Elecrow's own example for this board
|
||||
* (example/libraries/GFX_Library_for_Arduino/src/display/Arduino_ST7701_RGBPanel.h),
|
||||
* which is what example/RotaryScreen_2_1/RotaryScreen_2_1.ino passes to the
|
||||
* panel constructor.
|
||||
*
|
||||
* PRD 8.1: one wrong byte here gives garbage or nothing, with no useful
|
||||
* diagnostic. Do not "tidy" these values. Do not reorder them. The bank
|
||||
* selects (0xFF ...) change the meaning of every register that follows, so a
|
||||
* command moved across one is a different command.
|
||||
*
|
||||
* Cross-checked against example/esphome/esphome2.1.yaml, which carries the
|
||||
* same sequence minus three commands in bank 0x10 (0xC0, 0xC1, 0xC2) — and
|
||||
* which admits in its own comments that its colours come out wrong. The three
|
||||
* extra commands are kept.
|
||||
*
|
||||
* The leading software reset (0x01) is from the ESPHome version. It is not in
|
||||
* the Arduino one, but it costs 5ms and makes the starting state deterministic
|
||||
* rather than "whatever the previous firmware left behind".
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t cmd;
|
||||
uint8_t data[16];
|
||||
uint8_t data_len;
|
||||
uint16_t delay_ms;
|
||||
} st7701_cmd_t;
|
||||
|
||||
static const st7701_cmd_t st7701_init_seq[] = {
|
||||
/* Software reset. */
|
||||
{0x01, {0}, 0, 5},
|
||||
|
||||
/* ---- Bank 0x10 ---- */
|
||||
{0xFF, {0x77, 0x01, 0x00, 0x00, 0x10}, 5, 0},
|
||||
|
||||
{0xC0, {0x3B, 0x00}, 2, 0},
|
||||
{0xC1, {0x0B, 0x02}, 2, 0}, /* VBP */
|
||||
{0xC2, {0x00, 0x02}, 2, 0},
|
||||
|
||||
{0xCC, {0x10}, 1, 0},
|
||||
{0xCD, {0x08}, 1, 0},
|
||||
|
||||
/*
|
||||
* Not part of st7701_type5_init_operations, but Arduino_GFX's
|
||||
* begin() calls setRotation(0) straight afterwards, and that path sends
|
||||
* 0xC7 0x00 here (Y direction) before dropping back to bank 0x00 to write
|
||||
* MADCTL. The vendor example therefore does send it, so it belongs in the
|
||||
* sequence.
|
||||
*
|
||||
* That same setRotation(0) call is where the constructor's `bgr = true`
|
||||
* argument lands: with bgr set it writes MADCTL 0x00, which is what this
|
||||
* sequence already writes below. So bgr=true is a no-op at rotation 0 and
|
||||
* the RGB panel does not need its BGR flag set.
|
||||
*/
|
||||
{0xC7, {0x00}, 1, 0},
|
||||
|
||||
/* Positive voltage gamma control. */
|
||||
{0xB0, {0x02, 0x13, 0x1B, 0x0D,
|
||||
0x10, 0x05, 0x08, 0x07,
|
||||
0x07, 0x24, 0x04, 0x11,
|
||||
0x0E, 0x2C, 0x33, 0x1D}, 16, 0},
|
||||
|
||||
/* Negative voltage gamma control. */
|
||||
{0xB1, {0x05, 0x13, 0x1B, 0x0D,
|
||||
0x11, 0x05, 0x08, 0x07,
|
||||
0x07, 0x24, 0x04, 0x11,
|
||||
0x0E, 0x2C, 0x33, 0x1D}, 16, 0},
|
||||
|
||||
/* ---- Bank 0x11 ---- */
|
||||
{0xFF, {0x77, 0x01, 0x00, 0x00, 0x11}, 5, 0},
|
||||
|
||||
{0xB0, {0x5D}, 1, 0},
|
||||
{0xB1, {0x43}, 1, 0}, /* VCOM amplitude */
|
||||
{0xB2, {0x81}, 1, 0}, /* VGH, 12V */
|
||||
{0xB3, {0x80}, 1, 0},
|
||||
|
||||
{0xB5, {0x43}, 1, 0}, /* VGL, -8.3V */
|
||||
|
||||
{0xB7, {0x85}, 1, 0},
|
||||
{0xB8, {0x20}, 1, 0},
|
||||
|
||||
{0xC1, {0x78}, 1, 0},
|
||||
{0xC2, {0x78}, 1, 0},
|
||||
|
||||
{0xD0, {0x88}, 1, 0},
|
||||
|
||||
{0xE0, {0x00, 0x00, 0x02}, 3, 0},
|
||||
|
||||
{0xE1, {0x03, 0xA0, 0x00, 0x00,
|
||||
0x04, 0xA0, 0x00, 0x00,
|
||||
0x00, 0x20, 0x20}, 11, 0},
|
||||
|
||||
{0xE2, {0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00}, 13, 0},
|
||||
|
||||
{0xE3, {0x00, 0x00, 0x11, 0x00}, 4, 0},
|
||||
|
||||
{0xE4, {0x22, 0x00}, 2, 0},
|
||||
|
||||
{0xE5, {0x05, 0xEC, 0xA0, 0xA0,
|
||||
0x07, 0xEE, 0xA0, 0xA0,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}, 16, 0},
|
||||
|
||||
{0xE6, {0x00, 0x00, 0x11, 0x00}, 4, 0},
|
||||
|
||||
{0xE7, {0x22, 0x00}, 2, 0},
|
||||
|
||||
{0xE8, {0x06, 0xED, 0xA0, 0xA0,
|
||||
0x08, 0xEF, 0xA0, 0xA0,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}, 16, 0},
|
||||
|
||||
{0xEB, {0x00, 0x00, 0x40, 0x40,
|
||||
0x00, 0x00, 0x00}, 7, 0},
|
||||
|
||||
{0xED, {0xFF, 0xFF, 0xFF, 0xBA,
|
||||
0x0A, 0xBF, 0x45, 0xFF,
|
||||
0xFF, 0x54, 0xFB, 0xA0,
|
||||
0xAB, 0xFF, 0xFF, 0xFF}, 16, 0},
|
||||
|
||||
{0xEF, {0x10, 0x0D, 0x04, 0x08,
|
||||
0x3F, 0x1F}, 6, 0},
|
||||
|
||||
/* ---- Bank 0x13 ---- */
|
||||
{0xFF, {0x77, 0x01, 0x00, 0x00, 0x13}, 5, 0},
|
||||
{0xEF, {0x08}, 1, 0},
|
||||
|
||||
/* ---- Back to the command bank ---- */
|
||||
{0xFF, {0x77, 0x01, 0x00, 0x00, 0x00}, 5, 0},
|
||||
|
||||
{0x36, {0x00}, 1, 0}, /* MADCTL */
|
||||
{0x3A, {0x60}, 1, 0}, /* 0x70=RGB888, 0x60=RGB666, 0x50=RGB565 */
|
||||
|
||||
{0x11, {0}, 0, 120}, /* sleep out */
|
||||
{0x29, {0}, 0, 50}, /* display on */
|
||||
};
|
||||
|
||||
#define ST7701_INIT_SEQ_LEN \
|
||||
(sizeof(st7701_init_seq) / sizeof(st7701_init_seq[0]))
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "encoder.h"
|
||||
|
||||
#include "driver/pulse_cnt.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "pcf8574.h"
|
||||
|
||||
static const char *TAG = "encoder";
|
||||
|
||||
/*
|
||||
* Full quadrature decode counts 4 edges per mechanical detent on an EC28A.
|
||||
* PCNT counts in raw edges; we divide.
|
||||
*/
|
||||
#define COUNTS_PER_DETENT 4
|
||||
|
||||
#define PCNT_HIGH_LIMIT 1000
|
||||
#define PCNT_LOW_LIMIT (-1000)
|
||||
#define GLITCH_FILTER_NS 1000
|
||||
|
||||
#define BUTTON_POLL_MS 20
|
||||
#define BUTTON_DEBOUNCE_N 3
|
||||
#define LONG_PRESS_MS 800
|
||||
|
||||
static pcnt_unit_handle_t s_unit;
|
||||
static int s_residual; /* raw counts not yet worth a detent */
|
||||
|
||||
static volatile encoder_button_event_t s_btn_event;
|
||||
static volatile bool s_btn_down;
|
||||
|
||||
static void button_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
int stable = 0;
|
||||
bool debounced = false; /* true = pressed */
|
||||
int64_t pressed_at = 0;
|
||||
bool long_fired = false;
|
||||
|
||||
while (1) {
|
||||
bool level = true;
|
||||
/* P5 is INPUT_PULLUP: low means pressed. */
|
||||
bool pressed = (pcf8574_read(PCF_PIN_ENCODER_SW, &level) == ESP_OK)
|
||||
&& !level;
|
||||
|
||||
if (pressed == debounced) {
|
||||
stable = 0;
|
||||
} else if (++stable >= BUTTON_DEBOUNCE_N) {
|
||||
stable = 0;
|
||||
debounced = pressed;
|
||||
s_btn_down = pressed;
|
||||
|
||||
if (pressed) {
|
||||
pressed_at = esp_timer_get_time();
|
||||
long_fired = false;
|
||||
} else if (!long_fired) {
|
||||
/* Click reported on release, so a long press never also
|
||||
* produces a click. */
|
||||
s_btn_event = ENCODER_BTN_CLICK;
|
||||
}
|
||||
}
|
||||
|
||||
if (debounced && !long_fired &&
|
||||
(esp_timer_get_time() - pressed_at) > (LONG_PRESS_MS * 1000)) {
|
||||
long_fired = true;
|
||||
s_btn_event = ENCODER_BTN_LONG_PRESS;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BUTTON_POLL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t encoder_init(void)
|
||||
{
|
||||
pcnt_unit_config_t unit_cfg = {
|
||||
.high_limit = PCNT_HIGH_LIMIT,
|
||||
.low_limit = PCNT_LOW_LIMIT,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(pcnt_new_unit(&unit_cfg, &s_unit), TAG, "pcnt unit");
|
||||
|
||||
pcnt_glitch_filter_config_t filter = {
|
||||
.max_glitch_ns = GLITCH_FILTER_NS,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(pcnt_unit_set_glitch_filter(s_unit, &filter),
|
||||
TAG, "glitch filter");
|
||||
|
||||
/* Two channels, each watching one edge and using the other line as the
|
||||
* direction control — the standard full-quadrature setup. */
|
||||
pcnt_chan_config_t chan_a_cfg = {
|
||||
.edge_gpio_num = ENCODER_PIN_A,
|
||||
.level_gpio_num = ENCODER_PIN_B,
|
||||
};
|
||||
pcnt_channel_handle_t chan_a = NULL;
|
||||
ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_a_cfg, &chan_a),
|
||||
TAG, "chan a");
|
||||
|
||||
pcnt_chan_config_t chan_b_cfg = {
|
||||
.edge_gpio_num = ENCODER_PIN_B,
|
||||
.level_gpio_num = ENCODER_PIN_A,
|
||||
};
|
||||
pcnt_channel_handle_t chan_b = NULL;
|
||||
ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_b_cfg, &chan_b),
|
||||
TAG, "chan b");
|
||||
|
||||
ESP_RETURN_ON_ERROR(
|
||||
pcnt_channel_set_edge_action(chan_a,
|
||||
PCNT_CHANNEL_EDGE_ACTION_DECREASE,
|
||||
PCNT_CHANNEL_EDGE_ACTION_INCREASE),
|
||||
TAG, "chan a edge");
|
||||
ESP_RETURN_ON_ERROR(
|
||||
pcnt_channel_set_level_action(chan_a,
|
||||
PCNT_CHANNEL_LEVEL_ACTION_KEEP,
|
||||
PCNT_CHANNEL_LEVEL_ACTION_INVERSE),
|
||||
TAG, "chan a level");
|
||||
ESP_RETURN_ON_ERROR(
|
||||
pcnt_channel_set_edge_action(chan_b,
|
||||
PCNT_CHANNEL_EDGE_ACTION_INCREASE,
|
||||
PCNT_CHANNEL_EDGE_ACTION_DECREASE),
|
||||
TAG, "chan b edge");
|
||||
ESP_RETURN_ON_ERROR(
|
||||
pcnt_channel_set_level_action(chan_b,
|
||||
PCNT_CHANNEL_LEVEL_ACTION_KEEP,
|
||||
PCNT_CHANNEL_LEVEL_ACTION_INVERSE),
|
||||
TAG, "chan b level");
|
||||
|
||||
ESP_RETURN_ON_ERROR(pcnt_unit_enable(s_unit), TAG, "enable");
|
||||
ESP_RETURN_ON_ERROR(pcnt_unit_clear_count(s_unit), TAG, "clear");
|
||||
ESP_RETURN_ON_ERROR(pcnt_unit_start(s_unit), TAG, "start");
|
||||
|
||||
if (xTaskCreate(button_task, "enc_btn", 3072, NULL, 4, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "button task create failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ready: A=%d B=%d, button on expander P%d",
|
||||
ENCODER_PIN_A, ENCODER_PIN_B, PCF_PIN_ENCODER_SW);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int encoder_take_delta(void)
|
||||
{
|
||||
if (!s_unit) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int raw = 0;
|
||||
if (pcnt_unit_get_count(s_unit, &raw) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
pcnt_unit_clear_count(s_unit);
|
||||
|
||||
/*
|
||||
* Carry the remainder rather than discarding it. Dropping it would lose a
|
||||
* detent whenever a turn straddles two polls, which shows up as a knob
|
||||
* that occasionally ignores you.
|
||||
*/
|
||||
s_residual += raw;
|
||||
int detents = s_residual / COUNTS_PER_DETENT;
|
||||
s_residual -= detents * COUNTS_PER_DETENT;
|
||||
|
||||
return detents;
|
||||
}
|
||||
|
||||
encoder_button_event_t encoder_take_button_event(void)
|
||||
{
|
||||
encoder_button_event_t e = s_btn_event;
|
||||
s_btn_event = ENCODER_BTN_NONE;
|
||||
return e;
|
||||
}
|
||||
|
||||
bool encoder_button_is_down(void)
|
||||
{
|
||||
return s_btn_down;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Rotary encoder: A = GPIO42, B = GPIO4, button on expander P5.
|
||||
*
|
||||
* Quadrature decoding uses the PCNT peripheral with a glitch filter rather
|
||||
* than a polling task. The vendor example polls A in a task and infers
|
||||
* direction from B, which drops detents when the UI is busy — not acceptable
|
||||
* for a knob that sets a heating setpoint.
|
||||
*
|
||||
* The button is on the I2C expander, so it has to be polled. 20ms with a
|
||||
* 3-sample debounce is well inside human reaction time and costs one short
|
||||
* I2C transaction per poll.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define ENCODER_PIN_A 42
|
||||
#define ENCODER_PIN_B 4
|
||||
|
||||
typedef enum {
|
||||
ENCODER_BTN_NONE = 0,
|
||||
ENCODER_BTN_CLICK,
|
||||
ENCODER_BTN_LONG_PRESS,
|
||||
} encoder_button_event_t;
|
||||
|
||||
esp_err_t encoder_init(void);
|
||||
|
||||
/*
|
||||
* Detents accumulated since the last call, positive clockwise. Reading
|
||||
* clears the accumulator, so exactly one consumer should call this — the UI.
|
||||
*/
|
||||
int encoder_take_delta(void);
|
||||
|
||||
/*
|
||||
* Pending button event, cleared by reading. Long press is 800ms; the click is
|
||||
* reported on release so the two never both fire.
|
||||
*/
|
||||
encoder_button_event_t encoder_take_button_event(void);
|
||||
|
||||
/* Instantaneous button state, for diagnostics. */
|
||||
bool encoder_button_is_down(void);
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "pcf8574.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
static const char *TAG = "pcf8574";
|
||||
|
||||
static i2c_master_dev_handle_t s_dev;
|
||||
static SemaphoreHandle_t s_lock;
|
||||
static bool s_ready;
|
||||
|
||||
/*
|
||||
* Power-on state: all pins released high. Critically this is also
|
||||
* relay-off for the active-low module on P1, so the shadow is safe from the
|
||||
* first instruction, before init has even run.
|
||||
*/
|
||||
static uint8_t s_shadow = 0xFF;
|
||||
|
||||
#define LOCK() xSemaphoreTakeRecursive(s_lock, portMAX_DELAY)
|
||||
#define UNLOCK() xSemaphoreGiveRecursive(s_lock)
|
||||
|
||||
static esp_err_t flush_locked(void)
|
||||
{
|
||||
return i2c_master_transmit(s_dev, &s_shadow, 1, 100);
|
||||
}
|
||||
|
||||
esp_err_t pcf8574_init(void)
|
||||
{
|
||||
if (s_ready) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!s_lock) {
|
||||
s_lock = xSemaphoreCreateRecursiveMutex();
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = i2c_bus_add_device(PCF8574_I2C_ADDR, &s_dev);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "add device 0x%02X failed: %s",
|
||||
PCF8574_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Push the known-safe shadow out before anything else can ask for a pin
|
||||
* change. If the ESP32 rebooted while the relay was closed, the expander
|
||||
* still has that old latch — this is the write that clears it. */
|
||||
LOCK();
|
||||
err = flush_locked();
|
||||
UNLOCK();
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "not responding at 0x%02X: %s",
|
||||
PCF8574_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_ready = true;
|
||||
ESP_LOGI(TAG, "ready at 0x%02X, port=0x%02X", PCF8574_I2C_ADDR, s_shadow);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool pcf8574_is_ready(void)
|
||||
{
|
||||
return s_ready;
|
||||
}
|
||||
|
||||
esp_err_t pcf8574_write(uint8_t pin, bool level)
|
||||
{
|
||||
if (pin > 7) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
LOCK();
|
||||
uint8_t before = s_shadow;
|
||||
if (level) {
|
||||
s_shadow |= (uint8_t)(1u << pin);
|
||||
} else {
|
||||
s_shadow &= (uint8_t)~(1u << pin);
|
||||
}
|
||||
|
||||
esp_err_t err = flush_locked();
|
||||
if (err != ESP_OK) {
|
||||
/* Roll the shadow back so it keeps describing the chip rather than
|
||||
* our intent — the relay layer reasons about it. */
|
||||
s_shadow = before;
|
||||
}
|
||||
UNLOCK();
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t pcf8574_read(uint8_t pin, bool *level)
|
||||
{
|
||||
if (pin > 7 || !level) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint8_t port = 0;
|
||||
LOCK();
|
||||
esp_err_t err = i2c_master_receive(s_dev, &port, 1, 100);
|
||||
UNLOCK();
|
||||
|
||||
if (err == ESP_OK) {
|
||||
*level = (port & (1u << pin)) != 0;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t pcf8574_write_port(uint8_t value)
|
||||
{
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
LOCK();
|
||||
uint8_t before = s_shadow;
|
||||
s_shadow = value;
|
||||
esp_err_t err = flush_locked();
|
||||
if (err != ESP_OK) {
|
||||
s_shadow = before;
|
||||
}
|
||||
UNLOCK();
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t pcf8574_shadow(void)
|
||||
{
|
||||
return s_shadow;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* PCF8574 I/O expander @ 0x21.
|
||||
*
|
||||
* The PCF8574 has no direction register. Every pin is quasi-bidirectional:
|
||||
* writing 1 releases the pin to a weak pull-up (and lets it be read as an
|
||||
* input), writing 0 drives it hard low. So "output high" and "input" are the
|
||||
* same electrical state, and there is no way to read back what you last wrote
|
||||
* — reading returns the actual pin level.
|
||||
*
|
||||
* That is why this driver keeps a shadow byte. Without it, a read-modify-write
|
||||
* to set the touch reset line would sample the encoder button and the relay
|
||||
* line and write those levels back, which on the relay is a genuine hazard.
|
||||
* The shadow starts at 0xFF, matching the chip's power-on state (all pins
|
||||
* released high) — which is also the relay-off state for an active-low module.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define PCF8574_I2C_ADDR 0x21
|
||||
|
||||
/* Named per docs/HARDWARE.md. */
|
||||
#define PCF_PIN_TOUCH_RST 0
|
||||
#define PCF_PIN_RELAY 1 /* this project — heat call, active low */
|
||||
#define PCF_PIN_TOUCH_INT 2
|
||||
#define PCF_PIN_LCD_POWER 3
|
||||
#define PCF_PIN_LCD_RST 4
|
||||
#define PCF_PIN_ENCODER_SW 5
|
||||
/* P6, P7 free */
|
||||
|
||||
esp_err_t pcf8574_init(void);
|
||||
|
||||
/* True once init has succeeded. relay.c checks this before trusting writes. */
|
||||
bool pcf8574_is_ready(void);
|
||||
|
||||
/* Drive one pin. level=false pulls low; level=true releases to the pull-up. */
|
||||
esp_err_t pcf8574_write(uint8_t pin, bool level);
|
||||
|
||||
/* Read the actual level of one pin. */
|
||||
esp_err_t pcf8574_read(uint8_t pin, bool *level);
|
||||
|
||||
/* Write the whole port at once. Updates the shadow. */
|
||||
esp_err_t pcf8574_write_port(uint8_t value);
|
||||
|
||||
/* Last value written, not the pin levels. */
|
||||
uint8_t pcf8574_shadow(void);
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "i2c_bus.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "i2c";
|
||||
|
||||
static i2c_master_bus_handle_t s_bus;
|
||||
|
||||
esp_err_t i2c_bus_init(void)
|
||||
{
|
||||
if (s_bus) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
i2c_master_bus_config_t cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = I2C_BUS_SDA_GPIO,
|
||||
.scl_io_num = I2C_BUS_SCL_GPIO,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
|
||||
esp_err_t err = i2c_new_master_bus(&cfg, &s_bus);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "bus init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "bus up on SDA=%d SCL=%d @ %d Hz",
|
||||
I2C_BUS_SDA_GPIO, I2C_BUS_SCL_GPIO, I2C_BUS_FREQ_HZ);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_bus_add_device(uint8_t addr, i2c_master_dev_handle_t *out)
|
||||
{
|
||||
if (!s_bus) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
i2c_device_config_t cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = I2C_BUS_FREQ_HZ,
|
||||
};
|
||||
|
||||
return i2c_master_bus_add_device(s_bus, &cfg, out);
|
||||
}
|
||||
|
||||
int i2c_bus_scan(void)
|
||||
{
|
||||
if (!s_bus) {
|
||||
ESP_LOGE(TAG, "scan before init");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int found = 0;
|
||||
ESP_LOGI(TAG, "scanning...");
|
||||
|
||||
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
|
||||
if (i2c_master_probe(s_bus, addr, 50) == ESP_OK) {
|
||||
const char *who = "?";
|
||||
if (addr == 0x21) {
|
||||
who = "PCF8574 expander";
|
||||
} else if (addr == 0x15) {
|
||||
who = "CST826 touch";
|
||||
}
|
||||
ESP_LOGI(TAG, " 0x%02X %s", addr, who);
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == 0) {
|
||||
/* Loud, because this means the board or the pinout is wrong, and no
|
||||
* amount of display debugging past this point is worth doing. */
|
||||
ESP_LOGE(TAG, "no I2C devices found — check SDA=%d / SCL=%d wiring "
|
||||
"before debugging anything else",
|
||||
I2C_BUS_SDA_GPIO, I2C_BUS_SCL_GPIO);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "scan done, %d device(s)", found);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* i2c_bus — single owner of the board's I2C bus.
|
||||
*
|
||||
* SDA 38 / SCL 39, shared by the PCF8574 expander (0x21) and the CST826 touch
|
||||
* controller (0x15). The I2C connector on the case brings the same bus out, so
|
||||
* anything added later lands here too.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#define I2C_BUS_SDA_GPIO 38
|
||||
#define I2C_BUS_SCL_GPIO 39
|
||||
#define I2C_BUS_FREQ_HZ 400000
|
||||
|
||||
/* Initialise the bus. Safe to call more than once. */
|
||||
esp_err_t i2c_bus_init(void);
|
||||
|
||||
/* Add a device to the shared bus. */
|
||||
esp_err_t i2c_bus_add_device(uint8_t addr, i2c_master_dev_handle_t *out);
|
||||
|
||||
/*
|
||||
* Probe every address and log what answers.
|
||||
*
|
||||
* PRD 8.1: this is the cheap sanity check that tells you the board is alive
|
||||
* and the I2C pins are right, independently of the display. Expect 0x21 and
|
||||
* 0x15. Returns the number of devices found.
|
||||
*/
|
||||
int i2c_bus_scan(void);
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "cst826.h"
|
||||
|
||||
#include "display.h"
|
||||
#include "esp_log.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
static const char *TAG = "cst826";
|
||||
|
||||
#define REG_NUM_TOUCHES 0x02
|
||||
#define REG_TOUCH_DATA 0x03
|
||||
#define REG_CHIP_TYPE 0xAA
|
||||
|
||||
static i2c_master_dev_handle_t s_dev;
|
||||
static bool s_ready;
|
||||
|
||||
static esp_err_t read_reg(uint8_t reg, uint8_t *buf, size_t len)
|
||||
{
|
||||
return i2c_master_transmit_receive(s_dev, ®, 1, buf, len, 100);
|
||||
}
|
||||
|
||||
esp_err_t cst826_init(void)
|
||||
{
|
||||
if (s_ready) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t err = i2c_bus_add_device(CST826_I2C_ADDR, &s_dev);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "add device 0x%02X failed: %s",
|
||||
CST826_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t id = 0;
|
||||
err = read_reg(REG_CHIP_TYPE, &id, 1);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "no response at 0x%02X: %s",
|
||||
CST826_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
if (id != CST826_CHIP_ID) {
|
||||
/* Not fatal — the thermostat is fully usable from the encoder alone,
|
||||
* and refusing to boot over a touch panel would be a poor trade for
|
||||
* something that controls heat. */
|
||||
ESP_LOGW(TAG, "unexpected chip id 0x%02X (expected 0x%02X) — "
|
||||
"continuing with touch disabled", id, CST826_CHIP_ID);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
s_ready = true;
|
||||
ESP_LOGI(TAG, "ready at 0x%02X, chip id 0x%02X", CST826_I2C_ADDR, id);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool cst826_is_ready(void)
|
||||
{
|
||||
return s_ready;
|
||||
}
|
||||
|
||||
void cst826_read(cst826_touch_t *out)
|
||||
{
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
out->pressed = false;
|
||||
out->x = 0;
|
||||
out->y = 0;
|
||||
|
||||
if (!s_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t n = 0;
|
||||
if (read_reg(REG_NUM_TOUCHES, &n, 1) != ESP_OK || n == 0 || n > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t d[6];
|
||||
if (read_reg(REG_TOUCH_DATA, d, sizeof(d)) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t x = (uint16_t)((d[0] & 0x0F) << 8 | d[1]);
|
||||
uint16_t y = (uint16_t)((d[2] & 0x0F) << 8 | d[3]);
|
||||
|
||||
/* A garbage read past the panel edge means a bad transfer, not a touch at
|
||||
* the corner. Drop it rather than sending LVGL a bogus press. */
|
||||
if (x >= DISPLAY_H_RES || y >= DISPLAY_V_RES) {
|
||||
return;
|
||||
}
|
||||
|
||||
out->x = x;
|
||||
out->y = y;
|
||||
out->pressed = true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* CST826 capacitive touch controller @ I2C 0x15.
|
||||
*
|
||||
* Note this is CST826, not the CST816 the PRD names. Same family, but the chip
|
||||
* ID register reads 0x11 rather than 0xB4, so a CST816 driver's probe rejects
|
||||
* it and the board looks broken. Register map below is taken from the
|
||||
* Adafruit_CST8XX library bundled with Elecrow's example for this board.
|
||||
*
|
||||
* Protocol: read register 0x02 for the touch count, then a burst from 0x03 of
|
||||
* 6 bytes per touch:
|
||||
*
|
||||
* byte 0: [7:6] event, [3:0] X high nibble
|
||||
* byte 1: X low byte
|
||||
* byte 2: [7:4] touch id, [3:0] Y high nibble
|
||||
* byte 3: Y low byte
|
||||
* bytes 4,5: unused
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define CST826_I2C_ADDR 0x15
|
||||
#define CST826_CHIP_ID 0x11
|
||||
|
||||
typedef struct {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
bool pressed;
|
||||
} cst826_touch_t;
|
||||
|
||||
/*
|
||||
* Initialise and verify the chip ID.
|
||||
*
|
||||
* Requires i2c_bus_init() and the reset pulse that display.c's power-up
|
||||
* sequence performs on expander P0.
|
||||
*/
|
||||
esp_err_t cst826_init(void);
|
||||
|
||||
bool cst826_is_ready(void);
|
||||
|
||||
/* Latest touch state. Never fails hard — an I2C error reports "not pressed"
|
||||
* so a flaky touch controller cannot wedge the UI task. */
|
||||
void cst826_read(cst826_touch_t *out);
|
||||
Reference in new issue
Block a user