Skip to content

Multi-modulator architecture: modulator slot, N-pulse patterns, live handoff, tracking observer - #28

Open
datacrystals wants to merge 21 commits into
mainfrom
shepwm
Open

Multi-modulator architecture: modulator slot, N-pulse patterns, live handoff, tracking observer#28
datacrystals wants to merge 21 commits into
mainfrom
shepwm

Conversation

@datacrystals

@datacrystals datacrystals commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the multi-modulator architecture from Images/Gen6FW/docs/Multi_Modulator_SHEPWM_Plan.md (written after verifying every load-bearing assumption against the H723 datasheet and the base image). The branch ends in a bench-verified working state: FOC + variable-carrier SVPWM at 50 V bus, with synchronous pattern work parked pending SHE+FOC papers.

Architecture (steps 1–2)

  • Modulator interface (update/commit/enter/exit) with a single active slot; SVPWM/SPWM wrapped behind it, foc_active flag retired for a control-hook pointer
  • TIM1 ISR dispatches: control hook (closed loop) → active self-clocked modulator
  • Legacy FOC now uses the extrapolated encoder angle (same as the graph path)

Pattern engine (step 3)

  • TIM5 (32-bit) pattern timebase: one timer cycle = one electrical cycle; compare-chain ISR writes TIM1 forced-output modes (Option B — PE8–PE13 are TIM1-only pins, verified in DS13313; no HRTIM exists on H723)
  • Ping-pong event buffers in AXI SRAM, swap only at cycle wrap; dead time + BKIN break stay in circuit
  • Runtime N-pulse mode (no tables): npq cells/quarter, duty = high-time fraction; expansion validated against analytic fundamentals by FFT replica (incl. the half-cycle boundary-edge fix)
  • Offline SHE solver + validated N=9 table family kept for later (Tools/she_table_gen.py)

Live handoff (step 4) + node integration

  • Phase-locked ramp ⇄ pattern and FOC ⇄ pattern handoffs (angle capture, level-matched pins, MOE always on)
  • platform_api: modulation switch, electrical-frequency source, carrier set, live control dt
  • Nodes: Actuators.ModulationAuto (freq-threshold mode switch), Actuators.CarrierAuto (piecewise carrier schedule); handoff/shestart shell commands
  • Tracking observer (2nd-order PLL) replaces the 8 s windowed-RPM EMA: ~4 ms lag, glitch rejection, dt from DWT
  • CAN error-fault arming gated on KV bus enables

Bench-verified

  • FOC + 2k→3k→4k piecewise carrier schedule at 50 V
  • Live handoffs switch cleanly; stall recovery via encoder-fe supervisor
  • Rolled back (after bench regression): PI gain scheduling, Slew live-dt, current-boost carrier — see git history for the bisect trail

Parked / follow-ups (filed as issues)

Test plan

  • Emit + ARM build of foc_demo.json clean at every commit
  • FFT replica validation of pattern expansion
  • Bench: open ramp, FOC spin, handoffs, carrier sweeps at 10–50 V
  • Dyno / full-load validation

…c_active

Step 1+2 of the multi-modulator plan (docs/Multi_Modulator_SHEPWM_Plan.md):
- New Modulator interface (update/commit/enter/exit) + single active slot
- SVPWM and SPWM moved behind the interface with unchanged math
- TIM1 ISR dispatches via a control-hook pointer instead of the foc_active
  flag; hook is the future codegen controller attachment point
- Legacy FOC current loop now consumes the extrapolated encoder angle,
  matching the graph path (platform_get_encoder_angle_latest)
- platform_api / PWM_* signatures unchanged; verified with foc_demo emit+build
Step 3 of the multi-modulator plan (option B handoff; MI strategy (a):
offline table family + interpolation):

- Tools/she_table_gen.py: offline SHE solver (Newton + branch continuation),
  N=9 angles/quarter eliminating h5..h25, MI grid 0.50..0.90 step 0.02
  (per-unit of six-step); emits SheTables.h with per-point Fourier validation
- ShepwmModulator: one TIM5 cycle = one electrical cycle; quarter-wave table
  expanded to a merged 3-phase event list (incl. half-cycle boundary edges,
  validated against FFT: exact fundamental, eliminated harmonics at the
  timer-count quantization floor, triplens cancel line-line)
- Ping-pong pattern buffers in AXI SRAM; single swap + ARR update only at
  the cycle-wrap ISR; compare-chain ISR (TIM5 CH1, priority 3) writes TIM1
  OCxM forced modes via new driver primitives PWM_ForcePhaseLevel /
  PWM_ReleaseForcedOutputs (TIM1 always owns pins: dead time, BKIN trip and
  ADC trigger cadence stay in circuit)
- enter() phase-locks CNT to the electrical angle and level-matches initial
  pin states; exit() restores PWM1 mode
- Shell bring-up: shestart/shestop/sheset/shestatus; FOC/open-loop/graph
  supervisor start paths refuse to run while SHEPWM owns the slot
fdcanFaultInit() armed bus-off/error-passive notifications on both FDCANs
unconditionally at boot, so a bench with nothing attached latched a High
CanErrorPassive fault and blocked every motor start even when the user had
disabled the bus.  Arming now happens inside CanBus::init() after timing
config, only for buses enabled via Can.A.En/Can.B.En.
Simpler alternative to offline SHE tables: npq cells per quarter cycle,
each HIGH for the middle duty fraction (centered notch).  Angles computed
arithmetically at runtime; pulse count and duty are command arguments.

- buildEvents gains angle-count + start-level parameters (SHE notch
  convention and centered-notch pulse patterns share the walker)
- shestart/sheset take an optional 3rd arg: 'shestart 60 0.8 9' =
  9 pulses/qtr at duty 0.8; two-arg form stays SHE-table mode
- Event expansion validated against analytic fundamental (FFT replica):
  exact at duty 1.0 (six-step) through the duty~0.5 zero crossing;
  120 deg shifts and line-line triplen cancellation exact
- Commands merged into shestart/sheset (each shell command object costs
  ~208 B of nearly-full DTCMRAM)
- spwmSetAngle: seed the open-loop ramp phase on resume
- shepwmAngleRad: pattern angle in the ramp/FOC frame (TIM5 CNT + offset)
- handoff command: 'handoff 1 <pulses> <duty>' captures the ramp angle,
  stages the N-pulse pattern at the same fe, exits the ramp and enters the
  pattern phase-locked with level-matched pins; 'handoff 0' exits the
  pattern and resumes the ramp at the pattern angle with the OL MI
- handoff command object placed in AXI SRAM (DTCMRAM overflowed by 96 B)
- ModulationSwitch: base-image handoff primitives shared by the shell
  'handoff' command and the codegen platform_api (phase-locked both ways)
- platform_api: platform_get_ol_freq_hz / platform_modulation_mode /
  platform_modulation_to_pattern / platform_modulation_to_ramp
- New Actuators.ModulationAuto node template (app_loop, policy only):
  enter_hz/exit_hz hysteresis + pulses/duty params drive the HAL handoff
- foc_demo graph: ModAuto instance (enter 10 Hz, exit 8 Hz, 9 pulses/qtr,
  duty 0.85) so ramping past 10 Hz switches to N-pulse automatically
- OpenLoopController::stop releases the pattern modulator before coasting
- FocControlManager: electricalVoltageAngleRad() (Park angle + dq voltage
  phase), electricalSpeedRadPerSec(), suspendForHandoff() (hook off,
  outputs stay live), restartLastSetpoints() (flying restart)
- ModulationSwitch: FOC->pattern captures the applied voltage-vector angle
  and estimated fe, suspends the current loop, enters phase-locked;
  pattern->FOC restarts with the last setpoints (tracks entry origin)
- platform_get_elec_freq_hz: pattern fe in pattern mode, FOC speed
  estimate under FOC, else the OL ramp command; ModAuto node uses it, so
  the 10 Hz / 8 Hz thresholds now work from 'control start' + Iq as well
FOC cruising speed at Iq=8 A on 10 V is ~4.6 Hz electrical; the 10/8 Hz
thresholds were never crossed.
The 'control start' path is generated nodes + ControlSupervisor, not the
legacy FocControlManager, so the supervisor saw fe=0 and the handoff never
armed.  Fixed end to end:

- platform_get_elec_freq_hz: encoder speed x pole pairs (controller-
  agnostic; pattern/legacy-FOC/ramp only as fallbacks)
- ModulationSwitch graph branch: suspend = TIM1 update ISR off (generated
  step dies, MOE/outputs stay live), phase-lock angle from encoder +
  calibration with rpm-signed q-axis lead; resume re-enables the ISR with
  preserved domain state
- ControlSupervisor::stop releases the pattern modulator first
- Pulse cap 64 -> 128; ModAuto defaults to 101 pulses/qtr
- platform_get_elec_freq_hz prefers encoder speed even in pattern mode, so
  a pattern stall collapses fe and the ModAuto supervisor hands back
  automatically instead of latching (bench log: rotor stalled at handoff,
  pattern held its frozen 4 Hz forever)
- PWM_SetFrequency no longer forces RCR=1: preserves dual-update when a
  control hook is registered (live carrier change used to silently halve
  the FOC rate)
- platform_pwm_set_carrier_hz with 5 Hz deadband + 0.5..20 kHz clamp
- Actuators.CarrierAuto node: carrier = lerp(fsw_lo..fsw_hi, fe_lo..fe_hi);
  foc_demo instance 5 kHz -> 2 kHz over 0..5 Hz electrical
- CarrierAuto is now a 6-breakpoint piecewise-linear table (fsw<=0 ends it)
- Demo schedule: 1 kHz flat to 15 Hz, ramp to 2 kHz by 20, flat to 30,
  ramp to 4 kHz by 40, flat above
- ModAuto instance removed from the demo graph (synchronous modulation
  shelved pending a proper SHE+FOC method; template and engine remain)
The mode supervisors inherited ~8 s of lag from the speed estimate (40 ms
windows + alpha=0.005 EMA = 8 s time constant).  The encoder DMA ISR now
runs a theta/omega tracking observer at 60 Hz bandwidth: ~4 ms tracking
lag with per-sample angle noise strongly attenuated, dt from the DWT
cycle counter so it stays correct when the encoder trigger follows the
carrier.  diagnose() keeps only the (slow-by-nature) signal-quality
fault checks; every rpmMech() consumer — telemetry, angle extrapolation,
ModAuto/CarrierAuto — gets the fast estimate.
The observer's k1 correction was missing its dt factor, overcorrecting
~1/dt x per sample: omega estimate oscillated into garbage, polluting
both platform_get_elec_freq_hz (carrier schedule pinned at the top of its
range) and the extrapolated FOC commutation angle (iq thrash, erratic
spinning).

- Observer: proper Euler step theta += (omega + k1*err) * dt
- platform_get_control_dt(): live 1/TIM1-update-rate for generated code
- Control.Pi: dt from the live control rate (was baked 0.0002; a 1 kHz
  carrier would have made integral gain 2.5x hot)
A rotor can't move more than ~|omega|*dt between samples; an EMI/encoder
outlier can sit anywhere on the circle.  Such samples now coast (theta
advances by omega*dt, no correction) instead of kicking omega and the FOC
extrapolated angle.  Rejects are counted for diagnostics.
Gains were tuned at 5 kHz update; at the 2 kHz carrier floor the loop
crossover stays ~120 Hz while delay grows, eating phase margin (audible
ringing bursts at low rpm).  Kp/Ki now scale by DtRef/dt_live, clamped to
never exceed the tuned values, so delay margin is constant across the
carrier sweep.
At 2 kHz carrier/50 V bus the current ripple is ~80 A p-p - bigger than a
25 A command, so reversals at high iq rang regardless of gain scheduling.
CarrierAuto now adds boost_per_amp * max|i_phase| (80 Hz/A in the demo:
2 kHz floor at low current, ~4 kHz by 25 A).
Reverts PI gain scheduling, Slew live-dt and current-boost carrier after
the drive became unstable at trivial setpoints on the bench.  Restores
the node templates and demo graph to the state that ran at 50 V with
only occasional low-rpm ringing.
@datacrystals

Copy link
Copy Markdown
Contributor Author

Scope update / hard blocker found during review

Bench testing confirmed that closed-loop FOC at low effective switching frequencies is unstable and hazardous on this hardware. The root cause is current measurement: the existing single bottom-sample-per-PWM scheme does not represent average current once ripple grows, and closing FOC around that feedback leads to blow-ups.

So current oversampling / software ripple compensation is now a hard prerequisite for FOC + SHEPWM. I’ve added §10 to in the branch to record this.

Plan:

  • Put this PR on a narrower scope: keep the modulator refactor + manual/shell-driven SHEPWM, but do not merge closed-loop FOC ⇄ SHEPWM integration yet.
  • Start a separate track for current oversampling/ripple compensation.
  • Once that is validated against existing modulation, revisit FOC + SHEPWM.

/cc @datacrystals

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.

1 participant