Prerequisite: 01-what-is-control.md (block diagrams, transfer functions, poles)
Unlocks: 03-pid-controller.md — you need the motor transfer function to design a controller for it
Before you can design a PID controller, you need to know what you’re controlling. On an warehouse robot, that’s a brushed DC motor (earlier models) or brushless DC motor (BLDC, newer models) driving each wheel through a gearbox.
When you see a motor oscillating, or the robot can’t maintain speed on a ramp, or the current draw spikes unexpectedly — the explanation is in the motor’s physics. This lesson derives the motor model from first principles so you can predict these behaviors before they happen.
Real AMR scenario: A firmware update changed the motor PWM frequency from 20 kHz to 50 kHz. The motors ran hotter and drew more current at the same speed. Why? Because the motor’s electrical time constant ($L/R \approx 0.5$ ms) is comparable to the 20 µs PWM period at 50 kHz — the current ripple increased. You can’t predict this without understanding the electrical model.
A DC motor converts electrical energy to mechanical energy. It’s two systems coupled through electromagnetism:
The coupling goes both ways: - Current → torque (motor action) - Rotation → back-EMF voltage (generator action)
This bidirectional coupling is what makes the motor self-regulating: as it speeds up, back-EMF increases, which reduces current, which reduces torque, until equilibrium.
ELECTRICAL SIDE COUPLING MECHANICAL SIDE
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ │ │ │ │ │
│ V_in ──→ R ──→ L ──→│ τ = Kt · i │──→ │ J·(dω/dt) + B·ω │
│ ↑ │ │ │ │ = τ │
│ │ │ │ e = Ke · ω │◄── │ │
│ │ │ │ │ │ │
│ V_in = R·i + │ └──────────────┘ └──────────────────┘
│ L·(di/dt) + e │
│ │
└─────────────────┘
Apply Kirchhoff’s voltage law around the motor circuit:
$$V(t) = R \cdot i(t) + L \cdot \frac{di(t)}{dt} + e(t)$$
where: - $V(t)$ = applied voltage (from H-bridge PWM) - $R$ = winding resistance (Ω) — typically 0.5–5 Ω for small robot motors - $L$ = winding inductance (H) — typically 0.1–5 mH - $i(t)$ = motor current (A) - $e(t) = K_e \cdot \omega(t)$ = back-EMF voltage - $K_e$ = back-EMF constant (V·s/rad)
Electrical time constant: $\tau_e = L/R$
For a typical robot motor: $L \approx 1$ mH, $R \approx 2$ Ω → $\tau_e = 0.5$ ms. This means the current responds to voltage changes in ~0.5 ms — fast enough that for the speed loop (settling in ~10 ms), we can sometimes approximate the electrical dynamics as instantaneous.
Newton’s second law for rotation:
$$J \cdot \frac{d\omega(t)}{dt} = \tau_{motor}(t) - B \cdot \omega(t) - \tau_{load}(t)$$
where: - $J$ = total moment of inertia (kg·m²) — motor rotor + gearbox + wheel - $\omega(t)$ = angular velocity (rad/s) - $\tau_{motor} = K_t \cdot i(t)$ = motor torque - $K_t$ = torque constant (N·m/A) — numerically equal to $K_e$ in SI units - $B$ = viscous friction coefficient (N·m·s/rad) - $\tau_{load}$ = external load torque (friction, slope, payload)
Mechanical time constant: $\tau_m = J \cdot R / (K_t \cdot K_e)$
For a typical robot motor+gearbox: $\tau_m \approx 50–200$ ms. This is the dominant dynamic — mechanical response is 100–400× slower than electrical response.
In SI units, the torque constant and back-EMF constant are exactly equal. This isn’t a coincidence — it’s conservation of energy:
$$P_{electrical} = e \cdot i = K_e \omega \cdot i$$ $$P_{mechanical} = \tau \cdot \omega = K_t i \cdot \omega$$
For power to be conserved: $K_e = K_t$. This constant is often called simply $K$ or $K_m$ (motor constant).
Practical consequence: If the datasheet gives you $K_e$ = 0.01 V·s/rad, you immediately know $K_t$ = 0.01 N·m/A. One less parameter to measure.
Taking the Laplace transform of both equations and eliminating current:
$$\frac{\Omega(s)}{V(s)} = \frac{K_t}{(Ls + R)(Js + B) + K_t K_e}$$
This is a second-order system with two time constants (electrical and mechanical).
Simplified form (when $L \ll R$, i.e., electrical dynamics are much faster than mechanical — usually true for small motors):
$$\frac{\Omega(s)}{V(s)} \approx \frac{K_t / (RB + K_t K_e)}{\frac{RJ}{RB + K_t K_e} s + 1} = \frac{K_{motor}}{\tau_m s + 1}$$
where: - $K_{motor} = \frac{K_t}{RB + K_t K_e}$ = DC gain (rad/s per volt) - $\tau_m = \frac{RJ}{RB + K_t K_e}$ = electromechanical time constant
This is a first-order system! For most robot motor applications, you can model the motor as a simple first-order lag from voltage to speed.
Position is the integral of speed: $\Theta(s) = \Omega(s) / s$
$$\frac{\Theta(s)}{V(s)} = \frac{K_{motor}}{s(\tau_m s + 1)}$$
This has a pole at s = 0 (the integrator from speed to position), making it a Type 1 system. This means: - Position eventually tracks a step input with zero error (it’ll get there) - But it’s marginally stable — without damping, a position controller can oscillate
$$\frac{I(s)}{V(s)} = \frac{Js + B}{(Ls + R)(Js + B) + K_t K_e}$$
The current has a zero in the numerator — it initially spikes (when you apply voltage, back-EMF is zero, so all voltage drives current) then settles as the motor spins up and back-EMF reduces the current.
Current
│
│ peak (stall current = V/R)
│╲
│ ╲
│ ╲
│ ╲___________ steady-state (running current)
│
└──────────────────── t
0 τ_e τ_m
AMR relevance: This initial current spike is why motor drivers have current limiting. At stall (wheel blocked), current = V/R = 12V / 2Ω = 6A. The motor driver may only handle 3A continuous. The current PID loop (innermost loop) prevents this spike from damaging the driver.
A typical robot motor datasheet provides:
| Parameter | Symbol | Typical value | Unit |
|---|---|---|---|
| Nominal voltage | $V_{nom}$ | 12 | V |
| No-load speed | $\omega_{nl}$ | 6000 | RPM (= 628 rad/s) |
| No-load current | $I_{nl}$ | 0.2 | A |
| Stall torque | $\tau_{stall}$ | 0.5 | N·m |
| Stall current | $I_{stall}$ | 6.0 | A |
| Terminal resistance | $R$ | 2.0 | Ω |
| Rotor inertia | $J_{rotor}$ | 5.0 × 10⁻⁶ | kg·m² |
Derived parameters:
$$K_t = \frac{\tau_{stall}}{I_{stall}} = \frac{0.5}{6.0} = 0.083 \text{ N·m/A}$$
$$K_e = \frac{V_{nom} - R \cdot I_{nl}}{\omega_{nl}} = \frac{12 - 2 \times 0.2}{628} = 0.0185 \text{ V·s/rad}$$
Wait — $K_t \neq K_e$? That’s because the datasheet uses RPM and we need rad/s, and there’s friction loss at no-load.
More accurate: $K_e = K_t = 0.083$ V·s/rad. The discrepancy comes from friction ($B$) absorbing torque at no-load. Use the stall torque / stall current ratio as the most reliable estimate.
$$B = \frac{\tau_{stall} - K_t \cdot I_{stall}}{no_load_speed}$$
…but since $\tau_{stall} = K_t \cdot I_{stall}$ by definition, we get $B$ from the no-load torque:
$$B = \frac{K_t \cdot I_{nl}}{\omega_{nl}} = \frac{0.083 \times 0.2}{628} = 2.6 \times 10^{-5} \text{ N·m·s/rad}$$
robot motors have a gearbox (typical ratio $N$ = 50:1 to 100:1). The gearbox transforms:
| Parameter | Before gearbox | After gearbox |
|---|---|---|
| Speed | $\omega_{motor}$ | $\omega_{wheel} = \omega_{motor} / N$ |
| Torque | $\tau_{motor}$ | $\tau_{wheel} = \tau_{motor} \times N \times \eta$ |
| Inertia (reflected) | $J_{motor}$ | $J_{total} = J_{motor} \times N^2 + J_{wheel}$ |
Critical: Inertia reflects as $N^2$, not $N$. A 100:1 gearbox makes the motor see the wheel as 10,000× lighter. This is why geared motors can accelerate heavy robots quickly.
The effective mechanical time constant after gearing:
$$\tau_m = \frac{(J_{motor} N^2 + J_{wheel}) \cdot R}{K_t^2 + R \cdot B_{total}}$$
For AMR with $N = 50$, $J_{motor} = 5 \times 10^{-6}$: - $J_{reflected} = 5 \times 10^{-6} \times 2500 = 0.0125$ kg·m² - $J_{wheel} \approx 0.005$ kg·m² - $J_{total} \approx 0.0175$ kg·m² - $\tau_m \approx \frac{0.0175 \times 2}{0.083^2} \approx 5.1$ s
That’s very slow! This means the motor (after gearing) takes ~5 seconds to reach full speed from a voltage step. This is why you need a PID controller — proportional-only control with this time constant would be painfully slow. The derivative term predicts the trajectory; the integral term eliminates steady-state error.
Every DC motor has a linear speed-torque relationship:
Speed (ω)
ω_nl ──────╲
│ ╲
│ ╲
│ ╲
│ ╲
│ ╲
│ ╲
│ ╲
0 ─────────────────╲──── Torque (τ)
0 τ_stall
$$\omega = \omega_{nl} \left(1 - \frac{\tau}{\tau_{stall}}\right)$$
or equivalently: $\omega = \frac{V}{K_e} - \frac{R}{K_e K_t} \tau$
Operating point: The motor settles where the speed-torque line intersects the load curve. If the load increases (slope, heavier payload), the operating point moves left (lower speed, higher torque, higher current).
AMR relevance: When a robot carries a heavier bin, the load torque increases. Without feedback, the motor slows down. With speed PID, the controller increases PWM to maintain the setpoint — but current increases. If current exceeds the driver’s limit, it saturates. This is torque saturation — the controller wants more torque than the motor can provide. The robot slows down despite the controller’s best effort.
import numpy as np
import matplotlib.pyplot as plt
# Motor parameters (AMR-like geared motor)
R = 2.0 # Ω
L = 0.001 # H (1 mH — we'll include it for accuracy)
Kt = 0.083 # N·m/A = V·s/rad
Ke = Kt # SI units
J = 0.0175 # kg·m² (reflected through gearbox)
B = 0.001 # N·m·s/rad (friction)
# Transfer function: V(s) → ω(s)
# G(s) = Kt / [(Ls + R)(Js + B) + KtKe]
# = Kt / [LJs² + (LB + RJ)s + (RB + KtKe)]
a2 = L * J # s² coefficient
a1 = L * B + R * J # s¹ coefficient
a0 = R * B + Kt * Ke # s⁰ coefficient
K_dc = Kt / a0 # DC gain
print(f"DC gain: {K_dc:.2f} rad/s per volt")
print(f"Electrical τ: {L/R*1000:.2f} ms")
print(f"Mechanical τ: {R*J/(Kt*Ke)*1000:.1f} ms")
# Simulate step response using scipy
from scipy import signal
num = [Kt]
den = [a2, a1, a0]
sys = signal.TransferFunction(num, den)
t = np.linspace(0, 0.5, 5000)
t_out, y_out = signal.step(sys, T=t)
# Apply a 12V step
V_step = 12.0
speed_rpm = y_out * V_step * 60 / (2 * np.pi) # convert to RPM
plt.figure(figsize=(10, 6))
plt.plot(t_out * 1000, speed_rpm)
plt.xlabel('Time (ms)')
plt.ylabel('Speed (RPM)')
plt.title('Motor Step Response (12V applied at t=0)')
plt.grid(True)
plt.axhline(y=speed_rpm[-1], color='r', linestyle='--',
label=f'Steady state: {speed_rpm[-1]:.0f} RPM')
plt.legend()
plt.savefig('motor_step_response.png', dpi=100)
plt.show()
# Current transfer function: V(s) → I(s)
# I(s) = (Js + B) / [(Ls + R)(Js + B) + KtKe] * V(s)
num_i = [J, B]
den_i = [a2, a1, a0]
sys_i = signal.TransferFunction(num_i, den_i)
t_out, i_out = signal.step(sys_i, T=t)
current = i_out * V_step
plt.figure(figsize=(10, 6))
plt.plot(t_out * 1000, current)
plt.xlabel('Time (ms)')
plt.ylabel('Current (A)')
plt.title('Motor Current During Startup')
plt.axhline(y=V_step/R, color='r', linestyle='--',
label=f'Stall current: {V_step/R:.1f} A')
plt.axhline(y=current[-1], color='g', linestyle='--',
label=f'Running current: {current[-1]:.2f} A')
plt.legend()
plt.grid(True)
plt.savefig('motor_current_startup.png', dpi=100)
plt.show()
The transfer function model assumes linearity. Real motors have:
| Nonlinearity | Effect | When it matters |
|---|---|---|
| Stiction | Motor doesn’t move until torque exceeds static friction | Low-speed precision moves |
| Backlash | Gearbox has play; reversing direction → dead zone | Position control, direction changes |
| Saturation | PWM can’t exceed 100%; current can’t exceed driver limit | High-load situations |
| Cogging | Permanent magnets create torque ripple at certain positions | Low-speed smoothness |
| Temperature | R increases ~0.4%/°C; Kt changes with magnet temperature | Long duty cycles |
| Dead zone | Motor doesn’t respond below a minimum voltage (due to friction) | Very low speed commands |
robot motors exhibit ALL of these. The PID controller partially compensates, but extreme cases (stiction at very low speed, saturation on ramps) require explicit handling: - Dead zone compensation: Add a minimum PWM offset when speed command > 0 - Anti-backlash: In position mode, always approach from the same direction - Current limiting: Clamp the PID output to prevent driver damage
Real friction isn’t purely viscous ($B \cdot \omega$). It includes:
$$\tau_{friction} = B \cdot \omega + \tau_{coulomb} \cdot \text{sign}(\omega) + \tau_{stiction} \cdot \delta(\omega = 0)$$
Friction torque
│
│ ────────────── τ_stiction (breakaway)
│ /
│/ τ_coulomb
├─────────────────────── ω
│╲ -τ_coulomb
│ ╲
│ ────────────── -τ_stiction
│
The stiction problem: At zero speed, static friction is higher than kinetic friction. The motor must produce more torque to start moving than to keep moving. This creates a dead zone around zero speed where the PID controller requests torque but the motor doesn’t move. The integral term winds up. When the motor finally breaks free, the accumulated integral causes an overshoot.