Replaces Orin↔ESP32-S3 BALANCE CAN comms (0x300-0x303 / 0x400-0x401)
with binary serial framing over CH343 USB-CDC at 460800 baud.
Protocol matches bd-wim1 (sl-perception) exactly:
Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8-SMBUS]
CRC covers LEN+TYPE+PAYLOAD, big-endian multi-byte fields.
Commands (Orin→ESP32): HEARTBEAT/DRIVE/ESTOP/ARM/PID
Telemetry (ESP32→Orin): TELEM_STATUS, TELEM_VESC_LEFT (ID 56),
TELEM_VESC_RIGHT (ID 68), ACK/NACK
VESC CAN TWAI kept for motor control; drive commands from Orin
forwarded to VESCs via SET_RPM. Hardware note: SN65HVD230
rewired from IO43/44 to IO2/IO1 to free IO43/44 for CH343.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
293 lines
9.3 KiB
C
293 lines
9.3 KiB
C
/* 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 "config.h"
|
|
#include "driver/uart.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/queue.h"
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "orin";
|
|
|
|
/* ── Shared state ── */
|
|
orin_drive_t g_orin_drive = {0};
|
|
orin_pid_t g_orin_pid = {0};
|
|
orin_control_t g_orin_ctrl = {.armed = false, .estop = false, .hb_last_ms = 0};
|
|
|
|
/* ── CRC8-SMBUS (poly=0x07, init=0x00) ── */
|
|
static uint8_t crc8(const uint8_t *data, uint8_t len)
|
|
{
|
|
uint8_t crc = 0x00u;
|
|
for (uint8_t i = 0; i < len; i++) {
|
|
crc ^= data[i];
|
|
for (uint8_t b = 0; b < 8u; b++) {
|
|
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
|
|
}
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
/* ── Frame builder ── */
|
|
static void build_frame(orin_tx_frame_t *f, uint8_t out[/* ORIN_MAX_PAYLOAD + 4 */], uint8_t *out_len)
|
|
{
|
|
/* [SYNC][LEN][TYPE][PAYLOAD...][CRC] */
|
|
uint8_t crc_buf[2u + ORIN_MAX_PAYLOAD];
|
|
crc_buf[0] = f->len;
|
|
crc_buf[1] = f->type;
|
|
memcpy(&crc_buf[2], f->payload, f->len);
|
|
uint8_t crc = crc8(crc_buf, (uint8_t)(2u + f->len));
|
|
|
|
out[0] = ORIN_SYNC;
|
|
out[1] = f->len;
|
|
out[2] = f->type;
|
|
memcpy(&out[3], f->payload, f->len);
|
|
out[3u + f->len] = crc;
|
|
*out_len = (uint8_t)(4u + f->len);
|
|
}
|
|
|
|
/* ── Enqueue helpers ── */
|
|
static void enqueue(QueueHandle_t q, uint8_t type, const uint8_t *payload, uint8_t len)
|
|
{
|
|
orin_tx_frame_t f = {.type = type, .len = len};
|
|
if (len > 0u && payload) {
|
|
memcpy(f.payload, payload, len);
|
|
}
|
|
if (xQueueSend(q, &f, 0) != pdTRUE) {
|
|
ESP_LOGW(TAG, "tx queue full, dropped type=0x%02x", type);
|
|
}
|
|
}
|
|
|
|
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type)
|
|
{
|
|
enqueue(q, RESP_ACK, &cmd_type, 1u);
|
|
}
|
|
|
|
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err)
|
|
{
|
|
uint8_t p[2] = {cmd_type, err};
|
|
enqueue(q, RESP_NACK, p, 2u);
|
|
}
|
|
|
|
void orin_send_status(QueueHandle_t q,
|
|
int16_t pitch_x10, int16_t motor_cmd,
|
|
uint16_t vbat_mv, bal_state_t state, uint8_t flags)
|
|
{
|
|
/* int16 pitch_x10, int16 motor_cmd, uint16 vbat_mv, uint8 state, uint8 flags — BE */
|
|
uint8_t p[8];
|
|
p[0] = (uint8_t)((uint16_t)pitch_x10 >> 8u);
|
|
p[1] = (uint8_t)((uint16_t)pitch_x10);
|
|
p[2] = (uint8_t)((uint16_t)motor_cmd >> 8u);
|
|
p[3] = (uint8_t)((uint16_t)motor_cmd);
|
|
p[4] = (uint8_t)(vbat_mv >> 8u);
|
|
p[5] = (uint8_t)(vbat_mv);
|
|
p[6] = (uint8_t)state;
|
|
p[7] = flags;
|
|
enqueue(q, TELEM_STATUS, p, 8u);
|
|
}
|
|
|
|
void orin_send_vesc(QueueHandle_t q, uint8_t telem_type,
|
|
int32_t erpm, uint16_t voltage_mv,
|
|
int16_t current_ma, uint16_t temp_c_x10)
|
|
{
|
|
/* int32 erpm, uint16 voltage_mv, int16 current_ma, uint16 temp_c_x10 — BE */
|
|
uint8_t p[10];
|
|
uint32_t u = (uint32_t)erpm;
|
|
p[0] = (uint8_t)(u >> 24u);
|
|
p[1] = (uint8_t)(u >> 16u);
|
|
p[2] = (uint8_t)(u >> 8u);
|
|
p[3] = (uint8_t)(u);
|
|
p[4] = (uint8_t)(voltage_mv >> 8u);
|
|
p[5] = (uint8_t)(voltage_mv);
|
|
p[6] = (uint8_t)((uint16_t)current_ma >> 8u);
|
|
p[7] = (uint8_t)((uint16_t)current_ma);
|
|
p[8] = (uint8_t)(temp_c_x10 >> 8u);
|
|
p[9] = (uint8_t)(temp_c_x10);
|
|
enqueue(q, telem_type, p, 10u);
|
|
}
|
|
|
|
/* ── UART init ── */
|
|
void orin_serial_init(void)
|
|
{
|
|
uart_config_t cfg = {
|
|
.baud_rate = ORIN_UART_BAUD,
|
|
.data_bits = UART_DATA_8_BITS,
|
|
.parity = UART_PARITY_DISABLE,
|
|
.stop_bits = UART_STOP_BITS_1,
|
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
|
};
|
|
ESP_ERROR_CHECK(uart_param_config(ORIN_UART_PORT, &cfg));
|
|
ESP_ERROR_CHECK(uart_set_pin(ORIN_UART_PORT,
|
|
ORIN_UART_TX_GPIO, ORIN_UART_RX_GPIO,
|
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
|
ESP_ERROR_CHECK(uart_driver_install(ORIN_UART_PORT, ORIN_UART_RX_BUF, 0,
|
|
0, NULL, 0));
|
|
ESP_LOGI(TAG, "UART%d init OK: tx=%d rx=%d baud=%d",
|
|
ORIN_UART_PORT, ORIN_UART_TX_GPIO, ORIN_UART_RX_GPIO, ORIN_UART_BAUD);
|
|
}
|
|
|
|
/* ── RX parser state machine ── */
|
|
typedef enum {
|
|
WAIT_SYNC,
|
|
WAIT_LEN,
|
|
WAIT_TYPE,
|
|
WAIT_PAYLOAD,
|
|
WAIT_CRC,
|
|
} rx_state_t;
|
|
|
|
static void dispatch_cmd(uint8_t type, const uint8_t *payload, uint8_t len,
|
|
QueueHandle_t tx_q)
|
|
{
|
|
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
|
|
|
|
switch (type) {
|
|
case CMD_HEARTBEAT:
|
|
g_orin_ctrl.hb_last_ms = now_ms;
|
|
orin_send_ack(tx_q, type);
|
|
break;
|
|
|
|
case CMD_DRIVE:
|
|
if (len < 4u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
|
|
if (g_orin_ctrl.estop) { orin_send_nack(tx_q, type, ERR_ESTOP_ACTIVE); break; }
|
|
if (!g_orin_ctrl.armed) { orin_send_nack(tx_q, type, ERR_DISARMED); break; }
|
|
g_orin_drive.speed = (int16_t)(((uint16_t)payload[0] << 8u) | payload[1]);
|
|
g_orin_drive.steer = (int16_t)(((uint16_t)payload[2] << 8u) | payload[3]);
|
|
g_orin_drive.updated_ms = now_ms;
|
|
g_orin_ctrl.hb_last_ms = now_ms; /* drive counts as heartbeat */
|
|
orin_send_ack(tx_q, type);
|
|
break;
|
|
|
|
case CMD_ESTOP:
|
|
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
|
|
g_orin_ctrl.estop = (payload[0] != 0u);
|
|
if (g_orin_ctrl.estop) {
|
|
g_orin_drive.speed = 0;
|
|
g_orin_drive.steer = 0;
|
|
}
|
|
orin_send_ack(tx_q, type);
|
|
break;
|
|
|
|
case CMD_ARM:
|
|
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
|
|
if (g_orin_ctrl.estop && payload[0] != 0u) {
|
|
/* cannot arm while estop is active */
|
|
orin_send_nack(tx_q, type, ERR_ESTOP_ACTIVE);
|
|
break;
|
|
}
|
|
g_orin_ctrl.armed = (payload[0] != 0u);
|
|
if (!g_orin_ctrl.armed) {
|
|
g_orin_drive.speed = 0;
|
|
g_orin_drive.steer = 0;
|
|
}
|
|
orin_send_ack(tx_q, type);
|
|
break;
|
|
|
|
case CMD_PID:
|
|
if (len < 12u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
|
|
/* float32 big-endian: copy and swap bytes */
|
|
{
|
|
uint32_t raw;
|
|
raw = ((uint32_t)payload[0] << 24u) | ((uint32_t)payload[1] << 16u) |
|
|
((uint32_t)payload[2] << 8u) | (uint32_t)payload[3];
|
|
memcpy((void*)&g_orin_pid.kp, &raw, 4u);
|
|
raw = ((uint32_t)payload[4] << 24u) | ((uint32_t)payload[5] << 16u) |
|
|
((uint32_t)payload[6] << 8u) | (uint32_t)payload[7];
|
|
memcpy((void*)&g_orin_pid.ki, &raw, 4u);
|
|
raw = ((uint32_t)payload[8] << 24u) | ((uint32_t)payload[9] << 16u) |
|
|
((uint32_t)payload[10] << 8u) | (uint32_t)payload[11];
|
|
memcpy((void*)&g_orin_pid.kd, &raw, 4u);
|
|
g_orin_pid.updated = true;
|
|
}
|
|
orin_send_ack(tx_q, type);
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGW(TAG, "unknown cmd type=0x%02x", type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void orin_serial_rx_task(void *arg)
|
|
{
|
|
QueueHandle_t tx_q = (QueueHandle_t)arg;
|
|
rx_state_t state = WAIT_SYNC;
|
|
uint8_t rx_len = 0;
|
|
uint8_t rx_type = 0;
|
|
uint8_t payload[ORIN_MAX_PAYLOAD];
|
|
uint8_t pay_idx = 0;
|
|
|
|
uint8_t byte;
|
|
for (;;) {
|
|
int r = uart_read_bytes(ORIN_UART_PORT, &byte, 1, pdMS_TO_TICKS(10));
|
|
if (r <= 0) {
|
|
continue;
|
|
}
|
|
|
|
switch (state) {
|
|
case WAIT_SYNC:
|
|
if (byte == ORIN_SYNC) { state = WAIT_LEN; }
|
|
break;
|
|
|
|
case WAIT_LEN:
|
|
if (byte > ORIN_MAX_PAYLOAD) {
|
|
/* oversize — send NACK and reset */
|
|
orin_send_nack(tx_q, 0x00u, ERR_BAD_LEN);
|
|
state = WAIT_SYNC;
|
|
} else {
|
|
rx_len = byte;
|
|
state = WAIT_TYPE;
|
|
}
|
|
break;
|
|
|
|
case WAIT_TYPE:
|
|
rx_type = byte;
|
|
pay_idx = 0u;
|
|
state = (rx_len == 0u) ? WAIT_CRC : WAIT_PAYLOAD;
|
|
break;
|
|
|
|
case WAIT_PAYLOAD:
|
|
payload[pay_idx++] = byte;
|
|
if (pay_idx == rx_len) { state = WAIT_CRC; }
|
|
break;
|
|
|
|
case WAIT_CRC: {
|
|
/* Verify CRC over [LEN, TYPE, PAYLOAD] */
|
|
uint8_t crc_buf[2u + ORIN_MAX_PAYLOAD];
|
|
crc_buf[0] = rx_len;
|
|
crc_buf[1] = rx_type;
|
|
memcpy(&crc_buf[2], payload, rx_len);
|
|
uint8_t expected = crc8(crc_buf, (uint8_t)(2u + rx_len));
|
|
if (byte != expected) {
|
|
ESP_LOGW(TAG, "CRC fail type=0x%02x got=0x%02x exp=0x%02x",
|
|
rx_type, byte, expected);
|
|
orin_send_nack(tx_q, rx_type, ERR_BAD_CRC);
|
|
} else {
|
|
dispatch_cmd(rx_type, payload, rx_len, tx_q);
|
|
}
|
|
state = WAIT_SYNC;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void orin_serial_tx_task(void *arg)
|
|
{
|
|
QueueHandle_t tx_q = (QueueHandle_t)arg;
|
|
orin_tx_frame_t f;
|
|
uint8_t wire[4u + ORIN_MAX_PAYLOAD];
|
|
uint8_t wire_len;
|
|
|
|
for (;;) {
|
|
if (xQueueReceive(tx_q, &f, portMAX_DELAY) == pdTRUE) {
|
|
build_frame(&f, wire, &wire_len);
|
|
uart_write_bytes(ORIN_UART_PORT, (const char *)wire, wire_len);
|
|
}
|
|
}
|
|
}
|