The motor in the box
The board arrived first. The motors did not. I had ordered a MKS Dual FOC v3.2 Plus off AliExpress with a couple of small brushless rotors, because I wanted to start playing with field-oriented control properly and the controller is half the fun. The board landed in about a week. The motors were going to take three more.
I was impatient. I wanted to flash the ESP32, light up the driver, watch something commutate. For a while I sketched out ways to fake a load: resistor banks across the phases, a salvaged stepper coil, anything that would prove the gate driver was alive without a real rotor on it.1 Then I remembered the box.
Somewhere in the cupboard I had a stash of things kept from when I was younger and obsessed with how machines worked. Among them, I was almost sure, was a brushless motor pulled out of a floppy disk drive: an open-frame spindle outrunner, big magnet, visible coils, no plastic shroud hiding anything. I'd taken it out as a teenager because brushless motors were fascinating and I wanted to use it for something. Never figured out what. Never threw it away either.
I went and looked. It was still there.
So I soldered three wires onto the stator pads, put the multimeter across a pair, and read 15 Ω between any two phases. That's a wye-connected motor with about 7.5 Ω per phase (a forgiving, low-current rotor that would be hard to abuse at the voltages a small bench supply can produce). The plan crystallised right there at the desk: plug it into the controller, flash a minimal SimpleFOC sketch, and see what happened.
Field-oriented control, briefly
A brushless motor is just three coils and a spinning magnet. The hard part is timing: you have to push current through the right coils at the right instant, relative to where the rotor actually is. Get it wrong and the motor cogs, heats up, and goes nowhere.
Field-oriented control (FOC) solves this elegantly. It transforms the three
messy phase currents into two clean, rotor-aligned axes (d for flux, q for
torque), runs simple proportional-integral (PI) controllers there, and transforms
the result back into three phase voltages. You command torque directly and the math handles the
commutation.
The loop runs thousands of times a second. Conceptually it looks like this:
"You stop thinking about coils, and start thinking about torque."
For closed-loop FOC you need a sensor: the AS5600 magnetic encoder on the shaft tells the controller where the rotor actually is, so the math can put current on the right axis. I had one on order, and it hadn't arrived either. So this first session was open-loop only: no sensor, just ramp an electrical angle at a chosen rate and apply voltage at that angle. The rough-and-ready half of FOC. It either spins or it doesn't.2
The board is not what the forums say it is
The first surprise was the toolchain. SimpleFOC 2.3.4's ESP32 backend uses the
new motor-control PWM (MCPWM) API (driver/mcpwm_prelude.h), which only exists
in ESP-IDF 5,
but PlatformIO's official espressif32 platform pins the Arduino core at
2.0.17, on ESP-IDF 4. The build failed on a missing header: really a "your
platform is too old" problem in disguise. Switching to the pioarduino
fork3, which tracks arduino-esp32 3.x and ESP-IDF 5, was one line in
platformio.ini.
The second surprise was the board, for the boring reason that my revision had no published schematic. I had v3.2 Plus; the MKS-DUALFOC repository4 only documented v3.3. A forum post5 pointed me there anyway ("close enough, the difference is in the current sensor"), so I treated v3.3 as a proxy for my I/O map and left the current-sense differences for later. That gave me the pin map: M0 enable is GPIO 22, M0 PWM phases GPIO 32 / 33 / 25. Both channels live on one board (M0 and M1, separate enable pins); older v2.0 boards shared GPIO 12 for both, probably the source of half the forum confusion.
First flash, nothing happens
The first upload was uneventful. The toolchain compiled, pio run -t upload
flashed cleanly at 1152006, and the serial monitor came up at the same rate.
I sent a target velocity over Commander and waited for torque.
Nothing happened.
The motor sat there: no hum, no twitch, no hint of current. I leaned in for the PWM whine. Silence. I checked the supply: my bench was on 5 V, because that's what was already on the desk. The MKS Dual FOC needs at least 12 V to switch its MOSFETs, and a board-level divider on GPIO 13 lets the Makerbase firmware refuse to enable under about 11.1 V. At 5 V the gate-drive supply doesn't even come up. I swapped to a 12 V wall-wart and tried again.
Still nothing. Which channel: M0 or M1? I'd been driving the enable pin on GPIO 12, because an old MKS-ESP32FOC v2.0 example uses it for both motors. On v3.3, GPIO 12 is M1's enable; M0's is GPIO 22. My PWM lines were on M0's gate inputs while my enable drove M1's supply: I was lighting up the wrong channel and leaving M0 dark.
I changed one line in the sketch:
- BLDCDriver3PWM driver = BLDCDriver3PWM(32, 33, 25, 12);
+ BLDCDriver3PWM driver = BLDCDriver3PWM(32, 33, 25, 22);
Re-flashed. Hit T1 for a 1 rad/s open-loop target. And finally, this:
A faint, regular tick from the motor. Current was flowing in the right coils at the right time. The rotor was twitching, then stepping, then twitching again, but it was clearly doing something on command.
The motor rotates (sort of)
What I had now was the smallest possible SimpleFOC sketch: a driver, an open-loop velocity controller, a Commander binding so I could change the target without re-flashing.
#include <SimpleFOC.h>
// 30-slot / 20-pole floppy-spindle outrunner → 10 pole pairs.
// Pin map from MKS-DUALFOC v3.3 (no schematic published for v3.2 Plus).
BLDCMotor motor = BLDCMotor(10);
BLDCDriver3PWM driver = BLDCDriver3PWM(32, 33, 25, 22);
Commander commander = Commander(Serial);
void doTarget(char *cmd) { commander.scalar(&motor.target, cmd); }
void setup() {
Serial.begin(115200);
driver.voltage_power_supply = 12;
driver.voltage_limit = 3; // be gentle; ~0.4 A through 7.5 Ω
driver.init();
motor.linkDriver(&driver);
motor.controller = MotionControlType::velocity_openloop;
motor.voltage_limit = 3;
motor.velocity_limit = 20;
motor.useMonitoring(Serial);
motor.init(); // no initFOC; no sensor yet
commander.add('T', doTarget, "target velocity [rad/s]");
Serial.println(F("ready. open loop."));
}
void loop() {
motor.move(); // open loop: just ramp the angle
commander.run();
}
I bumped the voltage limit up gently (1 V, 2 V, 3 V) and watched. At low targets the rotor turned, but not smoothly: move, pause, move, pause. A staccato rotation, always the same direction, never quite continuous.
My first instinct was a wrong pole-pair count. SimpleFOC's open-loop ramp
builds the electrical angle as electrical_angle = target × pole_pairs × t, so
if pole_pairs is off the rotor desyncs: twitch, stutter, refuse to keep up.
The classic move-stop-move pattern is exactly what a wrong count looks like.
I tried 8, 11, 14. Same staccato every time, which was confusing until I read
more carefully: in open loop, almost any pole-pair count produces smooth
rotation at low enough speeds. The rotor is dragged along by a slowly rotating
field and sticks to it whether the electrical-to-mechanical ratio is right or
not, because mechanical speed comes straight from the commanded target, not from
pole_pairs. The move-stop pattern at higher speeds wasn't a pole-pair error
either. It was the open-loop slip ceiling: past a critical speed the small
fixed voltage can't hold the rotor in sync, so it slips a pole, catches up,
slips again.
So I needed a real way to count poles. Eyeballing was out: the rotor is a continuous ferrite ring magnet, smooth all the way around, no visible pole boundaries. Two physical methods, two answers. Rotating the unpowered shaft by hand, I felt the cogging detents click into place roughly every half-slot: thirty slots, two clicks each, ~60 detents per revolution, which for a three-phase 30-slot motor lines up with 20 poles, ten pole pairs. Then, rotor off, I walked a small probe magnet around the inner bell and counted the jumps as it locked from pole to pole: nine. Counted again, still nine.
Nine pairs would mean eighteen poles, contradicting the cogging count and
(later) what SimpleFOC's own MOT: PP check: reports once the encoder is in. I
went with ten, because the rest of the evidence agreed and the motor behaved
better there. But the probe kept nagging: its jumps were noticeably less crisp
at one end of the bell than the other, which I'd much later trace to magnet
eccentricity in the mount. I wouldn't bet money it isn't nine.
So the motor turned. Not well: a clear hitch every revolution, uneven sound, and any real load would have stalled it instantly. But it turned, on command, in the direction and roughly the speed I asked for. With a pole-pair count I was about 80 % sure of.
What I had not yet earned
This was a milestone, the kind you celebrate while privately suspecting it isn't quite what you think it is.
What I had proved: the toolchain compiled, the gate-drive rail came up, the EG2133 was switching, the three phases reached the motor, and SimpleFOC could ramp an angle and produce torque-at-an-angle. That's the bottom half of FIG. 1: the modulator, the driver, the motor itself.
What I had not proved: any of the top half. The sensor wasn't in the loop, the PI controllers weren't running, and the motor was being dragged around by a synthetic angle counter rather than closing any loop with the real rotor. Any board that can produce three balanced PWM phases passes this test, and almost any pole-pair count lets it look like it's working.
A few days on, I'd learn this the hard way. Open loop is a trap: "spins under open loop" cannot be transferred to "will spin under closed loop", because the two use different equations for the electrical angle and the open-loop side is blind to half of what can break the closed-loop side.7 The pole-pair count would bite too: nine versus ten only matters once the sensor is in the loop, and the closed-loop alignment routine would care even though open-loop hadn't.
That is a story for the next entry. For now the AS5600 and the new motors were both on their way, the board was alive, the sketch was minimal, and a salvaged floppy spindle was turning in stuttering little half-revolutions on the bench, doing what it had been kept in a box for two decades to do.
References & Links
- SimpleFOC — Arduino field-oriented control librarydocs.simplefoc.com
- MKS-DUALFOC repository — Makerbase's first-hand pinout and example sketchesgithub.com · makerbase-motor
- SimpleFOC community thread — pinout pointers for MKS Dual FOC v3.xcommunity.simplefoc.com
- pioarduino fork — arduino-esp32 3.x + ESP-IDF 5 PlatformIO platformgithub.com · pioarduino
- EG2133 three-phase gate driver — manufacturer datasheetegmicro.com
- AS5600 12-bit magnetic position sensorams-osram.com
- A primer on field-oriented control & the Park/Clarke transformswikipedia