Building a Field-Oriented Controller from Scratch

A walk-through of implementing FOC for BLDC motors on an STM32 without a library — covering the math, the code structure, and the hardware pitfalls.

  • Embedded
  • FOC
  • STM32
  • Motor Control

Field-oriented control (FOC) is the standard technique for driving brushless DC motors with high efficiency and smooth torque. Most tutorials either use a library that hides the interesting parts, or dive straight into the mathematics without explaining the intuition first.

This post walks through a from-scratch implementation on an STM32G4, covering what each transform actually does and the practical details that matter on real hardware.

What FOC Is Actually Doing

A BLDC motor has three phases. The naive approach is trapezoidal commutation — switching phases in discrete 6-step blocks. It works, but produces torque ripple and is inefficient at partial load.

FOC instead continuously rotates a magnetic field vector in the stator, keeping it orthogonal to the rotor’s field at all times. The key insight is that if you express stator currents in a reference frame that rotates with the rotor, the three-phase AC problem becomes two DC quantities you can control with simple PI loops.

Those two quantities are:

  • id — the flux-producing component (set to zero for maximum efficiency)
  • iq — the torque-producing component (this is your control handle)

The Transform Chain

ADC Interrupt (20 kHz)

Clarke Transform (3-phase → αβ)

Park Transform (αβ → dq, using rotor angle)

PI Controllers (id → vd, iq → vq)

Inverse Park (dq → αβ)

Space Vector PWM (αβ → three-phase duty cycles)

Timer Registers

All of this runs in the ADC interrupt at 20 kHz. At 170 MHz the STM32G4 has comfortable headroom — just make sure you enable the hardware FPU, or your trig calls will be painfully slow.

Clarke and Park

The Clarke transform converts three-phase currents (ia, ib, ic) into a stationary two-axis representation (iα, iβ). Because ia + ib + ic = 0, you only need two measurements:

float i_alpha = i_a;
float i_beta  = (i_a + 2.0f * i_b) * ONE_OVER_SQRT3;

The Park transform then rotates this into the rotor frame using the electrical angle θ:

float i_d = i_alpha * cosf(theta) + i_beta  * sinf(theta);
float i_q = i_beta  * cosf(theta) - i_alpha * sinf(theta);

After the PI loops, the inverse Park and space vector PWM get you back to duty cycles.

Current Sensing

This is where most budget designs struggle. Options:

  • Low-side shunt + op-amp — simple, but the measurement window closes when the low-side FET turns off
  • Inline shunt — accurate, but needs a good differential amplifier
  • Hall effect sensors — contactless, convenient, less accurate

I used 5 mΩ inline shunts with INA181 difference amplifiers. The critical detail: sample the ADC in the center of the PWM period, not at the start or end. Switching transients will ruin your measurements.

Lessons From Hardware

1. Rotor alignment before tuning. Before touching PI gains, verify your encoder feedback is phase-aligned to the stator. Even a small misalignment turns into torque ripple that no amount of gain tuning will fix.

2. Dead time compensation. Even 200 ns of gate driver dead time creates distortion at low speeds. You need to compensate for it — add voltage in the direction of current flow during the dead time interval.

3. Start with current loops only. Don’t add a speed loop until your id and iq control is clean. Lock the motor mechanically, sweep iq references, and verify current tracks the reference before adding any outer loops.

The full implementation is about 500 lines of C with no external dependencies.