#15403·betaflight

telemetry(crsf): move CRSF telemetry rate-limit gate from checkFunc into processCrsf()

Author: nerdCopterCreated Jul 8, 2026Updated Sep 12, 2026
LabelsInactive

AI Generated issue-ticket

Background

This is a post-merge follow-up for PR #15358 (fix for scheduler starvation, see issue #15066), which added a 50Hz rate-limit gate inside crsfTelemetryUpdateCheck() (src/main/telemetry/crsf.c), the scheduler checkFunc for TASK_TELEMETRY. The gate uses a local static timeUs_t lastTelemetryCheckTimeUs, with ad-hoc response paths (mspReplyPending, deviceInfoReplyPending) bypassing the gate so MSP-over-telemetry and device-info replies still drain at inbound frame rate.

This refactor was originally proposed by @haslinghuis across both review rounds on PR #15358, who identified both edge cases below individually and concluded both "dissolve if the 20 ms gate is moved into processCrsf() (which already gates on crsfLastCycleTime) instead of gating the whole checkFunc." @blckmn's later review (https://github.com/betaflight/betaflight/pull/15358#pullrequestreview-4648805312) independently restated the same suggestion.

Follow-up concurrence: https://github.com/betaflight/betaflight/pull/15358#issuecomment-4915955786

Problem

The current gate has two known residual edge cases, both assessed as non-blocking (self-healing, do not affect flight control):

  1. Baud renegotiation staleness: crsfRxUpdateBaudrate() (src/main/rx/crsf.c:687) swaps the TASK_TELEMETRY checkFunc NULL↔crsfTelemetryUpdateCheck during CRSF V3 baud negotiation. The static lastTelemetryCheckTimeUs is not reset on reinstatement, which can suppress the first periodic telemetry frame for up to 20ms after a baud renegotiation cycle.
  2. Long-idle staleness: if telemetryResponsePending stays false long enough for lastTelemetryCheckTimeUs to go stale relative to cmpTimeUs's signed 32-bit rollover window (~35 minutes), the first check after link reactivation can be delayed by up to one gate interval.

Proposed fix

Move the 20ms rate-limiting decision out of the checkFunc and into processCrsf(), which already tracks crsfLastCycleTime (currently a local static inside handleCrsfTelemetry(), src/main/telemetry/crsf.c:1317) and already performs its own interval gate at src/main/telemetry/crsf.c:1035 (CRSF_CYCLETIME_US / crsfScheduleCount).

Why this needs its own design pass, not a quick patch

  1. crsfLastCycleTime is not a drop-in replacement for lastTelemetryCheckTimeUs. crsfLastCycleTime is reset by every ad-hoc path (MSP reply, device-info, displayport clear, displayport chunk) as well as by periodic frames — it means "last time any telemetry activity happened." The checkFunc's lastTelemetryCheckTimeUs only advances on the periodic path by design, which is what allows ad-hoc traffic to be exempted from the gate. Naively reusing crsfLastCycleTime for the checkFunc gate means frequent ad-hoc traffic (e.g. a Lua script polling MSP) would continuously re-arm the periodic gate, potentially suppressing periodic sensor telemetry during heavy ad-hoc use.
  2. Different interval semantics. The checkFunc's gate is a flat CRSF_TELEMETRY_FRAME_INTERVAL_MAX_US (20ms). processCrsf()'s own gate is CRSF_CYCLETIME_US / crsfScheduleCount, which varies from ~16.7ms to 50ms+ depending on sensor configuration (schedule slot count). Merging the two requires deciding whether 20ms becomes a floor, a replacement, or coexists with the schedule-based interval.
  3. Cross-function state promotion. crsfTelemetryUpdateCheck() runs before the task wakes; processCrsf() / crsfLastCycleTime run inside handleCrsfTelemetry() after the task wakes. Sharing state between them requires promoting a local static to file scope and re-verifying every call site that touches it.

References