Multi-modulator architecture: modulator slot, N-pulse patterns, live handoff, tracking observer - #28
Open
datacrystals wants to merge 21 commits into
Open
Multi-modulator architecture: modulator slot, N-pulse patterns, live handoff, tracking observer#28datacrystals wants to merge 21 commits into
datacrystals wants to merge 21 commits into
Conversation
…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.
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:
/cc @datacrystals |
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
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)
Modulatorinterface (update/commit/enter/exit) with a single active slot; SVPWM/SPWM wrapped behind it,foc_activeflag retired for a control-hook pointerPattern engine (step 3)
npqcells/quarter, duty = high-time fraction; expansion validated against analytic fundamentals by FFT replica (incl. the half-cycle boundary-edge fix)Tools/she_table_gen.py)Live handoff (step 4) + node integration
platform_api: modulation switch, electrical-frequency source, carrier set, live control dtActuators.ModulationAuto(freq-threshold mode switch),Actuators.CarrierAuto(piecewise carrier schedule);handoff/shestartshell commandsBench-verified
Parked / follow-ups (filed as issues)
Test plan
foc_demo.jsonclean at every commit