Skip to content

refactor: split motor control out of main.c#33

Merged
AlexKlimaj merged 19 commits into
ark-releasefrom
feat/split-main-control
Jul 10, 2026
Merged

refactor: split motor control out of main.c#33
AlexKlimaj merged 19 commits into
ark-releasefrom
feat/split-main-control

Conversation

@AlexKlimaj

@AlexKlimaj AlexKlimaj commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Behavior-neutral modularization of motor control out of main.c, plus a top-level drive state machine:

Module Responsibility
commutation.c six-step step advance, BEMF poll helper, sine advance
bemf_zc.c ZC confirm ISR, COM timer schedule, startMotor
control_loop.c throttle map, duty slew, 20 kHz tick, DShot process wrapper
faults.c stuck rotor, signal timeout, BEMF timeout policy, interval stall
settings.c EEPROM load/apply, save, bootloader device-info
runtime_loop.c variable PWM, desync, IRQ priority, telemetry, ADC/LVC, sine vs BL branch
brushed.c brushed-mode loop (BRUSHED_MODE)
motor_runtime.c shared runtime definitions
esc_state.c top-level drive SM (arming / sine / open / closed / brake / faults)

main.c is init + thin main loop. Legacy flags stay for ISR hot path; SM is the named policy mode.

Bench

  • noprop_smoke_100pct_3a on 25 V / 3 A no-prop — full hold at 100% (exit 0)

Test plan

  • make ARK_4IN1_F051 HWCI_PERF=1
  • make SEQURE_4IN1_F421
  • HWCI noprop 100% smoke after state machine

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.
@AlexKlimaj AlexKlimaj force-pushed the feat/split-main-control branch from aef460e to 68494d7 Compare July 10, 2026 16:42
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.
@AlexKlimaj

Copy link
Copy Markdown
Member Author

Hardware validation (feat/split-main-control @ 206b5c4)

Bench: ARK 4IN1 F051 · HQ5136 prop · battery · Flight Stand 50 (SETUP A) and ARK FPV PX4 BDShot300 (SETUP B). Compared where noted against main_instrumented @ 5484cf3.

Summary

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).

@AlexKlimaj AlexKlimaj merged commit 59e7fed into ark-release Jul 10, 2026
5 checks passed
@AlexKlimaj AlexKlimaj deleted the feat/split-main-control branch July 10, 2026 22:25
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