This is the second of three labs on motors. In this installment we will use feedback to build the equivalent of “cruise control”, the ability of the motors to turn at a desired speed (RPM) regardless of load (within the torque limits of the motor).

Refer to the discussion in the lecture for background.

1. Prelab

In the previous lab we “informally” checked what happens if the motor is loaded by slowing it with our fingers. The same thing of course would also happen if the robot was going uphill, or pushing something. If it went downhill, it would speed up.

This load dependent behavior is often undesirable. For example, in many applications a robotic arm or machine is required to move at a given speed regardless of load. One solution would be over-designing the motor, such that typical loads have negligible impact on its output. Obviously this would be very inefficient.

A better approach is to use feedback. Imagine how you keep a car (without electronic cruise control) going at constant speed: you watch the speedometer, and if the cruising speed is below a desired value push the throttle, and, conversely, let up if the vehicle is going faster than the desired speed.

We will realize the same behavior in our robot with computer control. For this we need three elements:

  1. A means to detect the actual speed to compare it to the desired velocity. We will use the “encoders” for this purpose.

  2. The capability to control motor speed or torque. The H-bridge, also introduced in the last lab, serves this function.

  3. A controller, to be designed and tested in this lab.

1.1. Proportional Control

Proportional control is the simplest form of automatic control. We are already familiar with this type of control from our use of operational amplifiers[1]

In the space provided below, draw a block diagram of proportional speed control, showing the motor, encoder, H-bridge, and controller.

Write the code for proportional control of a single motor, using the template shown below:

class MotorController:

    def __init__(self, motor, encoder):
        '''Controller for a single motor
        motor: motor driver (DRV8833)
        encoder: motor encoder (Encoder)
        '''
        self.mot = motor
        self.end = encoder

    def p_control(self, desired_cps, P=1):
        '''Set motor control to rotate at desired_cps'''
        actual_cps = ...
        error = desired_cps - actual_cps
        self.mot.set_speed(P*error)
        # return speed (e.g. for plotting)
        return actual_cps

Save your program as motor_controller.py.

To use this class, first call the constructor with DRV8833 and Encoder instances from the prior lab. Then call p_control periodically (e.g. every Ts=20ms) using a timer:

import ...

desired_cps = ...    # controller setpoint
P = 1                # controller proportional gain
Ts = 20              # controller operating period in [ms]

controller = MotorController(...)

def callback(timer):
    global controller, desired_cps, P
    # proportional control and print actual_cps (for plotting)
    print(controller.p_control(desired_cps, P))

timer = Timer(0)
timer.init(period=Ts, mode=..., callback=...)

Save your program as p_controller.py and upload to the MCU and gradescope.

Figure 1 shows a typical result (when you do this experiment in the lab the output will differ slightly because of variations of the breaking force and system parameters). The plot has three traces: the desired rotation rate (Set Point), actual rate without cruise control (Open Loop) and in red the result with proportional control for P=2.

Controller parameters may differ in your setup. Experiment with the values!
lab2 p loaded compare
Figure 1. Cruise control with proportional control

The controller significantly reduces the variation of the rotation rate under load. However, it also introduces a large systematic error: the actual cps, even without load, is considerably less than the controller input (desired_cps).

We have not observed this systematic error when using feedback with operational amplifiers. What’s different? In our applications of feedback we always assumed that the gain is infinite. This is often a valid assumption - the openloop gain of many operational amplifiers, for example, is in excess of 105 - but it is not correct for our motor.

What about increasing the controller gain P to reduce this error? Figure 2 shows the result. Higher P indeed results in reduced systematic error, but also produces significant oscillations just after the step has been applied.

lab2 p
Figure 2. Cruise control for different values of controller gain P

Moreover, the systematic error is still significant. Try even higher values of P in the lab to see if you can make the systematic error <5% of the step height (1250cps in the plot).

1.2. Proportional-Integral Control

Reducing the systematic error to zero requires infinite gain, but, as we have seen, this results in undesired oscillations. A compromise is a controller with infinite gain at DC (frequency = 0), but lower gain at higher frequencies. The simplest circuit with this characteristic is an integrator:

\[y(t_1) = \frac{1}{\tau_i} \int_{0}^{t_1} x(t) dt\]

In this equation, x and y are the integrator input and output and \(\tau_i\) is the time constant.

In Python we approximate the integration with summation:

\[y(kT_s+T_s) = y(kT_s) + x(kT_s) \times I \times T_s\]

where \(x(kT_s)\) and \(y(kT_s)\) are the input and output of the integrator at the current time step \(kT_s\), \(I\times T_s\) is the integrator gain, and \(y(kT_s+T_s)\) is the new integrator output at time \(kT_s+T_s\).

Modify motor_controller.py as follows to add integral control:

class MotorController:

    def __init__(self, motor, encoder):
        # code from above
        ...
        # new: initialize integrator state
        self.integ = 0

    # add new method:
    def pi_control(self, desired_cps, Ts, P=1, I=1):
        actual_cps = ...
        error = ...
        # Ts in [ms]
        self.integ += error * Ts / 1000
        # clamp integrator, e.g. if desired_cps exceeds maximum motor speed
        self.integ = max(-150, min(self.integ, 150))
        self.mot.set_speed(P*error + I*self.integ)
        return actual_cps

Save your program as motor_controller.py and upload to the MCU and gradescope.

Also write pi_controller.py (similar to p_controller.py but calling pi_control rather than p_control) and upload to gradescope and the MCU.

2. Lab

Support the robot such that the wheels can turn freely. Test the cruise control, with first proportional control and then with the PI controller. To facilitate comparison, operate one motor with and the other without control (code from previous lab). Manually adjust the motor control duty cycle of the motor without control to match desired_cps.

Plot the step response for varying values of P. Your result should look similar to Figure 2. One you have found a good value for P, evaluate the behavior under load. Your output should look similar to Figure 1.

Checkoff:

Repeat your evaluation for PI control. Your plots should look similar to Figure 3 and Figure 4.

Checkoff:

lab2 pi
Figure 3. Cruise control with a PI Controller for varying values of I
lab2 pi loaded compare
Figure 4. Load test of cruise control with PI controller

1. Strictly speaking, feedback in operational amplifiers is not purely proportional because of the dynamics of the system. Take a controls or feedback course for an in-depth treatment.