saltylab-firmware/src/can_driver.c
sl-firmware 8bc2f9eeac feat: bxCAN integration for VESC motor control and Orin comms (Issue #674)
- can_driver: add filter bank 15 (all ext IDs → FIFO1) and widen bank 14
  to accept all standard IDs; add can_driver_send_ext/std and ext/std
  frame callbacks (can_driver_set_ext_cb / can_driver_set_std_cb)
- vesc_can: VESC 29-bit extended CAN protocol driver — send RPM to IDs 56
  and 68 (FSESC 6.7 Pro Mini Dual), parse STATUS/STATUS_4/STATUS_5
  big-endian payloads, alive timeout, JLINK_TLM_VESC_STATE at 1 Hz
- orin_can: Orin↔FC standard CAN protocol — HEARTBEAT/DRIVE/MODE/ESTOP
  commands in, FC_STATUS + FC_VESC broadcast at 10 Hz
- jlink: add JLINK_TLM_VESC_STATE (0x8E), jlink_tlm_vesc_state_t (22 bytes),
  jlink_send_vesc_state_tlm()
- main: wire vesc_can_init/orin_can_init; replace can_driver_send_cmd with
  vesc_can_send_rpm; inject Orin CAN speed/steer into balance PID; add
  Orin CAN estop/clear handling; add orin_can_broadcast at 10 Hz
- test: 56-test host-side suite for vesc_can; test/stubs/stm32f7xx_hal.h
  minimal HAL stub for all future host-side tests

Safety: balance PID runs independently on Mamba — if Orin CAN link drops
(orin_can_is_alive() == false) the robot continues balancing in-place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 19:10:11 -04:00

304 lines
9.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* CAN bus driver for BLDC motor controllers (Issue #597)
* CAN2 on PB12 (RX, AF9) / PB13 (TX, AF9) at 500 kbps
* Filter bank 14 (first CAN2 bank; SlaveStartFilterBank=14)
*/
#include "can_driver.h"
#include "stm32f7xx_hal.h"
#include <string.h>
static CAN_HandleTypeDef s_can;
static volatile can_feedback_t s_feedback[CAN_NUM_MOTORS];
static volatile can_stats_t s_stats;
static can_ext_frame_cb_t s_ext_cb = NULL;
static can_std_frame_cb_t s_std_cb = NULL;
void can_driver_init(void)
{
/* CAN2 requires CAN1 master clock for shared filter RAM */
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_CAN2_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* PB12 = CAN2_RX, PB13 = CAN2_TX, AF9 */
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_12 | GPIO_PIN_13;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF9_CAN2;
HAL_GPIO_Init(GPIOB, &gpio);
/* 500 kbps @ APB1=54 MHz: PSC=6, BS1=13tq, BS2=4tq, SJW=1tq
* bit_time = 6 × (1+13+4) / 54000000 = 2 µs → 500 kbps
* sample point = (1+13)/18 = 77.8% */
s_can.Instance = CAN2;
s_can.Init.Prescaler = CAN_PRESCALER;
s_can.Init.Mode = CAN_MODE_NORMAL;
s_can.Init.SyncJumpWidth = CAN_SJW_1TQ;
s_can.Init.TimeSeg1 = CAN_BS1_13TQ;
s_can.Init.TimeSeg2 = CAN_BS2_4TQ;
s_can.Init.TimeTriggeredMode = DISABLE;
s_can.Init.AutoBusOff = ENABLE; /* HW recovery after 128×11 bits */
s_can.Init.AutoWakeUp = DISABLE;
s_can.Init.AutoRetransmission = ENABLE;
s_can.Init.ReceiveFifoLocked = DISABLE;
s_can.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&s_can) != HAL_OK) {
s_stats.bus_off = 1u;
return;
}
/* Filter bank 14: 32-bit mask, FIFO0, accept ALL standard 11-bit frames.
* FilterIdHigh=0, FilterMaskHigh=0 → mask=0 passes every standard ID.
* FilterIdLow[2]=0 (IDE=0) → only standard frames; mask bit[2]=0 → don't
* check IDE bit, but standard frames have IDE=0 by design so all pass. */
CAN_FilterTypeDef flt = {0};
flt.FilterBank = 14u;
flt.FilterMode = CAN_FILTERMODE_IDMASK;
flt.FilterScale = CAN_FILTERSCALE_32BIT;
flt.FilterIdHigh = 0u;
flt.FilterIdLow = 0u;
flt.FilterMaskIdHigh = 0u;
flt.FilterMaskIdLow = 0u;
flt.FilterFIFOAssignment = CAN_RX_FIFO0;
flt.FilterActivation = CAN_FILTER_ENABLE;
flt.SlaveStartFilterBank = 14u;
if (HAL_CAN_ConfigFilter(&s_can, &flt) != HAL_OK) {
s_stats.bus_off = 1u;
return;
}
/* Filter bank 15: 32-bit mask, FIFO1, accept ALL extended 29-bit frames.
* For extended frames, the IDE bit sits at FilterIdLow[2].
* FilterIdLow = 0x0004 (IDE=1) → match extended frames.
* FilterMaskIdLow = 0x0004 → only the IDE bit is checked; all ext IDs pass.
* This routes every VESC STATUS / STATUS_4 / STATUS_5 frame to FIFO1. */
CAN_FilterTypeDef flt2 = {0};
flt2.FilterBank = 15u;
flt2.FilterMode = CAN_FILTERMODE_IDMASK;
flt2.FilterScale = CAN_FILTERSCALE_32BIT;
flt2.FilterIdHigh = 0u;
flt2.FilterIdLow = 0x0004u; /* IDE = 1 */
flt2.FilterMaskIdHigh = 0u;
flt2.FilterMaskIdLow = 0x0004u; /* only check IDE bit */
flt2.FilterFIFOAssignment = CAN_RX_FIFO1;
flt2.FilterActivation = CAN_FILTER_ENABLE;
flt2.SlaveStartFilterBank = 14u;
if (HAL_CAN_ConfigFilter(&s_can, &flt2) != HAL_OK) {
s_stats.bus_off = 1u;
return;
}
HAL_CAN_Start(&s_can);
memset((void *)s_feedback, 0, sizeof(s_feedback));
memset((void *)&s_stats, 0, sizeof(s_stats));
}
void can_driver_send_cmd(uint8_t node_id, const can_cmd_t *cmd)
{
if (node_id >= CAN_NUM_MOTORS || s_stats.bus_off) {
return;
}
/* Payload: [vel_lo, vel_hi, torque_lo, torque_hi] — little-endian */
uint8_t data[4];
data[0] = (uint8_t)((uint16_t)cmd->velocity_rpm & 0xFFu);
data[1] = (uint8_t)((uint16_t)cmd->velocity_rpm >> 8u);
data[2] = (uint8_t)((uint16_t)cmd->torque_x100 & 0xFFu);
data[3] = (uint8_t)((uint16_t)cmd->torque_x100 >> 8u);
CAN_TxHeaderTypeDef hdr = {0};
hdr.StdId = CAN_ID_VEL_CMD_BASE + (uint32_t)node_id;
hdr.IDE = CAN_ID_STD;
hdr.RTR = CAN_RTR_DATA;
hdr.DLC = 4u;
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(&s_can) > 0u) {
if (HAL_CAN_AddTxMessage(&s_can, &hdr, data, &mailbox) == HAL_OK) {
s_stats.tx_count++;
} else {
s_stats.err_count++;
}
}
}
void can_driver_send_enable(uint8_t node_id, bool enable)
{
if (node_id >= CAN_NUM_MOTORS || s_stats.bus_off) {
return;
}
uint8_t data[1] = { enable ? 1u : 0u };
CAN_TxHeaderTypeDef hdr = {0};
hdr.StdId = CAN_ID_ENABLE_CMD_BASE + (uint32_t)node_id;
hdr.IDE = CAN_ID_STD;
hdr.RTR = CAN_RTR_DATA;
hdr.DLC = 1u;
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(&s_can) > 0u) {
if (HAL_CAN_AddTxMessage(&s_can, &hdr, data, &mailbox) == HAL_OK) {
s_stats.tx_count++;
} else {
s_stats.err_count++;
}
}
}
void can_driver_process(void)
{
/* Check for bus-off */
if (s_can.Instance->ESR & CAN_ESR_BOFF) {
s_stats.bus_off = 1u;
return;
}
s_stats.bus_off = 0u;
/* Drain FIFO0 (standard-ID frames: Orin commands + legacy feedback) */
while (HAL_CAN_GetRxFifoFillLevel(&s_can, CAN_RX_FIFO0) > 0u) {
CAN_RxHeaderTypeDef rxhdr;
uint8_t rxdata[8];
if (HAL_CAN_GetRxMessage(&s_can, CAN_RX_FIFO0, &rxhdr, rxdata) != HAL_OK) {
s_stats.err_count++;
break;
}
if (rxhdr.IDE != CAN_ID_STD || rxhdr.RTR != CAN_RTR_DATA) {
continue;
}
/* Dispatch to registered standard-frame callback (Orin CAN protocol) */
if (s_std_cb != NULL) {
s_std_cb((uint16_t)rxhdr.StdId, rxdata, (uint8_t)rxhdr.DLC);
}
/* Legacy: decode feedback frames at 0x200-0x21F (Issue #597) */
uint32_t nid_u = rxhdr.StdId - CAN_ID_FEEDBACK_BASE;
if (nid_u < CAN_NUM_MOTORS && rxhdr.DLC >= 8u) {
uint8_t nid = (uint8_t)nid_u;
s_feedback[nid].velocity_rpm = (int16_t)((uint16_t)rxdata[0] | ((uint16_t)rxdata[1] << 8u));
s_feedback[nid].current_ma = (int16_t)((uint16_t)rxdata[2] | ((uint16_t)rxdata[3] << 8u));
s_feedback[nid].position_x100 = (int16_t)((uint16_t)rxdata[4] | ((uint16_t)rxdata[5] << 8u));
s_feedback[nid].temperature_c = (int8_t)rxdata[6];
s_feedback[nid].fault = rxdata[7];
s_feedback[nid].last_rx_ms = HAL_GetTick();
}
s_stats.rx_count++;
}
/* Drain FIFO1 (extended-ID frames: VESC STATUS/STATUS_4/STATUS_5) */
while (HAL_CAN_GetRxFifoFillLevel(&s_can, CAN_RX_FIFO1) > 0u) {
CAN_RxHeaderTypeDef rxhdr;
uint8_t rxdata[8];
if (HAL_CAN_GetRxMessage(&s_can, CAN_RX_FIFO1, &rxhdr, rxdata) != HAL_OK) {
s_stats.err_count++;
break;
}
if (rxhdr.IDE != CAN_ID_EXT || rxhdr.RTR != CAN_RTR_DATA) {
continue;
}
if (s_ext_cb != NULL) {
s_ext_cb(rxhdr.ExtId, rxdata, (uint8_t)rxhdr.DLC);
}
s_stats.rx_count++;
}
}
void can_driver_set_ext_cb(can_ext_frame_cb_t cb)
{
s_ext_cb = cb;
}
void can_driver_set_std_cb(can_std_frame_cb_t cb)
{
s_std_cb = cb;
}
void can_driver_send_ext(uint32_t ext_id, const uint8_t *data, uint8_t len)
{
if (s_stats.bus_off || len > 8u) {
return;
}
CAN_TxHeaderTypeDef hdr = {0};
hdr.ExtId = ext_id;
hdr.IDE = CAN_ID_EXT;
hdr.RTR = CAN_RTR_DATA;
hdr.DLC = len;
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(&s_can) > 0u) {
if (HAL_CAN_AddTxMessage(&s_can, &hdr, (uint8_t *)data, &mailbox) == HAL_OK) {
s_stats.tx_count++;
} else {
s_stats.err_count++;
}
}
}
void can_driver_send_std(uint16_t std_id, const uint8_t *data, uint8_t len)
{
if (s_stats.bus_off || len > 8u) {
return;
}
CAN_TxHeaderTypeDef hdr = {0};
hdr.StdId = std_id;
hdr.IDE = CAN_ID_STD;
hdr.RTR = CAN_RTR_DATA;
hdr.DLC = len;
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(&s_can) > 0u) {
if (HAL_CAN_AddTxMessage(&s_can, &hdr, (uint8_t *)data, &mailbox) == HAL_OK) {
s_stats.tx_count++;
} else {
s_stats.err_count++;
}
}
}
bool can_driver_get_feedback(uint8_t node_id, can_feedback_t *out)
{
if (node_id >= CAN_NUM_MOTORS || out == NULL) {
return false;
}
if (s_feedback[node_id].last_rx_ms == 0u) {
return false;
}
memcpy(out, (const void *)&s_feedback[node_id], sizeof(can_feedback_t));
return true;
}
bool can_driver_is_alive(uint8_t node_id, uint32_t now_ms)
{
if (node_id >= CAN_NUM_MOTORS) {
return false;
}
if (s_feedback[node_id].last_rx_ms == 0u) {
return false;
}
return (now_ms - s_feedback[node_id].last_rx_ms) < CAN_NODE_TIMEOUT_MS;
}
void can_driver_get_stats(can_stats_t *out)
{
if (out == NULL) {
return;
}
memcpy(out, (const void *)&s_stats, sizeof(can_stats_t));
}