Compare commits

...

16 Commits

Author SHA1 Message Date
8d411e2603 fix: resolve boot loop — increase stack sizes, silence RPM spam logging
drive_task stack 2048→4096 (ESP_LOGI with 7 args overflowed 2048 frame).
vesc_can_send_rpm: ESP_LOGI→ESP_LOGD (was logging 100x/sec at 50Hz×2).
sdkconfig.defaults: add CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 (SPI init
call chain overflowed default 3584-byte main task stack).

Firmware confirmed stable on bd-66hx: 1 boot cycle in 12 seconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:29:47 -04:00
Sebastien Vayrette
a05de8d49a fix: Skip CAN tasks when TWAI init fails (no transceiver)
Prevents boot loop when ESP32 has no CAN transceiver connected.
CAN tasks only start if g_twai_bus_off is false after init.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:58:39 -04:00
Sebastien Vayrette
2622696772 fix: Make TWAI init non-fatal + add recovery backoff
TWAI init now logs error and sets g_twai_bus_off instead of panicking.
Bus-off recovery loop increased from 100ms to 1000ms to prevent
watchdog reset when no CAN transceiver is connected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:56:11 -04:00
Sebastien Vayrette
affaefea3a fix: Revert to 40MHz SPI, remove early fill (was causing boot loop)
80MHz SPI + immediate display_fill_rect in init caused RTC_SW_CPU_RST
boot loop. Revert to 40MHz and let hud_task handle first draw.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:46:42 -04:00
Sebastien Vayrette
a41c62440c fix: Add full-screen clear after GC9A01 init + diagnostic log
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:44:50 -04:00
Sebastien Vayrette
bdc69c87d8 fix: Bump GC9A01 SPI clock to 80MHz to match Waveshare reference
Waveshare demo code uses 80MHz SPI. Our driver was at 40MHz.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:41:54 -04:00
Sebastien Vayrette
23b3b9970f fix: Correct GC9A01 display GPIO pins for Waveshare ESP32-S3-LCD-1.28
All 6 display pins were wrong — mapped to arbitrary GPIOs instead of
the actual Waveshare board pinout: DC=8, CS=9, SCK=10, MOSI=11, RST=12, BL=40.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:25:41 -04:00
Sebastien Vayrette
8e66430c86 fix: CAN on GPIO 15/16, UART IO on 17/18 — actual hardware wiring
Tee confirmed physical wiring on Waveshare ESP32-S3 board:
- GPIO 15 = CAN TX (SN65HVD230 TXD)
- GPIO 16 = CAN RX (SN65HVD230 RXD)
- GPIO 17/18 = inter-board UART to ESP32 IO

Previous configs (GPIO 2/1, 43/44) were spec assumptions that didn't
match the actual board wiring. GPIO 43/44 are internal to PCB, not
on the header where the transceiver is connected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 12:17:39 -04:00
330c2ab4fe feat: GC9A01 display driver + SAULT/voltage HUD; fix UART GPIO regression
- 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 <noreply@anthropic.com>
2026-04-20 11:51:14 -04:00
Sebastien Vayrette
9b4a31aa66 fix: Init TWAI before UART0 to prevent GPIO 43/44 pin conflict
UART0 init was claiming GPIO 43/44 before TWAI could use them for CAN.
Swapping init order ensures TWAI gets GPIO 43/44 (where the SN65HVD230
transceiver is physically wired per Waveshare board design).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 11:48:43 -04:00
Sebastien Vayrette
06219afe69 fix: Move CAN TWAI to GPIO 43/44 where transceiver is actually wired
Diagnostic proved UART protocol works (ACKs received) but CAN has zero
communication. Root cause: ESP32 connects to Orin via USB Serial/JTAG
(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y), NOT UART0 on GPIO 43/44.
The SN65HVD230 CAN transceiver is still physically on GPIO 43/44
(original pre-bd-66hx wiring was never changed).

Fix: Put TWAI on GPIO 43/44 where the transceiver actually is.
Move unused UART0 pin config to GPIO 17/18 to avoid conflict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 11:38:44 -04:00
34a937628d fix: guard TWAI tx against bus-off and fix recovery state machine
Three bugs blocked CAN forwarding when UART commands were received:
1. vesc_can_send_rpm had no g_twai_bus_off guard, flooding failed
   twai_transmit calls during BUS_OFF/RECOVERING states.
2. Recovery only handled TWAI_STATE_BUS_OFF; RECOVERING and STOPPED
   states were unhandled, leaving g_twai_bus_off=false while TWAI
   was still unusable.
3. No startup delay after twai_start() — VESC not yet ready to ACK
   caused immediate TEC runup to BUS_OFF at boot.

Fix: bus-off guard in send_rpm, full state machine in rx_task
(BUS_OFF→initiate, STOPPED→start, RECOVERING→wait), 200ms post-
start delay in vesc_can_init().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 11:38:44 -04:00
Sebastien Vayrette
dd52982a03 feat: Motor test firmware — orin_serial, sdkconfig, CMakeLists cleanup, tilt config
- orin_serial: tighten packet framing, add RX stats
- sdkconfig.defaults: strip unused components for faster build
- CMakeLists.txt: condense SRCS list, drop redundant REQUIRES
- config.h: add TILT_CUTOFF_DEG 25.0f
- vesc_can.h: add tx/rx error counters extern declarations
- main.c: clarify file-header comment, log bd-66hx at startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 10:43:08 -04:00
Sebastien Vayrette
302dfea6f4 fix: Add g_twai_bus_off diagnostic, auto-arm for motor testing
- Define g_twai_bus_off in vesc_can.c, declare in vesc_can.h (was
  referenced in main.c but never defined — build would fail)
- Add TWAI bus-off detection in vesc_can_rx_task
- main.c already has armed=true bypass and 1Hz gate diagnostics
  (added by another agent) — now compiles cleanly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 23:01:24 -04:00
Sebastien Vayrette
bdbd7a7c3e fix: Restore correct VESC CAN IDs (56/68) in config.h
A linter reverted VESC_ID_A/B to old values 61/79. Correct IDs per
bd-wim1 protocol are 56 (left) and 68 (right).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 22:43:47 -04:00
Sebastien Vayrette
b0abc7a90d fix: Strip OTA and balance-safety code for motor test firmware
Remove all OTA subsystem (gitea_ota, ota_self, uart_ota, ota_display)
and balance-bot safety checks (tilt cutoff, BAL_TILT_FAULT) so the
firmware builds without cJSON/WiFi/HTTP dependencies. Core UART protocol,
VESC CAN drive, differential steering, and PID tuning remain intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 22:43:24 -04:00
10 changed files with 464 additions and 149 deletions

View File

@ -1,22 +1,5 @@
idf_component_register( idf_component_register(
SRCS SRCS "main.c" "orin_serial.c" "vesc_can.c" "gc9a01.c"
"main.c"
"orin_serial.c"
"vesc_can.c"
"gitea_ota.c"
"ota_self.c"
"uart_ota.c"
"ota_display.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES REQUIRES driver freertos esp_timer
esp_wifi
esp_http_client
esp_https_ota
nvs_flash
app_update
mbedtls
cJSON
driver
freertos
esp_timer
) )

View File

@ -11,23 +11,35 @@
* to IO2/IO1 when deploying this firmware. See docs/SAUL-TEE-SYSTEM-REFERENCE.md. * to IO2/IO1 when deploying this firmware. See docs/SAUL-TEE-SYSTEM-REFERENCE.md.
*/ */
/* ── Orin serial (CH343 USB-to-UART, 1a86:55d3 on Orin side) ── */ /* ── Orin serial: USB Serial/JTAG (native USB, /dev/ttyACM0 on Orin) ── */
#define ORIN_UART_PORT UART_NUM_0 #define ORIN_UART_PORT UART_NUM_0
#define ORIN_UART_BAUD 460800 #define ORIN_UART_BAUD 460800
#define ORIN_UART_TX_GPIO 43 /* ESP32→CH343 RXD */ #define ORIN_UART_TX_GPIO 43 /* unused — Orin uses USB-CDC */
#define ORIN_UART_RX_GPIO 44 /* CH343 TXD→ESP32 */ #define ORIN_UART_RX_GPIO 44 /* unused — Orin uses USB-CDC */
#define ORIN_UART_RX_BUF 1024 #define ORIN_UART_RX_BUF 1024
#define ORIN_TX_QUEUE_DEPTH 16 #define ORIN_TX_QUEUE_DEPTH 16
/* ── VESC CAN TWAI (SN65HVD230 transceiver, rewired for bd-66hx) ── */ /* ── Inter-board UART (ESP32 Balance ↔ ESP32 IO) ── */
#define VESC_CAN_TX_GPIO 2 /* ESP32 TWAI TX → SN65HVD230 TXD */ #define IO_UART_TX_GPIO 17
#define VESC_CAN_RX_GPIO 1 /* SN65HVD230 RXD → ESP32 TWAI RX */ #define IO_UART_RX_GPIO 18
/* ── VESC CAN TWAI (SN65HVD230 on Waveshare header pins) ── */
#define VESC_CAN_TX_GPIO 15 /* GPIO15 → SN65HVD230 TXD */
#define VESC_CAN_RX_GPIO 16 /* GPIO16 ← SN65HVD230 RXD */
#define VESC_CAN_RX_QUEUE 32 #define VESC_CAN_RX_QUEUE 32
/* VESC node IDs — matched to bd-wim1 TELEM_VESC_LEFT/RIGHT mapping */ /* VESC node IDs — matched to bd-wim1 TELEM_VESC_LEFT/RIGHT mapping */
#define VESC_ID_A 56u /* TELEM_VESC_LEFT (0x81) */ #define VESC_ID_A 56u /* TELEM_VESC_LEFT (0x81) */
#define VESC_ID_B 68u /* TELEM_VESC_RIGHT (0x82) */ #define VESC_ID_B 68u /* TELEM_VESC_RIGHT (0x82) */
/* ── GC9A01 240×240 round display (Waveshare ESP32-S3-LCD-1.28, SPI2) ── */
#define DISP_DC_GPIO 8
#define DISP_CS_GPIO 9
#define DISP_SCK_GPIO 10
#define DISP_MOSI_GPIO 11
#define DISP_RST_GPIO 12
#define DISP_BL_GPIO 40
/* ── Safety / timing ── */ /* ── Safety / timing ── */
#define HB_TIMEOUT_MS 500u /* heartbeat watchdog: disarm if exceeded */ #define HB_TIMEOUT_MS 500u /* heartbeat watchdog: disarm if exceeded */
#define DRIVE_TIMEOUT_MS 500u /* drive command staleness timeout */ #define DRIVE_TIMEOUT_MS 500u /* drive command staleness timeout */

View File

@ -0,0 +1,269 @@
/* 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 <string.h>
#include <math.h>
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: DC=%d CS=%d SCK=%d MOSI=%d RST=%d BL=%d",
DISP_DC_GPIO, DISP_CS_GPIO, DISP_SCK_GPIO, DISP_MOSI_GPIO,
DISP_RST_GPIO, DISP_BL_GPIO);
}
/* ── 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);
}
}

View File

@ -0,0 +1,24 @@
#pragma once
/* gc9a01.h — GC9A01 240×240 round LCD SPI driver (bd-1yr8 display bead) */
#include <stdint.h>
/* ── 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

View File

@ -1,23 +1,61 @@
/* main.c — ESP32-S3 BALANCE app_main (bd-66hx + OTA beads) */ /* main.c — ESP32-S3 BALANCE app_main (bd-66hx)
*
* Initializes Orin serial and VESC CAN TWAI, creates tasks:
* orin_rx parse incoming Orin commands
* orin_tx transmit queued serial frames
* vesc_rx receive VESC CAN telemetry, proxy to Orin
* telem periodic TELEM_STATUS to Orin @ 10 Hz
* drive apply Orin drive commands to VESCs via CAN
*/
#include "orin_serial.h" #include "orin_serial.h"
#include "vesc_can.h" #include "vesc_can.h"
#include "gitea_ota.h" #include "gc9a01.h"
#include "ota_self.h"
#include "uart_ota.h"
#include "ota_display.h"
#include "config.h" #include "config.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_timer.h" #include "esp_timer.h"
#include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h>
static const char *TAG = "main"; static const char *TAG = "main";
static QueueHandle_t s_orin_tx_q; 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 ── */ /* ── Telemetry task: sends TELEM_STATUS to Orin at 10 Hz ── */
static void telem_task(void *arg) static void telem_task(void *arg)
{ {
@ -37,9 +75,10 @@ static void telem_task(void *arg)
state = BAL_ARMED; state = BAL_ARMED;
} }
/* flags: bit0=estop_active, bit1=heartbeat_timeout */ /* flags: bit0=estop_active, bit1=heartbeat_timeout, bit2=twai_bus_off */
uint8_t flags = (g_orin_ctrl.estop ? 0x01u : 0x00u) | uint8_t flags = (g_orin_ctrl.estop ? 0x01u : 0x00u) |
(hb_timeout ? 0x02u : 0x00u); (hb_timeout ? 0x02u : 0x00u) |
(g_twai_bus_off ? 0x04u : 0x00u);
/* Battery voltage from VESC_ID_A STATUS_5 (V×10 → mV) */ /* Battery voltage from VESC_ID_A STATUS_5 (V×10 → mV) */
uint16_t vbat_mv = (uint16_t)((int32_t)g_vesc[0].voltage_x10 * 100); uint16_t vbat_mv = (uint16_t)((int32_t)g_vesc[0].voltage_x10 * 100);
@ -56,22 +95,47 @@ static void telem_task(void *arg)
/* ── Drive task: applies Orin drive commands to VESCs @ 50 Hz ── */ /* ── Drive task: applies Orin drive commands to VESCs @ 50 Hz ── */
static void drive_task(void *arg) static void drive_task(void *arg)
{ {
uint32_t log_tick = 0;
bool was_driving = false;
for (;;) { for (;;) {
vTaskDelay(pdMS_TO_TICKS(20)); /* 50 Hz */ vTaskDelay(pdMS_TO_TICKS(20)); /* 50 Hz */
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL); uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
bool hb_timeout = (now_ms - g_orin_ctrl.hb_last_ms) > HB_TIMEOUT_MS; bool hb_timeout = (now_ms - g_orin_ctrl.hb_last_ms) > HB_TIMEOUT_MS;
bool drive_stale = (now_ms - g_orin_drive.updated_ms) > DRIVE_TIMEOUT_MS; bool drive_stale = (now_ms - g_orin_drive.updated_ms) > DRIVE_TIMEOUT_MS;
bool gates_ok = g_orin_ctrl.armed && !g_orin_ctrl.estop &&
!hb_timeout && !drive_stale;
int32_t left_erpm = 0; int32_t left_erpm = 0;
int32_t right_erpm = 0; int32_t right_erpm = 0;
if (g_orin_ctrl.armed && !g_orin_ctrl.estop && if (gates_ok) {
!hb_timeout && !drive_stale) {
int32_t spd = (int32_t)g_orin_drive.speed * RPM_PER_SPEED_UNIT; int32_t spd = (int32_t)g_orin_drive.speed * RPM_PER_SPEED_UNIT;
int32_t str = (int32_t)g_orin_drive.steer * RPM_PER_STEER_UNIT; int32_t str = (int32_t)g_orin_drive.steer * RPM_PER_STEER_UNIT;
left_erpm = spd + str; left_erpm = spd + str;
right_erpm = spd - str; right_erpm = spd - str;
if (!was_driving) {
ESP_LOGI(TAG, "drive ENABLED: spd=%d str=%d L=%ld R=%ld",
g_orin_drive.speed, g_orin_drive.steer,
(long)left_erpm, (long)right_erpm);
was_driving = true;
}
} else if (was_driving) {
ESP_LOGW(TAG, "drive BLOCKED: armed=%d estop=%d hb_timeout=%d drive_stale=%d",
g_orin_ctrl.armed, g_orin_ctrl.estop, hb_timeout, drive_stale);
was_driving = false;
}
/* 1 Hz gate-state diagnostic */
if ((now_ms - log_tick) >= 1000u) {
log_tick = now_ms;
ESP_LOGI(TAG, "gate: armed=%d estop=%d hb_age=%lums drv_age=%lums twai_off=%d L=%ld R=%ld",
g_orin_ctrl.armed, g_orin_ctrl.estop,
(unsigned long)(now_ms - g_orin_ctrl.hb_last_ms),
(unsigned long)(now_ms - g_orin_drive.updated_ms),
(int)g_twai_bus_off,
(long)left_erpm, (long)right_erpm);
} }
/* VESC_ID_A (56) = LEFT, VESC_ID_B (68) = RIGHT per bd-wim1 protocol */ /* VESC_ID_A (56) = LEFT, VESC_ID_B (68) = RIGHT per bd-wim1 protocol */
@ -82,32 +146,32 @@ static void drive_task(void *arg)
void app_main(void) void app_main(void)
{ {
ESP_LOGI(TAG, "ESP32-S3 BALANCE starting"); ESP_LOGI(TAG, "ESP32-S3 BALANCE bd-66hx starting");
/* OTA rollback health check — must be called within OTA_ROLLBACK_WINDOW_S */
ota_self_health_check();
/* Init peripherals */
orin_serial_init(); orin_serial_init();
vesc_can_init(); vesc_can_init();
gc9a01_init();
/* TX queue for outbound serial frames */ /* TX queue for outbound serial frames */
s_orin_tx_q = xQueueCreate(ORIN_TX_QUEUE_DEPTH, sizeof(orin_tx_frame_t)); s_orin_tx_q = xQueueCreate(ORIN_TX_QUEUE_DEPTH, sizeof(orin_tx_frame_t));
configASSERT(s_orin_tx_q); configASSERT(s_orin_tx_q);
/* Seed heartbeat timer so we don't immediately timeout */ /* Seed timers so we don't immediately trip hb_timeout or drive_stale */
g_orin_ctrl.hb_last_ms = (uint32_t)(esp_timer_get_time() / 1000LL); g_orin_ctrl.hb_last_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
g_orin_drive.updated_ms = g_orin_ctrl.hb_last_ms;
g_orin_ctrl.armed = true; /* bypass for motor testing */
/* Create tasks */ /* Create tasks */
xTaskCreate(orin_serial_rx_task, "orin_rx", 4096, s_orin_tx_q, 10, NULL); xTaskCreate(orin_serial_rx_task, "orin_rx", 4096, s_orin_tx_q, 10, NULL);
xTaskCreate(orin_serial_tx_task, "orin_tx", 2048, s_orin_tx_q, 9, NULL); xTaskCreate(orin_serial_tx_task, "orin_tx", 2048, s_orin_tx_q, 9, NULL);
xTaskCreate(vesc_can_rx_task, "vesc_rx", 4096, s_orin_tx_q, 10, NULL); if (!g_twai_bus_off) {
xTaskCreate(vesc_can_rx_task, "vesc_rx", 4096, s_orin_tx_q, 10, NULL);
xTaskCreate(drive_task, "drive", 4096, NULL, 8, NULL);
} else {
ESP_LOGW(TAG, "CAN disabled — vesc_rx and drive tasks not started");
}
xTaskCreate(telem_task, "telem", 2048, NULL, 5, 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);
/* OTA subsystem — WiFi version checker + display overlay */
gitea_ota_init();
ota_display_init();
ESP_LOGI(TAG, "all tasks started"); ESP_LOGI(TAG, "all tasks started");
/* app_main returns — FreeRTOS scheduler continues */ /* app_main returns — FreeRTOS scheduler continues */

View File

@ -1,18 +1,17 @@
/* orin_serial.c — Orin↔ESP32-S3 serial protocol (bd-66hx + bd-1s1s OTA cmds) */ /* orin_serial.c — Orin↔ESP32-S3 serial protocol implementation (bd-66hx)
*
* Implements the binary framing protocol matching bd-wim1 (Orin side).
* CRC8-SMBUS: poly=0x07, init=0x00, covers LEN+TYPE+PAYLOAD bytes.
*/
#include "orin_serial.h" #include "orin_serial.h"
#include "config.h" #include "config.h"
#include "gitea_ota.h"
#include "ota_self.h"
#include "uart_ota.h"
#include "version.h"
#include "driver/uart.h" #include "driver/uart.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_timer.h" #include "esp_timer.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include <string.h> #include <string.h>
#include <stdio.h>
static const char *TAG = "orin"; static const char *TAG = "orin";
@ -207,46 +206,6 @@ static void dispatch_cmd(uint8_t type, const uint8_t *payload, uint8_t len,
orin_send_ack(tx_q, type); orin_send_ack(tx_q, type);
break; break;
case CMD_OTA_CHECK:
/* Trigger an immediate Gitea version check */
gitea_ota_check_now();
orin_send_version_info(tx_q, OTA_TARGET_BALANCE,
BALANCE_FW_VERSION,
g_balance_update.available
? g_balance_update.version : "");
orin_send_version_info(tx_q, OTA_TARGET_IO,
IO_FW_VERSION,
g_io_update.available
? g_io_update.version : "");
orin_send_ack(tx_q, type);
break;
case CMD_OTA_UPDATE:
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
{
uint8_t target = payload[0];
bool triggered = false;
if (target == OTA_TARGET_IO || target == OTA_TARGET_BOTH) {
if (!uart_ota_trigger()) {
orin_send_nack(tx_q, type,
g_io_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
break;
}
triggered = true;
}
if (target == OTA_TARGET_BALANCE || target == OTA_TARGET_BOTH) {
if (!ota_self_trigger()) {
if (!triggered) {
orin_send_nack(tx_q, type,
g_balance_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
break;
}
}
}
orin_send_ack(tx_q, type);
}
break;
default: default:
ESP_LOGW(TAG, "unknown cmd type=0x%02x", type); ESP_LOGW(TAG, "unknown cmd type=0x%02x", type);
break; break;
@ -331,24 +290,3 @@ void orin_serial_tx_task(void *arg)
} }
} }
} }
/* ── OTA telemetry helpers (bd-1s1s) ── */
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
uint8_t state, uint8_t progress, uint8_t err)
{
/* TELEM_OTA_STATUS: uint8 target, uint8 state, uint8 progress, uint8 err */
uint8_t p[4] = {target, state, progress, err};
enqueue(q, TELEM_OTA_STATUS, p, 4u);
}
void orin_send_version_info(QueueHandle_t q, uint8_t target,
const char *current, const char *available)
{
/* TELEM_VERSION_INFO: uint8 target, char current[16], char available[16] */
uint8_t p[33];
p[0] = target;
strncpy((char *)&p[1], current, 16); p[16] = '\0';
strncpy((char *)&p[17], available ? available : "", 16); p[32] = '\0';
enqueue(q, TELEM_VERSION_INFO, p, 33u);
}

View File

@ -29,27 +29,14 @@
#define TELEM_STATUS 0x80u /* status @ 10 Hz */ #define TELEM_STATUS 0x80u /* status @ 10 Hz */
#define TELEM_VESC_LEFT 0x81u /* VESC ID 56 telemetry @ 10 Hz */ #define TELEM_VESC_LEFT 0x81u /* VESC ID 56 telemetry @ 10 Hz */
#define TELEM_VESC_RIGHT 0x82u /* VESC ID 68 telemetry @ 10 Hz */ #define TELEM_VESC_RIGHT 0x82u /* VESC ID 68 telemetry @ 10 Hz */
#define TELEM_OTA_STATUS 0x83u /* OTA state + progress (bd-1s1s) */
#define TELEM_VERSION_INFO 0x84u /* firmware version report (bd-1s1s) */
#define RESP_ACK 0xA0u #define RESP_ACK 0xA0u
#define RESP_NACK 0xA1u #define RESP_NACK 0xA1u
/* ── OTA commands (Orin → ESP32, bd-1s1s) ── */
#define CMD_OTA_CHECK 0x10u /* no payload: trigger Gitea version check */
#define CMD_OTA_UPDATE 0x11u /* uint8 target: 0=balance, 1=io, 2=both */
/* ── OTA target constants ── */
#define OTA_TARGET_BALANCE 0x00u
#define OTA_TARGET_IO 0x01u
#define OTA_TARGET_BOTH 0x02u
/* ── NACK error codes ── */ /* ── NACK error codes ── */
#define ERR_BAD_CRC 0x01u #define ERR_BAD_CRC 0x01u
#define ERR_BAD_LEN 0x02u #define ERR_BAD_LEN 0x02u
#define ERR_ESTOP_ACTIVE 0x03u #define ERR_ESTOP_ACTIVE 0x03u
#define ERR_DISARMED 0x04u #define ERR_DISARMED 0x04u
#define ERR_OTA_BUSY 0x05u
#define ERR_OTA_NO_UPDATE 0x06u
/* ── Balance state (mirrored from TELEM_STATUS.balance_state) ── */ /* ── Balance state (mirrored from TELEM_STATUS.balance_state) ── */
typedef enum { typedef enum {
@ -105,9 +92,3 @@ void orin_send_vesc(QueueHandle_t q, uint8_t telem_type,
int16_t current_ma, uint16_t temp_c_x10); int16_t current_ma, uint16_t temp_c_x10);
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type); void orin_send_ack(QueueHandle_t q, uint8_t cmd_type);
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err); void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err);
/* OTA telemetry helpers (bd-1s1s) */
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
uint8_t state, uint8_t progress, uint8_t err);
void orin_send_version_info(QueueHandle_t q, uint8_t target,
const char *current, const char *available);

View File

@ -12,11 +12,13 @@
#include "esp_timer.h" #include "esp_timer.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include <inttypes.h>
#include <string.h> #include <string.h>
static const char *TAG = "vesc_can"; static const char *TAG = "vesc_can";
vesc_state_t g_vesc[2] = {0}; vesc_state_t g_vesc[2] = {0};
volatile bool g_twai_bus_off = false;
/* Index for a given VESC node ID: 0=VESC_ID_A, 1=VESC_ID_B */ /* Index for a given VESC node ID: 0=VESC_ID_A, 1=VESC_ID_B */
static int vesc_idx(uint8_t id) static int vesc_idx(uint8_t id)
@ -33,17 +35,32 @@ void vesc_can_init(void)
(gpio_num_t)VESC_CAN_RX_GPIO, (gpio_num_t)VESC_CAN_RX_GPIO,
TWAI_MODE_NORMAL); TWAI_MODE_NORMAL);
gcfg.rx_queue_len = VESC_CAN_RX_QUEUE; gcfg.rx_queue_len = VESC_CAN_RX_QUEUE;
gcfg.tx_queue_len = 5;
twai_timing_config_t tcfg = TWAI_TIMING_CONFIG_500KBITS(); twai_timing_config_t tcfg = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t fcfg = TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_filter_config_t fcfg = TWAI_FILTER_CONFIG_ACCEPT_ALL();
ESP_ERROR_CHECK(twai_driver_install(&gcfg, &tcfg, &fcfg)); ESP_LOGI(TAG, "TWAI: installing driver tx=%d rx=%d 500kbps", VESC_CAN_TX_GPIO, VESC_CAN_RX_GPIO);
ESP_ERROR_CHECK(twai_start()); esp_err_t err = twai_driver_install(&gcfg, &tcfg, &fcfg);
ESP_LOGI(TAG, "TWAI init OK: tx=%d rx=%d 500kbps", VESC_CAN_TX_GPIO, VESC_CAN_RX_GPIO); if (err != ESP_OK) {
ESP_LOGE(TAG, "TWAI install failed (0x%x) — CAN disabled", err);
g_twai_bus_off = true;
return;
}
err = twai_start();
if (err != ESP_OK) {
ESP_LOGE(TAG, "TWAI start failed (0x%x) — CAN disabled", err);
g_twai_bus_off = true;
return;
}
vTaskDelay(pdMS_TO_TICKS(200));
ESP_LOGI(TAG, "TWAI: started OK — bus active");
} }
void vesc_can_send_rpm(uint8_t vesc_id, int32_t erpm) void vesc_can_send_rpm(uint8_t vesc_id, int32_t erpm)
{ {
if (g_twai_bus_off) { return; }
uint32_t ext_id = ((uint32_t)VESC_PKT_SET_RPM << 8u) | vesc_id; uint32_t ext_id = ((uint32_t)VESC_PKT_SET_RPM << 8u) | vesc_id;
twai_message_t msg = { twai_message_t msg = {
.extd = 1, .extd = 1,
@ -55,7 +72,11 @@ void vesc_can_send_rpm(uint8_t vesc_id, int32_t erpm)
msg.data[1] = (uint8_t)(u >> 16u); msg.data[1] = (uint8_t)(u >> 16u);
msg.data[2] = (uint8_t)(u >> 8u); msg.data[2] = (uint8_t)(u >> 8u);
msg.data[3] = (uint8_t)(u); msg.data[3] = (uint8_t)(u);
twai_transmit(&msg, pdMS_TO_TICKS(5)); ESP_LOGD(TAG, "send_rpm vesc_id=%u erpm=%" PRId32 " ext_id=0x%08" PRIx32, vesc_id, erpm, ext_id);
esp_err_t err = twai_transmit(&msg, pdMS_TO_TICKS(5));
if (err != ESP_OK) {
ESP_LOGW(TAG, "twai_transmit failed vesc_id=%u err=0x%x", vesc_id, err);
}
} }
void vesc_can_rx_task(void *arg) void vesc_can_rx_task(void *arg)
@ -64,9 +85,37 @@ void vesc_can_rx_task(void *arg)
twai_message_t msg; twai_message_t msg;
for (;;) { for (;;) {
if (twai_receive(&msg, pdMS_TO_TICKS(50)) != ESP_OK) { esp_err_t rx_err = twai_receive(&msg, pdMS_TO_TICKS(50));
if (rx_err != ESP_OK) {
if (rx_err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "twai_receive err=0x%x", rx_err);
}
twai_status_info_t si;
if (twai_get_status_info(&si) == ESP_OK) {
/* Mark bus-off for ANY non-running state so vesc_can_send_rpm
* won't flood with failed transmits during recovery. */
g_twai_bus_off = (si.state != TWAI_STATE_RUNNING);
if (si.state == TWAI_STATE_BUS_OFF) {
ESP_LOGE(TAG, "TWAI BUS OFF tx_err=%lu rx_err=%lu — recovering",
(unsigned long)si.tx_error_counter, (unsigned long)si.rx_error_counter);
twai_initiate_recovery();
vTaskDelay(pdMS_TO_TICKS(1000));
} else if (si.state == TWAI_STATE_STOPPED) {
esp_err_t serr = twai_start();
if (serr == ESP_OK) {
g_twai_bus_off = false;
ESP_LOGI(TAG, "TWAI recovered — bus active");
} else {
ESP_LOGE(TAG, "TWAI restart failed 0x%x — backing off", serr);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/* TWAI_STATE_RECOVERING: initiation already called, just wait. */
}
continue; continue;
} }
ESP_LOGI(TAG, "twai_receive OK id=0x%08" PRIx32 " dlc=%u", msg.identifier, msg.data_length_code);
if (!msg.extd) { if (!msg.extd) {
continue; /* ignore standard frames */ continue; /* ignore standard frames */
} }

View File

@ -27,6 +27,10 @@ typedef struct {
/* ── Globals (two VESC nodes: index 0 = VESC_ID_A=56, 1 = VESC_ID_B=68) ── */ /* ── Globals (two VESC nodes: index 0 = VESC_ID_A=56, 1 = VESC_ID_B=68) ── */
extern vesc_state_t g_vesc[2]; extern vesc_state_t g_vesc[2];
/* TWAI bus health — set by vesc_can_rx_task, read by telem_task for flags bit2 */
extern volatile bool g_twai_bus_off;
extern volatile uint32_t g_twai_tx_err_count;
extern volatile uint32_t g_twai_rx_err_count;
/* ── API ── */ /* ── API ── */
void vesc_can_init(void); void vesc_can_init(void);

View File

@ -5,15 +5,6 @@ CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
CONFIG_TWAI_ISR_IN_IRAM=y CONFIG_TWAI_ISR_IN_IRAM=y
CONFIG_UART_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y
CONFIG_ESP_CONSOLE_UART_DEFAULT=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
CONFIG_LOG_DEFAULT_LEVEL_INFO=y CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
# OTA — bd-3gwo: dual OTA partitions + rollback
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_OTA_ALLOW_HTTP=y
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y