FeedforwardController
FeedforwardController commands the force or torque an axis needs before the feedback loop has to ask for it.12 minute read
FeedforwardController commands the force or torque an axis needs before
the feedback loop has to ask for it. It models the machine — friction, inertia,
gravity, position-dependent disturbance — and adds the result open loop, so the
feedback controller is left correcting the model’s small error instead of
driving the whole motion. Less work for the loop means less lag and less
tracking error.
Every plant coefficient can vary with position: mass, position disturbance and
the friction scaling are all lookup tables on positionTarget. An axis whose
inertia or friction changes over its stroke needs no gain scheduling
elsewhere.
flowchart LR
i1(["positionTarget — reference position"]) --> B["FeedforwardController"]
i2(["velocityTarget — reference velocity"]) --> B
i3(["accelerationTarget — reference acceleration"]) --> B
i4(["externalFeedForwardForce"]) --> B
i5(["externalMass — mass or inertia estimate"]) --> B
i6(["gearRatio"]) --> B
i7(["disable"]) --> B
B --> o1(["positionFeedForwardForce"])
B --> o2(["velocityFeedForwardForce — friction"])
B --> o3(["accelerationFeedForwardForce — inertia"])
B --> o4(["totalFeedForwardForce"])
B --> o5(["isEnabled"])
totalFeedForwardForce=totalFeedForwardGain× (position + friction + inertia +externalFeedForwardGain× external). The inertia term is (acceleration −gravity) × mass, andgravityis negative by default (−9.8066), so switchingsubtractGravityOnon adds a constant holding force of 9.8066 × mass.totalFeedForwardGainis capped at 1.0 — feedforward is a model of your machine and cannot usefully exceed it.externalFeedForwardGainreaches 1.5, so a known-low external estimate can be over-commanded by 50%.
The block starts disabled (enable defaults to false) and its total is
zero until you switch it on. The three component outputs report whether or
not it is enabled, so you can commission every term with the output
disconnected.
Inside the block
The four terms, each in its own group, and their sum. Position:
positionLookup plus the periodic positionDisturbanceCorrection, which sees
the target with the backlash offset added and scaled by gearRatio. Velocity:
the frictionModel, its output scaled by the position-dependent
frictionGainLookup (clamped to 0 … 1), fed with the dead-zoned velocity.
Acceleration: the filtered, dead-zoned acceleration minus gravity (when
subtractGravityOn) times the mass — massLookup plus the gained
externalMass when useMassEstimate — held at or above zero. External: the
force times its gain. The sum is scaled by totalFeedForwardGain and the last
switch passes it only while isEnabled; the three component outputs are
reported regardless. Yellow squares are switches resting on their default
contact; white squares are arithmetic; name() blocks are functions.
Signals
Inputs
| Path | Unit | Range | Description |
|---|---|---|---|
positionTarget |
m or rad | unbounded | Reference position. Drives the position lookup, the mass lookup, the friction gain lookup and the disturbance correction. |
velocityTarget |
m/s or rad/s | unbounded | Reference velocity. Drives friction compensation, and its sign sets the backlash direction. Deliberately unfiltered — filtering it buys nothing for friction. |
accelerationTarget |
m/s² or rad/s² | unbounded | Reference acceleration. Drives the inertia term, through the optional smoothing filter. |
externalFeedForwardForce |
N or N·m | unbounded | An extra feedforward force from outside the block. It is scaled on the way through and never modified in place. |
externalMass |
kg or kg·m² | unbounded | An external mass or inertia estimate. It contributes only when useMassEstimate is true and externalMassGain is above 0 — both default off. A negative value cannot invert the inertia term. |
gearRatio |
motor revolutions per axis unit | above 0 | Scales the position fed to the disturbance correction. It is a mechanical constant, supplied by the axis layer rather than tuned here. |
disable |
- | - | True forces totalFeedForwardForce to zero regardless of enable. Use it for a runtime override from a supervisor; use enable for the configured intent. |
Outputs
| Path | Unit | Description |
|---|---|---|
positionFeedForwardForce |
N or N·m | The position-dependent component: the position lookup plus the disturbance correction. Published even while the block is disabled. |
velocityFeedForwardForce |
N or N·m | The friction component, after the position-dependent friction scaling. Published even while disabled. |
accelerationFeedForwardForce |
N or N·m | The inertia component, including the gravity offset when enabled. Published even while disabled. |
totalFeedForwardForce |
N or N·m | The sum of all four components, scaled by totalFeedForwardGain. Zero while the block is disabled — this is the only output that is gated. |
isEnabled |
- | True when enable is true and disable is false. |
Parameters
| Path | Unit | Default | Range | Effect |
|---|---|---|---|---|
enable |
- | false | - | Master switch. False forces totalFeedForwardForce to zero and leaves the component outputs reporting. |
totalFeedForwardGain |
- | 1.0 | 0 – 1 | Scales the sum of all terms. Use it to ramp feedforward in during commissioning. Values above 1 are corrected to 1 — a plant model cannot usefully be over-commanded as a whole. |
velocityDeadzone |
m/s or rad/s | 0.0 | 0 upward | Velocities smaller than this are treated as zero before friction compensation. It also suppresses the backlash direction, so a velocity inside the dead zone leaves the direction unchanged. |
accelerationDeadzone |
m/s² or rad/s² | 0.0 | 0 upward | Accelerations smaller than this are treated as zero before the inertia term. Applied after the smoothing filter. |
externalMassGain |
- | 0.0 | any | Scales externalMass before it joins the inertia term. Zero by default, so externalMass does nothing until you set this. |
useMassEstimate |
- | false | - | When false, externalMass is ignored entirely. The second of the two switches that gate the external mass. |
subtractGravityOn |
- | false | - | When true, the inertia term includes a constant gravity offset. Use it for a vertically actuated axis. |
gravity |
m/s² | −9.8066 | any | Gravitational acceleration, negative along the positive axis direction. The inertia term subtracts it, so the default adds a positive holding force. Reverse the sign if your positive direction points down. |
externalFeedForwardGain |
- | 1.0 | 0 – 1.5 | Scales externalFeedForwardForce. 0 switches the external term off. Out-of-range values are corrected silently. |
backlashCompensation |
m or rad | 0.0 | −0.1 – 0.1 | Offsets the position fed to the disturbance correction by this much, in the direction of travel. The offset holds its direction at standstill. Out-of-range values are corrected silently. |
All ten parameters are persistent and survive a controller restart. The inputs and outputs do not.
Parameters exist below this block, in six sub-trees, and most of the real
configuration lives there: positionLookup, massLookup,
frictionGainLookup, frictionModel, positionDisturbanceCorrection and
accelerationFIRFilter. See friction-model.md and
lookup.md for the two that need the most work.
The friction gain overrides an external link. This block writes
frictionModel/outputScalingFactor on every cycle, so linking anything into
that input has no effect. Configure the frictionGainLookup table instead.
Setup
-
Leave
enablefalse andtotalFeedForwardGainat 0. Every component output still reports, so you can commission each term with nothing reaching the actuator. -
Link
positionTarget,velocityTargetandaccelerationTargetfrom your setpoint generator, and confirm all three follow it on a trace. -
Inertia first. Configure
massLookupwith your axis’s mass or inertia — a single point is enough if it does not vary over the stroke. Command a known acceleration and checkaccelerationFeedForwardForceequals mass × acceleration. -
If the axis is vertical, set
gravityfor your sign convention and switchsubtractGravityOnon.accelerationFeedForwardForceshould now show a constant offset at standstill equal to 9.8066 × mass. -
Friction second. Configure
frictionModelfor your axis. Move at a steady velocity and checkvelocityFeedForwardForceagainst the force the feedback loop was previously supplying. -
Leave
frictionGainLookupalone at first. It ships as a single point returning 1.0, which leaves friction compensation unscaled. -
Set
enabletrue, then raisetotalFeedForwardGainfrom 0 toward 1 in steps, watching the feedback controller’s own output shrink.Step 7 puts real force into the actuator. On a loaded vertical axis with
subtractGravityOnset, going from gain 0 to 1 in one step commands the full gravity term at once. Ramp it, and keep the axis clear. -
Confirm the feedback controller’s output has dropped. That reduction is what feedforward bought you; if it has not moved, the model is wrong somewhere in steps 3 to 5.
Tuning
- Tune against the plant, not against the error. Feedforward is a model of your machine — mass, friction, gravity — and each term is measured, not dialled in until the error looks small.
- Work one term at a time, in this order: inertia, gravity, friction,
position, external. Set
totalFeedForwardGainto 0 and read the component outputs while you do it. - Measure the mass. Command a known acceleration with the loop closed and no
feedforward, and read the force the feedback controller supplies. Divide by
the acceleration. Put that number in
massLookup. - If the force differs at different positions, add more points to
massLookuprather than averaging. - Measure friction the same way: move at a steady velocity, read the feedback
controller’s output, and configure
frictionModelto reproduce it. Check both directions — friction is rarely symmetric. - If friction varies over the stroke, tune
frictionModelfor the worst-case position, then usefrictionGainLookupto fade it down everywhere else. The gain is clamped to 0–1, so the worst case is the ceiling by construction. - Raise
totalFeedForwardGainlast, and only to 1.0. If you feel you need more than 1.0, a term is undermodelled — go back to step 3. - Use
velocityDeadzoneandaccelerationDeadzoneto stop the terms chattering around zero, not to shape the response. - Enable
accelerationFIRFilteronly if a stepping inertia term is visibly exciting the structure. Keep its coefficients summing to 1.0 — their sum scales the whole inertia term, and the controller logs a warning at startup if it exceeds 1. - Set
backlashCompensationlast, and only if the axis has measurable backlash. It shifts the disturbance correction in the direction of travel.
Read each term’s contribution off the gap between the curves.
| Symptom | Cause | Action |
|---|---|---|
totalFeedForwardForce is zero and the components are not |
Expected: enable is false or disable is true, and only the total is gated |
Read isEnabled; set enable true |
| The feedback controller still does all the work | totalFeedForwardGain is 0, or the model is undermodelled |
Raise the gain toward 1; if it is already 1, remeasure mass and friction |
| Tracking got worse after enabling feedforward | A term is over-commanded, most often mass | Halve the value in massLookup and re-measure |
| The axis lurches the moment feedforward is enabled | Expected on a loaded vertical axis: the gravity term appears at full value with no fade | Ramp totalFeedForwardGain from 0 instead of switching enable |
| The axis drifts down on a vertical axis | subtractGravityOn is off, or gravity has the wrong sign for your convention |
Switch it on; if the drift doubles, reverse the sign of gravity |
| The gravity offset is twice what it should be | gravity has the wrong sign — the term subtracts it, so a positive value fights instead of holding |
Set gravity negative for a conventional upward-positive axis |
externalMass seems to be ignored |
Expected: it needs both useMassEstimate true and externalMassGain above 0 |
Set both |
| The inertia term is smaller than mass × acceleration | The smoothing filter’s coefficients sum to less than 1 | Set them to sum to exactly 1.0 |
| The inertia term is larger than mass × acceleration | The smoothing filter’s coefficients sum to more than 1 | Set them to sum to 1.0. A warning was logged at startup |
| The inertia term is noisy or steps hard | accelerationTarget is stepping |
Enable accelerationFIRFilter with coefficients summing to 1.0 |
| Friction compensation is too strong at one end of the stroke | Friction varies over the stroke | Tune frictionModel for the worst case, then fade it with frictionGainLookup |
| Linking a value into the friction model’s output scaling did nothing | Expected: this block writes that input every cycle | Configure the frictionGainLookup table instead |
| A friction gain step arrived as a ramp | Expected: the friction model rate-limits the scaling factor | Raise the friction model’s own scaling-factor rate if you need it faster |
| The friction term chatters around standstill | No velocity dead zone, or a discontinuous friction model | Set velocityDeadzone, or switch the friction model to its continuous form |
| The position term reads zero | Expected with an unconfigured lookup, or the table is not monotonic | Configure positionLookup; a non-monotonic table falls back to zero |
| The backlash offset flipped sign when the axis stopped | Not possible — the direction is held at standstill | Check the offset is within ±0.1; larger values are corrected silently |
| The backlash offset never applied at all | The axis has not moved yet, or every velocity was inside the dead zone | Move the axis once; the direction is zero until then |
totalFeedForwardGain reads back as 1 after writing more |
Expected: it is capped at 1.0 | If you need more, a term is undermodelled |
externalFeedForwardGain reads back as 1.5 |
Expected: that is its ceiling | Fix the external estimate instead |
| The external force faded away over several seconds | Not possible in this version — the gain is applied without modifying the input | Check whatever produces externalFeedForwardForce |
| An output went to a non-numeric value | A non-numeric value reached an input or a lookup table | Fix the source and restart the controller; this block has no reset |
| You need feedforward on several axes | Not possible — this block handles one axis | Use one instance per axis, which is what the axes layer does |
A conservative starting point for a horizontal axis with a 2 kg mass on a 1 ms task, with friction and position terms not yet configured:
enable = true
totalFeedForwardGain = 0.0
subtractGravityOn = false
gravity = -9.8066
useMassEstimate = false
externalMassGain = 0.0
externalFeedForwardGain = 1.0
velocityDeadzone = 0.0
accelerationDeadzone = 0.0
backlashCompensation = 0.0
massLookup: single point y = 2.0
Raise totalFeedForwardGain from 0 as Setup step 7 describes. This is a
starting point, not a final tuning.
Limits and errors
| Limit | Set by | What happens | Reported |
|---|---|---|---|
totalFeedForwardGain within 0 – 1 |
Fixed | A larger value is replaced by 1.0. Feedforward models the plant and cannot usefully exceed it | Silently; read the value back |
externalFeedForwardGain within 0 – 1.5 |
Fixed | A value outside the range is replaced by the nearest edge | Silently; read the value back |
backlashCompensation within ±0.1 |
Fixed | A value outside the range is replaced by the nearest edge. The bound is the same whether the axis is linear or rotary | Silently; read the value back |
| Friction gain within 0 – 1 | Fixed | A table value outside the range is replaced by the nearest edge, then rate-limited by the friction model before it applies | Not reported |
| Total mass | Floored at 0 | A negative mass — from the lookup or from externalMass — is replaced by 0, so the inertia term can never invert and fight the commanded acceleration |
Not reported |
| Smoothing filter gain | Nothing | Not checked at runtime. The filter’s coefficients scale the whole inertia term by their sum. A sum above 1.0 is logged as a warning at startup only — a coefficient edited while running is not re-checked | Logged at startup; not reported at runtime |
totalFeedForwardForce |
Nothing | Unbounded — whatever the model computes reaches the output. Bound it in the actuator’s own limiter | Not reported |
| Component outputs | Nothing | Unbounded, and reported whether the block is enabled or not | Not reported |
| Block state | Nothing | There is no reset input. enable false zeroes the total but leaves the friction model and filters holding their state |
Not reported |
| Channel count | Fixed | One axis per instance, always | Not reported |
The block logs one warning, at startup only: that the acceleration smoothing filter’s coefficients sum to more than 1.0 and are therefore amplifying the inertia term. Every other failure above shows as a value on a trace, not as a message.
Verified against motorcortex-control3 3.30.0 (bc348fd).