Skip to content

feat(demag): active demag — freewheel through the FET channel instead of the body diode#25

Draft
AlexKlimaj wants to merge 9 commits into
feat/demag-comp-arkfrom
feat/active-demag
Draft

feat(demag): active demag — freewheel through the FET channel instead of the body diode#25
AlexKlimaj wants to merge 9 commits into
feat/demag-comp-arkfrom
feat/active-demag

Conversation

@AlexKlimaj

@AlexKlimaj AlexKlimaj commented Jul 3, 2026

Copy link
Copy Markdown
Member

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_pwm complementary 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(), after commutate() 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 from step + rising) and re-arms COM_TIMER for active_demag_ticks = half the last measured demag time, re-capped at waitTime/2 at 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 of commutate() several µs earlier, so there is no cross-conduction with the outgoing drive.

Implementation: phaseXHIGH() outputs + activeDemagFetOn/Off() live in each MCU's phaseouts.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()'s step/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 before activeDemagFetOn(), 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 in active_demag_interlock_skips (perf struct v5, offset 92, fw_interlock_skips in 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)

  1. First runs with DEMAG_COMP=1 only, ACTIVE_DEMAG off, to establish the measurement baseline.
  2. Scope the floating phase to confirm the commanded FET matches the freewheel diode's side on both rising polarities and both directions.
  3. Then make ARK_4IN1_F051 HWCI_PERF=1 DEMAG_COMP=2 ACTIVE_DEMAG=1, first run low duty with current + FET-thermal watch, checking fw_interlock_skips ≈ 0.
  4. efficiency_sweep A/B (ACTIVE_DEMAG=0 vs =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_HIGH targets get the inverted HIGH_BITREG_ON mapping — compile-time correct but untested on such hardware.

Memory (ARK_4IN1_F051)

Build FLASH RAM
HWCI_PERF=1 DEMAG_COMP=2 ACTIVE_DEMAG=1 25328 B (92.4%) 7728 B (96.6%)

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

claude and others added 6 commits July 6, 2026 18:13
… 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
@AlexKlimaj AlexKlimaj force-pushed the feat/demag-comp-ark branch from bdb88fc to 46907ca Compare July 6, 2026 19:20
claude and others added 3 commits July 6, 2026 19:20
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
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