Motor System Identification
Why Measure Your Robot?
Every robot is different. Even two identical-looking robots have:
- Motor variations: Winding resistance, magnet strength, friction
- Mechanical differences: Gear mesh, bearing wear, wheel alignment
- Electronic tolerances: PWM timing, driver efficiency
The result: Setting PWM=120 on two robots produces different speeds.
System identification solves this by measuring your specific robot to build an accurate model.
What You'll Learn
In this tutorial, you'll:
- ✅ Collect calibration data using IMU measurements
- ✅ Fit a motor model using linear regression
- ✅ Identify key parameters: motor constant, deadband, time constant
- ✅ Use the model for better control (feedforward + feedback)
The Motor Model
We'll identify parameters for this simplified model:
Steady-State Model
Where: - \(\omega\) = Angular velocity (°/s) - \(K_{motor}\) = Motor constant (°/s per PWM unit) - \(PWM_{deadband}\) = Minimum PWM to overcome static friction
Inverse Model (for control)
Dynamic Model
Where \(\tau\) is the time constant (how fast the motor reaches final speed).
Calibration Process
Step 1: Hardware Setup
from machine import Pin, I2C
from picobot import Motors
from bmi160 import BMI160
# Initialize hardware
motors = Motors()
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
imu = BMI160(i2c)
Step 2: Run Calibration Script
Upload and run motor_calibration.py:
# On host PC
python motor_sysid.py # Start receiver first
# On Pico (via Thonny or ampy)
# Run motor_calibration.py
The calibration routine:
- Startup threshold: Slowly ramps PWM until motor starts
- Steady-state sweep: Measures speed at different PWM values
- Step response: Applies sudden PWM change, measures response time
- Motor balance: Checks if robot drives straight
Step 3: Analyze Results
The host script fits the model and generates:
{
"K_motor": 0.52,
"deadband": 28,
"tau_ms": 85,
"pwm_min_left": 32,
"pwm_min_right": 30,
"pwm_balance": 2.5
}
Understanding the Results
Motor Curve
Linear fit of PWM → angular velocity
What it tells you: - Slope (\(K_{motor}\)): How much speed you get per PWM unit - X-intercept (deadband): PWM where motor just starts moving - R²: How linear the relationship is (>0.95 is good)
Step Response
How fast the motor reaches final speed
What it tells you: - τ (time constant): Time to reach 63% of final speed - Typical values: 50-200 ms for small DC motors - Faster τ = snappier response, but may overshoot
Startup Threshold
Why this matters: - PWM below threshold → motor doesn't move (wasted energy) - Your control should never command PWM in the "dead zone"
Motor Balance
Why this matters: - Even "same" PWM produces different speeds - Software compensation keeps robot straight
Using the Model for Control
Load Calibration
from motor_model import MotorController
from picobot import Motors
from bmi160 import BMI160
motors = Motors()
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
imu = BMI160(i2c)
# Create controller and load calibration
controller = MotorController(motors)
controller.load_calibration('motor_model.json')
Feedforward Control
Use the inverse model to predict PWM for desired speed:
# Want robot to turn at 50°/s
controller.set_turn_rate(50)
# Want to drive forward at "speed" 100 while turning 20°/s left
controller.set_drive(forward=100, turn_rate=20)
Closed-Loop Control
Add feedback for better accuracy:
# Enable feedback using IMU
controller.set_turn_rate(50, use_feedback=True, imu=imu)
# In main loop - call update regularly
while True:
controller.update() # Applies PID correction
time.sleep_ms(20)
Tuning PID
controller.set_pid(
kp=1.0, # Proportional: higher = faster response
ki=0.1, # Integral: higher = eliminates steady-state error
kd=0.05 # Derivative: higher = reduces overshoot
)
Battery Voltage Compensation
Li-ion battery voltage varies with charge state (3.0V empty → 4.2V full). This affects motor speed!
from motor_model import MotorController, BatteryMonitor
# Setup battery monitor (needs voltage divider on ADC pin!)
battery = BatteryMonitor(adc_pin=28, divider_ratio=2.0)
# Create controller with battery monitoring
controller = MotorController(motors, battery=battery)
controller.load_calibration('motor_model.json')
# Check battery before operation
if not controller.check_battery():
print("Battery too low!")
else:
# Compensation is automatic - PWM adjusted for voltage
controller.set_turn_rate(50)
# Check status anytime
status = controller.get_battery_status()
print(f"Battery: {status['voltage']:.2f}V ({status['percent']}%)")
print(f"Compensation: {status['compensation_factor']:.2f}x")
How it works:
- During calibration, battery voltage is recorded (e.g., 4.0V)
- At runtime, current voltage is measured (e.g., 3.5V)
- PWM is scaled by V_calibration / V_current = 4.0/3.5 = 1.14×
- Result: consistent speed regardless of battery state
Wiring (voltage divider required for 1S Li-ion):
Quick Calibration
For quick testing without full calibration:
from motor_model import quick_calibrate, BatteryMonitor
# Optional: battery monitor for voltage compensation
battery = BatteryMonitor(adc_pin=28)
# 2-second calibration (records battery voltage automatically)
params = quick_calibrate(motors, imu, battery=battery)
print(f"K_motor = {params['K_motor']:.3f}")
print(f"Deadband = {params['deadband']}")
print(f"V_calibration = {params['V_calibration']:.2f}V")
# Apply to controller
controller.set_params(**params)
Hands-On Tasks
Task 1: Run Full Calibration
- Start
motor_sysid.pyon host - Run
motor_calibration.pyon Pico - Examine the generated plots
- Compare K_motor between left and right turns
Task 2: Validate the Model
Write a test that: 1. Commands specific turn rates (20, 40, 60, 80 °/s) 2. Measures actual turn rates with IMU 3. Compares predicted vs actual
for target in [20, 40, 60, 80]:
controller.set_turn_rate(target)
time.sleep_ms(500)
gyro = imu.get_gyro()
actual = abs(gyro['z'])
error = abs(target - actual)
print(f"Target: {target}°/s, Actual: {actual:.1f}°/s, Error: {error:.1f}°/s")
Task 3: Compare Control Modes
Test the same maneuver with: 1. Open-loop (no feedback) 2. Closed-loop (with IMU feedback)
Measure: - How close to target speed? - How consistent across multiple runs? - Response to disturbances (push the robot)
Task 4: Square Pattern
Use calibrated control to drive a precise square:
def drive_square(side_time_ms, turn_rate=90):
for _ in range(4):
# Drive forward
controller.set_drive(forward=100, turn_rate=0)
time.sleep_ms(side_time_ms)
# Turn 90 degrees (90°/s for 1 second = 90°)
controller.set_turn_rate(turn_rate, use_feedback=True, imu=imu)
start = time.ticks_ms()
angle = 0
while angle < 90:
controller.update()
gyro = imu.get_gyro()
dt = 0.02
angle += abs(gyro['z']) * dt
time.sleep_ms(20)
controller.stop()
time.sleep_ms(200)
Self-Assessment
Quick Check
🔹 What does \(K_{motor}\) tell you about the motor? 🔹 Why is there a deadband in the motor response? 🔹 What is the time constant τ and why does it matter? 🔹 How does feedforward control differ from feedback control? 🔹 Why might left and right motors have different characteristics?
Research Tasks
-
Non-linearity: Is the motor curve truly linear? Test at very low and very high PWM values.
-
Battery effect: How does battery voltage affect K_motor? Repeat calibration at different battery levels.
-
Temperature: Motors heat up during use. Does K_motor change after 5 minutes of operation?
-
Load effect: Add weight to the robot. How does this affect the model?
The Physics Behind It
Motor Equations (Advanced)
Electrical model: $\(V_{pwm} = I \cdot R + L \frac{dI}{dt} + K_e \cdot \omega\)$
Mechanical model: $\(J \frac{d\omega}{dt} = K_t \cdot I - B \cdot \omega - T_{friction}\)$
The parameters we identify relate to these: - \(K_{motor} \propto K_t / (R \cdot B)\) (steady-state) - \(\tau \approx J / B\) (mechanical time constant) - \(PWM_{deadband} \propto T_{friction}\) (static friction)
See Motor Basics for more details.