sl-firmware 6f0ad8e92e feat(firmware): Jetson binary serial protocol on USART1 (Issue #120)
New jlink module replaces ASCII-over-USB-CDC jetson_cmd with a dedicated
hardware UART binary protocol at 921600 baud for reliable Jetson comms.

- include/jlink.h: JLinkState struct, jlink_tlm_status_t (20-byte packed),
  command/telemetry IDs (0x01-0x07 cmd, 0x80 status), API declarations
- src/jlink.c: USART1 DMA2_Stream2_Channel4 circular RX (128 bytes),
  IDLE interrupt, CRC16-XModem (poly 0x1021) frame parser state machine,
  command dispatch (HEARTBEAT/DRIVE/ARM/DISARM/PID_SET/ESTOP),
  jlink_send_telemetry() blocking TX (≈0.28 ms per frame)
- include/config.h: JLINK_BAUD=921600, JLINK_HB_TIMEOUT_MS=1000,
  JLINK_TLM_HZ=50, FW_MAJOR/MINOR/PATCH version constants
- src/main.c: jlink_init(), jlink_process() in main loop, arm/disarm/
  estop/PID flag handling, 50 Hz STATUS telemetry TX, jlink takes
  priority over legacy jetson_cmd for speed/steer injection
- test/test_jlink_frames.py: 39 pytest tests (39/39 pass) — CRC16,
  frame building, parser state machine, drive/PID/status encoding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:22:34 -05:00

280 lines
8.7 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.

#include "jlink.h"
#include "config.h"
#include "stm32f7xx_hal.h"
#include <string.h>
/* ---- DMA circular RX buffer ---- */
#define JLINK_RX_BUF_LEN 128u /* must be power-of-2 */
static uint8_t s_rx_buf[JLINK_RX_BUF_LEN];
static uint32_t s_rx_tail = 0; /* consumer index (byte already processed) */
/* ---- HAL handles ---- */
static UART_HandleTypeDef s_uart;
static DMA_HandleTypeDef s_dma_rx;
/* ---- Volatile state ---- */
volatile JLinkState jlink_state;
/* ---- CRC16-XModem (poly 0x1021, init 0x0000) ---- */
static uint16_t crc16_xmodem(const uint8_t *data, uint16_t len)
{
uint16_t crc = 0x0000u;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (uint8_t b = 0; b < 8; b++) {
if (crc & 0x8000u)
crc = (crc << 1) ^ 0x1021u;
else
crc <<= 1;
}
}
return crc;
}
/* ---- jlink_init() ---- */
void jlink_init(void)
{
/* GPIO: PB6=TX AF7 (USART1_TX), PB7=RX AF7 (USART1_RX) */
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOB, &gpio);
/* DMA2 Stream2 Channel4 — USART1_RX circular */
__HAL_RCC_DMA2_CLK_ENABLE();
s_dma_rx.Instance = DMA2_Stream2;
s_dma_rx.Init.Channel = DMA_CHANNEL_4;
s_dma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
s_dma_rx.Init.PeriphInc = DMA_PINC_DISABLE;
s_dma_rx.Init.MemInc = DMA_MINC_ENABLE;
s_dma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
s_dma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
s_dma_rx.Init.Mode = DMA_CIRCULAR;
s_dma_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
s_dma_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&s_dma_rx);
__HAL_LINKDMA(&s_uart, hdmarx, s_dma_rx);
/* USART1 at JLINK_BAUD (921600) */
__HAL_RCC_USART1_CLK_ENABLE();
s_uart.Instance = USART1;
s_uart.Init.BaudRate = JLINK_BAUD;
s_uart.Init.WordLength = UART_WORDLENGTH_8B;
s_uart.Init.StopBits = UART_STOPBITS_1;
s_uart.Init.Parity = UART_PARITY_NONE;
s_uart.Init.Mode = UART_MODE_TX_RX;
s_uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
s_uart.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&s_uart);
/* Enable USART1 IDLE interrupt for circular buffer draining */
__HAL_UART_ENABLE_IT(&s_uart, UART_IT_IDLE);
HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* DMA2_Stream2 IRQ (for error handling) */
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
/* Start circular DMA RX — never stops */
HAL_UART_Receive_DMA(&s_uart, s_rx_buf, JLINK_RX_BUF_LEN);
memset((void *)&jlink_state, 0, sizeof(jlink_state));
s_rx_tail = 0;
}
/* ---- IRQ handlers ---- */
void USART1_IRQHandler(void)
{
/* Clear IDLE flag by reading SR then DR */
if (__HAL_UART_GET_FLAG(&s_uart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&s_uart);
/* jlink_process() drains the buffer from main loop — no work here */
}
HAL_UART_IRQHandler(&s_uart);
}
void DMA2_Stream2_IRQHandler(void)
{
HAL_DMA_IRQHandler(&s_dma_rx);
}
/* ---- jlink_is_active() ---- */
bool jlink_is_active(uint32_t now_ms)
{
if (jlink_state.last_rx_ms == 0u) return false;
return (now_ms - jlink_state.last_rx_ms) < JLINK_HB_TIMEOUT_MS;
}
/* ---- Frame dispatch ---- */
static void dispatch(const uint8_t *payload, uint8_t cmd, uint8_t plen)
{
/* Update heartbeat timestamp on every valid frame */
jlink_state.last_rx_ms = HAL_GetTick();
switch (cmd) {
case JLINK_CMD_HEARTBEAT:
/* Heartbeat only — no payload action needed */
break;
case JLINK_CMD_DRIVE:
if (plen == 4u) {
int16_t spd, str;
memcpy(&spd, payload, 2);
memcpy(&str, payload + 2, 2);
/* Clamp to ±1000 */
if (spd > 1000) spd = 1000;
if (spd < -1000) spd = -1000;
if (str > 1000) str = 1000;
if (str < -1000) str = -1000;
jlink_state.speed = spd;
jlink_state.steer = str;
}
break;
case JLINK_CMD_ARM:
jlink_state.arm_req = 1u;
break;
case JLINK_CMD_DISARM:
jlink_state.disarm_req = 1u;
break;
case JLINK_CMD_PID_SET:
if (plen == 12u) {
float kp, ki, kd;
memcpy(&kp, payload, 4);
memcpy(&ki, payload + 4, 4);
memcpy(&kd, payload + 8, 4);
/* Sanity bounds — same as USB CDC PID handler in main.c */
if (kp >= 0.0f && kp <= 500.0f) jlink_state.pid_kp = kp;
if (ki >= 0.0f && ki <= 50.0f) jlink_state.pid_ki = ki;
if (kd >= 0.0f && kd <= 50.0f) jlink_state.pid_kd = kd;
jlink_state.pid_updated = 1u;
}
break;
case JLINK_CMD_ESTOP:
jlink_state.estop_req = 1u;
break;
default:
break;
}
}
/* ---- jlink_process() — call from main loop every tick ---- */
/*
* Parser state machine.
* Frame: [STX][LEN][CMD][PAYLOAD 0..LEN-1][CRC_hi][CRC_lo][ETX]
* LEN = count of CMD + PAYLOAD bytes (1..253).
* CRC16-XModem over CMD+PAYLOAD.
* Maximum payload = 253 - 1 = 252 bytes (LEN field is 1 byte, max 0xFF=255,
* but we cap at 64 for safety).
*/
#define JLINK_MAX_PAYLOAD 64u
typedef enum {
PS_WAIT_STX = 0,
PS_WAIT_LEN,
PS_WAIT_DATA, /* receiving CMD + PAYLOAD (len bytes total) */
PS_WAIT_CRC_HI,
PS_WAIT_CRC_LO,
PS_WAIT_ETX,
} ParseState;
void jlink_process(void)
{
static ParseState s_state = PS_WAIT_STX;
static uint8_t s_len = 0; /* expected CMD+PAYLOAD length */
static uint8_t s_count = 0; /* bytes received so far in PS_WAIT_DATA */
static uint8_t s_frame[JLINK_MAX_PAYLOAD + 1u]; /* [0]=CMD, [1..]=PAYLOAD */
static uint8_t s_crc_hi = 0;
/* Compute how many bytes the DMA has written since last drain */
uint32_t head = JLINK_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(&s_dma_rx);
uint32_t bytes = (head - s_rx_tail) & (JLINK_RX_BUF_LEN - 1u);
for (uint32_t i = 0; i < bytes; i++) {
uint8_t b = s_rx_buf[s_rx_tail];
s_rx_tail = (s_rx_tail + 1u) & (JLINK_RX_BUF_LEN - 1u);
switch (s_state) {
case PS_WAIT_STX:
if (b == JLINK_STX) s_state = PS_WAIT_LEN;
break;
case PS_WAIT_LEN:
if (b == 0u || b > JLINK_MAX_PAYLOAD + 1u) {
/* Invalid length — resync */
s_state = PS_WAIT_STX;
} else {
s_len = b;
s_count = 0;
s_state = PS_WAIT_DATA;
}
break;
case PS_WAIT_DATA:
s_frame[s_count++] = b;
if (s_count == s_len) s_state = PS_WAIT_CRC_HI;
break;
case PS_WAIT_CRC_HI:
s_crc_hi = b;
s_state = PS_WAIT_CRC_LO;
break;
case PS_WAIT_CRC_LO: {
uint16_t rx_crc = ((uint16_t)s_crc_hi << 8) | b;
uint16_t calc_crc = crc16_xmodem(s_frame, s_len);
if (rx_crc == calc_crc)
s_state = PS_WAIT_ETX;
else
s_state = PS_WAIT_STX; /* CRC mismatch — drop */
break;
}
case PS_WAIT_ETX:
if (b == JLINK_ETX) {
/* Valid frame: s_frame[0]=CMD, s_frame[1..s_len-1]=PAYLOAD */
dispatch(s_frame + 1, s_frame[0], s_len - 1u);
}
/* Either way, go back to idle (resync on bad ETX) */
s_state = PS_WAIT_STX;
break;
}
}
}
/* ---- jlink_send_telemetry() ---- */
void jlink_send_telemetry(const jlink_tlm_status_t *status)
{
/*
* Frame: [STX][LEN][0x80][20 bytes STATUS][CRC_hi][CRC_lo][ETX]
* LEN = 1 (CMD) + 20 (payload) = 21
* Total frame length = 1+1+1+20+2+1 = 26 bytes
* At 921600 baud (10 bits/byte): 26×10/921600 ≈ 0.28ms — safe to block.
*/
static uint8_t frame[26];
const uint8_t plen = (uint8_t)sizeof(jlink_tlm_status_t); /* 20 */
const uint8_t len = 1u + plen; /* 21 */
frame[0] = JLINK_STX;
frame[1] = len;
frame[2] = JLINK_TLM_STATUS;
memcpy(&frame[3], status, plen);
uint16_t crc = crc16_xmodem(&frame[2], len); /* over CMD + PAYLOAD */
frame[3 + plen] = (uint8_t)(crc >> 8);
frame[3 + plen + 1] = (uint8_t)(crc & 0xFFu);
frame[3 + plen + 2] = JLINK_ETX;
HAL_UART_Transmit(&s_uart, frame, sizeof(frame), 5u);
}