Home » State Feedback With Integral Control: DC Motor Anti-Windup

State Feedback With Integral Control: DC Motor Anti-Windup

State feedback with integral control solves a known weakness of plain state feedback control: under a constant disturbance, the output settles away from the reference. The integral state removes that offset. However, it introduces a new failure mode, because an integrator keeps accumulating error while the actuator sits on its limit.

This article builds both fixes on a DC motor position controller. The motor parameters come from a real datasheet, and they make the numbers plausible. Holding a 0.1 N·m load at standstill takes 3.2 V, while the supply rail in the saturation test is ±3.5 V. So whenever the load is on, the controller works close to its limit.

By the end, you’ll have the plant model, three controllers built on it, the Simulink model, and the MATLAB scripts used for every plot. The full code is on GitHub: DC-Motor-State-Feedback-Control-Offset-Free.

Implementing State Feedback With Integral Control on a DC Motor

The article follows one plant through three controllers. The first is plain state feedback with a reference gain. The second is state feedback with integral control, which adds the integral of the tracking error as a fourth state. The third adds back-calculation anti-windup on that integral state, for when the voltage saturates.

All three controllers measure the full state: angle, speed and current. So there is no observer in this design (the tradeoffs section comes back to that assumption). The controller runs in discrete time with a 1 ms sample time, a forward-Euler integrator, and a zero-order hold on the voltage.

The Plant: A Brushed DC Motor

Electrical and Mechanical Equations

DC motor equivalent circuit with armature resistance, inductance, back-EMF, and rotor torque and friction
Armature circuit (R, L, back-EMF e) driving a rotor with inertia J and viscous friction b.

The armature circuit is a resistance and an inductance in series with the back-EMF of the rotor. Kirchhoff’s voltage law around the loop gives:

\(\displaystyle L\frac{di}{dt} + Ri = V – k_e\omega\)

On the mechanical side, the current produces a torque \(\tau = k_t i\), and viscous friction opposes the rotation. Newton’s second law for rotation gives:

\(J\dot{\omega} + b\omega = k_t i\)

Position is the integral of speed:

\(\dot{\theta} = \omega\)

The two constants \(k_t\) and \(k_e\) are almost the same number: 0.0187 N·m/A against 0.0191 V·s/rad. That’s no coincidence, since they describe the same energy conversion in two directions. The power the back-EMF takes from the circuit, \(k_e\omega i\), becomes the mechanical power \(k_t i\omega\) on the shaft. In an ideal motor with SI units, the two constants are equal. The small gap here comes from datasheet rounding and losses.

Parameters From a Real Datasheet

The electrical parameters come from the Moog C23-L33-W10 brush motor datasheet:

Parameter Symbol Value
Torque constant \(k_t\) 0.0187 N·m/A
Back-EMF constant \(k_e\) 0.0191 V·s/rad
Terminal resistance \(R\) 0.6 Ω
Terminal inductance \(L\) 0.35 mH
Viscous damping \(b\) 9.5 × 10⁻⁶ N·m·s/rad

The datasheet gives damping as 0.001 N·m/kRPM. One kRPM is about 104.7 rad/s, which gives the value in the table.

The load is a disc of radius 5 cm and mass 0.1 kg, so its inertia is:

\(J = \frac{1}{2} m r^2 = 1.25 \times 10^{-4}\ \text{kg·m}^2\)

The two time scales are far apart. The electrical time constant \(L/R\) is about 0.6 ms, while the mechanics respond in hundreds of milliseconds. The open-loop eigenvalues are 0, −4.9 and −1710 rad/s.

State-Space Model

With state \(x = [\theta\ \ \omega\ \ i]^T\), input \(u = V\) and output \(y = \theta\), the model is:

\(\dot{x} = Ax + Bu\)
\(y = Cx + Du\)

with

\(A = \begin{bmatrix} 0 & 1 & 0 \\ 0 & -b/J & k_t/J \\ 0 & -k_e/L & -R/L \end{bmatrix}, \quad B = \begin{bmatrix} 0 \\ 0 \\ 1/L \end{bmatrix}, \quad C = \begin{bmatrix} 1 & 0 & 0 \end{bmatrix}, \quad D = 0\)

The zero eigenvalue is the position integrator: with no voltage applied, the rotor stays wherever it stopped. That integrator is important in the next section, because it decides which errors cause an offset and which don’t.

The State Feedback Controller and Its Offset

Pole Placement and Reference Gain

The plain state feedback controller combines a gain on the full state with a gain on the reference:

\(u = K_r r – Kx\)

Substituting into the plant gives the closed loop:

\(\dot{x} = (A – BK)x + BK_r r\)
\(y = (C – DK)x + DK_r r\)

The gain \(K\) sets the closed-loop poles, and MATLAB’s place computes it directly:

K = place(A, B, [-15 -16 -17]);

This gives \(K \approx [0.0095\ \ {-0.0173}\ \ {-0.5832}]\). The reference gain \(K_r\) then scales the setpoint so that the DC gain from \(r\) to \(\theta\) is one. The closed-loop transfer function is:

\(G(s) = (C – DK)(sI – A + BK)^{-1}BK_r + DK_r\)

and setting \(G(0) = 1\) gives:

\(\displaystyle K_r = \frac{1}{(C – DK)(-A + BK)^{-1}B + D}\)

Here \(K_r = 0.0095\) V/rad, which is not 1, because it converts a position in radians into volts. In fact, it equals \(K_1\), the first element of \(K\), and that follows from the plant structure. At rest with no load, \(\omega = 0\), \(i = 0\) and \(u = 0\). The control law then reduces to \(K_r r = K_1 \theta\), so \(\theta = r\) requires \(K_r = K_1\).

One more property becomes useful later. From \(r\) to \(\theta\), the signal passes through three integrations (current, speed, position), and the closed loop has three poles. As a result, the transfer function has no finite zeros, and the step response is exactly three first-order lags at 15, 16 and 17 rad/s.

Why a Constant Load Leaves an Offset

Now add a constant load torque \(\tau_d\) on the shaft. At steady state the rotor is still, so \(\omega = 0\) and there is no back-EMF. The motor has to hold the load with current alone:

\(k_t i = -\tau_d\)

Driving that current takes a voltage \(Ri\), and the controller can only produce it from the state. With \(K_r = K_1\) and \(\omega = 0\), the control law reads \(Ri = K_1(r – \theta) – K_3 i\). Solving for the error:

\(\displaystyle e_{ss} = r – \theta_{ss} = -\frac{R + K_3}{K_1}\,\frac{\tau_d}{k_t}\)

In other words, the controller can’t produce a holding voltage without a position error. \(K_1\) acts as a position stiffness in V/rad, and with poles near −15 rad/s it’s a relatively soft spring. For \(\tau_d = 0.1\) N·m the error is −9.4 rad. The load pushes the rotor 9.4 rad past a reference of π rad, three times the size of the step.

Angle, voltage and current of a DC motor under a plain state feedback controller, with an offset after a load step
Plain state feedback (poles −15, −16, −17), linear model. After the 0.1 N·m load step at 2.5 s, the angle settles about 9 rad from the reference.

The plot comes from plain_state_feedback.m in the repo. It simulates the continuous linear model with lsim rather than the Simulink model for the sake of simplicity (I added this script at the end of the project, to show the steady-state error for more clarity). The voltage settles at −3.21 V and the current at −5.35 A. That’s the holding current the load needs, paid for with angle error.

Plant parameter errors alone don’t cause an offset here. At rest with no load, \(\theta = r\) whenever \(K_r = K_1\), whatever the true values of \(R\), \(L\), \(J\) or \(k_t\). The offset comes from constant disturbances instead: a load torque, Coulomb friction, or a biased current sensor.

Adding the Integral State: State Feedback With Integral Control

The Integral State

State feedback with integral control gives the controller a state that can only settle when the error is zero. The new state is the integral of the tracking error:

\(\dot{x}_{n+1} = r – y = r – (Cx + Du)\)

From here on it’s called \(\xi\), to match the anti-windup section. The control law now feeds back the plant states and the integral state:

\(u = -Kx – K_{n+1}\xi\)

where \(K\) is a \(1 \times n\) row of gains. Note that there is no \(K_r\) term: the reference enters only through \(\xi\).

The argument for zero error is simple. If the closed loop is stable and settles, every derivative goes to zero, including \(\dot\xi = r – \theta\). Therefore \(\theta = r\) at steady state, whatever the constant load. Zero error is a condition of equilibrium.

The Augmented Closed Loop

Substituting the control law into the plant equation gives:

\(\dot{x} = (A – BK)x – BK_{n+1}\xi\)

and substituting it into the integral state gives:

\(\dot{\xi} = r – (C – DK)x + DK_{n+1}\xi\)

Stacking both equations gives the augmented closed loop:

\(\begin{bmatrix}\dot{x} \\ \dot{\xi}\end{bmatrix} = \begin{bmatrix} A – BK & -BK_{n+1} \\ -C + DK & DK_{n+1}\end{bmatrix}\begin{bmatrix} x \\ \xi \end{bmatrix} + \begin{bmatrix}\Theta \\ 1\end{bmatrix} r\)

where \(\Theta\) is an \(n \times 1\) vector of zeros. In compact form:

\(\dot{x}_a = (A_a – B_aK_a)x_a + \begin{bmatrix}\Theta \\ 1\end{bmatrix} r\)
\(y_a = C_a x_a\)

with

\(A_a = \begin{bmatrix} A & \Theta \\ -C & 0 \end{bmatrix}, \quad B_a = \begin{bmatrix} B \\ -D \end{bmatrix}, \quad C_a = \begin{bmatrix} C – DK & -DK_{n+1}\end{bmatrix}, \quad K_a = \begin{bmatrix} K & K_{n+1}\end{bmatrix}\)

Designing state feedback with integral control is now standard pole placement on a fourth-order system. place(Aa, Ba, [-15 -16 -17 -18]) gives \(K_a \approx [0.0418\ \ {-0.0153}\ \ {-0.5769}\ \ {-0.1718}]\).

The reference path has gained one integration, so its relative degree is 4. With four poles, it still has no finite zeros, and the step response is four first-order lags at 15 to 18 rad/s. Compared with the plain state feedback controller, that’s one more pole and one more lag, so the step is slightly slower.

Simulink Implementation and Load Step Result

Plant and Controller in Simulink

Simulink model of state feedback with integral control for a DC motor, with plant and controller subsystems
Simulink model: plant, discrete controller with integral state, and ideal response.

The plant area implements the three ODEs with integrators, and a step block adds the load torque to the speed equation. Goto and From tags carry θ, ω and i to the controller. On the controller side, the tracking error goes into a discrete integrator K Ts/(z-1), a forward-Euler approximation of \(\xi\). A Mux then stacks the four states, the gain -K*u computes the voltage, and a zero-order hold sets the 1 ms control rate.

The green area holds the ideal response: four first-order lags in series, one per closed-loop pole. It shows what the linear design promises, so any gap between it and the simulated angle is visible.

The repo model of state feedback with integral control also contains a saturation block and the anti-windup path. Both are inactive by default (v_max = inf, Kaw = 0).

Initialisation Script

init.m sets the parameters, builds both models, and computes the gain:

%% Init params
m = 0.1;
r = 0.05;
J = 0.5*m*r^2;
b = 0.0000095;
kt = 0.0187;
R = 0.6;
L = 0.35/1000;
ke = 0.0191;

%% Controller params

% Compute A, B, C, D matrices
A = [0 1 0; 0 -b/J kt/J; 0 -ke/L -R/L];
B = [0; 0; 1/L];
C = [1 0 0];
D = 0;

% Compute A, B, C, D matrices for augmented system
Aa = [A, zeros(3,1); -C, 0];
Ba = [B; -D];

% Compute state feedback gains using pole placement for augmented system
K = place(Aa, Ba, [-15 -16 -17 -18]);

tSamp = 0.001;

% Reference
stp = pi;

% Max Voltage
v_max = inf;

% Back-calculation anti-windup gain (0 = disabled)
Kaw = 0;

The last three variables define the default scenario, and the anti-windup comparison overrides them later.

Load Step Result

The default scenario steps the reference from 0 to π rad at 1 s, then applies a 0.1 N·m load torque at 2.5 s. The load adds to the motor torque in the speed equation:

\(J\dot{\omega} + b\omega = k_t i + \tau_d\)

Angle, voltage and current of the DC motor with integral state feedback, rejecting a load torque step
State feedback with integral control: π rad step at 1 s and 0.1 N·m load at 2.5 s. The integral state removes the offset, and the voltage settles at −3.2 V.

Under state feedback with integral control, the angle follows the ideal response through the step. When the load disturbance is introduced, θ jumps to about 6.3 rad, and the integral state pulls it back to π by about 3.2 s. The voltage settles at −3.2 V and the current at −5.3 A. That’s the same holding current as with plain state feedback, now delivered with zero angle error.

That −3.2 V also sets up the next problem. The saturation test uses a ±3.5 V rail, so the load alone takes 92% of the available voltage.

Integrator Windup and Back-Calculation Anti-Windup

Why This Motor Saturates

At standstill there is no back-EMF, so the holding voltage is purely resistive:

\(i = \tau_d / k_t = 5.35\ \text{A}, \quad V = Ri = 3.21\ \text{V}\)

The ±3.5 V limit is a choice for this study, not a property of the motor: the rated terminal voltage for this winding is 12 V. The 0.1 N·m load is also 85% of the 0.117 N·m continuous stall torque. So this is a stress case, set up to force saturation, rather than a typical operating point. For a saturating transient as well, the comparison uses a 15π rad step, which the unconstrained design would follow with far more than 3.5 V.

This is where state feedback with integral control winds up. While the voltage sits on the rail, the angle lags the reference, and \(\xi\) keeps integrating that error. But the motor can’t respond to the extra command, so the error accumulates in \(\xi\) without doing anything. By the time the angle reaches the reference, \(\xi\) could be very large. The command then stays on the rail until the error has been negative long enough to unwind it, and the angle overshoots.

Back-Calculation on the Integral State

Back-calculation feeds the difference between the saturated and unsaturated commands back into the integrator:

\(\dot{\xi} = e + K_{aw}(u_{sat} – u)\)

With \(K_{aw} = 0\), this is the original controller. With \(K_{aw} > 0\), the extra term acts only while the command is outside the rail.

\(K_{aw}\) changes \(\xi\), but the saturation is on \(u\), and \(\xi\) reaches \(u\) through the gain \(K_{n+1}\) in the control law. Holding the other states constant:

\(\dot{u} \approx -K_{n+1}\dot{\xi} = -K_{n+1}K_{aw}(u_{sat} – u)\)

This is a first-order loop on \(u\), and its gain is the product \(-K_{n+1}K_{aw}\), not \(K_{aw}\) alone. Two consequences follow.

First, the sign. The loop pulls \(u\) back to the rail only if \(-K_{n+1}K_{aw} > 0\). Here place returned \(K_{n+1} = -0.17\), so any positive \(K_{aw}\) works. The sign of \(K_{n+1}\) comes from the pole placement, so it’s worth checking after any redesign.

Second, the speed. The loop returns \(u\) to the rail with a time constant:

\(\displaystyle T_t \approx \frac{1}{-K_{n+1}K_{aw}} = \frac{1}{0.17 \times 40} \approx 0.15\ \text{s}\)

If the poles move and \(K_{n+1}\) changes, the same \(K_{aw}\) gives a different \(T_t\). In the back-calculation anti-windup in C PID implementation, the integral term is already a voltage, so \(K_{aw}\) sets this rate on its own. Here \(\xi\) is in rad·s, and \(K_{n+1}\) converts it to volts.

Why back-calculation, and not a conditioning scheme such as Hanus’s? Conditioning inverts the controller’s direct feedthrough from error to command. Here that feedthrough is zero, because the integrator is the only path from \(e\) to \(u\). The map is strictly proper, so there’s nothing to invert. Back-calculation acts on the integrator itself, and it’s the same law as in that PID implementation.

Simulink control subsystem with saturation block and back-calculation anti-windup path on the integral state
Back-calculation: the saturation residual u_sat − u, scaled by K_aw, is added to the integrator input.

How the Anti-Windup Gain Was Tuned

I didn’t tune \(K_{aw}\) by hand. Instead, I asked Cursor, working through the MATLAB & Simulink Agentic Toolkit, to implement the anti-windup structure and tune it for a good response. That was the whole brief.

The agent turned “good” into a metric on its own: it swept \(K_{aw}\) and picked the value with the lowest integral of absolute error (IAE). It chose 40, and it reported that time on the voltage rails fell from 1.57 s to 0.92 s, which matches the logged data. It also reported that overshoot fell from 26% to about 10%. That number was wrong, since its own plot shows almost none. So the metric and the gain were sound, but the written summary still needed checking against the simulation output.

Result: 26% Overshoot Versus Almost None

Comparison of DC motor angle, voltage and current with and without back-calculation anti-windup under voltage saturation
15π rad step with a ±3.5 V rail: about 26% overshoot without anti-windup, almost none with K_aw = 40.

The comparison runs the 15π rad step with the ±3.5 V rail twice, with \(K_{aw} = 0\) and \(K_{aw} = 40\), and the same 0.1 N·m load at 2.5 s.

Without anti-windup, the voltage stays at +3.5 V until the angle has almost reached the reference, roughly 0.1 s longer than with anti-windup. θ peaks around 59 rad against 47.1 rad, about 26% overshoot, and then the voltage swings to the opposite rail to unwind \(\xi\). With anti-windup, the command leaves the rail earlier and the angle lands with almost no overshoot.

After the load step, both runs sit on −3.5 V for a while, since the holding voltage plus the transient demand exceeds the rail. Even so, the anti-windup run comes off the rail sooner and with less current ringing.

Anti-windup doesn’t make state feedback with integral control match the ideal response under a voltage limit, though. Both runs lag the green curve, because the rail limits how fast the motor can accelerate, whatever the integrator does. What anti-windup removes is the extra overshoot caused by the integrator’s stored error.

Comparison Script

The comparison script runs both cases on the same model:

%% Compare back-calculation anti-windup vs no anti-windup
init;

% Saturating step (init.m uses a different default scenario)
stp = 15*pi;
v_max = 3.5;
Kaw_aw = 40;  % 0 disables AW

mdl = 'DC_motor_state_feedback_control';

%% Simulate without and with anti-windup
in(1) = Simulink.SimulationInput(mdl);
in(1) = in(1).setVariable('Kaw', 0);
in(1) = in(1).setVariable('stp', stp);
in(1) = in(1).setVariable('v_max', v_max);

in(2) = Simulink.SimulationInput(mdl);
in(2) = in(2).setVariable('Kaw', Kaw_aw);
in(2) = in(2).setVariable('stp', stp);
in(2) = in(2).setVariable('v_max', v_max);

out = sim(in, 'UseFastRestart', 'on');

%% Logged signals (out(1) = no AW, out(2) = AW)
theta = out(1).logsout.get('theta').Values.Data;
t_theta = out(1).logsout.get('theta').Values.Time;

theta_aw = out(2).logsout.get('theta').Values.Data;
t_theta_aw = out(2).logsout.get('theta').Values.Time;

theta_ref = out(1).logsout.get('theta_ref').Values.Data;
t_theta_ref = out(1).logsout.get('theta_ref').Values.Time;

theta_id = out(1).logsout.get('theta_id').Values.Data;
t_theta_id = out(1).logsout.get('theta_id').Values.Time;

voltage = out(1).logsout.get('voltage').Values.Data;
t_voltage = out(1).logsout.get('voltage').Values.Time;

voltage_aw = out(2).logsout.get('voltage').Values.Data;
t_voltage_aw = out(2).logsout.get('voltage').Values.Time;

current = out(1).logsout.get('current').Values.Data;
t_current = out(1).logsout.get('current').Values.Time;

current_aw = out(2).logsout.get('current').Values.Data;
t_current_aw = out(2).logsout.get('current').Values.Time;

Each Simulink.SimulationInput overrides only Kaw, stp and v_max, so both runs share one model. With UseFastRestart, that model compiles once for both. The full script in the repo also contains the plotting code for the figure above.

Tradeoffs: Integral State, Disturbance Observer, or MPC

State feedback with integral control is the smallest change that removes the offset. It adds one state, needs no disturbance model, and rejects any constant disturbance wherever it enters the loop. On the other hand, it ties tracking and disturbance rejection together. The reference now goes through the integrator, so both share the same four poles, and faster rejection also means a faster, more aggressive step. It also needs anti-windup whenever the actuator can saturate.

A disturbance observer takes a different route. It adds a constant load-torque state to the plant model, estimates it, and cancels it. This keeps the \(K_r\) reference path of the plain state feedback controller and separates tracking from rejection. The price is a disturbance model and an observer to tune. The offset also disappears only if the modelled disturbance enters where the real one does.

Measured states matter as well. This design uses θ, ω and i directly. Without speed or current sensing, an observer is needed anyway, for example a Kalman filter, and a disturbance estimate then costs one extra state.

Linear model predictive control handles the ±3.5 V limit inside the optimisation, so there is no windup to patch afterwards. The cost is a QP solved every sample, here on a 1 ms loop.

Finally, clamping (conditional integration) is the simpler anti-windup alternative. It has no gain to tune, but it freezes \(\xi\) during saturation instead of unwinding it.

DIY Next Steps

The repo makes a few extensions easy to try:

  • Raise the load to 0.12 N·m. The holding voltage becomes 3.85 V, above the 3.5 V rail, so the motor can no longer hold the load at all. This becomes a physical limit and no anti-windup scheme fixes that.
  • Sweep \(K_{aw}\) over 0, 10, 40 and 200, and plot overshoot against time on the rail.
  • Remove the current measurement and estimate \(i\) with an observer.
  • Replace back-calculation with clamping and compare the responses.
  • Run plain_state_feedback.m with faster poles. The offset shrinks as \(K_1\) grows, but it never reaches zero.

Wrapping Up

Both flaws have physical causes. Plain state feedback needs a position error to produce the current that holds a constant load, so the angle settles away from the reference. The integral state removes that need, and zero error becomes a condition of equilibrium. However, the integrator also stores error it can’t act on while the voltage is saturated. Back-calculation unwinds it, at a rate set by \(K_{aw}\).

State feedback with integral control, with or without anti-windup, works only while the rail can supply the holding current. Beyond that point, no controller structure removes the offset, because the motor doesn’t have the torque.

One question stays open: integral state or disturbance observer for this motor? Both remove the offset, but they trade differently between tracking, rejection, and modelling effort. That comparison could be the subject of a follow-up article.

Useful References

More Stuff

If you’d rather handle the voltage limit inside the controller, the Practical Model Predictive Control course builds MPC from the model up. For the same back-calculation law in embedded code, see the PID controller in C article.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Menu