sl-perception 410ace3540 feat: battery coulomb counter (Issue #325)
Add coulomb counter for accurate SoC estimation independent of load:

- New coulomb_counter module: integrate current over time to track Ah consumed
  * coulomb_counter_init(capacity_mah) initializes with battery capacity
  * coulomb_counter_accumulate(current_ma) integrates current at 100 Hz
  * coulomb_counter_get_soc_pct() returns SoC 0-100% (255 = invalid)
  * coulomb_counter_reset() for charge-complete reset

- Battery module integration:
  * battery_accumulate_coulombs() reads motor INA219 currents and accumulates
  * battery_get_soc_coulomb() returns coulomb-based SoC with fallback to voltage
  * Initialize coulomb counter at startup with DEFAULT_BATTERY_CAPACITY_MAH

- Telemetry updates:
  * JLink STATUS: use coulomb SoC if available, fallback to voltage-based
  * CRSF battery frame: now includes remaining capacity in mAh (from coulomb counter)
  * CRSF capacity field was always 0; now reflects actual remaining mAh

- Mainloop integration:
  * Call battery_accumulate_coulombs() every tick for continuous integration
  * INA219 motor currents + 200 mA subsystem baseline = total battery draw

Motor current sources (INA219 addresses 0x40/0x41) provide most power draw;
Jetson ROS2 battery_node already prioritizes coulomb-based soc_pct from STATUS frame.

Default capacity: 2200 mAh (typical lab 3S LiPo); configurable via firmware parameter.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 17:35:34 -05:00

358 lines
14 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.

/*
* 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 1721811 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;
}
/* ------------------------------------------------------------------ */
/* Telemetry TX helpers */
/* ------------------------------------------------------------------ */
/*
* Build a CRSF frame in `buf` and return the total byte count.
* buf must be at least CRSF_MAX_FRAME_LEN bytes.
* frame_type : CRSF type byte (e.g. 0x08 battery, 0x21 flight mode)
* payload : frame payload bytes (excluding type, CRC)
* plen : payload length in bytes
*/
static uint8_t crsf_build_frame(uint8_t *buf, uint8_t frame_type,
const uint8_t *payload, uint8_t plen) {
/* Total frame = SYNC + LEN + TYPE + PAYLOAD + CRC */
uint8_t frame_len = 2u + 1u + plen + 1u; /* SYNC + LEN + TYPE + payload + CRC */
if (frame_len > CRSF_MAX_FRAME_LEN) return 0;
buf[0] = CRSF_SYNC; /* 0xC8 */
buf[1] = (uint8_t)(plen + 2u); /* LEN = TYPE + payload + CRC */
buf[2] = frame_type;
memcpy(&buf[3], payload, plen);
buf[frame_len - 1] = crsf_frame_crc(buf, frame_len);
return frame_len;
}
/*
* crsf_send_battery() — type 0x08 battery sensor.
* voltage_mv → units of 100 mV (big-endian uint16)
* capacity_mah → remaining capacity in mAh (Issue #325, coulomb counter)
* remaining_pct→ 0100 % (uint8)
*/
void crsf_send_battery(uint32_t voltage_mv, uint32_t capacity_mah,
uint8_t remaining_pct) {
uint16_t v100 = (uint16_t)(voltage_mv / 100u); /* 100 mV units */
/* Convert capacity (mAh) to 3-byte big-endian: cap_hi, cap_mid, cap_lo */
uint32_t cap = capacity_mah & 0xFFFFFFu; /* 24-bit cap max */
/* Payload: [v_hi][v_lo][current_hi][current_lo][cap_hi][cap_mid][cap_lo][remaining] */
uint8_t payload[8] = {
(uint8_t)(v100 >> 8), (uint8_t)(v100 & 0xFF),
0, 0, /* current: not available on STM32, always 0 for now */
(uint8_t)((cap >> 16) & 0xFF), /* cap_hi */
(uint8_t)((cap >> 8) & 0xFF), /* cap_mid */
(uint8_t)(cap & 0xFF), /* cap_lo */
remaining_pct,
};
uint8_t frame[CRSF_MAX_FRAME_LEN];
uint8_t flen = crsf_build_frame(frame, 0x08u, payload, sizeof(payload));
if (flen) HAL_UART_Transmit(&s_uart, frame, flen, 5u);
}
/*
* crsf_send_flight_mode() — type 0x21 flight mode text.
* Displays on the handset's OSD/status bar.
* "ARMED\0" when armed (5 payload bytes + null)
* "DISARM\0" when not (7 payload bytes + null)
*/
void crsf_send_flight_mode(bool armed) {
const char *text = armed ? "ARMED" : "DISARM";
uint8_t plen = (uint8_t)(strlen(text) + 1u); /* include null terminator */
uint8_t frame[CRSF_MAX_FRAME_LEN];
uint8_t flen = crsf_build_frame(frame, 0x21u, (const uint8_t *)text, plen);
if (flen) HAL_UART_Transmit(&s_uart, frame, flen, 5u);
}