Skip to content

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.

Your assumption:              Reality:
  PWM 120 → 50 °/s              Robot A: PWM 120 → 48 °/s
                                 Robot B: PWM 120 → 53 °/s

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

\[\omega = K_{motor} \cdot (PWM - PWM_{deadband})\]

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)

\[PWM = \frac{\omega_{desired}}{K_{motor}} + PWM_{deadband}\]

Dynamic Model

\[\omega(t) = \omega_{final} \cdot (1 - e^{-t/\tau})\]

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:

  1. Startup threshold: Slowly ramps PWM until motor starts
  2. Steady-state sweep: Measures speed at different PWM values
  3. Step response: Applies sudden PWM change, measures response time
  4. 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

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 - : How linear the relationship is (>0.95 is good)

Step Response

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

Left motor:  starts at PWM = 32
Right motor: starts at PWM = 30

Why this matters: - PWM below threshold → motor doesn't move (wasted energy) - Your control should never command PWM in the "dead zone"

Motor Balance

Forward drift: +2.3 °/s (robot curves left)
Compensation: add 2.5 PWM to left motor

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):

Battery+ ──┬── R1 (10kΩ) ──┬── GPIO28 (ADC)
           │               │
           └── R2 (10kΩ) ──┴── GND


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

  1. Start motor_sysid.py on host
  2. Run motor_calibration.py on Pico
  3. Examine the generated plots
  4. 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

  1. Non-linearity: Is the motor curve truly linear? Test at very low and very high PWM values.

  2. Battery effect: How does battery voltage affect K_motor? Repeat calibration at different battery levels.

  3. Temperature: Motors heat up during use. Does K_motor change after 5 minutes of operation?

  4. 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.


Next Steps