M.Eng (Hons) First Class
Software engineer at Starship Technologies and co-founder of Purple Parrot. Previously at CERN and ESA. MEng (Hons, 1st) from the University of Plymouth. Based in Tallinn, Estonia.
A complete reference for understanding and implementing FOC on brushless motors, covering the maths, transforms, PID design, modelling, and safety.
A 3-phase brushless motor produces torque when current flows through its windings in the presence of a magnetic field. The torque produced is proportional to sin(θ) where θ is the angle between the stator magnetic field and the rotor magnetic field.
Maximum torque occurs at θ = 90°.

Left: fields aligned — lines go straight through, no sideways force. Centre: 90° — lines are most bent, maximum force (rubber band trying to straighten). Right: angle sweeping live — torque tracks sin(θ).

Your X4114 is an outrunner — coils fixed in the centre, magnet bell spins around the outside.

Left: motor cross-section — rotor flux (red) is fixed, stator MMF (yellow) sweeps around. Green arrows show force on conductors. Right: the resulting sin(θ) torque curve with current operating point marked.
Naive trapezoidal (six-step) commutation switches the stator field in discrete 60° steps, meaning θ wanders between 60° and 120° throughout each electrical cycle. At low speeds this produces torque ripple — the motor stutters, vibrates, and runs inefficiently.
FOC continuously rotates the stator field so θ is always exactly 90°, regardless of rotor position or speed. The result:

KV is defined as no-load RPM per volt. Converting to SI:
ω (rad/s per volt) = KV × 2π / 60
In an ideal motor, electrical power equals mechanical power:
P = V × I = τ × ω
Therefore:
τ / I = V / ω = 1 / (KV × 2π/60) = 60 / (2π × KV)
This gives the torque constant:
Kt (Nm/A) = 60 / (2π × KV)
KV and Kt are inverses. Lower KV = higher Kt = more torque per amp. Same motor, different winding.
The same motor also acts as a generator. When spinning, it produces a back-EMF (BEMF):
BEMF (V) = Ke × ω
In SI units, Ke = Kt (numerically identical in a lossless motor).
Each motor phase is modelled as a series circuit:
V_phase = R×I + L×(dI/dt) + BEMF
Where:
This is the equation FOC is solving in real time across all three phases simultaneously.
The rotor has multiple pole pairs (p). One mechanical revolution = p electrical revolutions.
θ_electrical = p × θ_mechanical
ω_electrical = p × ω_mechanical
For your X4114 with 22 poles (11 pole pairs): one physical revolution = 11 electrical cycles. The FOC controller always works in electrical angle.
Fortescue (1918) proved that any unbalanced 3-phase system can be decomposed into three balanced sets of phasors:
For a balanced 3-phase motor under normal operation, only the positive sequence component exists. The negative and zero sequences represent faults, imbalances, or asymmetries.
In matrix form, for phase voltages Va, Vb, Vc:
[V0] [1 1 1 ] [Va]
[V+] = [1 a a²] [Vb] × (1/3)
[V-] [1 a² a ] [Vc]
where a = e^(j2π/3) = -0.5 + j×0.866 (120° rotation operator)
Why this matters for FOC:
In practice, FOC handles this automatically. But Fortescue is the theoretical foundation: the Clarke transform is essentially extracting the positive sequence α and β components from the three-phase signals.
Three phase quantities (Ia, Ib, Ic) always satisfy:
Ia + Ib + Ic = 0
So only two are independent. Carrying three variables is redundant. Clarke collapses them into two orthogonal components in a stationary 2D reference frame.
Iα = Ia
Iβ = (Ia + 2×Ib) / √3
Or in matrix form (amplitude-invariant scaling):
[Iα] [1 0 ] [Ia]
[Iβ] = [1/√3 2/√3] [Ib]
The (α, β) vector traces a circle as the motor spins. Its magnitude is constant for balanced sinusoidal currents, and it rotates at the electrical frequency.
At this stage the signal is still AC — it rotates continuously. We haven’t yet made it DC-controllable.

To convert back for PWM generation:
Va = Vα
Vb = (-Vα + √3×Vβ) / 2
Vc = (-Vα - √3×Vβ) / 2
The (α, β) vector rotates at electrical frequency. A PI controller can’t regulate an AC signal to zero error. We need to de-rotate it into a frame that’s stationary from the rotor’s perspective.
Rotate the (α, β) frame by the rotor’s electrical angle θ:
Id = Iα×cos(θ) + Iβ×sin(θ)
Iq = -Iα×sin(θ) + Iβ×cos(θ)
Matrix form:
[Id] [ cos(θ) sin(θ)] [Iα]
[Iq] = [-sin(θ) cos(θ)] [Iβ]
If θ is the true rotor angle, then (Id, Iq) are DC values during steady-state operation. PI controllers can now regulate them to setpoints.
Torque is produced only by the component of stator MMF that is perpendicular to the rotor flux — that’s the q-axis.
τ = (3/2) × p × Kt × Iq (for a surface-mount PM motor)
Where p = number of pole pairs.


To go back to (α, β) for PWM:
Vα = Vd×cos(θ) - Vq×sin(θ)
Vβ = Vd×sin(θ) + Vq×cos(θ)
In the dq rotating frame, the motor equations simplify considerably. For a surface-mount PMSM (permanent magnet synchronous motor):
Vd = R×Id + L×(dId/dt) - ω×L×Iq
Vq = R×Iq + L×(dIq/dt) + ω×L×Id + ω×λ
Where:
The terms ω×L×Iq and ω×L×Id are cross-coupling terms — the d-axis current induces a voltage on the q-axis and vice versa. At high speed these become significant and must be decoupled (feed-forward compensation) for good dynamic response.
The term ω×λ on the q-axis is the back-EMF.
Vd = R×Id - ω×L×Iq
Vq = R×Iq + ω×L×Id + ω×λ
At Id = 0 (which is the normal FOC setpoint):
Vd = -ω×L×Iq (purely from cross-coupling)
Vq = R×Iq + ω×λ (resistive drop + BEMF)
This tells you directly what voltage headroom you need at a given speed and current.
The inverter can only produce a limited voltage magnitude:
|V| = √(Vd² + Vq²) ≤ Vbus / √3 (for SVPWM)
At high speed, BEMF (ω×λ) consumes most of the available voltage, leaving little for resistive drop and current regulation. This is the base speed limit — above it you need field weakening.
Developed by Akagi (1983), instantaneous PQ theory expresses real and reactive power directly in the αβ frame without needing to know the rotor angle. Useful for grid-tied inverters and for understanding power flow in motor drives.
In the αβ frame:
p = Vα×Iα + Vβ×Iβ (instantaneous real power)
q = Vβ×Iα - Vα×Iβ (instantaneous imaginary/reactive power)
Or in matrix form:
[p] [Vα Vβ] [Iα]
[q] = [Vβ -Vα] [Iβ]
In a motor context:
Torque-producing power = p - p_losses = τ × ω_mech
Reactive power = q = energy oscillating in inductances
In the dq frame, the equivalents are:
P = (3/2) × (Vd×Id + Vq×Iq)
Q = (3/2) × (Vq×Id - Vd×Iq)
Setting Id = 0 for maximum torque per amp (MTPA):
P = (3/2) × Vq×Iq (all real power on q-axis)
Q = (3/2) × Vq×Id = 0 (no reactive power drawn — optimal)
This is why Id = 0 is optimal for surface-mount PM motors: it minimises the reactive current drawn from the supply for a given torque.
Here is the complete signal flow:
The cross-coupling terms from the dq model are added as feed-forward to improve dynamic response:
Vd_output = PI_d_output - ω×L×Iq (remove q-axis coupling from d)
Vq_output = PI_q_output + ω×L×Id + ω×λ (remove d-axis coupling + BEMF)
Without decoupling, the PI controllers fight the cross-coupling terms. With it, each axis is independent and the PI only needs to handle resistive drop — much easier to tune.
For motor current control, a PI (not PID) is standard — derivative action amplifies sensor noise on current measurements.
u(t) = Kp × e(t) + Ki × ∫e(t)dt
In discrete time (z-domain):
u[n] = u[n-1] + Kp×(e[n] - e[n-1]) + Ki×Ts×e[n]
Where Ts = sample period.
The current loop bandwidth is typically set to:
f_bandwidth = (1/10) × f_switching
For a 20kHz switching frequency: bandwidth ≈ 2kHz.
The current loop time constant is dominated by L/R:
τ_elec = L / R
PI gains from pole-zero cancellation:
Kp = L / (2 × τ_desired) ← set τ_desired to achieve target bandwidth
Ki = R / (2 × τ_desired)
Or equivalently:
τ_desired = 1 / (2π × f_bandwidth)
Kp = L × 2π × f_bandwidth
Ki = R × 2π × f_bandwidth
The speed loop is slower than the current loop (typically 10× slower bandwidth). It outputs an Iq setpoint. Uses PI with anti-windup.
τ_speed = J / (Kp_speed)
Where J = rotor inertia (kg·m²). Set Kp_speed to achieve desired speed loop bandwidth.
Slowest loop, outputs speed setpoint. Often a simple P controller with velocity feed-forward is sufficient:
ω_ref = Kp_pos × (θ_ref - θ) + Kff × dθ_ref/dt
When the motor saturates (hits voltage or current limits), the integrator keeps accumulating error and causes overshoot on recovery. Anti-windup clamps the integrator:
if |output| > limit:
stop integrating
Or the back-calculation method:
integrator += Ki×e - Kaw×(output_clamped - output_unclamped)
Where Kaw is the anti-windup gain (typically 1/Kp to 10/Kp).
Sinusoidal PWM (SPWM) wastes ~15% of DC bus voltage. SVPWM achieves a modulation index of 1/√3 ≈ 0.577 vs SPWM’s 0.5 — the same 15% gain in available voltage output from the same DC bus.
A 3-phase inverter with 3 half-bridges has 2³ = 8 switching states. Six produce non-zero voltage vectors (V1–V6) spaced 60° apart, and two produce zero vectors (V0, V7).
V3 (010)
|
V2(110) | V4(011)
\ | /
\ | /
\ | /
\ | /
V1(100)──┼──V5(001)
/ | \
/ | \
/ | \
/ | \
V6(101) | (000)V0, (111)V7
|
T1 = Ts × (|V_ref|/Vbus) × sin(60° - θ_sector) × √3
T2 = Ts × (|V_ref|/Vbus) × sin(θ_sector) × √3
T0 = Ts - T1 - T2
|V_ref|_max = Vbus / √3 ≈ 0.577 × Vbus
Beyond this, over-modulation occurs (distorted currents, torque ripple returns).
τ_ref → Iq_ref = τ_ref / Kt
For compliant legs (Boston Dynamics style), you want the leg to behave like a spring-damper:
τ = Kp × (θ_ref - θ) + Kd × (ω_ref - ω)
This is implemented as a torque command (Iq_ref) calculated from position and velocity error with virtual stiffness Kp and damping Kd. No integrator — pure proportional + derivative on the torque output. This is why FOC torque control is essential: you need to command torque precisely and instantly.
At base speed, BEMF = supply voltage. No headroom remains to push more current. Speed cannot increase further at full torque.
Inject negative Id current. This creates a stator flux that opposes the rotor permanent magnet flux, reducing the effective λ:
λ_effective = λ_PM + L×Id (Id is negative, so λ_effective < λ_PM)
BEMF = ω × λ_effective — now BEMF is lower, leaving voltage headroom to maintain Iq at high speed.
In the dq plane, the operating point must stay within:
(Vd)² + (Vq)² ≤ (Vbus/√3)²
Field weakening shifts the operating point along the voltage limit circle by increasing negative Id as speed rises.
Before tuning, you need R, L, λ, J, B (friction).
With motor stationary, apply a known DC voltage across two phases and measure current:
R_phase = V / I / 2 (two phases in series for star connection)
Or measure with a milliohm meter directly.
Apply a known voltage step, measure current ramp rate:
L = V × dt / dI
Or use an LCR meter at 1kHz. For star connection, measured between two terminals = 2×L_phase.
Spin the motor at known speed with a power drill, measure no-load phase voltage (RMS) between two phases:
BEMF_line = √3 × λ × ω_electrical
λ = BEMF_line / (√3 × p × ω_mechanical)
Apply a known torque (known Iq), measure acceleration:
J = τ / α = (Kt × Iq) / (dω/dt)
From the dq model, the q-axis current response to voltage input (at Id = 0, fixed speed):
Iq(s) / Vq(s) = 1 / (R + s×L)
This is a first-order low-pass with time constant τ = L/R. Your current PI controller must close a loop around this plant.
The closed-loop current bandwidth with PI (pole-zero cancellation):
f_cl = R / (2π × L) × (1 + Kp/Ki × s) — PI zero cancels the plant pole
After cancellation, the closed loop becomes:
G_cl(s) = 1 / (1 + s × L/Kp)
Bandwidth = Kp / (2π × L). Set Kp to get your target bandwidth.
Hard limit on Iq_ref:
Iq_ref = min(Iq_commanded, I_max)
Set I_max from the motor’s continuous current rating, not peak. For the 4822 motor, continuous = 11A → Iq_max = 11A for sustained operation.
For short bursts (< 5 seconds), you can allow up to peak rating (15A for the 4822) with a thermal model.
Maintain a software thermal accumulator:
T_estimate[n] = T_estimate[n-1] + (I²×R×Ts) / C_thermal - (T_estimate[n-1] - T_ambient) / R_thermal
Where C_thermal is motor thermal mass and R_thermal is thermal resistance (from datasheet or measured). Derate current when T_estimate exceeds threshold.
Minimum bus voltage check — if Vbus drops below minimum operating voltage (e.g. LiPo cutoff), shut down rather than collapse the battery:
if Vbus < V_cutoff: fault()
Maximum phase voltage — SVPWM handles this automatically through the modulation index limit, but verify your Vd and Vq don’t request more than Vbus/√3.
Hardware comparator on gate driver (e.g. DRV8323) triggers in < 1μs — faster than software can respond. Set the hardware limit 20–30% above software limit as a last resort.
If the encoder signal is lost or jumps discontinuously, the Park transform uses a wrong θ — immediately producing maximum voltage in the wrong direction. Implement:
Both high and low FETs in a half-bridge must never conduct simultaneously — this shorts the DC bus. Gate drivers enforce dead-time (typically 100–200ns). Do not override dead-time settings.
Fault hierarchy:
1. Overcurrent (hardware) → immediate gate disable
2. Over-temperature → ramp down current, then gate disable
3. Under-voltage → gate disable
4. Encoder loss → gate disable (freewheel)
5. Software watchdog → gate disable
Always fail to freewheel (high-impedance), not to brake (active short), unless you have explicitly verified the brake is safe for your load.
Without an encoder, rotor angle must be estimated from phase voltages and currents.
From the dq model, rearrange for BEMF:
BEMF_α = Vα - R×Iα - L×(dIα/dt)
BEMF_β = Vβ - R×Iβ - L×(dIβ/dt)
The BEMF vector points at the rotor flux position:
θ_estimated = atan2(BEMF_β, BEMF_α) - 90°
Works well above ~10% of base speed. Below this, BEMF is too small to measure reliably.
Inject a small high-frequency voltage (500Hz–2kHz) on the d-axis. In a non-salient motor (surface-mount PM), there’s no saliency to detect. In an interior PM motor or any motor with anisotropy, the response current has a component modulated by rotor position. Demodulate to extract angle.
For surface-mount motors like the X4114, saliency is minimal — HFI is unreliable. Use an encoder.
Rather than directly using atan2 (noisy), run a PLL on the BEMF:
error = BEMF_β × cos(θ_est) - BEMF_α × sin(θ_est)
ω_est += Ki_pll × error
θ_est += ω_est × Ts + Kp_pll × error
The PLL tracks the angle smoothly and rejects noise. Used in moteus and most production sensorless FOC.
moteus by mjbots is an open-source FOC controller designed for exactly the robot actuator use case. Key features:
servo.pid_dq.kp # d and q axis current Kp
servo.pid_dq.ki # d and q axis current Ki
servo.pid_position.kp # position loop Kp
servo.pid_position.ki # position loop Ki
servo.pid_position.kd # position loop Kd
servo.flux_brake_min_voltage # field weakening threshold
servo.max_current_A # current limit
servo.derate_temperature # thermal derating start temp (°C)
servo.fault_temperature # hard fault temperature (°C)
python3 -m moteus.moteus_tool --target 1 --calibrate
This spins the motor slowly, measures R, L, λ, and pole count automatically.
Verify encoder direction — command a small positive torque, verify the reported position increases. If not, set motor.invert = true.
Current loop bandwidth — start with kp = L × 2000 × 2π, ki = R × 2000 × 2π (targeting 2kHz bandwidth). Observe step response on Id and Iq.
Position loop — command a step position change. Start with kp = 1.0, kd = 0.01. Increase kp until oscillation, then back off 50%.
import moteus
import asyncio
async def main():
c = moteus.Controller()
await c.set_stop()
# Torque control
state = await c.set_torque(torque=0.5) # 0.5 Nm
# Position control with impedance
state = await c.set_position(
position=0.0, # rotations
velocity=0.0, # rotations/s
kp_scale=1.0, # scale on position gain
kd_scale=1.0, # scale on velocity gain
maximum_torque=5.0 # Nm
)
asyncio.run(main())
async def leg_impedance_control(controller, q_ref, kp=10.0, kd=0.5):
state = await controller.query()
q_actual = state.values[moteus.Register.POSITION]
qd_actual = state.values[moteus.Register.VELOCITY]
tau = kp * (q_ref - q_actual) + kd * (0.0 - qd_actual)
await controller.set_torque(torque=tau)
This is the core of a virtual spring-damper leg — same principle as MIT Mini Cheetah and Boston Dynamics Spot.
| Stage | Transform | Frame | Purpose |
|---|---|---|---|
| 3-phase currents | — | abc stationary | Raw measurements |
| Clarke | αβ transform | αβ stationary | Remove redundancy |
| Park | dq transform | dq rotating | Make signals DC |
| PI control | — | dq rotating | Regulate Id=0, Iq=torque |
| Inverse Park | Reverse rotation | αβ stationary | Back to stationary |
| Inverse Clarke / SVPWM | — | abc stationary | Drive FET gates |
The entire elegance of FOC is that two rotations (Clarke + Park) convert a 3-phase AC problem into a 2-channel DC regulation problem, where one channel is torque and the other is zero.