From 330c2ab4fe43d4a57b011b48c0fb02de43cae356 Mon Sep 17 00:00:00 2001 From: sl-firmware Date: Mon, 20 Apr 2026 11:51:14 -0400 Subject: [PATCH] feat: GC9A01 display driver + SAULT/voltage HUD; fix UART GPIO regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gc9a01.c/h: GC9A01 240x240 round LCD SPI driver (SPI2, GPIO 9-14) 5x7 bitmap font with scaling, display_fill_rect/draw_string/draw_arc - main.c: hud_task — "SAULT" orange header (scale=3) + battery voltage white on black (scale=4), updates at 1 Hz from g_vesc[0].voltage_x10 - config.h: add DISP_* GPIO defines; revert 06219af UART regression — lsusb on Orin confirms /dev/ttyACM0 = CH343 (1a86:55d3) wired to GPIO 43/44, not native USB; UART must stay on 43/44, CAN stays on 2/1 (SN65HVD230 physical rewire to GPIO 2/1 still required for CAN to work) - CMakeLists.txt: add gc9a01.c Co-Authored-By: Claude Sonnet 4.6 --- esp32s3/balance/main/CMakeLists.txt | 3 +- esp32s3/balance/main/config.h | 30 +++- esp32s3/balance/main/gc9a01.c | 267 ++++++++++++++++++++++++++++ esp32s3/balance/main/gc9a01.h | 24 +++ esp32s3/balance/main/main.c | 39 +++- 5 files changed, 351 insertions(+), 12 deletions(-) create mode 100644 esp32s3/balance/main/gc9a01.c create mode 100644 esp32s3/balance/main/gc9a01.h diff --git a/esp32s3/balance/main/CMakeLists.txt b/esp32s3/balance/main/CMakeLists.txt index 2fd31d5..c803efe 100644 --- a/esp32s3/balance/main/CMakeLists.txt +++ b/esp32s3/balance/main/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( - SRCS "main.c" "orin_serial.c" "vesc_can.c" + SRCS "main.c" "orin_serial.c" "vesc_can.c" "gc9a01.c" INCLUDE_DIRS "." + REQUIRES driver freertos esp_timer ) diff --git a/esp32s3/balance/main/config.h b/esp32s3/balance/main/config.h index 531ea40..30d6015 100644 --- a/esp32s3/balance/main/config.h +++ b/esp32s3/balance/main/config.h @@ -11,27 +11,39 @@ * to IO2/IO1 when deploying this firmware. See docs/SAUL-TEE-SYSTEM-REFERENCE.md. */ -/* ── Orin serial: USB Serial/JTAG (native USB, no GPIO needed) ── - * sdkconfig: CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y - * The Orin↔ESP32 link uses the built-in USB-CDC peripheral. - * UART0 on GPIO 43/44 is NOT used for Orin comms. +/* ── Orin serial: CH343 USB-UART bridge (1a86:55d3 = /dev/ttyACM0 on Orin) ── + * Physical path: Orin USB → CH343 → GPIO 43 (TX) / GPIO 44 (RX). + * NOTE: lsusb on Orin shows ONLY CH343, not ESP32 native USB (303a:xxxx). + * USB Serial/JTAG is console-only — it is not the Orin↔ESP32 data path. */ #define ORIN_UART_PORT UART_NUM_0 #define ORIN_UART_BAUD 460800 -#define ORIN_UART_TX_GPIO 17 /* unused — Orin uses USB-CDC */ -#define ORIN_UART_RX_GPIO 18 /* unused — Orin uses USB-CDC */ +#define ORIN_UART_TX_GPIO 43 /* ESP32 → CH343 RXD */ +#define ORIN_UART_RX_GPIO 44 /* CH343 TXD → ESP32 */ #define ORIN_UART_RX_BUF 1024 #define ORIN_TX_QUEUE_DEPTH 16 -/* ── VESC CAN TWAI (SN65HVD230 transceiver on original GPIO 43/44) ── */ -#define VESC_CAN_TX_GPIO 43 /* ESP32 TWAI TX → SN65HVD230 TXD */ -#define VESC_CAN_RX_GPIO 44 /* SN65HVD230 RXD → ESP32 TWAI RX */ +/* ── VESC CAN TWAI (SN65HVD230 target GPIO 2/1 per bd-66hx design) ── + * HARDWARE NOTE: SN65HVD230 transceiver must be physically wired to GPIO 2 + * (TXD) and GPIO 1 (RXD). Old prototype wiring used GPIO 43/44 but those + * pins are now committed to UART (CH343). Rewire required before CAN works. + */ +#define VESC_CAN_TX_GPIO 2 /* ESP32 TWAI TX → SN65HVD230 TXD */ +#define VESC_CAN_RX_GPIO 1 /* SN65HVD230 RXD → ESP32 TWAI RX */ #define VESC_CAN_RX_QUEUE 32 /* VESC node IDs — matched to bd-wim1 TELEM_VESC_LEFT/RIGHT mapping */ #define VESC_ID_A 56u /* TELEM_VESC_LEFT (0x81) */ #define VESC_ID_B 68u /* TELEM_VESC_RIGHT (0x82) */ +/* ── GC9A01 240×240 round display (bd-1yr8, SPI2) ── */ +#define DISP_CS_GPIO 12 +#define DISP_DC_GPIO 11 +#define DISP_RST_GPIO 10 +#define DISP_BL_GPIO 9 +#define DISP_MOSI_GPIO 13 +#define DISP_SCK_GPIO 14 + /* ── Safety / timing ── */ #define HB_TIMEOUT_MS 500u /* heartbeat watchdog: disarm if exceeded */ #define DRIVE_TIMEOUT_MS 500u /* drive command staleness timeout */ diff --git a/esp32s3/balance/main/gc9a01.c b/esp32s3/balance/main/gc9a01.c new file mode 100644 index 0000000..3541649 --- /dev/null +++ b/esp32s3/balance/main/gc9a01.c @@ -0,0 +1,267 @@ +/* gc9a01.c — GC9A01 240×240 round LCD SPI driver (bd-1yr8 display bead) */ + +#include "gc9a01.h" +#include "config.h" +#include "driver/spi_master.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include +#include + +static const char *TAG = "gc9a01"; +static spi_device_handle_t s_spi; + +/* ── 5×7 bitmap font, one byte per column (bit0 = top row), ASCII 32..126 ── */ +static const uint8_t s_font[95][5] = { + {0x00,0x00,0x00,0x00,0x00}, /* ' ' */ {0x00,0x00,0x5F,0x00,0x00}, /* '!' */ + {0x00,0x07,0x00,0x07,0x00}, /* '"' */ {0x14,0x7F,0x14,0x7F,0x14}, /* '#' */ + {0x24,0x2A,0x7F,0x2A,0x12}, /* '$' */ {0x23,0x13,0x08,0x64,0x62}, /* '%' */ + {0x36,0x49,0x55,0x22,0x50}, /* '&' */ {0x00,0x05,0x03,0x00,0x00}, /* '\'' */ + {0x00,0x1C,0x22,0x41,0x00}, /* '(' */ {0x00,0x41,0x22,0x1C,0x00}, /* ')' */ + {0x14,0x08,0x3E,0x08,0x14}, /* '*' */ {0x08,0x08,0x3E,0x08,0x08}, /* '+' */ + {0x00,0x50,0x30,0x00,0x00}, /* ',' */ {0x08,0x08,0x08,0x08,0x08}, /* '-' */ + {0x00,0x60,0x60,0x00,0x00}, /* '.' */ {0x20,0x10,0x08,0x04,0x02}, /* '/' */ + {0x3E,0x51,0x49,0x45,0x3E}, /* '0' */ {0x00,0x42,0x7F,0x40,0x00}, /* '1' */ + {0x42,0x61,0x51,0x49,0x46}, /* '2' */ {0x21,0x41,0x45,0x4B,0x31}, /* '3' */ + {0x18,0x14,0x12,0x7F,0x10}, /* '4' */ {0x27,0x45,0x45,0x45,0x39}, /* '5' */ + {0x3C,0x4A,0x49,0x49,0x30}, /* '6' */ {0x01,0x71,0x09,0x05,0x03}, /* '7' */ + {0x36,0x49,0x49,0x49,0x36}, /* '8' */ {0x06,0x49,0x49,0x29,0x1E}, /* '9' */ + {0x00,0x36,0x36,0x00,0x00}, /* ':' */ {0x00,0x56,0x36,0x00,0x00}, /* ';' */ + {0x08,0x14,0x22,0x41,0x00}, /* '<' */ {0x14,0x14,0x14,0x14,0x14}, /* '=' */ + {0x00,0x41,0x22,0x14,0x08}, /* '>' */ {0x02,0x01,0x51,0x09,0x06}, /* '?' */ + {0x32,0x49,0x79,0x41,0x3E}, /* '@' */ {0x7E,0x11,0x11,0x11,0x7E}, /* 'A' */ + {0x7F,0x49,0x49,0x49,0x36}, /* 'B' */ {0x3E,0x41,0x41,0x41,0x22}, /* 'C' */ + {0x7F,0x41,0x41,0x22,0x1C}, /* 'D' */ {0x7F,0x49,0x49,0x49,0x41}, /* 'E' */ + {0x7F,0x09,0x09,0x09,0x01}, /* 'F' */ {0x3E,0x41,0x49,0x49,0x3A}, /* 'G' */ + {0x7F,0x08,0x08,0x08,0x7F}, /* 'H' */ {0x00,0x41,0x7F,0x41,0x00}, /* 'I' */ + {0x20,0x40,0x41,0x3F,0x01}, /* 'J' */ {0x7F,0x08,0x14,0x22,0x41}, /* 'K' */ + {0x7F,0x40,0x40,0x40,0x40}, /* 'L' */ {0x7F,0x02,0x0C,0x02,0x7F}, /* 'M' */ + {0x7F,0x04,0x08,0x10,0x7F}, /* 'N' */ {0x3E,0x41,0x41,0x41,0x3E}, /* 'O' */ + {0x7F,0x09,0x09,0x09,0x06}, /* 'P' */ {0x3E,0x41,0x51,0x21,0x5E}, /* 'Q' */ + {0x7F,0x09,0x19,0x29,0x46}, /* 'R' */ {0x46,0x49,0x49,0x49,0x31}, /* 'S' */ + {0x01,0x01,0x7F,0x01,0x01}, /* 'T' */ {0x3F,0x40,0x40,0x40,0x3F}, /* 'U' */ + {0x1F,0x20,0x40,0x20,0x1F}, /* 'V' */ {0x3F,0x40,0x38,0x40,0x3F}, /* 'W' */ + {0x63,0x14,0x08,0x14,0x63}, /* 'X' */ {0x07,0x08,0x70,0x08,0x07}, /* 'Y' */ + {0x61,0x51,0x49,0x45,0x43}, /* 'Z' */ {0x00,0x7F,0x41,0x41,0x00}, /* '[' */ + {0x02,0x04,0x08,0x10,0x20}, /* '\\' */ {0x00,0x41,0x41,0x7F,0x00}, /* ']' */ + {0x04,0x02,0x01,0x02,0x04}, /* '^' */ {0x40,0x40,0x40,0x40,0x40}, /* '_' */ + {0x00,0x01,0x02,0x04,0x00}, /* '`' */ {0x20,0x54,0x54,0x54,0x78}, /* 'a' */ + {0x7F,0x48,0x44,0x44,0x38}, /* 'b' */ {0x38,0x44,0x44,0x44,0x20}, /* 'c' */ + {0x38,0x44,0x44,0x48,0x7F}, /* 'd' */ {0x38,0x54,0x54,0x54,0x18}, /* 'e' */ + {0x08,0x7E,0x09,0x01,0x02}, /* 'f' */ {0x0C,0x52,0x52,0x52,0x3E}, /* 'g' */ + {0x7F,0x08,0x04,0x04,0x78}, /* 'h' */ {0x00,0x44,0x7D,0x40,0x00}, /* 'i' */ + {0x20,0x40,0x44,0x3D,0x00}, /* 'j' */ {0x7F,0x10,0x28,0x44,0x00}, /* 'k' */ + {0x00,0x41,0x7F,0x40,0x00}, /* 'l' */ {0x7C,0x04,0x18,0x04,0x78}, /* 'm' */ + {0x7C,0x08,0x04,0x04,0x78}, /* 'n' */ {0x38,0x44,0x44,0x44,0x38}, /* 'o' */ + {0x7C,0x14,0x14,0x14,0x08}, /* 'p' */ {0x08,0x14,0x14,0x18,0x7C}, /* 'q' */ + {0x7C,0x08,0x04,0x04,0x08}, /* 'r' */ {0x48,0x54,0x54,0x54,0x20}, /* 's' */ + {0x04,0x3F,0x44,0x40,0x20}, /* 't' */ {0x3C,0x40,0x40,0x20,0x7C}, /* 'u' */ + {0x1C,0x20,0x40,0x20,0x1C}, /* 'v' */ {0x3C,0x40,0x30,0x40,0x3C}, /* 'w' */ + {0x44,0x28,0x10,0x28,0x44}, /* 'x' */ {0x0C,0x50,0x50,0x50,0x3C}, /* 'y' */ + {0x44,0x64,0x54,0x4C,0x44}, /* 'z' */ {0x00,0x08,0x36,0x41,0x00}, /* '{' */ + {0x00,0x00,0x7F,0x00,0x00}, /* '|' */ {0x00,0x41,0x36,0x08,0x00}, /* '}' */ + {0x10,0x08,0x08,0x10,0x08}, /* '~' */ +}; + +/* ── Static buffers (internal SRAM → DMA-safe) ── */ +static uint8_t s_line_buf[240 * 2]; +static uint8_t s_char_buf[5 * 5 * 7 * 5 * 2]; /* max scale=5: 25×35×2 */ + +/* ── Low-level SPI helpers ── */ +static void write_cmd(uint8_t cmd) +{ + gpio_set_level(DISP_DC_GPIO, 0); + spi_transaction_t t = { .length = 8, .flags = SPI_TRANS_USE_TXDATA }; + t.tx_data[0] = cmd; + spi_device_polling_transmit(s_spi, &t); +} + +static void write_bytes(const uint8_t *data, size_t len) +{ + if (!len) return; + gpio_set_level(DISP_DC_GPIO, 1); + spi_transaction_t t = { .length = len * 8, .tx_buffer = data }; + spi_device_polling_transmit(s_spi, &t); +} + +static inline void write_byte(uint8_t b) { write_bytes(&b, 1); } + +/* ── Address window ── */ +static void set_window(int x1, int y1, int x2, int y2) +{ + uint8_t d[4]; + d[0] = x1 >> 8; d[1] = x1 & 0xFF; d[2] = x2 >> 8; d[3] = x2 & 0xFF; + write_cmd(0x2A); write_bytes(d, 4); + d[0] = y1 >> 8; d[1] = y1 & 0xFF; d[2] = y2 >> 8; d[3] = y2 & 0xFF; + write_cmd(0x2B); write_bytes(d, 4); +} + +/* ── GC9A01 register init ── */ +static void init_regs(void) +{ + write_cmd(0xEF); + write_cmd(0xEB); write_byte(0x14); + write_cmd(0xFE); + write_cmd(0xEF); + write_cmd(0xEB); write_byte(0x14); + write_cmd(0x84); write_byte(0x40); + write_cmd(0x85); write_byte(0xFF); + write_cmd(0x86); write_byte(0xFF); + write_cmd(0x87); write_byte(0xFF); + write_cmd(0x88); write_byte(0x0A); + write_cmd(0x89); write_byte(0x21); + write_cmd(0x8A); write_byte(0x00); + write_cmd(0x8B); write_byte(0x80); + write_cmd(0x8C); write_byte(0x01); + write_cmd(0x8D); write_byte(0x01); + write_cmd(0x8E); write_byte(0xFF); + write_cmd(0x8F); write_byte(0xFF); + { uint8_t d[] = {0x00,0x20}; write_cmd(0xB6); write_bytes(d,2); } + write_cmd(0x36); write_byte(0x08); /* MADCTL: normal, BGR */ + write_cmd(0x3A); write_byte(0x05); /* COLMOD: 16-bit RGB565 */ + { uint8_t d[] = {0x08,0x08,0x08,0x08}; write_cmd(0x90); write_bytes(d,4); } + write_cmd(0xBD); write_byte(0x06); + write_cmd(0xBC); write_byte(0x00); + { uint8_t d[] = {0x60,0x01,0x04}; write_cmd(0xFF); write_bytes(d,3); } + write_cmd(0xC3); write_byte(0x13); + write_cmd(0xC4); write_byte(0x13); + write_cmd(0xC9); write_byte(0x22); + write_cmd(0xBE); write_byte(0x11); + { uint8_t d[] = {0x10,0x0E}; write_cmd(0xE1); write_bytes(d,2); } + { uint8_t d[] = {0x21,0x0C,0x02}; write_cmd(0xDF); write_bytes(d,3); } + { uint8_t d[] = {0x45,0x09,0x08,0x08,0x26,0x2A}; write_cmd(0xF0); write_bytes(d,6); } + { uint8_t d[] = {0x43,0x70,0x72,0x36,0x37,0x6F}; write_cmd(0xF1); write_bytes(d,6); } + { uint8_t d[] = {0x45,0x09,0x08,0x08,0x26,0x2A}; write_cmd(0xF2); write_bytes(d,6); } + { uint8_t d[] = {0x43,0x70,0x72,0x36,0x37,0x6F}; write_cmd(0xF3); write_bytes(d,6); } + { uint8_t d[] = {0x1B,0x0B}; write_cmd(0xED); write_bytes(d,2); } + write_cmd(0xAE); write_byte(0x77); + write_cmd(0xCD); write_byte(0x63); + { uint8_t d[] = {0x07,0x07,0x04,0x0E,0x0F,0x09,0x07,0x08,0x03}; + write_cmd(0x70); write_bytes(d,9); } + write_cmd(0xE8); write_byte(0x34); + { uint8_t d[] = {0x18,0x0D,0xB7,0x18,0x0D,0x8B,0x88,0x08}; + write_cmd(0x62); write_bytes(d,8); } + { uint8_t d[] = {0x18,0x0D,0xB7,0x58,0x1E,0x0B,0x00,0xA7,0x88,0x08}; + write_cmd(0x63); write_bytes(d,10); } + { uint8_t d[] = {0x20,0x07,0x04}; write_cmd(0x64); write_bytes(d,3); } + { uint8_t d[] = {0x10,0x85,0x80,0x00,0x00,0x4E,0x00}; + write_cmd(0x74); write_bytes(d,7); } + { uint8_t d[] = {0x3E,0x07}; write_cmd(0x98); write_bytes(d,2); } + write_cmd(0x35); /* TEON */ + write_cmd(0x21); /* INVON */ + write_cmd(0x11); /* SLPOUT */ + vTaskDelay(pdMS_TO_TICKS(120)); + write_cmd(0x29); /* DISPON */ + vTaskDelay(pdMS_TO_TICKS(20)); +} + +/* ── Public init ── */ +void gc9a01_init(void) +{ + /* SPI bus */ + spi_bus_config_t bus = { + .mosi_io_num = DISP_MOSI_GPIO, + .miso_io_num = -1, + .sclk_io_num = DISP_SCK_GPIO, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 4096, + }; + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO)); + + spi_device_interface_config_t dev = { + .clock_speed_hz = 40 * 1000 * 1000, + .mode = 0, + .spics_io_num = DISP_CS_GPIO, + .queue_size = 1, + }; + ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev, &s_spi)); + + /* DC, RST, BL GPIOs */ + gpio_set_direction(DISP_DC_GPIO, GPIO_MODE_OUTPUT); + gpio_set_direction(DISP_RST_GPIO, GPIO_MODE_OUTPUT); + gpio_set_direction(DISP_BL_GPIO, GPIO_MODE_OUTPUT); + + /* Hardware reset */ + gpio_set_level(DISP_RST_GPIO, 0); vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(DISP_RST_GPIO, 1); vTaskDelay(pdMS_TO_TICKS(120)); + + init_regs(); + + /* Backlight on */ + gpio_set_level(DISP_BL_GPIO, 1); + ESP_LOGI(TAG, "GC9A01 init OK"); +} + +/* ── display_fill_rect ── */ +void display_fill_rect(int x, int y, int w, int h, uint16_t rgb565) +{ + if (w <= 0 || h <= 0) return; + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x + w > 240) w = 240 - x; + if (y + h > 240) h = 240 - y; + if (w <= 0 || h <= 0) return; + + set_window(x, y, x + w - 1, y + h - 1); + write_cmd(0x2C); + + uint8_t hi = rgb565 >> 8, lo = rgb565 & 0xFF; + for (int i = 0; i < w * 2; i += 2) { s_line_buf[i] = hi; s_line_buf[i+1] = lo; } + for (int row = 0; row < h; row++) { write_bytes(s_line_buf, (size_t)(w * 2)); } +} + +/* ── Glyph rasteriser (handles scale 1..5) ── */ +static void draw_char_s(int x, int y, char c, uint16_t fg, uint16_t bg, int scale) +{ + if ((uint8_t)c < 32 || (uint8_t)c > 126) return; + if (scale < 1) scale = 1; + if (scale > 5) scale = 5; + const uint8_t *g = s_font[(uint8_t)c - 32]; + int cw = 5 * scale, ch = 7 * scale; + + uint8_t *p = s_char_buf; + for (int row = 0; row < 7; row++) { + for (int sr = 0; sr < scale; sr++) { + for (int col = 0; col < 5; col++) { + uint16_t color = ((g[col] >> row) & 1) ? fg : bg; + uint8_t hi = color >> 8, lo = color & 0xFF; + for (int sc = 0; sc < scale; sc++) { *p++ = hi; *p++ = lo; } + } + } + } + set_window(x, y, x + cw - 1, y + ch - 1); + write_cmd(0x2C); + write_bytes(s_char_buf, (size_t)(cw * ch * 2)); +} + +/* ── display_draw_string / display_draw_string_s ── */ +void display_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg) +{ + display_draw_string_s(x, y, str, fg, bg, 1); +} + +void display_draw_string_s(int x, int y, const char *str, + uint16_t fg, uint16_t bg, int scale) +{ + int cx = x; + while (*str) { + draw_char_s(cx, y, *str++, fg, bg, scale); + cx += 6 * scale; + } +} + +/* ── display_draw_arc ── */ +void display_draw_arc(int cx, int cy, int r, int start_deg, int end_deg, + int thickness, uint16_t color) +{ + for (int deg = start_deg; deg <= end_deg; deg++) { + float rad = (float)deg * (3.14159265f / 180.0f); + int px = cx + (int)((float)r * cosf(rad)); + int py = cy + (int)((float)r * sinf(rad)); + int half = thickness / 2; + display_fill_rect(px - half, py - half, thickness, thickness, color); + } +} diff --git a/esp32s3/balance/main/gc9a01.h b/esp32s3/balance/main/gc9a01.h new file mode 100644 index 0000000..6c7eaa7 --- /dev/null +++ b/esp32s3/balance/main/gc9a01.h @@ -0,0 +1,24 @@ +#pragma once +/* gc9a01.h — GC9A01 240×240 round LCD SPI driver (bd-1yr8 display bead) */ + +#include + +/* ── Initialise SPI bus + GC9A01. Call once from app_main. ── */ +void gc9a01_init(void); + +/* ── Display primitives (also satisfy ota_display.h contract) ── */ +void display_fill_rect(int x, int y, int w, int h, uint16_t rgb565); +void display_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg); +void display_draw_string_s(int x, int y, const char *str, + uint16_t fg, uint16_t bg, int scale); +void display_draw_arc(int cx, int cy, int r, + int start_deg, int end_deg, int thickness, uint16_t color); + +/* ── Colour palette (RGB565) ── */ +#define COL_BG 0x0000u +#define COL_WHITE 0xFFFFu +#define COL_GREEN 0x07E0u +#define COL_YELLOW 0xFFE0u +#define COL_RED 0xF800u +#define COL_BLUE 0x001Fu +#define COL_ORANGE 0xFD20u diff --git a/esp32s3/balance/main/main.c b/esp32s3/balance/main/main.c index 518aa11..eb0c23a 100644 --- a/esp32s3/balance/main/main.c +++ b/esp32s3/balance/main/main.c @@ -10,18 +10,52 @@ #include "orin_serial.h" #include "vesc_can.h" +#include "gc9a01.h" #include "config.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_log.h" #include "esp_timer.h" +#include #include +#include static const char *TAG = "main"; static QueueHandle_t s_orin_tx_q; +/* ── HUD task: SAULT brand + battery voltage on GC9A01 display ── */ +static void hud_task(void *arg) +{ + /* Draw static "SAULT" header in orange — scale=3: each char 15×21 px */ + const int hs = 3; + int hx = (240 - 5 * 6 * hs) / 2; + display_draw_string_s(hx, 16, "SAULT", COL_ORANGE, COL_BG, hs); + + char prev[16] = ""; + for (;;) { + vTaskDelay(pdMS_TO_TICKS(1000)); + + int16_t v10 = g_vesc[0].voltage_x10; + char vstr[16]; + if (v10 <= 0) { + snprintf(vstr, sizeof(vstr), " --.-V"); + } else { + snprintf(vstr, sizeof(vstr), "%2d.%dV", v10 / 10, abs(v10 % 10)); + } + + if (strcmp(vstr, prev) != 0) { + const int vs = 4; /* scale=4: each char 20×28 px */ + int nch = (int)strlen(vstr); + int vx = (240 - nch * 6 * vs) / 2; + display_fill_rect(0, 95, 240, 7 * vs + 2, COL_BG); + display_draw_string_s(vx, 97, vstr, COL_WHITE, COL_BG, vs); + strncpy(prev, vstr, sizeof(prev) - 1); + } + } +} + /* ── Telemetry task: sends TELEM_STATUS to Orin at 10 Hz ── */ static void telem_task(void *arg) { @@ -114,9 +148,9 @@ void app_main(void) { ESP_LOGI(TAG, "ESP32-S3 BALANCE bd-66hx starting"); - /* Init CAN FIRST — TWAI must claim GPIO 43/44 before UART0 can grab them */ - vesc_can_init(); orin_serial_init(); + vesc_can_init(); + gc9a01_init(); /* TX queue for outbound serial frames */ s_orin_tx_q = xQueueCreate(ORIN_TX_QUEUE_DEPTH, sizeof(orin_tx_frame_t)); @@ -133,6 +167,7 @@ void app_main(void) xTaskCreate(vesc_can_rx_task, "vesc_rx", 4096, s_orin_tx_q, 10, NULL); xTaskCreate(telem_task, "telem", 2048, NULL, 5, NULL); xTaskCreate(drive_task, "drive", 2048, NULL, 8, NULL); + xTaskCreate(hud_task, "hud", 4096, NULL, 3, NULL); ESP_LOGI(TAG, "all tasks started"); /* app_main returns — FreeRTOS scheduler continues */