feat(demag): demag compensation — measure demag time, cut power on demag-past-ZC (PR #2 rebased)#24
Draft
AlexKlimaj wants to merge 13 commits into
Draft
feat(demag): demag compensation — measure demag time, cut power on demag-past-ZC (PR #2 rebased)#24AlexKlimaj wants to merge 13 commits into
AlexKlimaj wants to merge 13 commits into
Conversation
46907ca to
a0db164
Compare
Member
Author
|
Bench validation complete on the ARK 4IN1 F051 + Flight Stand 50. Summary (full report:
Recommendation: merge as the comp-OFF path (adaptive timeout + perf v5 + review fixes; demag is off by default via eeprom |
… and globals Split the CAN-section reserved[8] into demag_compensation (byte 184), active_demag (byte 185) and reserved[6]. Add the runtime globals (demag_comp_level, blanking_length, auto_blanking, active_demag, active_demag_fet_on, active_demag_ticks, demag_happened) and load them in loadEEpromSettings() with 0xff-erased-flash guards, gated on HAS_DEMAG_COMP so non-supporting MCU layers compile the feature out. demag_happened is an unconditional volatile uint32_t (monotonic counter for the hwci perf export). last_duty_cycle becomes volatile since the demag edge ISR will write it. No behavior change: demag_comp_level stays 0 (HAS_DEMAG_COMP is not defined by any MCU layer yet). Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
…mag fet helpers Per MCU layer (f051, g071, g431, l431): - phaseouts.h: declare phaseA/B/CHIGH, LOW and FLOAT, plus activeDemagFetOn()/activeDemagFetOff(); define HAS_PHASE_HIGH and HAS_DEMAG_COMP so Src/main.c can gate the feature per MCU. - phaseouts.c: enable HIGH_BITREG_ON and add phaseXHIGH() for both the direct-drive and PWM_ENABLE_BRIDGE variants (high fet held hard on, used for active demag). - phaseouts.c: implement activeDemagFetOn()/activeDemagFetOff() here instead of main.c (deviation from the original PR) so gcc inlines the phase bodies in-TU. The helpers stay in flash (not RAM_FUNC): with the inlined phase bodies the pair costs ~500 B of RAM, which does not fit the F051's 8 KB alongside the HWCI_PERF instrumentation, and the 1WS flash fetch penalty is well below the 0.5 us COM timer tick. The fet is picked by the floating phase (step) and conducting side (rising). - comparator.c changeCompInput(): when auto_blanking is armed, program the EXTI trigger with reversed polarity so the first comparator edge after commutation is the demag release edge; f051 keeps RAM_FUNC. Interrupt dispatch to demagEdgeRoutine() lands with the routine itself in the next commit so every commit links. Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
After each confirmed zero cross, interruptRoutine() arms auto_blanking (when demag_comp_level is set, input > 400 and commutation_interval > 100). changeCompInput() then programs the comparator EXTI with reversed polarity, so the first edge after commutation is the demag release edge (the freewheel diode stops clamping the floating phase). The comparator IRQ handlers route that edge to the new demagEdgeRoutine(), which: - computes blanking_length = time since zero cross minus waitTime (the commutation point), clamped to average_interval - derives active_demag_ticks (half the measured demag time, capped at waitTime/2) for the next commit's active freewheel scheduling - on an invalid measurement (edge before the commutation point) zeroes both blanking_length and active_demag_ticks instead of leaving stale ticks behind (fix over the original PR) - restores normal comparator polarity via changeCompInput() - if demag ran past half the commutation interval: counts demag_happened, powers off with allOff(), cuts duty per demag_comp_level (1: -25%, 2: -50%, 3: -75%, snapshotting the volatile duty_cycle once) and re-enters interruptRoutine() demagEdgeRoutine() is RAM_FUNC since it is called from the RAM-resident F051 comparator ISR. The ark-release glitch-tolerant zero-cross confirm loop is untouched and guards the re-entry. auto_blanking and the active demag state are reset on startMotor(), desync and bemf timeout. Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Replace the fixed INTERVAL_TIMER_COUNT > 45000 bemf timeout with an adaptive threshold once the motor is running steadily (zero_crosses > 50): four times the average commutation interval. A demag-stalled or desynced motor is then caught within a few expected commutations instead of after a fixed 45 ms worth of timer ticks. During startup the original 45000 threshold is kept. Separate commit so it can be reverted alone if bench testing shows the tighter threshold trips on aggressive throttle chops. Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Appends the demag compensation block after v4's zc_phase_hist histogram (renumbered from v4 during the rebase: #29 took v4 on ark-release): demag_events (u32, monotonic mirror of main.c demag_happened), blanking_len_last/max (u16, measured demag time in INTERVAL_TIMER ticks from demagEdgeRoutine). Fed from the main-loop snapshot path - zero ISR cost. host_cmd stays frozen at offset 60; total size 148 -> 156 bytes. Host: FIELDS_V5 + decode, DWARF vintage probe (demag_events marker), model columns, fw_demag_events metric (per steady tail and per run, wrap-safe u32 delta, None on pre-v5 firmware), report column, sim demag mirror of injected desyncs, tests incl. u32 wrap and v2/v3/v4 back-compat. Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
'make <TARGET> DEMAG_COMP=<1|2|3>' forces the demag compensation level at boot via -DHWCI_DEMAG_COMP, applied in loadEEpromSettings() after the 0xff guards and inside the HAS_DEMAG_COMP gate. The eeprom byte 184 stays untouched, so A/B bench sessions flip the feature by reflashing firmware without a config-tool pass. Production builds leave it unset and are unaffected. (The ACTIVE_DEMAG companion knob rides with the active-demag feature branch.) Co-authored-by: dakejahl <37091262+dakejahl@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Firmware fixes found during review of the demag compensation path: - blanking_length measured the demag duration against waitTime, but PeriodElapsedCallback recomputes waitTime for the NEXT cycle before the demag edge fires. Snapshot the scheduling delay (demag_wait_ticks) when auto_blanking is armed and measure against that, so a throttle transient no longer inflates the measurement and spuriously trips the duty cut. - Adaptive bemf stall timeout (average_interval * 4) can exceed the 16-bit INTERVAL_TIMER range (ARR = 0xffff), making stall detection unreachable at low commutation rates. Clamp to 45000 so a slow-turning stall is always caught (the pre-PR fixed threshold was always reachable). - The demag duty cut wrote duty_cycle/last_duty_cycle directly from the comparator ISR, which preempts tenKhzRoutine and can be clobbered by its unguarded "last_duty_cycle = duty_cycle" slew write-back (lost cut, phase re-energizes at full duty). Hand the cut to tenKhzRoutine via a latch + pending flag; it applies on the same tick the CCRs actually change, so the cut can no longer be raced away. - startMotor cleared auto_blanking AFTER commutate(), but commutate()-> changeCompInput() programs comparator polarity off auto_blanking, so a stale arm from a prior run (e.g. a stop path that didn't clear it) could arm reversed polarity for the first commutation. Clear demag state before commutate() via a new resetDemagState() helper (also used at the desync and bemf-timeout reset sites so a future state field can't be missed). - The HWCI_DEMAG_COMP bench override was applied after active_demag was derived from demag_comp_level, so a forced level never propagated to the derived gate. Apply the override before the derivation. - Gate the auto_blanking arm in interruptRoutine with HAS_DEMAG_COMP so the non-ported MCU families no longer pay a volatile test in the hottest ISR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…MCUs) enableCompInterrupts() unmasked the comparator EXTI without clearing the pending flag. An edge latched while masked during the zero-cross -> commutation wait window (comStep/PWM ring) therefore fires the ISR the instant of unmask. On the demag path that hits demagEdgeRoutine, whose branch has none of the interval/confirm gates the normal path uses, so it records a near-zero blanking_length and disarms the measurement - the feature silently reads ~0 under exactly the noisy high-load conditions it targets. Clear the pending flag(s) immediately before unmasking so the first edge seen after unmask is genuine. On the two-comparator ports (g071/g431) clear both comparator lines so a stale flag on the non-current line cannot be consumed by the ungated two-line ISR dispatch either. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These command-line flags become -D defines but are not part of any object's path, so "make <target> DEMAG_COMP=2" after a prior build returned "Nothing to be done" and an A/B bench run could flash firmware built at the wrong demag level (or with no perf struct). Record the flags in a stamp file that every ELF depends on; when they change, the stamp's contents and mtime change and the ELF is rebuilt. Uses $(file) rather than the shell to stay portable across the repo's Windows/Unix make wrappers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the hardware-CI results for this PR: - Inertness A/B (no prop, 3A supply) vs ark-release HEAD: PASS. The compiled-in-but-off demag port does not add zero-cross jitter (2.31%->2.35% mean, flat max) and leaves CPU headroom intact. - Demag-event A/B (HQ5136 prop + 6S): comp-ON (DEMAG_COMP=2) is NET-HARMFUL. Identical demag_step_stress snap-steps ran clean with comp off but desynced hard with it on - at 100% throttle the motor collapsed from ~30000 RPM to a mean of ~8500 (595 bemf-timeout samples, 290 fw_demag_events) where comp-off had zero. Root cause: the F051 demag-edge ISR branch is an ungated, unconfirmed parallel edge path (no interval gate, no confirm loop), the arm gate has no stability check so it arms through fast accel, and the allOff+cut+re-entrant interruptRoutine response sustains the desync. Verdict: demag comp ships OFF by default (eeprom 0xff), so this branch is safe to merge as comp-OFF infra (adaptive timeout + perf v5 + review fixes). The compensation itself needs the rework in the doc before it is enabled. Adds noprop_smoke_3a.yaml (2.8A abort) for current-limited-supply bring-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1abe208 to
b5c7288
Compare
- Introduced ACTIVE_DEMAG option in Makefile to enable active demag at boot. - Enhanced ADC interrupt handlers across multiple MCU files to include a minimum time gate for demag release edges. - Updated main.c to manage demag state and track consecutive failed demag measures. - Added new metrics for active demag interlock skips in performance tracking. - Updated performance structures and metrics processing to accommodate new interlock metrics. - Created new YAML profiles for no-prop smoke tests at various throttle levels. - Enhanced test coverage for new metrics and performance structures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bench validation is complete (ARK 4IN1 F051 + Flight Stand 50). Full report:
hwci/DEMAG_BENCH_FINDINGS.md.the compiled-in-but-off demag port adds no zero-cross jitter (2.31 % → 2.35 %
mean, flat max) and no CPU regression.
6S, the
demag_step_stresssnap-steps ran clean with comp off butdesynced hard with
DEMAG_COMP=2: at 100 % throttle the motor collapsed from~30 000 RPM to a mean of ~8 500 (595 bemf-timeout samples vs 0 off). Root
cause: the F051 demag-edge ISR branch is an ungated/unconfirmed parallel edge
path, the arm has no stability check (arms through fast accel), and the
allOff+ duty-cut + re-entrantinterruptRoutine()response sustains thedesync.
Demag comp is off by default (eeprom 184/185 default
0xff), so this branchis safe to merge as the comp-OFF path — adaptive stall timeout + perf v5
telemetry + the review fixes below, all bench-clean. The compensation itself
needs the rework in the report (stability-gate the arm, gate/confirm the demag
edge, soften the response) before it is enabled;
DEMAG_COMP=<n>stays abench-only experiment until then.
Summary
Rebase of the demag-compensation half of draft PR #2 onto
ark-release. The original branch predates #14/#19/#21, which rewrote the F051 comparator ISR path it patches (inlined comparator read, RAM-resident hot path, glitch-tolerant confirm loop), so this is a hand re-application as a reviewable commit series rather than a textual rebase. The active demag half (freewheel through the FET channel instead of the body diode) is split out into stacked PR #25 so its hazard profile can be reviewed and bench-gated separately.Rebased 2026-07-06 onto current
ark-release. Since #29 now owns perf struct v4 (the 32-bin PWM-phase histogram) and #30 added the turn-on grid compensation, the demag perf fields were renumbered to struct v5 (offsets 148–154), appended after the histogram;host_cmdstays frozen at 60. Both features coexist; all 244 offline tests pass.What it does
After each zero-cross (when
demag_comp_levelis set, throttle > 20%, and the motor is up to speed),interruptRoutine()armsauto_blanking:changeCompInput()then configures the comparator EXTI with reversed polarity, so the first edge seen after commutation is the demag release edge — the moment the freewheel diode stops clamping the floating phase. The newdemagEdgeRoutine()(RAM_FUNC) timestamps it:blanking_length = time-since-ZC − waitTime(measured against the snapshotted scheduling delay, see fixes) is the measured demag duration, and the comparator is flipped back to normal polarity to catch the true zero-cross.If demag runs past half the commutation interval (the ZC is being swallowed), it counts
demag_happened, powers off the bridge, cuts duty by 25/50/75% per compensation level, and re-entersinterruptRoutine(). Bench-proven inadequate — see the status banner: this response path sustains desync under load rather than recovering from it.Also included, as a deliberately separable commit (
feat(bemf-timeout)): the stall threshold adapts to the commutation rate (average_interval * 4oncezero_crosses > 50, clamped to 45000 — see fixes) instead of the fixed 22.5 ms — revert alone if it misbehaves on aggressive throttle chops.Fixes to the original PR logic (found during the port + code review)
demag_happenedwasuint8_ton non-CAN builds (saturates at 255 — useless for hwci gating); now unconditionallyvolatile uint32_t.default:case; now an explicitcase 3:with a no-cut default so out-of-range values can't apply the harshest cut.duty_cycle(volatile) was double-read while computing the cut; now snapshotted once.time_since_zc <= waitTime) now zeroes both the measurement and the derived active-freewheel time, so a garbage measurement can't persist.blanking_lengthis clamped toaverage_interval.auto_blanking/measurement state is reset on startup, desync, and bemf-timeout paths (viaresetDemagState(), cleared beforecommutate()on start so a stale arm can't set the wrong polarity).blanking_lengthis measured against a snapshotted scheduling delay (demag_wait_ticks), not the next-cyclewaitTimethatPeriodElapsedCallbackrecomputes before the demag edge fires.average_interval*4unreachable at low RPM, disabling stall detection.tenKhzRoutinevia a latch/pending flag instead of a direct ISR write raced by the slew write-back.demagEdgeRoutinewith a bogus timestamp.HWCI_PERF/DEMAG_COMPchanges force a rebuild (an A/B session could otherwise flash a stale artifact).Scope & gating
MCU layer ported for f051, g071, g431, l431 (all compile; g431's
uint32_t commutation_intervalextern fix preserved). AHAS_DEMAG_COMPgate keeps every other MCU family fully inert (verified: SEQURE_4IN1_F421 builds with the feature compiled out). Both eeprom fields (bytes 184/185, in the CAN-block reserved space) default off via 0xff guards. New hot-path code is RAM_FUNC on F051.hwci integration
host_cmdfrozen at 60):demag_events(offset 148, mirror ofdemag_happened),blanking_len_last(152),blanking_len_max(154); total 156 bytes. Host decode with full v1–v5 back-compat (used live to decode the v4 ark-release firmware during the inertness A/B),fw_demag_eventsmetric (a new key alongside the host-derived detection, so old baselines stay comparable), report, sim, and tests: 244 passed.make ARK_4IN1_F051 HWCI_PERF=1 DEMAG_COMP=2forces the level at boot without touching the eeprom, so A/B sessions flip the feature by reflashing.Memory (ARK_4IN1_F051, HWCI_PERF=1 DEMAG_COMP=2)
RAM headroom on HWCI builds is thin: the v5 struct now carries #29's 64-byte histogram and the demag block. The next RAM-costing F051 feature (or a comp-ON A/B that also wants the histogram) will need a trade — dropping the phase histogram from the bench build frees the most.
Bench validation — RESULTS (2026-07-06)
Full report and data pointers:
hwci/DEMAG_BENCH_FINDINGS.md.noprop_smoke_3a) — PASS. vs ark-release HEAD: zero-cross jitter flat (2.31 % → 2.35 % mean, 12.2 % → 12.2 % max), CPU 62 % → 61 %, demag/bemf-timeout 0/0. Theauto_blankingbranch adds no jitter when off.demag_step_stressA/B (HQ5136 prop + 6S) — comp-ON FAILS. comp-off ran the 90/95/100 % snap-steps clean (0 bemf timeouts, RPM held ~28 650 at 100 %);DEMAG_COMP=2desynced hard (595 bemf-timeout samples, 3 host desync events, 290 fw_demag_events; step100 RPM collapsed to a mean ~8 500).fw_demag_eventsdid not drop with comp on — it caused the desyncs. See report for the root cause and the rework fix path.efficiency_sweepgate vsbaselines/ARK_4IN1_F051_HQ5136.json— not run (blocked on the comp-ON failure; efficiency is moot until the feature works).🤖 Generated with Claude Code
https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6