02. Practical Case Study: DC Motor Speed Control & PI Controller Design¶
Electromechanical Modeling, Open-Loop Steady-State Error, and Closed-Loop Synthesis
In this practical engineering case study, we develop a first-principles model of a Direct Current (DC) motor, analyze its uncompensated open-loop behavior, and design a Proportional-Integral (PI) speed tracking controller.
import matplotlib.pyplot as plt
import numpy as np
import ctrlpy as cp
from ctrlpy.controllers import pi_controller
from ctrlpy.plotting_plotly import plot_bode_plotly
%matplotlib inline
1. Physical System Modeling & Governing Equations¶
A DC motor consists of an electrical armature circuit coupled with a mechanical rotating inertia:
Governing Differential Equations:¶
Armature Electrical Circuit (Kirchhoff's Voltage Law): $$V(t) = R\, i(t) + L\, \frac{di(t)}{dt} + e_b(t)$$ where $e_b(t) = K_e\, \omega(t)$ is the back-electromotive force (back-EMF).
Rotor Mechanical Dynamics (Newton's 2nd Law for Rotation): $$J\, \frac{d\omega(t)}{dt} + b\, \omega(t) = \tau_m(t) - \tau_L(t)$$ where $\tau_m(t) = K_t\, i(t)$ is the motor electromagnetic torque, and $\tau_L(t)$ is external load disturbance.
Transfer Function Derivation¶
Taking the Laplace transform with zero initial conditions and $\tau_L(s) = 0$: $$V(s) = (L s + R) I(s) + K_e \Omega(s)$$ $$K_t I(s) = (J s + b) \Omega(s) \implies I(s) = \frac{J s + b}{K_t} \Omega(s)$$
Substituting $I(s)$ into the electrical equation: $$V(s) = \left[ \frac{(L s + R)(J s + b)}{K_t} + K_e \right] \Omega(s)$$
Thus, the voltage-to-speed plant transfer function is: $$G(s) = \frac{\Omega(s)}{V(s)} = \frac{K_t}{J L s^2 + (J R + b L) s + (b R + K_t K_e)}$$
2. Parameter Instantiation & Open-Loop Characterization¶
We consider typical industrial motor parameters:
- Rotor moment of inertia: $J = 0.01\text{ kg}\cdot\text{m}^2$
- Viscous friction coefficient: $b = 0.1\text{ N}\cdot\text{m}\cdot\text{s}$
- Torque constant: $K_t = 0.01\text{ N}\cdot\text{m}/\text{A}$
- Back-EMF constant: $K_e = 0.01\text{ V}\cdot\text{s}/\text{rad}$
- Armature resistance: $R = 1.0\ \Omega$
- Armature inductance: $L = 0.5\text{ H}$
# Physical parameters
J = 0.01 # kg*m^2
b = 0.1 # N*m*s
Kt = 0.01 # N*m/A
Ke = 0.01 # V*s/rad
R = 1.0 # Ohm
L = 0.5 # Henry
# Compute polynomial coefficients
# Denominator: J*L*s^2 + (J*R + b*L)*s + (b*R + Kt*Ke)
num_plant = [Kt]
den_plant = [J * L, J * R + b * L, b * R + Kt * Ke]
# Instantiate plant transfer function
G_motor = cp.tf(num_plant, den_plant)
print("DC Motor Plant Transfer Function G_motor(s):")
print(G_motor)
print(f"Open-loop Poles: {G_motor.poles()}")
print(f"Open-loop DC Gain: {G_motor.num[-1] / G_motor.den[-1]:.5f} (rad/s)/V")
Open-Loop Step Response & Tracking Error Analysis¶
Let's simulate the motor's response to a unit step voltage input $V(t) = 1.0\text{ V}$:
# Simulate open-loop step response
ol_step = cp.step_response(G_motor, T=4.0)
y_ss_ol = ol_step.steady_state_value()
error_ss_ol = 1.0 - y_ss_ol
print("--- Open-Loop Characteristics ---")
print(f"Steady-State Speed : {y_ss_ol:.4f} rad/s")
print(f"Steady-State Error : {error_ss_ol:.4f} ({(error_ss_ol * 100):.2f}%)")
print(f"Rise Time (10%-90%): {ol_step.rise_time():.4f} s")
print(f"Settling Time (2%) : {ol_step.settling_time(tolerance=0.02):.4f} s")
Notice that the open-loop DC gain is only $\approx 0.0999\text{ (rad/s)/V}$, leading to a massive steady-state tracking error of $90.01\%$. This highlights why closed-loop feedback control is essential!
3. Controller Design: Proportional (P) vs Proportional-Integral (PI)¶
Proportional (P) Control¶
With a proportional gain $C(s) = K_p$, the steady-state tracking error to a unit step is: $$e_{ss} = \frac{1}{1 + K_p K_{dc}}$$ Although increasing $K_p$ reduces steady-state error, it cannot eliminate it without infinite gain (which causes severe instability and actuator saturation).
Proportional-Integral (PI) Control¶
Adding an integrator $C(s) = K_p + \frac{K_i}{s} = \frac{K_p s + K_i}{s}$ introduces an open-loop pole at $s = 0$ (Type 1 system), guaranteeing zero steady-state tracking error ($e_{ss} = 0$).
# 1. Proportional Controller
Kp_p = 100.0
C_p = cp.tf([Kp_p], [1.0])
# Closed-loop with P control
T_p = cp.feedback(cp.series(C_p, G_motor), 1.0)
# 2. PI Controller: C(s) = (Kp*s + Ki) / s
# Tuned parameters for fast settling and minimal overshoot
Kp_pi = 150.0
Ki_pi = 500.0
C_pi = pi_controller(Kp=Kp_pi, Ki=Ki_pi)
# Closed-loop with PI control
T_pi = cp.feedback(cp.series(C_pi, G_motor), 1.0)
print("PI Controller C_pi(s):")
print(C_pi)
print("\nClosed-Loop Transfer Function T_pi(s):")
print(T_pi)
print(f"Closed-Loop Poles with PI: {T_pi.poles()}")
4. Performance Comparison: Open-Loop vs P vs PI Control¶
# Simulate step responses for all three configurations
t_sim = np.linspace(0.0, 1.5, 1000)
resp_ol = cp.step_response(G_motor, T=t_sim)
resp_p = cp.step_response(T_p, T=t_sim)
resp_pi = cp.step_response(T_pi, T=t_sim)
print("=== Closed-Loop PI Performance ===")
print(f"Steady-State Value : {resp_pi.steady_state_value():.5f} rad/s")
print(f"Steady-State Error : {abs(1.0 - resp_pi.steady_state_value()):.6f}")
print(f"Rise Time (10%-90%): {resp_pi.rise_time():.4f} s")
print(f"Settling Time (2%) : {resp_pi.settling_time(tolerance=0.02):.4f} s")
print(f"Percent Overshoot : {resp_pi.overshoot():.2f} %")
# Matplotlib Comparison Plot
plt.figure(figsize=(10, 5))
plt.plot(resp_ol.t, resp_ol.y, "k--", label=f"Open-Loop (yss={resp_ol.steady_state_value():.2f})")
plt.plot(
resp_p.t, resp_p.y, "g-.", label=f"P-Control Kp=100 (yss={resp_p.steady_state_value():.2f})"
)
plt.plot(
resp_pi.t,
resp_pi.y,
"b-",
lw=2,
label=f"PI-Control Kp=150, Ki=500 (yss={resp_pi.steady_state_value():.4f})",
)
plt.axhline(1.0, color="r", linestyle=":", label="Reference Target (1.0 rad/s)")
plt.title("DC Motor Speed Control: Step Response Comparison")
plt.xlabel("Time [s]")
plt.ylabel(r"Rotor Speed $\omega(t)$ [rad/s]")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
5. Load Disturbance Rejection Analysis¶
An important metric in industrial motor drives is the ability to reject external load torque disturbances $\tau_L(t)$ (such as friction spikes or varying cutting loads).
The closed-loop transfer function from load torque $\tau_L(s)$ to rotor speed $\Omega(s)$ is: $$G_{dist}(s) = \frac{\Omega(s)}{\tau_L(s)} = -\frac{G_{mech}(s)}{1 + C(s) G(s)}$$ where $G_{mech}(s) = \frac{Ls + R}{JLs^2 + (JR + bL)s + (bR + K_t K_e)}$.
# Disturbance transfer function numerator: -(L*s + R)
num_dist = [-L, -R]
G_mech = cp.tf(num_dist, den_plant)
# Closed-loop disturbance transfer functions
T_dist_p = cp.feedback(G_mech, C_p)
T_dist_pi = cp.feedback(G_mech, C_pi)
# Simulate response to a step load disturbance of 0.1 N*m applied at t=0
t_dist = np.linspace(0.0, 1.5, 1000)
dist_resp_p = cp.step_response(T_dist_p * 0.1, T=t_dist)
dist_resp_pi = cp.step_response(T_dist_pi * 0.1, T=t_dist)
plt.figure(figsize=(10, 4.5))
plt.plot(dist_resp_p.t, dist_resp_p.y, "r-.", label="P-Control Disturbance Response")
plt.plot(dist_resp_pi.t, dist_resp_pi.y, "b-", lw=2, label="PI-Control Disturbance Response")
plt.axhline(0.0, color="k", linestyle="--")
plt.title(r"Load Torque Disturbance Rejection ($\Delta \tau_L = 0.1\text{ N}\cdot\text{m}$)")
plt.xlabel("Time [s]")
plt.ylabel(r"Speed Deviation $\Delta\omega(t)$ [rad/s]")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
Notice that the PI controller completely restores the motor speed back to zero deviation ($\Delta\omega = 0$), whereas the P controller leaves a permanent speed drop!
6. Stability Margins & Frequency Robustness¶
# Open-loop loop transfer function L(s) = C_pi(s) * G_motor(s)
L_loop = cp.series(C_pi, G_motor)
sm_pi = cp.margin(L_loop)
print("=== Loop Stability Margins (PI Control) ===")
print(f"Gain Margin (GM) : {sm_pi.gm_db:.2f} dB")
print(f"Phase Margin (PM) : {sm_pi.pm_deg:.2f}°")
print(f"Gain Crossover Frequency : {sm_pi.wcg:.2f} rad/s")
print(f"Phase Crossover Frequency : {sm_pi.wcp:.2f} rad/s")
# Interactive Plotly Bode Plot with margins
fig_bode_motor = plot_bode_plotly(L_loop, margins=True)
fig_bode_motor.show()
Key Takeaways¶
- First-principles physical equations directly translate into
ctrlpyTransfer Function models. - Uncompensated DC motors suffer from substantial steady-state speed tracking error.
- Proportional-Integral (PI) controllers eliminate steady-state error and reject static load torque disturbances.
- Frequency-domain stability margins ensure robust closed-loop stability against model parameter variations.
Next, see 03_mass_spring_damper.ipynb for mechanical resonance, Root Locus parameter sweeps, and PID tuning!