feat(f051): PWM-synchronized comparator blanking to attack the beat-band jitter hump#23
feat(f051): PWM-synchronized comparator blanking to attack the beat-band jitter hump#23AlexKlimaj wants to merge 2 commits into
Conversation
PWM switching noise aliasing into zero-cross detection produces a jitter beat-band hump (11-26% at 21-24k RPM) on all firmware variants. The F051 COMP has no hardware blanking, so gate in software: in the comparator ISR, after the accept gate, if TIM1->CNT sits inside a dirty window around a PWM switching edge (turn-on at rollover CNT=0, turn-off at CNT=CCR, each smeared by DEAD_TIME plus fixed-duration switching ring), spin-wait - bounded by COMP_BLANK_MAX_SPIN - until it exits, then run the existing confirm loop. The edge is only ever DELAYED (<= ~4.2 us), never rejected: a discarded true crossing may never re-trigger and would end in the 22.5 ms bemf timeout. The 20 kHz routine publishes the off-edge window lower bound after its single SET_DUTY_CYCLE_ALL (all running/brake paths funnel through it; the other call sites run with phase interrupts masked). A single uint16_t is atomic on M0, so the ISR reads it with one ldrh. Edge cases hold by construction: 100% duty blanks only the rollover window; near-100% truncates the off window at ARR via the unsigned compare while the rollover window covers the wrap; low duty clamps lo to 0 and the windows merge; the gate uses only CNT/lo/constants so variable PWM needs no ARR term; startup polling mode never reaches this path; boot value 0 degenerates to the rollover guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Firmware side (HWCI_PERF builds only; production unaffected): - hwci_perf struct v3 appends two monotonic u32 counters after the v2 jitter block (host_cmd stays frozen at offset 60 per the versioning contract): zc_blank_engaged at offset 80, zc_confirm_reject at 84; sizeof grows 80 -> 88. - HWCI_PERF_BLANK_ENGAGED() fires on the COMP_PWM_BLANKING gate's rare (dirty) path in the F051 comparator ISR; HWCI_PERF_CONFIRM_REJECT() fires on the F051 glitch-tolerant confirm loop's early-out reject (F051-only: the strict non-F051 loop is not instrumented since the blanking change is F051-scoped). Both counters are monotonic, so hwci_perf_reset_stats() deliberately leaves them alone. Host side: - perf.py: FIELDS_V3 = FIELDS_V2 + the two counters, registered in FIELDS_BY_VERSION; host_cmd offset assertion still holds. Columns flow generically as perf_zc_blank_engaged / perf_zc_confirm_reject. - perf_reader.py: DWARF layout probe now distinguishes v1/v2/v3. - metrics.py: report-only blank_engaged_per_zc per steady point, wrap-safe windowed delta(zc_blank_engaged)/delta(zc_count) like zc_jitter_window; healthy ~ 0.05-0.3, 0 = gate not engaging, ~1.0 = windows miscomputed. Rendered in the report steady-point table. - sim.py: emits plausible v3 fields (10-20% blank engagement, rare confirm rejects ballooning during desync) so sim runs exercise decode. - tests: v3 layout/roundtrip, frozen v2 ELF probe for the DWARF cross-check and PerfReader backward-compat, blank_engaged_per_zc metric math (deltas, u32 wrap, pre-v3 None, frozen counters), sim v3 counters. 123 passed (115 base kept green). Note: a sibling PR (demag compensation) also claims perf v3 with different fields at offset 80; whichever merges second rebases to v4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
a18dfda to
aaddcf2
Compare
Bench verdict: hypothesis refuted — recommend not merging the blanking gate ❌🔬Rebased onto
The implementation itself works exactly as designed: Falsification test (the PR's own fallback): doubled windows (ON 128 / OFF_LEN 144, ~11% of the PWM period blanked) → real desync at t70 — firmware eRPM collapsed 163k→54k while the rotor held 23.5k, current spiked 67.6 A, harness safety aborted ( Two things worth salvaging:
Runs referenced are on the bench PC. Recommend closing this PR (or stripping it to the salvageable instrumentation) — the negative result and the phase-clustering datum are the real deliverables. 🤖 Generated with Claude Code |
Problem
The zc-jitter beat band (21–24k RPM: t60 25.6% / t70 17.4% on the current baseline, 11–20% on every firmware variant including upstream) is PWM switching noise aliasing into zero-cross detection. As the ZC instant slowly sweeps through the PWM period, some commutations land their comparator edge right on a switching transient. PR #22 flagged "PWM-synchronized comparator blanking" as the fix to attack this at the source.
Design
The key constraint: an accepted edge must never be discarded. After a true crossing the comparator level stays crossed, so a rejected edge may never re-trigger and the ZC would be lost until the 22.5 ms bemf timeout. Hardware blanking doesn't exist on the F051 COMP, and gating samples inside the confirm loop would silently change the 42/10/7 wall-clock windows that #14/#21 just retuned.
So the gate lives at ISR entry, and it delays instead of rejecting: after the existing half-interval check accepts the edge, read
TIM1->CNTonce; if it sits in a dirty window around a PWM switching edge —[0, 64)at the turn-on rollover,[CCR−8, CCR+64)at turn-off (dead time 25 + fixed-duration ring + margin) — spin (hard-capped at 24 iterations ≈ 4.2 µs) until the window exits, then run the unchanged confirm loop on a clean signal. A spike-induced false edge gets rejected by the glitch-tolerant confirm within ~3 samples; a true crossing is time-stamped at the window exit instead of somewhere random inside the transient.The only published state is a single
uint16_toff-window bound, written intenKhzRoutineright afterSET_DUTY_CYCLE_ALL(16-bit stores are atomic on M0 — no masking dance). The gate uses only CNT + that bound + constants, so variable PWM needs nothing extra. All otherSET_DUTY_CYCLE_ALLcall sites run with phase interrupts masked (verified: brushed, sine startup, disarm/stop paths). F051-only viaCOMP_PWM_BLANKING, same scoping pattern as the #21 glitch-tolerant loop.Edge cases: at 100% duty there are no switching edges and the residual blanked span is a harmless ≤1.5 µs delay; near-100% the unsigned compare truncates the off window at ARR and the rollover window covers the wrap; at low duty the windows merge and the long off-time stays fully quiet.
Instrumentation (hwci_perf v3)
zc_blank_engaged(offset 80) — accepts that hit a dirty window. Healthy ≈ 0.05–0.3 per ZC (reported asblank_engaged_per_zcper steady point); 0 = gate not engaging, ~1.0 = windows miscomputed.zc_confirm_reject(offset 84) — F051 confirm-loop early-outs, for watching the spike-reject mechanism.perf.pyv3, DWARF cross-check, metrics/report/sim/tests updated).Measured cost
-Werror: ARK_4IN1_F051 (plain +HWCI_PERF=1) and AIRBOT_PHOENIX_12S_G071 (non-F051 unaffected). hwci offline tests: 123 passed (8 new).Bench validation (before undrafting)
jitter_probeA/B vs baseline (sha 66d8c9a): success = t60/t70 at least halved from 25.6%/17.4%, ideally toward the t100/t20 floor (2–5%);blank_engaged_per_zcin the healthy band.jitter_probe_midinterleaved A/B for t40/t50/t80.efficiency_sweepgate vsbaselines/ARK_4IN1_F051_HQ5136.json— hard requirementdemag_events == 0andbemf_timeout_samples == 0(the "blanking never loses a ZC" proof), efficiency and CPU/ctrl-exec within thresholds.COMP_BLANK_ON_TICKS/COMP_BLANK_OFF_LENin +16 steps; a v2 refinement (timestamp compensation via INTERVAL_TIMER snapshot at ISR entry) is designed but deliberately out of v1.🤖 Generated with Claude Code
https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Generated by Claude Code