Loading header...

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 m of 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

  1. Run the script unchanged and record final error, overshoot, and settling time.
  2. Set Ki = 0.0 and compare final error.
  3. Set Kd = 0.0 and observe overshoot or oscillation.
  4. Increase Kp until overshoot becomes unacceptable.
  5. Set max_force = 1.0 and observe slower response and longer saturation.
  6. Remove the anti-windup block and compare recovery after saturation.
  7. Increase mass to 2.0 and retune using only one gain change at a time.
  8. Add a load force by changing acceleration to (force - damping * velocity - 0.4) / mass and 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.

Mind Map

mindmap root((Axis Tuning Exercise)) Core idea Simulate before hardware Tune from logged response Saturation changes loop Formulas Error equals setpoint minus position PID force from P plus I plus D Acceleration equals force minus damping velocity over mass Settling band equals plus or minus 0.02 m Applications Linear stage Robot joint Servo axis Camera slider Design rules One gain at a time Clamp force Use anti windup Verify direction Log metrics Practical checks Overshoot under ten percent Settling time Final error Saturation duration Load change Common mistakes Tune while saturated No anti windup No fixed sample time Ignore delay Unsafe hardware jump