Skip to content
LIBRARY
CascadeServoController.md

CascadeServoController

Cascade motion controller with position, velocity, and force loops.

The controller implements the traditional servo cascade: a proportional position controller commands a velocity, a PI velocity controller commands a force, and a PI force controller commands the actuator input (for example a valve spool position or a motor current).

s_ref ──►(P)──►(+ v_ff)──►[limit]──►(PI)──►(+ ka_ff·a_ff)──►(PI)──► u
             ▲                    ▲                        ▲
           s_meas               v_meas                   f_meas

The velocity reference v_ff is added directly to the position controller output without any gain, forming the velocity setpoint. The acceleration reference a_ff is added to the velocity controller output scaled by ka_ff, which approximates the moving mass (or reflected inertia) so that the force required to realize the reference acceleration is fed forward. Both PI controllers use back-calculation anti-windup.

The controller is written in per-unit form so that the same tuning transfers across actuator sizes. The physical scale of the machine enters only through three rating parameters: v_max (rated speed), f_max (rated effort — a force, torque, or current) and u_max (command range), each of which acts both as the loop limit and as the normalizing scale for its signal. The loop gains k_vel and k_force are then dimensionless (commanded fraction of the downstream scale per unit error as a fraction of the upstream scale), so their default value of 1 is a sensible starting point for anything from a small electric servo motor to a large hydraulic cylinder — only the three ratings change between machines. The position gain k_pos is a loop bandwidth (1/s) and is likewise size-independent.

Usage

MultibodyComponents.CascadeServoController(k_pos=5, v_max=1, k_vel=1, Ti_vel=0.2, f_max=1, ka_ff=0, k_force=1, Ti_force=0.05, u_max=1)

Parameters:

NameDescriptionUnitsDefault value
k_posPosition-loop bandwidth: commanded velocity per unit position error (rad/s or 1/s)5
v_maxRated actuator speed; sets both the velocity limit and the velocity scale1
k_velDimensionless velocity-loop gain (commanded effort as a fraction of f_max per unit velocity error as a fraction of v_max)1
Ti_velIntegrator time constant of the velocity loops0.2
f_maxRated actuator effort (force, torque, or current); sets both the effort limit and the effort scale1
ka_ffAcceleration feedforward gain, approximately the moving mass or reflected inertia0
k_forceDimensionless force-loop gain (commanded output as a fraction of u_max per unit effort error as a fraction of f_max)1
Ti_forceIntegrator time constant of the force loops0.05
u_maxActuator command range; sets both the output limit and the command scale1

Connectors

  • s_ref - This connector represents a real signal as an input to a component (RealInput)

  • v_ff - This connector represents a real signal as an input to a component (RealInput)

  • a_ff - This connector represents a real signal as an input to a component (RealInput)

  • s_meas - This connector represents a real signal as an input to a component (RealInput)

  • v_meas - This connector represents a real signal as an input to a component (RealInput)

  • f_meas - This connector represents a real signal as an input to a component (RealInput)

  • u - This connector represents a real signal as an output from a component (RealOutput)

Behavior

Source

dyad
"""
Cascade motion controller with position, velocity, and force loops.

The controller implements the traditional servo cascade: a proportional
position controller commands a velocity, a PI velocity controller commands a
force, and a PI force controller commands the actuator input (for example a
valve spool position or a motor current).

```
s_ref ──►(P)──►(+ v_ff)──►[limit]──►(PI)──►(+ ka_ff·a_ff)──►(PI)──► u
             ▲                    ▲                        ▲
           s_meas               v_meas                   f_meas
```

The velocity reference `v_ff` is added directly to the position controller
output without any gain, forming the velocity setpoint. The acceleration
reference `a_ff` is added to the velocity controller output scaled by `ka_ff`,
which approximates the moving mass (or reflected inertia) so that the force
required to realize the reference acceleration is fed forward. Both PI
controllers use back-calculation anti-windup.

The controller is written in per-unit form so that the same tuning transfers
across actuator sizes. The physical scale of the machine enters only through
three rating parameters: `v_max` (rated speed), `f_max` (rated effort — a force,
torque, or current) and `u_max` (command range), each of which acts both as the
loop limit and as the normalizing scale for its signal. The loop gains `k_vel`
and `k_force` are then dimensionless (commanded fraction of the downstream scale
per unit error as a fraction of the upstream scale), so their default value of
`1` is a sensible starting point for anything from a small electric servo motor
to a large hydraulic cylinder — only the three ratings change between machines.
The position gain `k_pos` is a loop bandwidth (`1/s`) and is likewise
size-independent.
"""
component CascadeServoController
  "Position reference"
  s_ref = RealInput() {
    "Dyad": {"placement": {"diagram": {"x1": -40, "y1": 450, "x2": 60, "y2": 550}}}
  }
  "Velocity reference, added to the velocity setpoint without gain"
  v_ff = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 230, "y1": -40, "x2": 330, "y2": 60, "rot": 90}}
    }
  }
  "Acceleration reference, added to the force setpoint scaled by ka_ff"
  a_ff = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 660, "y1": -30, "x2": 760, "y2": 70, "rot": 90}}
    }
  }
  "Measured position"
  s_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 160, "y1": 940, "x2": 260, "y2": 1040, "rot": 270}}
    }
  }
  "Measured velocity"
  v_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 450, "y1": 940, "x2": 550, "y2": 1040, "rot": 270}}
    }
  }
  "Measured actuator force"
  f_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 740, "y1": 940, "x2": 840, "y2": 1040, "rot": 270}}
    }
  }
  "Actuator command"
  u = RealOutput() {
    "Dyad": {"placement": {"diagram": {"x1": 960, "y1": 450, "x2": 1060, "y2": 550}}}
  }
  err_pos = BlockComponents.Math.Feedback() {
    "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 460, "x2": 230, "y2": 540}}}
  }
  gain_pos = BlockComponents.Math.Gain(final k = k_pos) {
    "Dyad": {"placement": {"diagram": {"x1": 270, "y1": 460, "x2": 350, "y2": 540}}}
  }
  add_v = BlockComponents.Math.Add() {
    "Dyad": {"placement": {"diagram": {"x1": 380, "y1": 510, "x2": 460, "y2": 430}}}
  }
  lim_v = BlockComponents.Nonlinear.Limiter(y_max = v_max) {
    "Dyad": {"placement": {"diagram": {"x1": 500, "y1": 440, "x2": 580, "y2": 520}}}
  }
  pid_vel = BlockComponents.Continuous.LimPID(final k = k_vel * f_max / v_max, final Ti = Ti_vel, final Td = 0, final y_max = f_max, final y_min = -f_max, final k_ff = ka_ff) {
    "Dyad": {"placement": {"diagram": {"x1": 630, "y1": 450, "x2": 730, "y2": 550}}}
  }
  pid_force = BlockComponents.Continuous.LimPID(final k = k_force * u_max / f_max, final Ti = Ti_force, final Td = 0, final y_max = u_max, final y_min = -u_max) {
    "Dyad": {"placement": {"diagram": {"x1": 790, "y1": 450, "x2": 890, "y2": 550}}}
  }
  "Zero feedforward for the force loop"
  zero_ff = BlockComponents.Sources.Constant(k = 0) {
    "Dyad": {
      "placement": {"diagram": {"x1": 800, "y1": 150, "x2": 880, "y2": 230, "rot": 90}}
    }
  }
  "Position-loop bandwidth: commanded velocity per unit position error (rad/s or 1/s)"
  parameter k_pos::Real = 5
  "Rated actuator speed; sets both the velocity limit and the velocity scale"
  parameter v_max::Real = 1
  "Dimensionless velocity-loop gain (commanded effort as a fraction of f_max per unit velocity error as a fraction of v_max)"
  parameter k_vel::Real = 1
  "Integrator time constant of the velocity loop"
  parameter Ti_vel::Time = 0.2
  "Rated actuator effort (force, torque, or current); sets both the effort limit and the effort scale"
  parameter f_max::Real = 1
  "Acceleration feedforward gain, approximately the moving mass or reflected inertia"
  parameter ka_ff::Real = 0
  "Dimensionless force-loop gain (commanded output as a fraction of u_max per unit effort error as a fraction of f_max)"
  parameter k_force::Real = 1
  "Integrator time constant of the force loop"
  parameter Ti_force::Time = 0.05
  "Actuator command range; sets both the output limit and the command scale"
  parameter u_max::Real = 1
relations
  connect(s_ref, err_pos.u1) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(s_meas, err_pos.u2) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 190, "y": 990}], "E": 2}]}}
  connect(err_pos.y, gain_pos.u) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(gain_pos.y, add_v.u1) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 365.8, "y": 500}, {"x": 365.8, "y": 494}], "E": 2}]
    }
  }
  connect(v_ff, add_v.u2) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 366.8, "y": 10}, {"x": 366.8, "y": 446}], "E": 2}]
    }
  }
  connect(add_v.y, lim_v.u) {
    "Dyad": {"edges": [{"S": 1, "M": [{"x": 480, "y": 470}, {"x": 480, "y": 480}], "E": 2}]}
  }
  connect(lim_v.y, pid_vel.u_s) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 602.3, "y": 480}, {"x": 602.3, "y": 477}], "E": 2}]
    }
  }
  connect(v_meas, pid_vel.u_m) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 602.8, "y": 990}, {"x": 602.8, "y": 523}], "E": 2}]
    }
  }
  connect(a_ff, pid_vel.u_ff) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 680, "y": 20}], "E": 2}]}}
  connect(pid_vel.y, pid_force.u_s) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 742.8, "y": 500}, {"x": 742.8, "y": 477}], "E": 2}]
    }
  }
  connect(f_meas, pid_force.u_m) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 742.8, "y": 990}, {"x": 742.8, "y": 523}], "E": 2}]
    }
  }
  connect(zero_ff.y, pid_force.u_ff) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(pid_force.y, u) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
metadata {
  "Dyad": {"icons": {"default": "dyad://MultibodyComponents/CascadeServoController.svg"}}
}
end
Flattened Source
dyad
"""
Cascade motion controller with position, velocity, and force loops.

The controller implements the traditional servo cascade: a proportional
position controller commands a velocity, a PI velocity controller commands a
force, and a PI force controller commands the actuator input (for example a
valve spool position or a motor current).

```
s_ref ──►(P)──►(+ v_ff)──►[limit]──►(PI)──►(+ ka_ff·a_ff)──►(PI)──► u
             ▲                    ▲                        ▲
           s_meas               v_meas                   f_meas
```

The velocity reference `v_ff` is added directly to the position controller
output without any gain, forming the velocity setpoint. The acceleration
reference `a_ff` is added to the velocity controller output scaled by `ka_ff`,
which approximates the moving mass (or reflected inertia) so that the force
required to realize the reference acceleration is fed forward. Both PI
controllers use back-calculation anti-windup.

The controller is written in per-unit form so that the same tuning transfers
across actuator sizes. The physical scale of the machine enters only through
three rating parameters: `v_max` (rated speed), `f_max` (rated effort — a force,
torque, or current) and `u_max` (command range), each of which acts both as the
loop limit and as the normalizing scale for its signal. The loop gains `k_vel`
and `k_force` are then dimensionless (commanded fraction of the downstream scale
per unit error as a fraction of the upstream scale), so their default value of
`1` is a sensible starting point for anything from a small electric servo motor
to a large hydraulic cylinder — only the three ratings change between machines.
The position gain `k_pos` is a loop bandwidth (`1/s`) and is likewise
size-independent.
"""
component CascadeServoController
  "Position reference"
  s_ref = RealInput() {
    "Dyad": {"placement": {"diagram": {"x1": -40, "y1": 450, "x2": 60, "y2": 550}}}
  }
  "Velocity reference, added to the velocity setpoint without gain"
  v_ff = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 230, "y1": -40, "x2": 330, "y2": 60, "rot": 90}}
    }
  }
  "Acceleration reference, added to the force setpoint scaled by ka_ff"
  a_ff = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 660, "y1": -30, "x2": 760, "y2": 70, "rot": 90}}
    }
  }
  "Measured position"
  s_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 160, "y1": 940, "x2": 260, "y2": 1040, "rot": 270}}
    }
  }
  "Measured velocity"
  v_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 450, "y1": 940, "x2": 550, "y2": 1040, "rot": 270}}
    }
  }
  "Measured actuator force"
  f_meas = RealInput() {
    "Dyad": {
      "placement": {"diagram": {"x1": 740, "y1": 940, "x2": 840, "y2": 1040, "rot": 270}}
    }
  }
  "Actuator command"
  u = RealOutput() {
    "Dyad": {"placement": {"diagram": {"x1": 960, "y1": 450, "x2": 1060, "y2": 550}}}
  }
  err_pos = BlockComponents.Math.Feedback() {
    "Dyad": {"placement": {"diagram": {"x1": 150, "y1": 460, "x2": 230, "y2": 540}}}
  }
  gain_pos = BlockComponents.Math.Gain(final k = k_pos) {
    "Dyad": {"placement": {"diagram": {"x1": 270, "y1": 460, "x2": 350, "y2": 540}}}
  }
  add_v = BlockComponents.Math.Add() {
    "Dyad": {"placement": {"diagram": {"x1": 380, "y1": 510, "x2": 460, "y2": 430}}}
  }
  lim_v = BlockComponents.Nonlinear.Limiter(y_max = v_max) {
    "Dyad": {"placement": {"diagram": {"x1": 500, "y1": 440, "x2": 580, "y2": 520}}}
  }
  pid_vel = BlockComponents.Continuous.LimPID(final k = k_vel * f_max / v_max, final Ti = Ti_vel, final Td = 0, final y_max = f_max, final y_min = -f_max, final k_ff = ka_ff) {
    "Dyad": {"placement": {"diagram": {"x1": 630, "y1": 450, "x2": 730, "y2": 550}}}
  }
  pid_force = BlockComponents.Continuous.LimPID(final k = k_force * u_max / f_max, final Ti = Ti_force, final Td = 0, final y_max = u_max, final y_min = -u_max) {
    "Dyad": {"placement": {"diagram": {"x1": 790, "y1": 450, "x2": 890, "y2": 550}}}
  }
  "Zero feedforward for the force loop"
  zero_ff = BlockComponents.Sources.Constant(k = 0) {
    "Dyad": {
      "placement": {"diagram": {"x1": 800, "y1": 150, "x2": 880, "y2": 230, "rot": 90}}
    }
  }
  "Position-loop bandwidth: commanded velocity per unit position error (rad/s or 1/s)"
  parameter k_pos::Real = 5
  "Rated actuator speed; sets both the velocity limit and the velocity scale"
  parameter v_max::Real = 1
  "Dimensionless velocity-loop gain (commanded effort as a fraction of f_max per unit velocity error as a fraction of v_max)"
  parameter k_vel::Real = 1
  "Integrator time constant of the velocity loop"
  parameter Ti_vel::Time = 0.2
  "Rated actuator effort (force, torque, or current); sets both the effort limit and the effort scale"
  parameter f_max::Real = 1
  "Acceleration feedforward gain, approximately the moving mass or reflected inertia"
  parameter ka_ff::Real = 0
  "Dimensionless force-loop gain (commanded output as a fraction of u_max per unit effort error as a fraction of f_max)"
  parameter k_force::Real = 1
  "Integrator time constant of the force loop"
  parameter Ti_force::Time = 0.05
  "Actuator command range; sets both the output limit and the command scale"
  parameter u_max::Real = 1
relations
  connect(s_ref, err_pos.u1) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(s_meas, err_pos.u2) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 190, "y": 990}], "E": 2}]}}
  connect(err_pos.y, gain_pos.u) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(gain_pos.y, add_v.u1) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 365.8, "y": 500}, {"x": 365.8, "y": 494}], "E": 2}]
    }
  }
  connect(v_ff, add_v.u2) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 366.8, "y": 10}, {"x": 366.8, "y": 446}], "E": 2}]
    }
  }
  connect(add_v.y, lim_v.u) {
    "Dyad": {"edges": [{"S": 1, "M": [{"x": 480, "y": 470}, {"x": 480, "y": 480}], "E": 2}]}
  }
  connect(lim_v.y, pid_vel.u_s) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 602.3, "y": 480}, {"x": 602.3, "y": 477}], "E": 2}]
    }
  }
  connect(v_meas, pid_vel.u_m) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 602.8, "y": 990}, {"x": 602.8, "y": 523}], "E": 2}]
    }
  }
  connect(a_ff, pid_vel.u_ff) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 680, "y": 20}], "E": 2}]}}
  connect(pid_vel.y, pid_force.u_s) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 742.8, "y": 500}, {"x": 742.8, "y": 477}], "E": 2}]
    }
  }
  connect(f_meas, pid_force.u_m) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 742.8, "y": 990}, {"x": 742.8, "y": 523}], "E": 2}]
    }
  }
  connect(zero_ff.y, pid_force.u_ff) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  connect(pid_force.y, u) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
metadata {
  "Dyad": {"icons": {"default": "dyad://MultibodyComponents/CascadeServoController.svg"}}
}
end


Test Cases

No test cases defined.

  • Examples

  • Experiments

  • Analyses