A motor drive bug found on hardware costs a session; one found on hardware at the wrong moment costs a power stage. So the MMC is built simulation-first: every control path runs against a virtual motor in CI before it touches a real one, and the host tools cannot tell which one they are talking to. That only works if the code under test is the code that ships.

Three layers

Where the code lives

Crates in the MMC workspace
mmc-coretransforms · PI · SVPWM · observer · six-step · hallsmmc-drive Engine::tick()modes · probes · trips · params · telemetrytrait MotorBoard (mmc-hal)G474 boardF302 boardsim boardmmc-hostcapture · profile · apply · panelmmc-protoUART or TCPmmc-simvirtual motor · inverter · halls
  • mmc-core is the math: transforms, PI controllers, SVPWM, the flux observer, six-step commutation, hall decoding. It is no_std and never allocates, so it builds for a Cortex-M0+ and for a PC unchanged.
  • mmc-drive is everything the firmware does with that math: the drive-mode state machine, the measurement probes, protection trips, the parameter table and its flash format, host commands and telemetry. Its Engine::tick() is what the ADC interrupt calls.
  • A MotorBoard trait is the drive's whole view of the hardware: sample the currents, set three duties, enable phases, read the halls, report a driver fault. Each board crate implements it and does nothing else — clocks, timers, ADC triggers, serial and flash.

The middle layer is the important one, and it came late. Until the second board arrived, the drive logic lived inside the first board's 1763-line main.rs, mixed in with register writes. Moving it out took that file to about 560 lines and made it possible to run the firmware's own drive logic — not a re-implementation of it — against a simulated board in CI, at both 10 and 20 kHz.

One protocol, two transports

Telemetry and commands use one framing — COBS with a CRC16 — over UART to the hardware or TCP to the simulator. Capture, profile and apply all take either a serial port or a network address. If the sim and the device disagree on anything the host can see, the shared tooling shows it.

The interrupt has a budget

At a 20 kHz control rate, the whole control step — read the ADC, run the transforms and PIs, update the observer, write the duties, check the trips, snapshot telemetry — has 50 µs. On a 170 MHz G474 that is 8500 CPU cycles; on a 72 MHz F302 at the same rate, 3600. The firmware measures its own worst case with the CPU's cycle counter. These are the measured numbers:

ISR worst case vs the budget

Measured on hardware · change the control rate

Cycle counts are fixed by the code; the budget is clock ÷ control rate. The F302 runs its control at 10 kHz for exactly this reason. Six-step costs less than FOC mostly because the flux observer does not run in it.

Floating-point work on these MCUs is cheap when it is single precision. One early worst case was dominated by libm trig silently running in emulated double precision; replacing it brought the G474 interrupt to 13.9 µs.

What the simulator catches, and what it misses

A simulator is only as good as the physics in it. The MMC's record so far:

Found in simulation first
  • Dead time inflating the observer's flux magnitude by 1.5× at low speed while leaving the angle alone.
  • The observer's flux magnitude reading low at low speed because its leak was compensated in angle but not magnitude.
  • Six-step's zero-cross reference: modelling the bridge's drops reproduced, unprompted, why the sense divider forced a 12 V bus.
  • A dead-time measurement ladder that predicts each rung and stops before one that would trip the current limit.
Missed until hardware
  • The sim rotor was 50× too light at low speed: it had no Coulomb friction.
  • A light real rotor outran the I-f start's forced angle and exposed a wrap-around bug the heavy sim rotor never reached.
  • Stiction and cogging, which shaped every fix to the hall position loop.
  • The sim's parameter table drifting behind the firmware's, so tuning "applied" to the sim silently reverted.

Each miss became a model change: Coulomb friction from the profiler's measurement, a dead-time model shared between the simulated inverter and the controller's compensation, a parameter table that lives with the simulated device the way it does in firmware RAM. The simulator does not need to be perfect. It needs to be fixed every time the bench disagrees with it.

Next up

Part 6 stacks a position loop on top of everything so far, using only a motor's hall sensors, and runs into the limit the simulator could not see: friction.