Exercise: Model and Tune a Closed-Loop Axis
This exercise uses a small software model before hardware tuning. The model is intentionally simple: a mass, damping, command force limit, and discrete PID controller. The goal is not perfect physics. The goal is to learn how gain, saturation, delay, and load affect a closed-loop axis before risking a real mechanism.
Learning Objectives
By the end of this exercise, you should be able to:
- Implement a discrete position loop with fixed sample time.
- Simulate a simple mass-damper plant with actuator saturation.
- Tune P, PD, PI, and PID behavior using response data.
- Recognize overshoot, slow settling, final offset, windup, and undersized actuator symptoms.
- Define safe first steps before transferring a control loop to hardware.
Prerequisites
You should already understand:
- Setpoint, feedback, error, actuator, plant, and saturation.
- Proportional, integral, and derivative terms.
- Basic Python syntax and running a script from a terminal.
- The difference between position, velocity, acceleration, force, and mass.
Use a software-only simulation for this exercise. Do not connect a real motor until direction, limits, emergency stop, and mechanical guarding have been reviewed.
Task
Simulate a horizontal linear axis with command saturation. Tune it so a step from 0 m to 1.0 m:
- has less than
10%overshoot; - settles within
0.02 mof the target; - does not stay saturated for the whole move;
- reports final position, final error, overshoot, and settling time.
Then deliberately break one condition at a time and explain the symptom.
Buildable Code
Save this as axis_pid_demo.py and run it with Python 3.
Ts = 0.01
steps = 700
setpoint = 1.0
position = 0.0
velocity = 0.0
integral = 0.0
previous_error = 0.0
Kp = 10.0
Ki = 0.5
Kd = 2.5
max_force = 4.0
mass = 1.0
damping = 1.2
settling_band = 0.02
history = []
for k in range(steps):
t = k * Ts
error = setpoint - position
derivative = (error - previous_error) / Ts
integral += error * Ts
raw_force = Kp * error + Ki * integral + Kd * derivative
force = max(-max_force, min(max_force, raw_force))
# Anti-windup: if force saturates in the same direction as the error,
# undo this sample's integration because the actuator cannot deliver it.
if force != raw_force and (raw_force * error) > 0.0:
integral -= error * Ts
acceleration = (force - damping * velocity) / mass
velocity += acceleration * Ts
position += velocity * Ts
previous_error = error
history.append((t, setpoint, position, velocity, force, error))
max_position = max(row[2] for row in history)
overshoot = max(0.0, max_position - setpoint)
settling_time = None
for i, row in enumerate(history):
remaining = history[i:]
if all(abs(r[2] - setpoint) <= settling_band for r in remaining):
settling_time = row[0]
break
print("t(s) set(m) pos(m) vel(m/s) force(N) err(m)")
for row in history[::35]:
print(f"{row[0]:4.2f} {row[1]:6.2f} {row[2]:6.3f} {row[3]:8.3f} {row[4]:8.3f} {row[5]:7.3f}")
print()
print(f"final position = {position:.4f} m")
print(f"final error = {setpoint - position:.4f} m")
print(f"overshoot = {overshoot:.4f} m ({overshoot / setpoint * 100:.1f}%)")
if settling_time is None:
print(f"settling time = not settled within +/-{settling_band:.3f} m")
else:
print(f"settling time = {settling_time:.2f} s within +/-{settling_band:.3f} m")
Expected Behavior
With the default gains, the axis should move toward 1.0 m, saturate early, reduce force as it approaches the target, and settle near the target. Small overshoot is acceptable if it stays below 10% and returns inside the 0.02 m band.
The printed samples are not a plot, but they should show the trend:
- early positive force and increasing position;
- decreasing error as position approaches the target;
- force changing sign or reducing to slow the axis;
- final position close to
1.0 m.
Verification Steps
- Run the script unchanged and record final error, overshoot, and settling time.
- Set
Ki = 0.0and compare final error. - Set
Kd = 0.0and observe overshoot or oscillation. - Increase
Kpuntil overshoot becomes unacceptable. - Set
max_force = 1.0and observe slower response and longer saturation. - Remove the anti-windup block and compare recovery after saturation.
- Increase
massto2.0and retune using only one gain change at a time. - Add a load force by changing acceleration to
(force - damping * velocity - 0.4) / massand note why integral action helps.
Common Failure Symptoms
| Symptom | Likely cause | First action |
|---|---|---|
| Oscillation grows | Kp too high, Kd too low, or sample delay too large |
reduce Kp, add damping, check Ts |
| Slow response | Kp too low or force limit too small |
raise Kp cautiously, check saturation |
| Final offset remains | no integral action or constant load | add small Ki |
| Big overshoot after saturation | integral windup | add anti-windup or reduce demand |
| Force stays clipped | actuator undersized | reduce acceleration target or choose larger actuator |
| Response changes after load increase | gains tuned only for unloaded plant | retune with realistic load |
Debugging Guidance
Change one value at a time and keep notes. Print or plot setpoint, position, velocity, force, and error. If the force is saturated for most of the move, tuning cannot create more actuator capability. If the response oscillates with low gains, inspect the plant model, sample time, sign convention, and damping.
For real hardware, repeat the same discipline: low current limit, small move, verified direction, reachable emergency stop, mechanical hard stops protected, and no body parts near the mechanism. Stop immediately if measured position moves away from the setpoint.
Extension Challenge
Add one sample of measurement delay:
measured_position = history[-1][2] if history else position
error = setpoint - measured_position
Retune the controller. Then add measurement noise:
import random
measured_position = position + random.uniform(-0.002, 0.002)
Compare derivative on error with derivative on measurement. Explain why derivative can make noisy systems worse.
Concise Explained Solution
A workable tuning uses enough Kp to move quickly, enough Kd to damp overshoot, and small Ki to remove residual error. The force limit is part of the design, not an inconvenience: when force saturates, anti-windup prevents the integral term from accumulating a command the actuator cannot deliver. If the force limit is too low, the correct engineering fix may be a lower acceleration requirement, a lighter load, a different transmission, or a larger actuator.
Summary
This exercise shows why closed-loop tuning must be measured, not guessed. A simple model can reveal overshoot, saturation, windup, and load sensitivity before hardware is exposed to unsafe motion. The same workflow carries to a real axis: verify direction, limit energy, change one gain at a time, and judge the loop from logged setpoint, feedback, error, and actuator command.
Further Reading
- Control Tutorials for MATLAB and Simulink, PID step response.
- Karl Astrom and Richard Murray, Feedback Systems.
- Brett Beauregard, practical PID implementation notes.
- Tim Wescott, embedded control articles on PID tuning and sampling.