feat: CRSF/ELRS RC integration (Phase 2) #35

Merged
seb merged 1 commits from sl-firmware/crsf-elrs into main 2026-02-28 21:58:06 -05:00
Collaborator

Summary

  • Protocol choice: implemented from spec (CRSFforArduino requires Arduino framework; Betaflight extraction has deep scheduler dependencies). Protocol derived from CRSF spec, cross-checked against Betaflight src/main/rx/crsf.c.
  • UART4 PA0=TX / PA1=RX, 420000 baud 8N1, oversampling×8. APB1=54MHz → 418604 baud (0.33% error)
  • DMA1 Stream2 Channel4 circular 64-byte RX buffer. IDLE interrupt drains at frame end; DMA half/complete callbacks protect against buffer wrap
  • CRC8 DVB-S2 (poly 0xD5) validated on every frame
  • Parser: SYNC(0xC8)→LEN→DATA state machine with length sanity check
  • 16-channel unpack: 11-bit extraction from 22-byte payload
  • Frame 0x16 (RC channels): 16ch unpacked, last_rx_ms + armed updated
  • Frame 0x14 (link stats): RSSI dBm, LQ%, SNR captured
  • Channel mapping: CH1[0]=steer (-400..+400), CH5[4]=arm switch
  • Failsafe: disarm if >300ms without frame, but only after first frame received (USB-only mode unaffected)
  • RC arm: CH5 rising edge → safety_arm_start(), same ARMING_HOLD_MS interlock; falling edge → cancel/disarm
  • Steering: CH1 → motor_driver_update() steer_cmd with ramping/headroom already handled
  • RSSI/LQ telemetry: rssi/lq JSON fields when RC alive; web UI hidden row revealed on first packet

Test plan

  • Build passes (verified: 7.9% Flash, 1.8% RAM)
  • With ELRS RX on UART4: last_rx_ms updates, crsf_state.channels valid
  • CH5 high → arm hold → motors enable after 3s
  • CH5 low while armed → immediate disarm
  • CH1 stick left/right → differential steering
  • Disconnect RX → disarm within 300ms
  • USB-only mode: no spurious disarm (last_rx_ms stays 0)
  • JSON stream shows rssi/lq fields; web UI row appears

🤖 Generated with Claude Code

## Summary - **Protocol choice**: implemented from spec (CRSFforArduino requires Arduino framework; Betaflight extraction has deep scheduler dependencies). Protocol derived from CRSF spec, cross-checked against Betaflight `src/main/rx/crsf.c`. - **UART4** PA0=TX / PA1=RX, 420000 baud 8N1, oversampling×8. APB1=54MHz → 418604 baud (0.33% error) - **DMA1 Stream2 Channel4** circular 64-byte RX buffer. IDLE interrupt drains at frame end; DMA half/complete callbacks protect against buffer wrap - **CRC8 DVB-S2** (poly 0xD5) validated on every frame - **Parser**: SYNC(0xC8)→LEN→DATA state machine with length sanity check - **16-channel unpack**: 11-bit extraction from 22-byte payload - **Frame 0x16** (RC channels): 16ch unpacked, `last_rx_ms` + `armed` updated - **Frame 0x14** (link stats): RSSI dBm, LQ%, SNR captured - **Channel mapping**: CH1[0]=steer (-400..+400), CH5[4]=arm switch - **Failsafe**: disarm if >300ms without frame, but only after first frame received (USB-only mode unaffected) - **RC arm**: CH5 rising edge → safety_arm_start(), same ARMING_HOLD_MS interlock; falling edge → cancel/disarm - **Steering**: CH1 → `motor_driver_update()` steer_cmd with ramping/headroom already handled - **RSSI/LQ telemetry**: `rssi`/`lq` JSON fields when RC alive; web UI hidden row revealed on first packet ## Test plan - [ ] Build passes (verified: 7.9% Flash, 1.8% RAM) - [ ] With ELRS RX on UART4: `last_rx_ms` updates, `crsf_state.channels` valid - [ ] CH5 high → arm hold → motors enable after 3s - [ ] CH5 low while armed → immediate disarm - [ ] CH1 stick left/right → differential steering - [ ] Disconnect RX → disarm within 300ms - [ ] USB-only mode: no spurious disarm (last_rx_ms stays 0) - [ ] JSON stream shows `rssi`/`lq` fields; web UI row appears 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sl-firmware added 1 commit 2026-02-28 21:09:57 -05:00
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>
Owner

Merge conflicts with main after PRs #31/#33/#34 landed. Please rebase on latest main and resolve conflicts in src/main.c and ui/index.html.

Merge conflicts with main after PRs #31/#33/#34 landed. Please rebase on latest main and resolve conflicts in `src/main.c` and `ui/index.html`.
sl-firmware force-pushed sl-firmware/crsf-elrs from accf7fdf39 to fbfde24aba 2026-02-28 21:55:05 -05:00 Compare
seb approved these changes 2026-02-28 21:57:57 -05:00
seb left a comment
Owner

Flash-tested all 5 PRs together. Builds, streams, no conflicts.

Flash-tested all 5 PRs together. Builds, streams, no conflicts.
seb merged commit 939800a9fb into main 2026-02-28 21:58:06 -05:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: seb/saltylab-firmware#35
No description provided.