Closing three loops on an AKM11E servo

Three nested FOC loops (current 20kHz, velocity 5kHz, position 1kHz) on a bare metal STM32F411 with a DRV8353RS gate driver, driving a ball screw linear stage. Every loop is designed from measured frequency response data.

Stage
150 mm, 2 mm lead
AKM11E motor, 0.24µm per encoder count
Position loop
123.8 Hz
−3dB bandwidth, 61.2° phase margin
Step response
2.2 ms rise
32µm step: 9.0% overshoot, 11.8ms settling (2%)
10 mm trapezoidal move
±0.73 µm
following error at cruise; 1.71µm peak on the ramps
Motor and stage during a chirp test
01

Purpose and approach

Build and verify a single axis embedded servo controller like the ones used in industrial precision motion. The project started from a conversation with a former coworker at a wafer dicing company, whose team is thinking about building their own motion controller for a machine with an XYθ stage and a laser galvo. This drive is a single axis proof of concept for that, with the usual industrial targets: a 20kHz current loop update and a current loop crossover around 500Hz.

The loops are closed from the inside out: current, velocity, then position. Each one follows the same three steps: measure the plant with a frequency sweep on the hardware, design the controller against that measurement, then sweep the closed loop to check the result. Each outer loop targets 60° of phase margin and should be three to five times slower than the loop inside it. The data is analyzed offline in Python.

With the loops closed, the stage friction is measured and fed forward, and the drive runs real trapezoidal moves commanded from the Pi.

02

Hardware & firmware architecture

Hardware and firmware built from scratch: STM32F411, TI DRV8353RS gate driver, Raspberry Pi 5 for telemetry and analysis. No vendor motor control SDK.

Raspberry Pi 5 STM32F411 position 1kHz velocity 5kHz current 20kHz DRV8353RS AKM11E + encoder GPIO trigger SPI2 telemetry SPI1 + PWM center aligned, 20kHz, mode 1 3-phase drive current sense (ADC injected, PWM synced) encoder (quadrature, TIM2, via line receiver)
Actuation & sensing (STM32F411)

TIM1 runs 20kHz center aligned PWM. The same timer triggers the ADC at the counter peak, the middle of the low side on time, which is when the shunts see phase current. The encoder is read in hardware by TIM2.

Control & comms

Current loop at 20kHz, velocity at 5kHz, position at 1kHz. The DRV8353 is configured over SPI1. SPI2 streams telemetry to the Pi, which does the analysis in Python.

03

Current loop

3.1: Plant (open loop sweep)

A voltage chirp on vq, measure iq, and fit P(s) = 1/(Ls+R).

R (nominal)
1.55 Ω
L (nominal)
1.02 mH
R (fitted)
-
L (fitted)
-
Electrical pole
-
Coherence ≥0.8
1 to 940 Hz
Plant response: P(s) = iq / vq
measured, coherence ≥0.8 coherence 0.5 to 0.8 fitted model nominal (datasheet R/L)

Fitted R is within 0.7% of the datasheet (1.54Ω vs 1.55Ω), which says the current sensing chain is right. L reads 17% high (1.20mH vs 1.02mH); the cause is still open (see open items), so the design uses the closed loop values.

3.2: Controller

The PI zero cancels the plant pole (Ki/Kp = R/L), so Kp sets the crossover. Gains use R=1.73Ω and L=1.05mH from a closed loop sweep.

C(s) = Kp + Ki/s P(s) = 1/(Ls + R) Ki/Kp = R/L → cancels the plant pole gc = Kp / L (rad/s, for perfect cancellation)
Target gc
500 Hz
Kp (deployed)
3.30 V/A
Ki (deployed)
5435 V/A·s
Predicted gc
-
Predicted PM
-

The crossover stays at 500Hz for now: the 5kHz telemetry cannot verify a faster loop (see open items).

3.3: Closed loop

A chirp on iq,cmd with the PI closed gives the closed loop response. Loop gain comes from L = H/(1-H).

DC gain
-
-3dB BW
-
Closed loop response: H(s) = iq,meas / iq,cmd, measured
measured, coherence ≥0.8 coherence 0.5 to 0.8 one pole + 113µs delay fit

Crossover 504Hz against the 500Hz design, 72.7° phase margin. Crossover and bandwidth are not the same number: −3dB lands at 767Hz, about 1.5× crossover.

Working back from the measured crossover gives L = 1.04mH, close to the datasheet 1.02mH and the design value 1.05mH.

Current noise at standstill is about 8mA RMS (2.8 ADC counts).

04

Velocity loop

4.1: Plant (P only closed loop sweep)

From here on the motor drives the linear stage (150mm travel, 2mm lead). An open loop sweep did not work below about 10Hz because of friction, so a light P only loop was closed and the plant backed out of it: P(s) = H/(1−H)/Kp.

P(s) = K / (τs + 1) K = 177.2 rad/s per A tau = 14.19 ms (fc = 11.2 Hz)
K
177.2 rad/s/A
τ
14.19 ms
Plant pole
11.2 Hz
Identified with
P only
Plant backed out of a closed loop P only chirp, 15 rad/s amplitude
measured, coherence ≥0.8 coherence 0.5 to 0.8 K/(τs+1), K 177, τ 14.2 ms
4.2: Controller and closed loop

PI zero on the plant pole (11.2Hz). Kp raised until phase margin was about 67°.

C(s) = Kp + Ki/s P(s) = K / (τs + 1) Ki/Kp = 1/τ → zero on the measured 11.2Hz plant pole Kp from the measured loop: gain for ~67° PM, not from a bandwidth target
Kp
0.090 A/(rad/s)
Ki
6.34 A/rad
Peak |H|
0.12 dB
Gain crossover
116.9 Hz
Closed loop BW
171 Hz
Phase margin
67.0°
Closed loop verification with the designed PI
measured, coherence ≥0.8 coherence 0.5 to 0.8
05

Friction feedforward

The velocity loop's frequency response changed with sweep amplitude (8 vs 15 rad/s). Friction in the ball screw and linear bearings is not linear, so the response depends on how hard the loop is driven. This section measures the friction and feeds it forward.

Motor coupled to the NB BG2602A ballscrew stage on the bench
5.1: Friction characterization

Hold constant speeds from 1 to 23 rad/s in both directions and record the current at each one.

Coulomb Fc
94.4 mA
4.3 mN·m
Viscous Fv
2.04 mA·s/rad
0.093 mN·m·s/rad
Fit R²
0.913
Repeatability
±5 mA
Steady state iq vs. commanded velocity, both directions
+direction -direction linear fit
5.2: Feedforward and re verification

Added Coulomb plus viscous friction as feedforward in the velocity loop, using tanh() so it does not chatter at zero speed, then reran the 8 vs 15 rad/s sweep.

DC gain, before FF
-8.67 / -4.02 dB
amp=8 / amp=15, 2x different
DC gain, after FF
-0.07 / -0.09 dB
amp=8 / amp=15, converged
Before feedforward: H(s)=velmeas/velcmd, amp=8 vs amp=15 rad/s
amp=8 rad/s amp=15 rad/s
After feedforward: same comparison
amp=8 rad/s amp=15 rad/s

With feedforward the 8 and 15 rad/s sweeps match within 0.02dB at DC, so the response no longer depends on amplitude. The deployed loops run with it on.

06

Position loop

6.1: Controller

A P controller around the closed velocity loop. Position is the integral of velocity, so the loop gain is L(s) = Kp · Hvel(s) / s. The integrator adds 90° of lag, which makes the phase margin 90° minus the velocity loop's lag at crossover.

The measured velocity loop lags about 29° at 60Hz, so Kp was set for a 60Hz crossover. The sweep below measures 61.2Hz with 61.2° of margin.

Kp
380 (rad/s)/rad
Output clamp
±80 rad/s
Inner / outer gc
1.9×
6.2: Closed loop
Gain crossover
61.2 Hz
Phase margin
61.2°
Closed loop −3dB
123.8 Hz †

† At 0.10 rad drive. The −3dB number drops if the loop is driven into its clamps; crossover and margin do not.

Closed loop verification, whole system: Hpos(s) = posmeas / poscmd
measured, coherence ≥0.8 coherence 0.5 to 0.8
6.3: Step response
Rise time (10 to 90%)
2.2 ms
Overshoot
9.0%
Settling (2%)
11.8 ms
Step response, 0°→5.73° (small signal)
pos_cmd pos_meas
07

Trajectory tracking

The Pi sends one MoveCmd per move over SPI, and the STM32 generates the trapezoidal profile and runs it.

With friction fed forward, the error left at cruise is the v/Kp lag of a P position loop: 38.4µm at 10mm/s. Velocity feedforward removes it by passing the generator's speed straight to the velocity loop. The move below is a 10mm trapezoid at 10mm/s and 250mm/s² with both feedforwards on.

Following error at cruise
±0.73 µm
3 encoder counts, 0.20 µm RMS
Peak on the ramps
1.71 µm
acceleration not fed forward
Move
1.04 s
10mm trapezoid, 10mm/s
Deployed move: commanded/measured position, following error, velocity
commanded measured following error
The same move on the stage, out and back
08

Conclusion

Every loop was designed from a measured plant and checked with a separate closed loop measurement.

LoopRateBandwidth (−3dB)Phase margin
Current20kHz767 Hz72.7°
Velocity5kHz171 Hz67.0°
Position1kHz123.8 Hz61.2°

Friction feedforward removed the amplitude dependence, and velocity feedforward cut cruise following error from 38.4µm to within ±0.73µm. The same process applies to any servo axis: measure the plant, design to it, measure the result.

09

Open items