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_measThe 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:
| Name | Description | Units | Default value |
|---|---|---|---|
k_pos | Position-loop bandwidth: commanded velocity per unit position error (rad/s or 1/s) | – | 5 |
v_max | Rated actuator speed; sets both the velocity limit and the velocity scale | – | 1 |
k_vel | Dimensionless velocity-loop gain (commanded effort as a fraction of f_max per unit velocity error as a fraction of v_max) | – | 1 |
Ti_vel | Integrator time constant of the velocity loop | s | 0.2 |
f_max | Rated actuator effort (force, torque, or current); sets both the effort limit and the effort scale | – | 1 |
ka_ff | Acceleration feedforward gain, approximately the moving mass or reflected inertia | – | 0 |
k_force | Dimensionless force-loop gain (commanded output as a fraction of u_max per unit effort error as a fraction of f_max) | – | 1 |
Ti_force | Integrator time constant of the force loop | s | 0.05 |
u_max | Actuator command range; sets both the output limit and the command scale | – | 1 |
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
"""
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"}}
}
endFlattened Source
"""
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"}}
}
endTest Cases
No test cases defined.
Related
Examples
Experiments
Analyses