Feedforward control explained

Why a proportional controller is always behind a moving target, and how sending the command the model already knows is needed fixes it. Runnable programs drive the same 60 cm move with feedback alone, feedforward alone and both together, and chart the error of each.

Guidefree, runs in your browser

Feedforward is sending the command you already know is needed, instead of waiting for an error to tell you. If you know the robot should be moving at 12 cm/s, and you know roughly what command makes 12 cm/s, send that command now. The feedback loop then has nothing to do except fix the part the model got wrong, which is a much smaller job. Nearly every serious controller is built this way: a feedforward term that makes the motion happen, and a feedback term that cleans up after it. On this page a small robot makes the same 60 cm move five times over, and each demo below is a real program you can change and run.

The chart under each robot shows the error in cm: where the plan says the robot should be, minus where it actually is. Plus means the robot is behind the plan. A good controller keeps that line flat on zero while the move is running, not just at the end of it.

The move the robot is following

Every demo follows the same plan: a trapezoidal profile for a 60 cm move, with a speed limit of 12 cm/s and an acceleration limit of 8 cm/s/s. The plan(t) function gives the position, the speed and the acceleration the move should have at time t. Nothing else changes between the demos except the line that works out the command.

feedback only        command = K × error
feedforward only     command = model(speed the plan wants)
both                 command = model(speed the plan wants) + K × error
Block diagram: the plan feeds both a feedforward model and a feedback loop, and their two commands are addedfeedforwardmakes the movefeedbacktrims itthe planat time tspeed andaccelerationthe model, backwards100 (v + τ a) / 18.266 % at cruiseposition+errorK1 % at cruisecmddrive and robotposition(): what actually happened
The plan feeds both halves. The model turns the speed and acceleration it wants into a command and sends it straight out, which at cruise is 66 percent. The loop compares the plan's position with the robot's and adds K times the difference, which in the last demo is under 2 percent. Feedforward makes the move, feedback trims it.

Feedback alone is always behind

A proportional controller only produces a command when there is an error. So if the command needs to be non zero for the whole move, the error has to be non zero for the whole move. There is no tuning that gets round that. It is what a proportional controller is.

K × e = v      so      e = v / K

At 12 cm/s with a gain of 2, that is 6 cm behind, for as long as the cruise lasts.

The error grows to 6.8 cm during the ramp and stays near it for the whole cruise, exactly as v / K predicts. It only closes at the end, once the plan stops moving.
The program
from bugbot import *
connect()

DISTANCE, V, A = 60.0, 12.0, 8.0     # the move: cm, cm/s, cm/s/s
K, DT = 2.0, 0.1                     # feedback gain, and the tick

t_acc = V / A
d_acc = V * V / (2 * A)
t_flat = (DISTANCE - 2 * d_acc) / V
total = 2 * t_acc + t_flat

def plan(t):
    # where the move should be and how fast, t seconds in
    if t < t_acc:
        return 0.5 * A * t * t, A * t
    if t < t_acc + t_flat:
        return d_acc + V * (t - t_acc), V
    if t < total:
        left = total - t
        return DISTANCE - 0.5 * A * left * left, A * left
    return DISTANCE, 0.0

start = position()[1]
t = 0.0
while t < total + 2.0:
    want, v_ref = plan(t)
    error = want - (position()[1] - start)
    drive(5 * (K * error), 0, 0)     # 100 % is 20 cm/s, so 5 % per cm/s
    plot("error", error)
    wait(DT)
    t = t + DT
stop()
print("ended", round(position()[1] - start, 1), "cm of", DISTANCE)
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The error climbs during the ramp, reaches 6.8 cm, and sits between 6.5 and 6.8 cm for the whole cruise. The formula said 6, and the extra 0.7 is itself worth understanding: this robot's motors give about 18.2 cm/s at full command instead of the nominal 20, so the error has to be about a tenth bigger to produce the same speed.

When the plan stops at the end, the robot catches up and finishes 0.3 cm short. That is the thing to notice about feedback alone: the final position is fine, and the robot was about 6.7 cm behind for the five seconds in between. If something else has to be at that place at that time, a moving saw, another robot, a ball, then being late is being wrong.

Raising the gain shrinks the gap but never removes it, and it costs you elsewhere: the drive's quarter second lag turns high gain into overshoot, and sensor noise gets multiplied straight into the motors.

Feedforward alone

Now the other extreme. The command comes only from the plan, through the model, and the robot's position is read only so that the chart has something to draw.

The command comes only from the plan. The robot drifts steadily behind, to 8.2 cm at worst, and ends 6.3 cm short with nothing to pull it back.
The program
from bugbot import *
connect()

DISTANCE, V, A = 60.0, 12.0, 8.0
DT = 0.1

t_acc = V / A
d_acc = V * V / (2 * A)
t_flat = (DISTANCE - 2 * d_acc) / V
total = 2 * t_acc + t_flat

def plan(t):
    if t < t_acc:
        return 0.5 * A * t * t, A * t
    if t < t_acc + t_flat:
        return d_acc + V * (t - t_acc), V
    if t < total:
        left = total - t
        return DISTANCE - 0.5 * A * left * left, A * left
    return DISTANCE, 0.0

start = position()[1]
t = 0.0
while t < total + 2.0:
    want, v_ref = plan(t)
    error = want - (position()[1] - start)   # for the chart only
    drive(5 * v_ref, 0, 0)                   # the model, and nothing else
    plot("error", error)
    wait(DT)
    t = t + DT
stop()
print("ended", round(position()[1] - start, 1), "cm of", DISTANCE)
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The error grows all the way through the move, because nothing is watching it. The model says 100 percent is 20 cm/s and the robot does 18.2, so the robot is 9 percent slow all the way and the shortfall piles up: 8.2 cm at the worst point, 6.3 cm at the end. About a centimetre of the total comes from the dead band, because the first and last 0.4 s of the ramps ask for under 15 percent, which does nothing at all.

Notice what the shape of that error tells you. It drifts. A drift means the model is wrong by a steady fraction, and a model error is something you can measure once and fix for good, which is exactly what the last two demos do.

Both together

One line changes: the two terms are added.

The model does the driving and the gain corrects it: the error stays inside 1.8 cm all the way, and the robot finishes 0.8 cm past the mark.
The program
from bugbot import *
connect()

DISTANCE, V, A = 60.0, 12.0, 8.0
K, DT = 2.0, 0.1

t_acc = V / A
d_acc = V * V / (2 * A)
t_flat = (DISTANCE - 2 * d_acc) / V
total = 2 * t_acc + t_flat

def plan(t):
    if t < t_acc:
        return 0.5 * A * t * t, A * t
    if t < t_acc + t_flat:
        return d_acc + V * (t - t_acc), V
    if t < total:
        left = total - t
        return DISTANCE - 0.5 * A * left * left, A * left
    return DISTANCE, 0.0

start = position()[1]
t = 0.0
while t < total + 2.0:
    want, v_ref = plan(t)
    error = want - (position()[1] - start)
    drive(5 * (v_ref + K * error), 0, 0)
    plot("error", error)
    wait(DT)
    t = t + DT
stop()
print("ended", round(position()[1] - start, 1), "cm of", DISTANCE)
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The worst error drops from 6.8 cm to 1.8 cm, and through the cruise it sits at about half a centimetre. That number is predictable too. The model is 9 percent out, so what is left for the feedback to supply is 9 percent of 12 cm/s, and the error needed to produce it is 0.09 × 12 / (0.91 × 2), which is 0.6 cm. Feedback no longer has to make the motion happen. It only has to make up the difference.

The following error measured with feedback alone, feedforward alone, and the two together01234567802468time, serror, cmfeedback alone6.8 cm behindfeedforward alone8.2 cm behindbothnever past 1.8 cm
The same move three ways. Feedback alone settles 6.8 cm behind the plan and stays there for the whole cruise. Feedforward alone drifts to 8.2 cm, because nothing is watching. Together the error never passes 1.8 cm.

That is the division of labour the whole of control engineering is built on:

  • Feedforward makes it happen. It is fast, it costs nothing in stability because it is not inside the loop, and it is only as good as your model.
  • Feedback fixes what feedforward got wrong. It handles the things no model knows: a slope, a sticky wheel, a flat battery, somebody's foot.

Neither one is enough on its own. Feedforward alone drifts, because nothing measures the result. Feedback alone lags, because it needs an error before it acts.

What goes into the model

The feedforward term is your model of the machine, run backwards: you want this motion, so what command produces it? Every term you can name makes it better.

  • The gain. The basic term, and the one every demo above uses: command = 100 × v / v_max. Using the nominal v_max leaves you the 9 percent seen above. Using the robot's own measured 18.2 cm/s removes it.
  • The lag, or acceleration. The drive takes about a quarter of a second to reach the speed it is asked for, so during a ramp the robot is always a little behind. Asking for v + τ × a instead of v cancels it. On a machine where inertia is what fights you rather than a lag, the same term is written m × a, and it is the largest term in a fast robot arm.
  • Gravity. An arm holding itself out horizontally needs a command just to stay still, and that command changes with the angle: G × cos(angle). A lift needs one that changes with the load. Neither has anything to do with error, so neither belongs in the feedback path. Gravity compensation is why a good robot arm can be pushed around by hand with the motors on.
  • Friction. Below 15 percent this robot's motors do nothing at all, which is its version of stiction. The feedforward answer is a dead band inverse: whenever you want the machine to move at all, start the command at the dead band and add your request on top. Coulomb friction, which is roughly constant and always opposes motion, gets the same treatment with the sign of the speed.

Here is the same move with two of those terms: the measured gain, and the lag term. There is still no feedback at all.

The measured gain instead of the nominal one, plus the lag term, and open loop the error never passes 0.7 cm and ends at 0.3 cm.
The program
from bugbot import *
connect()

DISTANCE, V, A = 60.0, 12.0, 8.0
DT, TAU = 0.1, 0.25
GAIN = 18.2                          # cm/s at 100 %, measured on this robot

t_acc = V / A
d_acc = V * V / (2 * A)
t_flat = (DISTANCE - 2 * d_acc) / V
total = 2 * t_acc + t_flat

def plan(t):
    # position, speed and acceleration the move should have
    if t < t_acc:
        return 0.5 * A * t * t, A * t, A
    if t < t_acc + t_flat:
        return d_acc + V * (t - t_acc), V, 0.0
    if t < total:
        left = total - t
        return DISTANCE - 0.5 * A * left * left, A * left, -A
    return DISTANCE, 0.0, 0.0

start = position()[1]
t = 0.0
while t < total + 2.0:
    want, v_ref, a_ref = plan(t)
    error = want - (position()[1] - start)   # for the chart only
    drive(100 * (v_ref + TAU * a_ref) / GAIN, 0, 0)
    plot("error", error)
    wait(DT)
    t = t + DT
stop()
print("ended", round(position()[1] - start, 1), "cm of", DISTANCE)
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

Open loop, never once looking at where the robot is, the error stays under 0.7 cm and the move finishes 0.3 cm short. Compare that with the 8.2 cm of the first feedforward demo. The controller did not change. The model did.

The speed a lagged drive reaches when asked for the ramp, and when asked for the ramp plus tau times the acceleration012051015time, sspeed, cm/sask for vloses 2.9 cm of positionthe plan012051015time, sask for v + τ a0.1 cm ahead of the planthe plan
The drive needs about 0.25 s to reach the speed it is asked for, so a plain ramp command leaves the robot 2.9 cm behind by the end of the ramp, and it stays behind for the whole cruise. Asking for the speed plus τ times the acceleration during the ramp gives that back: 0.1 cm ahead of the plan instead.

Everything together

Put the feedback term back on top of the good model and you have what a real motion controller does.

The good model plus the gain: the error stays inside a third of a centimetre for the whole move.
The program
from bugbot import *
connect()

DISTANCE, V, A = 60.0, 12.0, 8.0
K, DT, TAU = 2.0, 0.1, 0.25
GAIN = 18.2                          # cm/s at 100 %, measured on this robot

t_acc = V / A
d_acc = V * V / (2 * A)
t_flat = (DISTANCE - 2 * d_acc) / V
total = 2 * t_acc + t_flat

def plan(t):
    if t < t_acc:
        return 0.5 * A * t * t, A * t, A
    if t < t_acc + t_flat:
        return d_acc + V * (t - t_acc), V, 0.0
    if t < total:
        left = total - t
        return DISTANCE - 0.5 * A * left * left, A * left, -A
    return DISTANCE, 0.0, 0.0

start = position()[1]
t = 0.0
while t < total + 2.0:
    want, v_ref, a_ref = plan(t)
    error = want - (position()[1] - start)
    drive(100 * (v_ref + TAU * a_ref + K * error) / GAIN, 0, 0)
    plot("error", error)
    wait(DT)
    t = t + DT
stop()
print("ended", round(position()[1] - start, 1), "cm of", DISTANCE)
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The error never leaves the band between minus 0.34 and plus 0.30 cm. On the same move, feedback alone was 6.8 cm behind and feedforward alone was 8.2 cm behind. Neither half is doing anything clever. They are each doing the job they are good at.

What feedforward cannot do

It cannot see. If the robot is jammed against a chair leg, the feedforward term will happily command cruise speed for the rest of the move and report nothing wrong. Anything that depends on what actually happened has to come through the feedback path, which is why the loop is never optional and why the error is still the thing to plot.

It also cannot fix a model that is wrong in a way you did not think of. A feedforward term built on a gain you measured once, on a full battery, on a clean mat, is worth less on a flat battery and a dusty one. Feedback is what absorbs the difference, and a growing error on the chart is the machine telling you the model has drifted.

Where this is taught

Questions

What is feedforward control?

Sending a command worked out from what you want to happen, rather than from an error that has already happened. A model of the machine is run backwards: you want this speed, so this is the command that produces it. Feedforward acts before any error appears, so it is not in the feedback loop and cannot make it unstable.

What is the difference between feedforward and feedback?

Feedback measures the result and corrects what went wrong, so it always needs an error to act on. Feedforward predicts the command needed and sends it, so it acts immediately but never checks the result. Feedback is only as good as its gain and its loop rate; feedforward is only as good as its model. Real controllers use both.

Why does a proportional controller lag behind a ramp?

Because it only produces a command when there is an error. Following a ramp needs a steady command, so it needs a steady error, and that error settles at v / K: the speed of the ramp divided by the gain. On this page, 12 cm/s with a gain of 2 leaves the robot about 6.7 cm behind for the whole cruise.

Does feedforward remove the need for a feedback loop?

No. Feedforward cannot see, so anything the model does not know about, a slope, a jam, a flat battery, goes uncorrected. The demos here show feedforward alone drifting 6.3 cm short of the mark. It takes both: feedforward to make the motion, feedback to clean up.

What is a gravity feedforward term?

A command sent purely to hold a weight up, worked out from the geometry rather than from an error. For a robot arm it changes with the angle, roughly G × cos(angle), so the feedback loop no longer has to carry the weight as a permanent error. It is what lets a good arm be pushed around by hand with its motors on.

What is a friction feedforward term?

A command added to overcome friction before the machine has to be dragged through it. On this robot the motors do nothing below 15 percent, so the term is a dead band inverse: any request to move at all starts at 15 percent and goes up from there. For Coulomb friction, which is roughly constant and opposes motion, the term is a constant with the sign of the speed.

How do you add feedforward to a PID controller?

Add the term outside the loop: command = feedforward(what the plan wants) + PID(error). Nothing about the PID changes, and its gains usually do not need retuning, because the error it sees is smaller from the first tick. In motion control the feedforward is usually a velocity term plus an acceleration term, with a friction term underneath.

Is feedforward the same as open loop control?

Feedforward on its own is open loop: the command depends only on the plan and the model. The difference is what people mean by the words. Open loop usually describes a whole controller that never measures anything. Feedforward describes one term inside a controller that does measure, and whose feedback part is still there to catch what the model missed.

Learn it step by step

These lessons build the same ideas one at a time, each with tasks the simulator marks.

  1. U10.5 Feedforward and feedback Following a trajectory, University
  2. U5.2 Proportional control Feedback control, University
  3. U5.4 The integral term Feedback control, University
  4. U1.2 A command is a request The robot as a system, University
  5. U1.6 Measuring your robot The robot as a system, University
  6. U3.4 Calibration Odometry and drift, University
  7. U10.2 Velocity profiles Following a trajectory, University
  8. U10.6 How far behind Following a trajectory, University
  9. U12.2 Fitting a model to data Learning, and the capstone, University
Open the lessons