refactor: split motor control out of main.c#33
Conversation
Extract behavior-neutral control path units so demag, faults, and port work can land without editing a 2500-line god file: - commutation.c: getBemfState, commutate, advanceincrement, zcfoundroutine - bemf_zc.c: PeriodElapsedCallback, interruptRoutine, startMotor - control_loop.c: setInput, tenKhzRoutine, PID/current helpers - motor_runtime.h: extern map for shared runtime state still defined in main F051 duty/COM timer macros use statement form for GCC 15 -Werror. Builds verified: ARK_4IN1_F051 (HWCI_PERF=1) and SEQURE_4IN1_F421.
Behavior-neutral move of stuck-rotor, signal-timeout, BEMF timeout policy, and interval-stall handling out of main/control_loop so the control path is easier to reason about and demag work can layer on top.
Further behavior-neutral split of main.c: - settings.c: load/save EEPROM apply + bootloader checkDeviceInfo - brushed.c: BRUSHED_MODE control loop - runtime_loop.c: variable PWM, desync, IRQ priority, telemetry, ADC/LVC, and sine/stepper vs brushless running branch main.c drops to ~840 lines (init + thin main loop).
Ownership of shared motor-control state now lives with motor_runtime (definitions in .c, declarations in .h). main.c is init + thin main loop only (~535 lines). Expand motor_runtime.h for symbols modules previously pulled via local externs.
Introduce esc_state (DISARMED → ARMING → ARMED_IDLE → SINE/OPEN/CLOSED → BRAKE / FAULT_*) as the named policy mode. Transitions update both the enum and legacy flags so ISRs and existing modules keep reading armed/ running/stepper_sine/old_routine without hot-path changes. Wire arming, stuck/signal/LVC faults, desync/stall, sine handoff, and main-loop reconcile. Smoke: noprop 100% on 3 A bench (pass).
- Transition allow-list with escCommitState; illegal edges counted (optional ESC_STATE_STRICT hang); reconcile forces without the table - Policy helpers mirror legacy flags for 20 kHz/ISR-coupled paths; reconcile at tenKhz + main loop keeps esc_state aligned for logs - hwci_perf v5: esc_state + esc_illegal_edge_count; host decoder, CSV columns, sim, and unit tests - test_esc_state.py mirrors the C edge table Bench: noprop 100% hold, CLOSED_LOOP, 0 illegal edges, 0 BEMF timeouts
Fail noprop smoke when the motor never really spun: require high-throttle running fraction, zero stuck-rotor latch (bemf>=102), OPEN/CLOSED esc_state, zero illegal edges, and min RPM floors. Gates are profile-configured, independent of baseline regression, and drive hwci analyze/ci exit codes. Verified: good free-run exit 0; stuck-latch run exit 1.
Add scripts/cppcheck-ark.sh (make cppcheck) with max-configs=1 so targets.h does not explode config space, suppressions for CMSIS/HAL, and a GitHub Actions workflow that installs cppcheck and runs on every PR. CI fails on error/warning only; style findings are advisory.
1) Size / sources - Exclude brushed.c unless target name contains BRUSHED; exclude hwci_perf.c unless HWCI_PERF=1 (F051 production drops ~0.8% flash) - scripts/check-size-ark.sh + make size-check-ark enforce 98% flash / 97% RAM on ARK F051 with HWCI_PERF - CI job size-ark in static-analysis workflow 2) HAL-facing extract - adc_app.c: vendor ADC kick/temp/bus/current (out of runtime_loop) - pwm_app.c: setPwmDeadTime() (out of settings load path)
- Host model (esc_state_model.py) parses esc_allowed[] from firmware C (single source of truth) and mirrors reconcile/named transitions - Expanded tests: arm→open→closed, sine handoff, stall, stuck/signal/LVC, illegal-edge counter, flag-backed predicates before reconcile - Policy call sites use escIsArmed/escIsDriving/escInSineStart/ escInPollZcDrive/escInOpenLoop/escInClosedLoop after tenKhz reconcile - Size gate ceilings 99% flash / 98% RAM for tight F051+HWCI builds
Drop unused heap reservation, keep the sine table in flash, and inline esc_state predicates so RAM-resident callers avoid long-call veneers. HWCI RAM ~97.6%→82.1%; flash region ~98.5%→98.1%. Tighten size-gate RAM budget to 90%.
Shrink F051 flash by compiling most code for size. Keep commutation, BEMF/ZC, 20 kHz control, and F051 phaseouts at -O3 via RAM_FUNC optimize attributes and a phaseouts pragma (single-shot link cannot mix trailing -O3 per file). HWCI ~98%→88% flash; size gate flash 95%.
Restore MCU_SITL checkDeviceInfo / poles guard in settings.c, portable flash section for filename, uint32 desync_happened for DroneCAN/SITL, serial_telemetry include for SITL telemetry path, and longer DShot signal-loss coast settle for CI.
aef460e to
68494d7
Compare
The 3 A supply brick current-limits; a host max_current near 3 A was aborting clean free-run holds on stand glitches. Raise host current limit and leave temp/rpm/thrust gates in place.
Global -O3 again for main-loop throughput (was -Os for flash). Stop calling escReconcileFromFlags from tenKhzRoutine — flag-backed predicates already drive policy there; reconcile once per main loop only, removing the F051 RAM→flash veneer on every 20 kHz tick. Size gate flash limit 99% for HWCI_PERF images.
- getAbsDif is static inline (desync and other hot callers). - faultSignalTimeoutTick is RAM_FUNC so tenKhzRoutine does not veneer into flash every 50 us on F051. Tried inlining variable-PWM/desync into main; rolled back after free-run 100% stalls. Prefer same-TU/out-of-line for those policies.
Bring main_instrumented BDShot baseline onto feat/split-main-control: HWCI_PERF v6 fields (rx/tx/telem/EDT), dshot.c hooks, host decoder/CSV/sim, PX4 capture/sustain scripts, and docs. Version is v6 here (v3–v5 already used for zc/phase/esc_state). Hardware: ARK FPV BDShot300 free-run on battery — RPM tracks throttle (~7.7k@12% … ~18k@40%), soft ramp-down, unit tests green.
Two exclusive setups used at different times: - SETUP A: Flight Stand drives uni DShot (rig.yaml / rig.flightstand.yaml, hwci run + noprop profiles). No PX4 on the signal pin. - SETUP B: ARK FPV BDShot owns the pin (rig.px4_bdshot.yaml with throttle_backend none, px4_motor_stream / capture scripts). Add NullThrottle, BENCH_SETUPS.md, and clear labels on scripts/profiles so the two paths cannot be mixed by accident.
Hardware validation (feat/split-main-control @
|
| Test | Setup | Result |
|---|---|---|
| BDShot free-run ladder to 100% vs baseline | B (PX4) | PASS — no regression vs main_instrumented |
| Prop smoke to 30% | A (Flight Stand) | PASS |
| Efficiency sweep 10→100% | A (Flight Stand) | PASS |
low_rpm_crawl (8→3%) |
A (Flight Stand) | PASS |
startup_reliability (10 cold starts) |
A (Flight Stand) | PASS |
1. BDShot A/B free-run to 100% (SETUP B — PX4 owns signal)
Artifacts: hwci/runs/ab-bdshot100-20260710_160854/
| cmd | A max RPM | B max RPM | ΔB−A | A mean | B mean |
|---|---|---|---|---|---|
| 10% | 8 842 | 8 800 | −42 | 6 879 | 6 917 |
| 20% | 12 242 | 12 285 | +43 | 10 752 | 10 753 |
| 40% | 18 428 | 18 200 | −228 | 18 140 | 18 014 |
| 60% | 25 971 | 26 214 | +243 | 25 561 | 25 837 |
| 80% | 33 485 | 33 485 | 0 | 32 820 | 32 814 |
| 100% free-run | ~3 k mean | ~3 k mean | — | both collapse | same on A & B |
| 50% after | recovers | recovers | — | ~21.7 k | ~21.8 k |
- Peak RPM both: 33 485; spin_frac A=0.952 / B=0.959
error_countΔ: A=+2143, B=0- No high-throttle armed drop on either side
- Note: 100% free-run RPM collapse (~3 k while servo=full) is shared by baseline and split, not a modularization regression. After step-down to 50%, both recover.
Verdict: no BDShot free-run regression of the refactor vs instrumented upstream.
2. Prop smoke to 30% (SETUP A — Flight Stand)
Run: hwci/runs/split-prop-smoke30-20260710_161808
| Hold | RPM | Thrust | Current |
|---|---|---|---|
| 10% | ~4 632 | ~19 gf | ~0.23 A |
| 20% | ~8 383 | ~57 gf | ~0.71 A |
| 30% | ~11 542 | ~140 gf | ~1.55 A |
Abort: none · armed drop: none · demag: 0 · max I: 1.67 A
3. Efficiency sweep 10→100% (SETUP A)
Run: hwci/runs/split-efficiency-sweep-100-20260710_161927
Profile: efficiency_sweep
| Throttle | RPM | Thrust (gf) | I (A) | g/W |
|---|---|---|---|---|
| 10% | 3 591 | 11 | 0.15 | 3.69 |
| 20% | 8 381 | 68 | 0.72 | 4.00 |
| 30% | 11 526 | 139 | 1.55 | 3.80 |
| 40% | 14 348 | 229 | 2.76 | 3.54 |
| 50% | 17 134 | 349 | 4.60 | 3.28 |
| 60% | 19 690 | 444 | 7.01 | 2.79 |
| 70% | 21 849 | 545 | 9.93 | 2.46 |
| 80% | 23 790 | 649 | 13.16 | 2.27 |
| 90% | 25 434 | 748 | 16.83 | 2.09 |
| 100% | 26 809 | 806 | 20.79 | 1.88 |
- Abort: none · armed drop: none · demag: 0 · t100 run_frac=1.0
- Peak ~1044 gf / 22.2 A · idle loop ~38.3 kHz · CPU max ~74% · FET ~39 °C
4. low_rpm_crawl (SETUP A)
Run: hwci/hwci/runs/split-crawl-startup-20260710_162126/low_rpm_crawl
(weak-BEMF: spin 10% then 8→6→4→3%)
| Hold | RPM | Thrust (gf) | run_frac | zc_jitter % |
|---|---|---|---|---|
| 8% | 3 801 | 10.8 | 1.000 | 0.06 |
| 6% | 2 989 | 7.1 | 1.000 | 3.18 |
| 4% | 2 117 | 3.0 | 1.000 | 1.12 |
| 3% | 1 682 | 1.3 | 1.000 | 1.45 |
Abort: none · demag: 0 · BEMF timeout samples: 0 · armed drop: none
5. startup_reliability (SETUP A)
Run: …/startup_reliability — 10× cold start at 8%
| Metric | Result |
|---|---|
| Attempts | 10 |
| Failures | 0 |
| Time-to-run max | 16.3 ms |
| Time-to-run mean | ~11 ms |
| Demag | 0 |
Setups (do not mix signal wiring)
| SETUP A | SETUP B | |
|---|---|---|
| Signal | Flight Stand ESC out | ARK FPV BDShot |
| Config | rig.yaml / config/rig.flightstand.yaml |
config/rig.px4_bdshot.yaml |
| Command | hwci run --profile … |
scripts/px4_motor_stream.py |
| Docs | README | docs/BENCH_SETUPS.md |
Bottom line: modular split holds prop-on thrust-stand sweep through 100%, weak-BEMF crawl, cold starts, and matches main_instrumented on BDShot free-run through 80% (shared free-run quirk at 100% on both trees).
Summary
Behavior-neutral modularization of motor control out of
main.c, plus a top-level drive state machine:commutation.cbemf_zc.ccontrol_loop.cfaults.csettings.cruntime_loop.cbrushed.cBRUSHED_MODE)motor_runtime.cesc_state.cmain.cis init + thin main loop. Legacy flags stay for ISR hot path; SM is the named policy mode.Bench
noprop_smoke_100pct_3aon 25 V / 3 A no-prop — full hold at 100% (exit 0)Test plan
make ARK_4IN1_F051 HWCI_PERF=1make SEQURE_4IN1_F421