board.c 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. /**
  2. * @file board.c
  3. * @brief Waveshare ESP32-S3-Touch-LCD-1.54 board implementation
  4. *
  5. * Board with integrated ST7789 SPI display (with touch via CST816S I2C),
  6. * and ES8311 I2C-controlled stereo audio codec for DAC output.
  7. */
  8. #include "iot_board.h"
  9. #include "dac.h"
  10. #include "dac_es8311.h"
  11. #include "playback_control.h"
  12. #include "settings.h"
  13. #include "driver/gpio.h"
  14. #include "driver/i2c_master.h"
  15. #include "esp_adc/adc_oneshot.h"
  16. #include "esp_adc/adc_cali.h"
  17. #include "esp_adc/adc_cali_scheme.h"
  18. #include "esp_log.h"
  19. #include "esp_sleep.h"
  20. #include "freertos/FreeRTOS.h"
  21. #include "freertos/task.h"
  22. #include "lvgl.h"
  23. #include "esp_lvgl_port.h"
  24. #include "sdkconfig.h"
  25. static const char TAG[] = "Waveshare-ESP32-S3-Touch-LCD-1.54";
  26. static bool s_board_initialized = false;
  27. static bool s_touch_deferred = false;
  28. // I2C bus handle for ES8311 + CST816S touch controller
  29. static i2c_master_bus_handle_t s_i2c_dac_bus_handle = NULL;
  30. // CST816S touch controller (I2C addr 0x15)
  31. #define CST816S_ADDR 0x15
  32. #define CST816S_REG_GESTURE_ID 0x01
  33. #define CST816S_REG_FINGER_NUM 0x02
  34. #define CST816S_REG_XPOS_H 0x03
  35. #define CST816S_REG_CHIP_STATUS 0xA7
  36. #define CST816S_CHIP_ID 0xB4
  37. #define CST816S_REG_CONFIG_START 0x5D
  38. // Display touch parameters (swap + mirror applied at panel level)
  39. #define TOUCH_SWAP_XY true
  40. #define TOUCH_MIRROR_X true
  41. #define TOUCH_MIRROR_Y false
  42. #define TOUCH_WIDTH 240
  43. #define TOUCH_HEIGHT 240
  44. static i2c_master_dev_handle_t s_cst816s_dev = NULL;
  45. static lv_indev_t *s_touch_indev = NULL;
  46. static bool s_gpio7_state = false;
  47. static void set_gpio7_level(bool level) {
  48. if (level != s_gpio7_state) {
  49. gpio_set_level((gpio_num_t)7, level ? 1 : 0);
  50. s_gpio7_state = level;
  51. ESP_LOGI(TAG, "GPIO7 changed state to %s", level ? "HIGH" : "LOW");
  52. }
  53. }
  54. static void init_gpio7(void) {
  55. gpio_reset_pin((gpio_num_t)7);
  56. gpio_set_direction((gpio_num_t)7, GPIO_MODE_OUTPUT);
  57. set_gpio7_level(true);
  58. }
  59. // Battery power latch (GPIO2 / BAT_EN). The board powers up momentarily when
  60. // KEY_PWR (GPIO5) is pressed; firmware must drive BAT_EN HIGH to hold the
  61. // latch closed so the board keeps running on battery after USB is removed.
  62. // Driving it LOW opens the latch and powers the board off.
  63. #define BAT_EN_GPIO ((gpio_num_t)2)
  64. void board_power_latch_init(void) {
  65. // A prior power-off may have left the pin held LOW across deep sleep; release
  66. // the hold before re-driving it so the latch can close again.
  67. gpio_hold_dis(BAT_EN_GPIO);
  68. gpio_reset_pin(BAT_EN_GPIO);
  69. gpio_set_direction(BAT_EN_GPIO, GPIO_MODE_OUTPUT);
  70. gpio_set_level(BAT_EN_GPIO, 1);
  71. ESP_LOGI(TAG, "Battery power latch held (GPIO2 HIGH)");
  72. }
  73. void board_power_off(void) {
  74. ESP_LOGI(TAG, "Powering off — releasing battery latch (GPIO2 LOW)");
  75. // Release the latch and hold the pin LOW so it survives the transition.
  76. gpio_set_level(BAT_EN_GPIO, 0);
  77. gpio_hold_en(BAT_EN_GPIO);
  78. gpio_deep_sleep_hold_en();
  79. // Halt the CPU in deep sleep so it stops drawing current and cannot
  80. // re-latch. On battery the rail now collapses and the board powers off
  81. // cleanly; if USB is still supplying the rail, the board stays in deep
  82. // sleep (screen off) until USB is removed or it is reset. Without this the
  83. // rail sags just far enough to trip the brownout reset, which reboots and
  84. // re-drives the latch HIGH — the board appears to "blink off and restart".
  85. esp_deep_sleep_start();
  86. }
  87. // ============================================================================
  88. // Battery monitor (GPIO1 ADC via a 1:2 divider, GPIO3 charge status)
  89. // ============================================================================
  90. // Ported from the Waveshare bsp_power_manager: the battery rail is read on
  91. // ADC1 channel 0 (GPIO1) through a 2:1 divider, so the measured voltage is
  92. // multiplied by 2 (using a 3.0 scale to match the reference).
  93. #define BAT_ADC_CHANNEL ADC_CHANNEL_0 // GPIO1
  94. #define BAT_CHG_GPIO ((gpio_num_t)3) // CHG_STAT, active low = charging
  95. static adc_oneshot_unit_handle_t s_bat_adc = NULL;
  96. static adc_cali_handle_t s_bat_cali = NULL;
  97. static bool s_bat_calibrated = false;
  98. static void board_battery_init(void) {
  99. // Charge-status input
  100. gpio_config_t chg_cfg = {
  101. .intr_type = GPIO_INTR_DISABLE,
  102. .mode = GPIO_MODE_INPUT,
  103. .pin_bit_mask = 1ULL << BAT_CHG_GPIO,
  104. .pull_up_en = GPIO_PULLUP_ENABLE,
  105. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  106. };
  107. gpio_config(&chg_cfg);
  108. adc_oneshot_unit_init_cfg_t unit_cfg = {.unit_id = ADC_UNIT_1};
  109. if (adc_oneshot_new_unit(&unit_cfg, &s_bat_adc) != ESP_OK) {
  110. ESP_LOGW(TAG, "Battery ADC init failed");
  111. s_bat_adc = NULL;
  112. return;
  113. }
  114. adc_oneshot_chan_cfg_t chan_cfg = {
  115. .bitwidth = ADC_BITWIDTH_DEFAULT,
  116. .atten = ADC_ATTEN_DB_12,
  117. };
  118. adc_oneshot_config_channel(s_bat_adc, BAT_ADC_CHANNEL, &chan_cfg);
  119. adc_cali_curve_fitting_config_t cali_cfg = {
  120. .unit_id = ADC_UNIT_1,
  121. .chan = BAT_ADC_CHANNEL,
  122. .atten = ADC_ATTEN_DB_12,
  123. .bitwidth = ADC_BITWIDTH_DEFAULT,
  124. };
  125. s_bat_calibrated =
  126. (adc_cali_create_scheme_curve_fitting(&cali_cfg, &s_bat_cali) == ESP_OK);
  127. ESP_LOGI(TAG, "Battery monitor initialized (cali=%s)",
  128. s_bat_calibrated ? "yes" : "no");
  129. }
  130. static float board_battery_voltage(void) {
  131. if (!s_bat_adc || !s_bat_calibrated) {
  132. return -1.0f;
  133. }
  134. int raw = 0;
  135. if (adc_oneshot_read(s_bat_adc, BAT_ADC_CHANNEL, &raw) != ESP_OK) {
  136. return -1.0f;
  137. }
  138. int mv = 0;
  139. if (adc_cali_raw_to_voltage(s_bat_cali, raw, &mv) != ESP_OK) {
  140. return -1.0f;
  141. }
  142. return ((float)mv / 1000.0f) * 3.0f; // divider compensation (matches BSP)
  143. }
  144. bool board_battery_read(int *percent, bool *charging) {
  145. if (!s_bat_adc) {
  146. return false;
  147. }
  148. float v = board_battery_voltage();
  149. if (v < 0.0f) {
  150. return false;
  151. }
  152. if (percent) {
  153. // Piecewise-linear interpolation over a single-cell LiPo discharge curve.
  154. // Smooths the reading instead of jumping in coarse 20% steps.
  155. static const struct {
  156. float v;
  157. int pct;
  158. } curve[] = {
  159. {3.30f, 0}, {3.50f, 10}, {3.60f, 20}, {3.68f, 35}, {3.74f, 50},
  160. {3.82f, 65}, {3.92f, 80}, {4.02f, 90}, {4.12f, 98}, {4.20f, 100},
  161. };
  162. const size_t n = sizeof(curve) / sizeof(curve[0]);
  163. int pct;
  164. if (v <= curve[0].v) {
  165. pct = curve[0].pct;
  166. } else if (v >= curve[n - 1].v) {
  167. pct = curve[n - 1].pct;
  168. } else {
  169. pct = curve[n - 1].pct;
  170. for (size_t i = 1; i < n; i++) {
  171. if (v < curve[i].v) {
  172. float span = curve[i].v - curve[i - 1].v;
  173. float frac = (v - curve[i - 1].v) / span;
  174. float pct_span = (float)(curve[i].pct - curve[i - 1].pct);
  175. pct = curve[i - 1].pct + (int)(frac * pct_span + 0.5f);
  176. break;
  177. }
  178. }
  179. }
  180. *percent = pct;
  181. }
  182. if (charging) {
  183. *charging = (gpio_get_level(BAT_CHG_GPIO) == 0);
  184. }
  185. return true;
  186. }
  187. #ifdef CONFIG_MUTE_GPIO
  188. static esp_err_t init_mute_gpio(void) {
  189. if (CONFIG_MUTE_GPIO < 0) {
  190. return ESP_OK;
  191. }
  192. gpio_config_t io_conf = {
  193. .pin_bit_mask = (1ULL << CONFIG_MUTE_GPIO),
  194. .mode = GPIO_MODE_OUTPUT,
  195. .pull_up_en = GPIO_PULLUP_DISABLE,
  196. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  197. .intr_type = GPIO_INTR_DISABLE,
  198. };
  199. esp_err_t err = gpio_config(&io_conf);
  200. if (err != ESP_OK) {
  201. ESP_LOGE(TAG, "Failed to configure mute GPIO: %s", esp_err_to_name(err));
  202. return err;
  203. }
  204. // Initialize to unmuted state — set opposite of active level
  205. gpio_set_level(CONFIG_MUTE_GPIO, !CONFIG_MUTE_GPIO_LEVEL);
  206. ESP_LOGI(TAG, "Mute GPIO %d initialized (active %s, init %s)",
  207. CONFIG_MUTE_GPIO, CONFIG_MUTE_GPIO_LEVEL ? "high" : "low",
  208. CONFIG_MUTE_GPIO_LEVEL ? "low" : "high");
  209. return ESP_OK;
  210. }
  211. #endif
  212. // ============================================================================
  213. // CST816S Touch Driver
  214. // ============================================================================
  215. static bool cst816s_i2c_write_reg(i2c_master_bus_handle_t bus,
  216. i2c_master_dev_handle_t dev, uint8_t reg,
  217. uint8_t val, int timeout_ms) {
  218. (void)bus;
  219. uint8_t buf[2] = {reg, val};
  220. return i2c_master_transmit(dev, buf, 2, timeout_ms) == ESP_OK;
  221. }
  222. static bool cst816s_read_reg(i2c_master_bus_handle_t bus,
  223. i2c_master_dev_handle_t dev, uint8_t reg,
  224. uint8_t *val, size_t len, int timeout_ms) {
  225. (void)bus;
  226. // I2C register read: write register address, then read data
  227. return i2c_master_transmit_receive(dev, &reg, 1, val, len, timeout_ms) ==
  228. ESP_OK;
  229. }
  230. static void cst816s_reset(void) {
  231. #if BOARD_I2C_TOUCH_RST_GPIO >= 0
  232. gpio_reset_pin((gpio_num_t)BOARD_I2C_TOUCH_RST_GPIO);
  233. gpio_set_direction((gpio_num_t)BOARD_I2C_TOUCH_RST_GPIO, GPIO_MODE_OUTPUT);
  234. gpio_set_level((gpio_num_t)BOARD_I2C_TOUCH_RST_GPIO, 0);
  235. vTaskDelay(pdMS_TO_TICKS(10));
  236. gpio_set_level((gpio_num_t)BOARD_I2C_TOUCH_RST_GPIO, 1);
  237. vTaskDelay(pdMS_TO_TICKS(50));
  238. #endif
  239. }
  240. static esp_err_t init_touch_controller(void) {
  241. const int timeout_ms = 100;
  242. cst816s_reset();
  243. // Register CST816S on I2C bus
  244. i2c_device_config_t dev_cfg = {
  245. .dev_addr_length = I2C_ADDR_BIT_LEN_7,
  246. .device_address = CST816S_ADDR,
  247. .scl_speed_hz = 400000,
  248. };
  249. esp_err_t err =
  250. i2c_master_bus_add_device(s_i2c_dac_bus_handle, &dev_cfg, &s_cst816s_dev);
  251. if (err != ESP_OK) {
  252. ESP_LOGE(TAG, "Failed to add CST816S I2C device: %s", esp_err_to_name(err));
  253. return err;
  254. }
  255. // Configure GPIO 48 (INT) as interrupt input
  256. const gpio_config_t int_cfg = {
  257. .pin_bit_mask = (1ULL << BOARD_I2C_TOUCH_INT_GPIO),
  258. .mode = GPIO_MODE_INPUT,
  259. .pull_up_en = GPIO_PULLUP_ENABLE,
  260. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  261. .intr_type = GPIO_INTR_ANYEDGE,
  262. };
  263. gpio_config(&int_cfg);
  264. // Write default configuration registers
  265. struct reg_config {
  266. uint8_t reg;
  267. uint8_t val;
  268. };
  269. static const struct reg_config cfg[] = {
  270. {0xB1, 0x5B}, // Touch mode settings
  271. {0xB2, 0x0E}, // Gate switch time
  272. {0xB3, 0x00}, // Button mode disable
  273. {0x94, 0x0B}, // Touch count interrupt enable
  274. {0x95, 0x0B}, // Touch count interrupt enable
  275. {0x96, 0x01}, // Auto sleep after touch
  276. {0x98, 0x3D}, // Monitor period
  277. {0x99, 0x2B}, // Sleep time
  278. {0x9A, 0x01}, // LED rate
  279. };
  280. for (size_t i = 0; i < sizeof(cfg) / sizeof(cfg[0]); i++) {
  281. cst816s_i2c_write_reg(s_i2c_dac_bus_handle, s_cst816s_dev, cfg[i].reg,
  282. cfg[i].val, timeout_ms);
  283. }
  284. // Verify device ID by reading register 0xA7
  285. uint8_t chip_id = 0;
  286. if (cst816s_read_reg(s_i2c_dac_bus_handle, s_cst816s_dev,
  287. CST816S_REG_CHIP_STATUS, &chip_id, 1, timeout_ms)) {
  288. ESP_LOGI(TAG, "CST816S chip ID: 0x%02X", chip_id);
  289. } else {
  290. ESP_LOGW(TAG, "Failed to read CST816S chip ID");
  291. }
  292. // Clear any pending interrupts by reading status register
  293. uint8_t status = 0;
  294. cst816s_read_reg(s_i2c_dac_bus_handle, s_cst816s_dev, CST816S_REG_GESTURE_ID,
  295. &status, 1, timeout_ms);
  296. ESP_LOGI(TAG, "CST816S touch controller initialized");
  297. return ESP_OK;
  298. }
  299. static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
  300. (void)indev;
  301. if (!s_cst816s_dev) {
  302. data->state = LV_INDEV_STATE_RELEASED;
  303. return;
  304. }
  305. const int timeout_ms = 10;
  306. // Read number of active fingers.
  307. uint8_t touch_count = 0;
  308. if (!cst816s_read_reg(s_i2c_dac_bus_handle, s_cst816s_dev,
  309. CST816S_REG_FINGER_NUM, &touch_count, 1, timeout_ms)) {
  310. data->state = LV_INDEV_STATE_RELEASED;
  311. return;
  312. }
  313. touch_count &= 0x0F;
  314. if (touch_count == 0) {
  315. data->state = LV_INDEV_STATE_RELEASED;
  316. return;
  317. }
  318. // Read touch coordinates from XPOSH, XPOSL, YPOSH, YPOSL.
  319. uint8_t xybuf[4] = {0};
  320. if (!cst816s_read_reg(s_i2c_dac_bus_handle, s_cst816s_dev, CST816S_REG_XPOS_H,
  321. xybuf, sizeof(xybuf), timeout_ms)) {
  322. data->state = LV_INDEV_STATE_RELEASED;
  323. return;
  324. }
  325. uint16_t x = (uint16_t)(((xybuf[0] & 0x0F) << 8) | xybuf[1]);
  326. uint16_t y = (uint16_t)(((xybuf[2] & 0x0F) << 8) | xybuf[3]);
  327. // Apply swap and mirror to match display orientation
  328. if (TOUCH_SWAP_XY) {
  329. uint16_t tmp = x;
  330. x = y;
  331. y = tmp;
  332. }
  333. if (TOUCH_MIRROR_X) {
  334. x = TOUCH_WIDTH - 1 - x;
  335. }
  336. if (TOUCH_MIRROR_Y) {
  337. y = TOUCH_HEIGHT - 1 - y;
  338. }
  339. data->point.x = x;
  340. data->point.y = y;
  341. data->state = LV_INDEV_STATE_PRESSED;
  342. // Detect tap (brief press) — trigger mute toggle
  343. // We use a simple heuristic: if the indev was previously released and now
  344. // pressed, it's a tap. LVGL input device state tracking handles this
  345. // internally.
  346. }
  347. static esp_err_t init_lvgl_touch(void) {
  348. if (s_touch_indev != NULL) {
  349. return ESP_OK;
  350. }
  351. // Create LVGL input device
  352. s_touch_indev = lv_indev_create();
  353. lv_indev_set_type(s_touch_indev, LV_INDEV_TYPE_POINTER);
  354. lv_indev_set_read_cb(s_touch_indev, touch_read_cb);
  355. ESP_LOGI(TAG, "LVGL touch input device created");
  356. return ESP_OK;
  357. }
  358. // ============================================================================
  359. // Board resource lookup
  360. // ============================================================================
  361. board_res_handle_t iot_board_get_handle(int id) {
  362. switch (id) {
  363. #ifdef CONFIG_DAC_ES8311
  364. case BOARD_I2C_DAC_ID:
  365. return (board_res_handle_t)s_i2c_dac_bus_handle;
  366. #endif
  367. case BOARD_I2C_TOUCH_ID:
  368. return (board_res_handle_t)s_i2c_dac_bus_handle;
  369. default:
  370. return NULL;
  371. }
  372. }
  373. // ============================================================================
  374. // Board init
  375. // ============================================================================
  376. esp_err_t iot_board_init(void) {
  377. esp_err_t err = ESP_OK;
  378. if (s_board_initialized) {
  379. ESP_LOGW(TAG, "Board already initialized");
  380. return ESP_OK;
  381. }
  382. // Hold the battery power latch closed first so the board survives USB
  383. // removal.
  384. board_power_latch_init();
  385. board_battery_init();
  386. init_gpio7();
  387. #ifdef CONFIG_MUTE_GPIO
  388. err = init_mute_gpio();
  389. if (err != ESP_OK) {
  390. return err;
  391. }
  392. #endif
  393. #if defined(CONFIG_DAC_ES8311)
  394. // Initialize I2C bus for ES8311 + CST816S touch controller
  395. // Pins 41 (SCL) and 42 (SDA) shared between ES8311 and CST816S
  396. i2c_master_bus_config_t i2c_cfg = {
  397. .i2c_port = 0,
  398. .sda_io_num = BOARD_I2C_TOUCH_SDA_GPIO,
  399. .scl_io_num = BOARD_I2C_TOUCH_SCL_GPIO,
  400. .clk_source = I2C_CLK_SRC_DEFAULT,
  401. .glitch_ignore_cnt = 7,
  402. .flags.enable_internal_pullup = true,
  403. };
  404. err = i2c_new_master_bus(&i2c_cfg, &s_i2c_dac_bus_handle);
  405. if (err != ESP_OK) {
  406. ESP_LOGE(TAG, "Failed to initialize DAC I2C bus: %s", esp_err_to_name(err));
  407. return err;
  408. }
  409. ESP_LOGI(TAG, "DAC I2C bus initialized: sda=%d, scl=%d",
  410. BOARD_I2C_TOUCH_SDA_GPIO, BOARD_I2C_TOUCH_SCL_GPIO);
  411. // Register and initialize ES8311 DAC
  412. dac_register(&dac_es8311_ops);
  413. err = dac_init(s_i2c_dac_bus_handle);
  414. if (err != ESP_OK) {
  415. ESP_LOGE(TAG, "Failed to initialize ES8311 DAC: %s", esp_err_to_name(err));
  416. return err;
  417. }
  418. // Restore saved volume (ES8311 boots at 0 dB until programmed)
  419. float vol_db;
  420. if (ESP_OK == settings_get_volume(&vol_db)) {
  421. dac_set_volume(vol_db);
  422. }
  423. // Initialize CST816S touch controller hardware (I2C device, GPIO, registers)
  424. // LVGL input device creation is deferred — lvgl_port_lock() is not available
  425. // until display_init() runs later in main.c
  426. if (init_touch_controller() != ESP_OK) {
  427. ESP_LOGW(TAG, "Touch controller init failed, continuing without touch");
  428. }
  429. // Mark that deferred LVGL touch init is pending
  430. s_touch_deferred = true;
  431. #endif
  432. s_board_initialized = true;
  433. ESP_LOGI(TAG, "Waveshare ESP32-S3-Touch-LCD-1.54 initialized");
  434. return ESP_OK;
  435. }
  436. void iot_board_init_lvgl_resources(void) {
  437. if (!s_touch_deferred) {
  438. return;
  439. }
  440. s_touch_deferred = false;
  441. if (!s_cst816s_dev) {
  442. ESP_LOGW(TAG, "Touch controller not initialized, skipping LVGL init");
  443. return;
  444. }
  445. for (int attempt = 0; attempt < 5; attempt++) {
  446. if (!lvgl_port_lock(1000)) {
  447. vTaskDelay(pdMS_TO_TICKS(100));
  448. continue;
  449. }
  450. esp_err_t err = init_lvgl_touch();
  451. if (err != ESP_OK) {
  452. ESP_LOGW(TAG, "LVGL touch init failed, continuing without touch");
  453. } else {
  454. ESP_LOGI(TAG, "Deferred LVGL touch init complete");
  455. }
  456. lvgl_port_unlock();
  457. return;
  458. }
  459. ESP_LOGW(TAG, "Failed to acquire LVGL lock — touch init skipped");
  460. s_touch_deferred = true;
  461. }
  462. esp_err_t iot_board_deinit(void) {
  463. s_board_initialized = false;
  464. return ESP_OK;
  465. }