When is classical control not enough? PID will work well in almost any single-input single-output loop. However, it struggles when the actuator hits a hard limit, or when the reference moves and you want to look ahead. That’s where linear model predictive control can add real value. It solves a constrained optimisation at every sample, with the constraints written in directly rather than added later on with anti-windup and gain scheduling.
This article walks through a complete linear MPC design on the simplest plant that still has interesting dynamics: a mass sliding on a damped surface, pushed by a force. Two states, one input, one measured output. By the end you’ll have the state-space model, the tuning choices, the MATLAB code, and a clean step response to read.
The full code lives on GitHub: position_control_mpc.
The plant: a damped mass
The plant is a 10 kg object sliding on a surface with linear damping coefficient k = 0.5 NĀ·s/m, pushed by a force F.

Massādamper plant. Force F as input, damping kż opposing motion, position z as output.
The dynamic equation comes straight from Newton:
\(m\ddot{z}(t) = F – k\dot{z}(t)\)
Linear, time-invariant, second order. No spring, so no restoring force; the system is an integrator in velocity, then another in position. Push it once and it slides; nothing brings it back to zero on its own. Damping causes the velocity to decay with no force.
That’s a nice teaching plant for MPC. Simple enough to follow by hand, but a P controller, for example, would struggle to do a good job. It would be either too slow or too oscillating.
State-space model for linear model predictive control
MATLAB’s mpc() object expects an LTI model in state-space form. Let’s convert the ODE.
We pick the state as \(x = [\dot{z},\ z]^T\). Input \(u = F\), output \(y = z\). Then:
\(\dot{x} = Ax + Bu\)
\(y = Cx + Du\)
with
\(A = \begin{bmatrix} -k/m & 0 \\ 1 & 0 \end{bmatrix}, \quad B = \begin{bmatrix} 1/m \\ 0 \end{bmatrix}, \quad C = \begin{bmatrix} 0 & 1 \end{bmatrix}, \quad D = 0\)
Note the C matrix: \(C = [0\ 1]\). We measure only the position, not the velocity.
This is where the state estimator comes in. Linear MPC needs the full state to predict \(p\) steps ahead. MATLAB’s mpc() builds a Kalman filter by default from the state-space model, using sensible noise covariances based on the scale factors selected in the design. You can swap in a custom estimator with setEstimator, but for this example we will keep the default.
The model is continuous-time here. The MPC object discretises it internally at the sample time you specify.
The MPC cost function
What is linear MPC actually doing at each sample? Solving a quadratic programming problem. Minimise a cost over a finite prediction horizon, subject to the plant model and the input/output constraints.
The cost function MATLAB uses is:
\(J = \sum_{i=1}^{p} w_y (r_i – y_i)^2 + \sum_{i=0}^{p-1} w_u (u_i – u_{\text{target}_i})^2 + \sum_{i=0}^{p-1} w_{\Delta u}(u_i – u_{i-1})^2\)
Three terms, three competing concerns:
- \(w_y\) penalises tracking error. Higher value, more aggressive setpoint chase.
- \(w_u\) penalises being far from a desired actuator value. Useful when there’s a preferred operating point (a thrust trim, a valve mid-position).
- \(w_{\Delta u}\) penalises moving the input fast. This is what gives you smooth commands.
We can compare that to PID. In both cases we have three gains, interacting with each other. As there is a tuning problem with a PID, there is a tuning problem with linear model predictive control, there’s no getting away.
Scaling matters before weights. Position is in metres, force is in Newtons. The raw numbers are on different scales, so the weighted squared errors are too. Scale factors normalise everything to dimensionless units. Getting the scaling right helps significantly with weight tuning.
For this plant:
- Output variable scale factor: 2 (position range roughly ±2 m)
- Manipulated variable scale factor: 10 (force range ±10 N)
Then the weights:
- \(w_y = 1\)
- \(w_u = 0\)
- \(w_{\Delta u} = 0.1\)
\(w_u = 0\) means that we don’t have a “preferred” force. We only care that the input doesn’t move too fast (and want to weigth the importance of force rate of change vs reference tracking), hence the rate weight.
Control architecture with Kalman filter
The closed loop has three blocks: setpoint generation, MPC plus Kalman filter, plant.

Closed-loop architecture. Position setpoint feeds the linear MPC block (with built-in Kalman filter); the optimised force drives the plant; measured position closes the loop.
The setpoint \(z_{\text{ref}}\) feeds the MPC block. The MPC predicts \(p\) steps ahead using the current state estimate, solves the QP, and outputs the first move of the optimal force sequence. That force drives the plant. The position is measured and fed back to the MPC plus Kalman filter, which updates the state estimate for the next step.
Everything runs at one sample time, \(T_s = 0.1\) s. Estimator update, QP solve, command application, all on the same clock. Multi-rate MPC is possible but adds complexity and it’s not covered in this article.
The Kalman filter is implicit in mpc(). The constructor builds it under the hood from the state-space model you supply. If you need to customise (different process noise covariance, an unknown-input observer for disturbance estimation), you can call setEstimator on the MPC object after construction. It will be covered in future examples.
MATLAB implementation
Here is the full design. Twenty-five lines.
%% Parameters
m = 10;
k = 0.5;
%% State space model
A = [-k/m 0; 1 0];
B = [1/m; 0];
C = [0 1];
D = 0;
% Create state-space model
object_sys = ss(A, B, C, D);
%% Create MPC object
MV = struct('Min', -10, 'Max', 10, 'ScaleFactor', 10);
OV = struct('Min', -2, 'Max', 2, 'ScaleFactor', 2);
Weights = struct('MV', 0, 'MVRate', 0.1, 'OV', 1);
Ts = 0.1;
mpcobj = mpc(object_sys, Ts, 10, 2, Weights, MV, OV);
%% Simulate controller
Tsim = 5; % seconds
steps = round(Tsim/Ts); % simulation iterations
r = 1*ones(steps, 1); % reference signal
[y,t,u] = sim(mpcobj,steps,r);
The mpc() call carries the design. Arguments in order: plant model, sample time, prediction horizon (\(p = 10\)), control horizon (\(m = 2\)), weights, MV constraints, OV constraints.
A few words on the horizons. Prediction horizon \(p = 10\) means the controller looks 1 s into the future at every step. Control horizon \(m = 2\) means the QP optimises two free input moves; after step 2, the input is held constant for the rest of the prediction horizon. That keeps the QP small (two decision variables, not ten) without much performance loss on this plant.
Why \(T_s = 0.1\) s? The open-loop velocity time constant is \(m/k = 20\) s. Position has no time constant of its own (pure integrator). A sample time two orders of magnitude faster than the dominant pole is a good starting point, then we can iterate if the result does not suit us.
The plotting code from the carousel is cosmetic. Black background, white axes, magenta and green stairs. It’s not part of the controller and is omitted here for brevity. Full version in the repo.
Simulation results

Step response of the closed loop. Position (top) reaches the 1 m setpoint with about 5% overshoot in roughly 3 s; control effort (bottom) saturates against the ±10 N limit during the transient.
The position climbs from 0 to 1 m in about 1.7 s, overshoots by ~5% around t = 2.3 s, and settles by t = 3 s. The control effort is worth mentioning: it saturates at +10 N for the first second, drops hard through zero to ā10 N to brake the mass, crosses zero again, and settles to zero. No steady-state force is needed since there’s no spring or load.
That bang-bang shape is what an optimal controller can do when the input constraint is active. With an unconstrained PID you’d see a smoother force profile that asked for, say, 50 N, then got clipped by the actuator. The MPC sees the limit in the QP and shapes the optimal sequence around it.
The staircase look on both signals is the zero-order hold at \(T_s = 0.1\) s. Each step is one sample of held command.
One honest caveat. The model used inside the MPC for prediction is identical to the model used to simulate the plant. No model mismatch, no disturbance, no measurement noise. This is the best case. Don’t read this result as “MPC works”; read it as “the design is internally consistent”. The real tests come next:
- Step disturbance on the force (a load step). Does the controller handle it?
- Parameter mismatch. Simulated \(m = 12\) kg, MPC still thinks 10 kg. Does it still settle?
- Measurement noise on the position. Does the Kalman filter clean it up enough that the QP solution doesn’t chatter?
Each of those is one parameter change away in this code. Worth running before trusting any of it on hardware. We will see more of this in the future.
Why linear MPC for this problem
Would a PID with anti-windup do the same job on this plant? Yes. With a sensible \(K_p\), \(K_i\), \(K_d\) and well designed anti-windup, you’d get a similar step response. So why teach MPC on it?
Because the plant is simple enough to keep in your head while the workflow is new. You’re not fighting the dynamics; you’re learning the design steps. Pick the state, write the matrices, choose the horizons, set the weights, run sim.
The best way to learn is to start from an easy case, get comfortable with process, explore, make mistakes, fix the bugs, and once it comes easy move to a more complex case.
Where does linear model predictive control actually earn its complexity?
- Input rate AND magnitude limits. PID handles magnitude with saturation and anti-windup. Combined with a rate limit (slew rate on a servo, for example), certain misbehaviours can surface. MPC writes both as inequality constraints in the QP and solves once.
- MIMO plants. Two inputs, three outputs, cross-coupling, etc. PID needs cascading, decoupling, override logic. MPC handles it in one cost function.
- Predictable references. If you know the reference 1 s ahead, you can design MPC to use it. PID can’t.
DIY next steps
If you want to push this example further on your own, three extensions are a simple change away:
- Add a load disturbance step. Verify if the integrator action recovers it.
- Introduce model mismatch (simulated \(m = 12\) kg, controller still thinks \(m = 10\) kg). See whether it still settles.
- Add Gaussian noise on the position measurement. See how the Kalman filter handles it before the QP sees the state estimate.
Wrapping up
Linear MPC isn’t justified by complexity. It’s justified by constraints. On a damped mass with a step setpoint and a generous actuator, you don’t need it. PID with anti-windup will do. Add a rate-limited actuator, a moving reference, or a second output to coordinate, and the calculus changes. That’s when the QP earns its CPU cycles.
The next step on this same plant is adaptive MPC. Same cost function, same workflow, but the model gets relinearised at each step around the current operating point. That handles plants where the linearisation drifts as the state moves. Article coming.
Useful References
- Simone Bertoni. Practical Model Predictive Control
- Maciejowski, J. M. (2002). Predictive Control with Constraints. Prentice Hall.
- MathWorks. MPC Toolbox Documentation.
- Project code: position_control_mpc on GitHub.
- Project code – Open in MATLAB Online:
More stuff
If you are interested in PID design, the PID controller course walks through the gain tuning and anti-windup choices that this article assumes you’ve already seen.



