Protocol choice: implemented from spec (CRSFforArduino needs Arduino
framework; Betaflight extraction has deep scheduler dependencies).
Protocol verified against Betaflight src/main/rx/crsf.c + CRSF spec.
crsf.c:
- UART4 PA0=TX/PA1=RX (GPIO_AF8_UART4), 420000 baud 8N1, oversampling×8
APB1=54MHz → BRR=0x101 → 418604 baud (0.33% error, within spec)
- DMA1 Stream2 Channel4, circular 64-byte buffer, IDLE interrupt
DMA half/complete callbacks drain buffer; IDLE fires at frame boundary
- CRC8 DVB-S2 (polynomial 0xD5) validated on every frame
- Parser state machine: SYNC(0xC8)→LEN→DATA with length sanity check
- 11-bit channel unpack for all 16 channels from 22-byte payload
- RC channels frame (0x16): unpacks 16ch, updates last_rx_ms + armed
- Link stats frame (0x14): captures RSSI dBm, LQ%, SNR dB
crsf.h: added rssi_dbm, link_quality, snr fields to CRSFState
config.h: CRSF_ARM_THRESHOLD=1750, CRSF_STEER_MAX=400, CRSF_FAILSAFE_MS=300
main.c:
- crsf_init() called after motor_driver_init()
- RC failsafe: disarm if (now - last_rx_ms) > CRSF_FAILSAFE_MS, but only
after RC was first seen (last_rx_ms != 0) — USB-only mode unaffected
- RC arm: CH5 rising edge → safety_arm_start(); falling edge → disarm
Same ARMING_HOLD_MS interlock as USB arm command
- RC steer: CH1 → crsf_to_range() → ±CRSF_STEER_MAX → motor_driver steer
- RSSI/LQ: appended to JSON when safety_rc_alive() ("rssi","lq" fields)
ui/index.html: hidden RC RSSI row revealed on first packet with rssi/lq
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
294 lines
11 KiB
C
294 lines
11 KiB
C
/*
|
||
* crsf.c — CRSF/ExpressLRS RC receiver driver
|
||
*
|
||
* Hardware: UART4, PA0=TX PA1=RX (GPIO_AF8_UART4), 420000 baud 8N1
|
||
* DMA: DMA1 Stream2 Channel4 (UART4_RX), circular 64-byte buffer
|
||
* UART IDLE interrupt → drain; DMA half/complete callbacks → drain
|
||
*
|
||
* CRSF frame layout:
|
||
* [0xC8] [LEN] [TYPE] [PAYLOAD...] [CRC8-DVB-S2]
|
||
* LEN = bytes after itself = TYPE + PAYLOAD + CRC (max 62)
|
||
* CRC covers TYPE through last PAYLOAD byte (not SYNC, not LEN, not CRC)
|
||
*
|
||
* Supported frame types:
|
||
* 0x16 — RC channels (22 bytes, 16 ch × 11 bit packed)
|
||
* 0x14 — Link statistics (RSSI, LQ, SNR)
|
||
*
|
||
* Channel mapping (0-indexed):
|
||
* CH1 [0] = steering (-1000..+1000)
|
||
* CH2 [1] = speed/lean override (future use)
|
||
* CH5 [4] = arm switch (> CRSF_ARM_THRESHOLD = armed)
|
||
* CH6 [5] = mode (< 992 = RC, > 992 = auto)
|
||
*
|
||
* Protocol reference: ExpressLRS/CRSF_Spec, verified against Betaflight src/main/rx/crsf.c
|
||
*/
|
||
|
||
#include "crsf.h"
|
||
#include "config.h"
|
||
#include "stm32f7xx_hal.h"
|
||
#include <string.h>
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* DMA circular receive buffer */
|
||
/* ------------------------------------------------------------------ */
|
||
#define CRSF_DMA_BUF_SIZE 64u /* must be power-of-2 >= 2× max frame (26) */
|
||
|
||
static uint8_t s_dma_buf[CRSF_DMA_BUF_SIZE];
|
||
static volatile uint16_t s_dma_head = 0; /* last processed position */
|
||
|
||
static UART_HandleTypeDef s_uart;
|
||
static DMA_HandleTypeDef s_dma_rx;
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Frame parser state */
|
||
/* ------------------------------------------------------------------ */
|
||
#define CRSF_SYNC 0xC8u
|
||
#define CRSF_FRAME_RC 0x16u /* RC channels packed */
|
||
#define CRSF_FRAME_LINK 0x14u /* Link statistics */
|
||
#define CRSF_MAX_FRAME_LEN 64u
|
||
|
||
typedef enum { ST_SYNC, ST_LEN, ST_DATA } parse_state_t;
|
||
|
||
static parse_state_t s_ps = ST_SYNC;
|
||
static uint8_t s_frame[CRSF_MAX_FRAME_LEN];
|
||
static uint8_t s_flen = 0; /* total expected frame bytes */
|
||
static uint8_t s_fpos = 0; /* bytes received so far */
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Public state */
|
||
/* ------------------------------------------------------------------ */
|
||
volatile CRSFState crsf_state = {0};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* CRC8 DVB-S2 — polynomial 0xD5 */
|
||
/* ------------------------------------------------------------------ */
|
||
static uint8_t crc8_dvb_s2(uint8_t crc, uint8_t a) {
|
||
crc ^= a;
|
||
for (int i = 0; i < 8; i++) {
|
||
crc = (crc & 0x80u) ? ((crc << 1) ^ 0xD5u) : (crc << 1);
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
static uint8_t crsf_frame_crc(const uint8_t *frame, uint8_t frame_len) {
|
||
/* CRC covers frame[2] (type) .. frame[frame_len-2] (last payload byte) */
|
||
uint8_t crc = 0;
|
||
for (uint8_t i = 2; i < frame_len - 1; i++) {
|
||
crc = crc8_dvb_s2(crc, frame[i]);
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* 11-bit channel unpacking — 16 channels from 22 bytes */
|
||
/* ------------------------------------------------------------------ */
|
||
static void unpack_channels(const uint8_t *payload, uint16_t *ch) {
|
||
uint32_t bits = 0;
|
||
int loaded = 0, idx = 0, src = 0;
|
||
while (idx < 16) {
|
||
while (loaded < 11 && src < 22) {
|
||
bits |= (uint32_t)payload[src++] << loaded;
|
||
loaded += 8;
|
||
}
|
||
ch[idx++] = bits & 0x7FFu;
|
||
bits >>= 11;
|
||
loaded -= 11;
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Frame processing */
|
||
/* ------------------------------------------------------------------ */
|
||
static void process_frame(const uint8_t *frame, uint8_t frame_len) {
|
||
/* Validate minimum length and CRC */
|
||
if (frame_len < 4) return;
|
||
if (frame[frame_len - 1] != crsf_frame_crc(frame, frame_len)) return;
|
||
|
||
uint8_t type = frame[2];
|
||
const uint8_t *payload = &frame[3];
|
||
uint8_t payload_len = frame_len - 4; /* type + payload + crc = frame[1], minus type and crc */
|
||
|
||
switch (type) {
|
||
case CRSF_FRAME_RC:
|
||
if (payload_len < 22) return;
|
||
unpack_channels(payload, (uint16_t *)crsf_state.channels);
|
||
crsf_state.last_rx_ms = HAL_GetTick();
|
||
/* Update arm switch state from CH5 (index 4) */
|
||
crsf_state.armed = (crsf_state.channels[4] > CRSF_ARM_THRESHOLD);
|
||
break;
|
||
|
||
case CRSF_FRAME_LINK:
|
||
/* Link stats payload:
|
||
* [0] uplink RSSI ant1 (value = -dBm, so negate for dBm)
|
||
* [2] uplink link quality (0-100 %)
|
||
* [3] uplink SNR (signed dB)
|
||
*/
|
||
if (payload_len < 4) return;
|
||
crsf_state.rssi_dbm = -(int8_t)payload[0];
|
||
crsf_state.link_quality = payload[2];
|
||
crsf_state.snr = (int8_t)payload[3];
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Byte-level parser state machine */
|
||
/* ------------------------------------------------------------------ */
|
||
static void parse_byte(uint8_t b) {
|
||
switch (s_ps) {
|
||
case ST_SYNC:
|
||
if (b == CRSF_SYNC) {
|
||
s_frame[0] = b;
|
||
s_ps = ST_LEN;
|
||
}
|
||
break;
|
||
|
||
case ST_LEN:
|
||
/* LEN = bytes remaining after this field (type + payload + crc), max 62 */
|
||
if (b >= 2 && b <= 62) {
|
||
s_frame[1] = b;
|
||
s_flen = b + 2u; /* total frame = SYNC + LEN + rest */
|
||
s_fpos = 2;
|
||
s_ps = ST_DATA;
|
||
} else {
|
||
s_ps = ST_SYNC; /* invalid length — resync */
|
||
}
|
||
break;
|
||
|
||
case ST_DATA:
|
||
s_frame[s_fpos++] = b;
|
||
if (s_fpos >= s_flen) {
|
||
process_frame(s_frame, s_flen);
|
||
s_ps = ST_SYNC;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* DMA buffer drain — called from IDLE IRQ and DMA half/complete CBs */
|
||
/* ------------------------------------------------------------------ */
|
||
static void dma_drain(void) {
|
||
/* DMA CNDTR counts DOWN from buf size; write position = size - CNDTR */
|
||
uint16_t pos = (uint16_t)(CRSF_DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(&s_dma_rx));
|
||
uint16_t head = s_dma_head;
|
||
while (head != pos) {
|
||
parse_byte(s_dma_buf[head]);
|
||
head = (head + 1u) & (CRSF_DMA_BUF_SIZE - 1u);
|
||
}
|
||
s_dma_head = pos;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* IRQ handlers */
|
||
/* ------------------------------------------------------------------ */
|
||
void UART4_IRQHandler(void) {
|
||
/* IDLE line detection — fires when bus goes quiet between frames */
|
||
if (__HAL_UART_GET_FLAG(&s_uart, UART_FLAG_IDLE)) {
|
||
__HAL_UART_CLEAR_IDLEFLAG(&s_uart);
|
||
dma_drain();
|
||
}
|
||
HAL_UART_IRQHandler(&s_uart);
|
||
}
|
||
|
||
void DMA1_Stream2_IRQHandler(void) {
|
||
HAL_DMA_IRQHandler(&s_dma_rx);
|
||
}
|
||
|
||
/* DMA half-complete: drain first half of circular buffer */
|
||
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *h) {
|
||
if (h->Instance == UART4) dma_drain();
|
||
}
|
||
|
||
/* DMA complete: drain second half (buffer wrapped) */
|
||
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *h) {
|
||
if (h->Instance == UART4) dma_drain();
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Public API */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
/*
|
||
* crsf_init() — configure UART4 + DMA1 and start circular receive.
|
||
*
|
||
* UART4: PA0=TX, PA1=RX, AF8_UART4, 420000 baud 8N1, oversampling×8.
|
||
* APB1 = 54 MHz → BRR = 0x101 → actual 418604 baud (0.33% error, within CRSF spec).
|
||
*
|
||
* DMA: DMA1 Stream2 Channel4, peripheral→memory, circular, byte width.
|
||
* IDLE interrupt + DMA half/complete callbacks drain the circular buffer.
|
||
*/
|
||
void crsf_init(void) {
|
||
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||
__HAL_RCC_UART4_CLK_ENABLE();
|
||
__HAL_RCC_DMA1_CLK_ENABLE();
|
||
|
||
/* PA0=TX, PA1=RX, AF8_UART4 */
|
||
GPIO_InitTypeDef gpio = {0};
|
||
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1;
|
||
gpio.Mode = GPIO_MODE_AF_PP;
|
||
gpio.Pull = GPIO_PULLUP;
|
||
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
|
||
gpio.Alternate = GPIO_AF8_UART4;
|
||
HAL_GPIO_Init(GPIOA, &gpio);
|
||
|
||
/* DMA1 Stream2 Channel4 — UART4_RX, circular byte transfers */
|
||
s_dma_rx.Instance = DMA1_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_LOW;
|
||
s_dma_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||
HAL_DMA_Init(&s_dma_rx);
|
||
__HAL_LINKDMA(&s_uart, hdmarx, s_dma_rx);
|
||
|
||
/* UART4: 420000 8N1, oversampling×8 (better tolerance at high baud) */
|
||
s_uart.Instance = UART4;
|
||
s_uart.Init.BaudRate = 420000;
|
||
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_8;
|
||
HAL_UART_Init(&s_uart);
|
||
|
||
/* Start circular DMA — runs indefinitely, no need to restart */
|
||
HAL_UART_Receive_DMA(&s_uart, s_dma_buf, CRSF_DMA_BUF_SIZE);
|
||
|
||
/* IDLE line interrupt — fires when UART bus goes quiet (end of frame) */
|
||
__HAL_UART_ENABLE_IT(&s_uart, UART_IT_IDLE);
|
||
|
||
HAL_NVIC_SetPriority(DMA1_Stream2_IRQn, 6, 0);
|
||
HAL_NVIC_EnableIRQ(DMA1_Stream2_IRQn);
|
||
HAL_NVIC_SetPriority(UART4_IRQn, 5, 0);
|
||
HAL_NVIC_EnableIRQ(UART4_IRQn);
|
||
}
|
||
|
||
/*
|
||
* crsf_parse_byte() — kept for compatibility; direct call path not used
|
||
* when DMA is active, but available for unit testing or UART-IT fallback.
|
||
*/
|
||
void crsf_parse_byte(uint8_t byte) {
|
||
parse_byte(byte);
|
||
}
|
||
|
||
/*
|
||
* crsf_to_range() — map raw CRSF value 172–1811 to [min, max].
|
||
* Midpoint 992 maps to (min+max)/2.
|
||
*/
|
||
int16_t crsf_to_range(uint16_t val, int16_t min, int16_t max) {
|
||
int32_t v = (int32_t)val;
|
||
int32_t r = min + (v - 172) * (int32_t)(max - min) / (1811 - 172);
|
||
if (r < min) r = min;
|
||
if (r > max) r = max;
|
||
return (int16_t)r;
|
||
}
|