Skip to content

feat(f051): PWM-synchronized comparator blanking to attack the beat-band jitter hump#23

Closed
AlexKlimaj wants to merge 2 commits into
ark-releasefrom
feat/pwm-sync-blanking
Closed

feat(f051): PWM-synchronized comparator blanking to attack the beat-band jitter hump#23
AlexKlimaj wants to merge 2 commits into
ark-releasefrom
feat/pwm-sync-blanking

Conversation

@AlexKlimaj

Copy link
Copy Markdown
Member

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->CNT once; 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_t off-window bound, written in tenKhzRoutine right after SET_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 other SET_DUTY_CYCLE_ALL call sites run with phase interrupts masked (verified: brushed, sine startup, disarm/stop paths). F051-only via COMP_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 as blank_engaged_per_zc per 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.
  • Host decode (perf.py v3, DWARF cross-check, metrics/report/sim/tests updated).

⚠️ Merge-order note: the demag-compensation PR also claims perf v3 with different fields at offset 80. Whichever merges second rebases its fields after the first's and bumps to v4.

Measured cost

  • Quiet path (the ~93% common case): 12 added instructions ≈ 20 cycles (~0.42 µs), verified by disassembly; handler stays RAM-resident (0x20000b9c).
  • ARK_4IN1_F051: FLASH +80 B (85.0%), RAM +80 B (84.7%).
  • Builds clean under -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)

  1. jitter_probe A/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_zc in the healthy band.
  2. jitter_probe_mid interleaved A/B for t40/t50/t80.
  3. Full efficiency_sweep gate vs baselines/ARK_4IN1_F051_HQ5136.json — hard requirement demag_events == 0 and bemf_timeout_samples == 0 (the "blanking never loses a ZC" proof), efficiency and CPU/ctrl-exec within thresholds.
  4. If residual jitter remains: widen COMP_BLANK_ON_TICKS/COMP_BLANK_OFF_LEN in +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

claude added 2 commits July 6, 2026 11:47
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
@AlexKlimaj AlexKlimaj force-pushed the feat/pwm-sync-blanking branch from a18dfda to aaddcf2 Compare July 6, 2026 17:48
@AlexKlimaj

Copy link
Copy Markdown
Member Author

Bench verdict: hypothesis refuted — recommend not merging the blanking gate ❌🔬

Rebased onto ark-release (aaddcf2, clean), 133 offline tests pass, both build variants clean. Full interleaved A/B protocol on the bench (A = ark-release, B = this PR, A,B,A,B same pack, runs/ab23-{A1,B1,A2,B2}):

point A (head) B (blanking) verdict
t30 4.90 / 5.33 4.51 / 4.59 ~0.5 pt better (marginal)
t60 (hump) 23.56 / 23.49 23.89 / 23.37 null
t70 (hump) 18.65 / 17.12 17.59 / 17.06 null
t90 7.06 / 7.13 7.70 / 7.76 +0.6 pt worse (repeats ±0.1 both sides)
t100 3.02 / 2.80 3.38 / 3.26 +0.4 pt worse

The implementation itself works exactly as designed: blank_engaged_per_zc sits in the predicted healthy band (0.04–0.20), 0 demag / 0 bemf timeouts in all four runs (the never-lose-a-ZC constraint holds), ctrl exec unchanged. But the hump doesn't move — the beat-band jitter is not caused by comparator edges firing inside the PWM switching windows. The small t90/t100 regression is the cost side of delay-to-window-exit, and CPU ran ~1.3 pts higher.

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 (runs/ab23-Bwide). Bench verified healthy afterwards (FETs peaked 31.9 °C; normal smoke after, runs/ab23-health-check). So the approach isn't under-tuned — delaying edges destabilizes commutation before it ever helps. Dead end in both directions.

Two things worth salvaging:

  1. zc_confirm_reject instrumentation — genuinely useful for monitoring the fix(f051): glitch-tolerant zero-cross confirm to cut commutation jitter #21 tolerance behavior; could respin as a tiny v3-instrumentation-only PR.
  2. A real clue about the hump's cause: at t30 the engagement ratio is 0.19 vs the 0.057 a uniform edge distribution would give — zero-crossings cluster near PWM switching edges (3.3× enrichment). That's ZC↔PWM phase correlation (injection locking), which points at the hump being a control-loop/aliasing phenomenon — the commutation timing itself hunting around PWM-phase lock points — not detection-instant corruption. Next investigation that would confirm it: a small PWM-phase histogram in the perf struct (log TIM1->CNT at each accepted ZC), mapping ZC-phase distribution vs RPM; and if confirmed, the fix lives in commutation scheduling (e.g. the designed-but-deferred timestamp compensation), not in gating the comparator.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants