feat(demag): active demag — freewheel through the FET channel instead of the body diode#25
Draft
AlexKlimaj wants to merge 9 commits into
Draft
feat(demag): active demag — freewheel through the FET channel instead of the body diode#25AlexKlimaj wants to merge 9 commits into
AlexKlimaj wants to merge 9 commits into
Conversation
… 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 v3's zc_confirm_reject: 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 84 -> 92 bytes. Host: FIELDS_V4 + 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-v4 firmware), report column, sim demag mirror of injected desyncs, tests incl. u32 wrap and v2/v3 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
bdb88fc to
46907ca
Compare
When active_demag is enabled and a valid demag time was measured, PeriodElapsedCallback() schedules a second COM_TIMER event right after commutation: activeDemagFetOn() turns the floating phase's conducting- side fet hard on (selected by step and rising) so the demag current circulates through the channel instead of the body diode, cutting diode conduction losses. The second COM_TIMER event, active_demag_ticks later (half the measured demag time, re-capped at waitTime/2 in case waitTime shrank since the measurement), turns it off via a guard at the top of the callback that runs before commutate() and HWCI_PERF_ZC(), so it never double-counts a commutation or skews jitter metrics. If the zero cross arrives before the off event, interruptRoutine() force-offs the fet and clears active_demag_fet_on so the armed COM timer event is not eaten by the top guard. Scheduling stays after HWCI_PERF_ZC() so perf jitter metrics are unaffected by the extra work. 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=<n> ACTIVE_DEMAG=1' additionally forces the active freewheel at boot via -DHWCI_ACTIVE_DEMAG; eeprom byte 185 stays untouched so bench A/B runs flip the feature by reflashing firmware. Production builds leave it unset and are unaffected. 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
…wheel During the demag window the floating phase is diode-clamped to the far rail, which the comparator reads as the post-cross level (!rising) - the same signal the reversed-polarity demag-edge arm watches. Read it just before activeDemagFetOn(): the pre-cross level means either the step/rising -> fet mapping is wrong for this hardware/direction (energizing that fet would put VBAT across the winding in the re-magnetizing direction - an uncontrolled VBAT/L current ramp the 1 kHz current loop cannot catch) or demag has already released (the fet would short the bemf). Either way skip the freewheel for that step and count it, turning a mapping fault from a hardware-damage event into a diagnostic counter at the cost of one comparator read. Exported as perf struct v5: active_demag_interlock_skips (u32, off 92, monotonic, main-loop mirrored). Host decode, DWARF vintage probe, model column, fw_interlock_skips summary metric (whole-run wrap-safe delta, None pre-v5), sim mirror, tests (143 pass). Bench interpretation: ~0 healthy; ~= one per commutation = wrong mapping, abort active-demag benching (the veto keeps the hardware safe while you scope it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
ce6dc83 to
c7a389d
Compare
1abe208 to
b5c7288
Compare
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.
Summary
Stacked on #24 (demag compensation) — the active-demag half of draft PR #2. During normal PWM chopping AM32 already turns on the opposite-side FET (
comp_pwmcomplementary drive, default on); the remaining body-diode conduction window is post-commutation demag: the winding that just switched off dumps its stored current through a body diode (~0.7–1 V drop) until it decays. This PR turns on the FET whose body diode is carrying that freewheel current, so the demag current circulates through the channel (mΩ Rds(on)) instead — measurable g/W at mid/high load, where demag current is largest.Rebased 2026-07-06 onto the rebased #24 (perf v4 world), and now includes a comparator interlock (below) that turns the wrong-mapping hazard into a diagnostic counter.
How it works
PeriodElapsedCallback(), aftercommutate()and the hwci ZC hook (so jitter metrics are unaffected): if active demag is enabled and #24's measurement is armed,activeDemagFetOn()drives the floating phase's conducting-side FET (chosen fromstep+rising) and re-arms COM_TIMER foractive_demag_ticks= half the last measured demag time, re-capped atwaitTime/2at use time. The second COM event hits a guard at the top of the callback that floats the phase again and returns. If the true zero-cross arrives before the off-event,interruptRoutine()force-floats and clears the flag so the pending COM event isn't swallowed.The FET-on window therefore always ends well before the zero-cross is expected, leaving the BEMF sensing window untouched. The previously conducting FET has been off since
comStep()at the top ofcommutate()several µs earlier, so there is no cross-conduction with the outgoing drive.Implementation:
phaseXHIGH()outputs +activeDemagFetOn/Off()live in each MCU'sphaseouts.c(in-TU so the phase bodies inline; f051/g071/g431/l431,HAS_PHASE_HIGH-gated). On F051 the helpers stay in flash deliberately: RAM_FUNC placement cost 504 B and overflowed the HWCI-build RAM; the 1-wait-state penalty is well under the 0.5 µs COM-timer tick that schedules them.Comparator interlock — wrong mapping becomes a counter, not a damage event
The failure mode if
activeDemagFetOn()'sstep/rising→ FET mapping is wrong for a board/direction combo is not a rail short (the conducting body diode commutates off when the node is pulled to the opposite rail) — it's worse in a quieter way: the full bus voltage lands across the "floating" winding in the re-magnetizing direction, an uncontrolled VBAT/L current ramp (~2.4 A/µs at 24 V/10 µH) that the DRV8328 (no VDS trip) and the 1 kHz firmware current loop cannot catch. The DRV8328's built-in dead-time enforcement does not protect here: only one gate is commanded, which is a perfectly legal input pattern — it only guards same-leg INH/INL overlap.So this PR adds a one-comparator-read interlock: during demag the floating phase is diode-clamped to the far rail, which the comparator reads as the post-cross level (
!rising) — the same signal #24's reversed-polarity demag-edge arm watches. Just beforeactiveDemagFetOn(), if the comparator instead reads the pre-cross level, the mapping is wrong (or demag has already released, where energizing the FET would short the BEMF) — skip the freewheel for that step and count it inactive_demag_interlock_skips(perf struct v5, offset 92,fw_interlock_skipsin run summaries). Interpretation: ~0 healthy; ≈ one per commutation = wrong mapping — the veto keeps the hardware safe while you scope it.Bench protocol (still applies — the interlock is a backstop, not a substitute)
DEMAG_COMP=1only,ACTIVE_DEMAGoff, to establish the measurement baseline.risingpolarities and both directions.make ARK_4IN1_F051 HWCI_PERF=1 DEMAG_COMP=2 ACTIVE_DEMAG=1, first run low duty with current + FET-thermal watch, checkingfw_interlock_skips ≈ 0.efficiency_sweepA/B (ACTIVE_DEMAG=0vs=1): expect a g/W gain at mid/high load;fw_demag_events == 0,bemf_timeout_samples == 0,fw_interlock_skips ≈ 0, FET temps flat.USE_INVERTED_HIGHtargets get the invertedHIGH_BITREG_ONmapping — compile-time correct but untested on such hardware.Memory (ARK_4IN1_F051)
272 B RAM free on the HWCI build — flagged as the binding constraint for the next F051 feature.
hwci offline tests: 143 passed. Builds verified: ARK_4IN1_F051 (plain / HWCI_PERF / both knobs), GEN_64K_G071, REF_G431, NEUTRON_L431, SEQURE_4IN1_F421.
🤖 Generated with Claude Code
https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6