Lab 5: PID

During this lab, I implemented closed loop control via PID. The goal was to get the robot to stop exactly some distance from a wall.

Prelab

Bluetooth

Since tuning and debugging the PID controller requires a lot of iteration, I setup my bluetooth communication to receive the PID parameters from the remote computer, and then send back the data it collected.

case START_ROBOT:
{
    // get pid params
    char char_arr[8];
    success = robot_cmd.get_next_value(char_arr);
    if (success) kp = atof(char_arr);
    success = robot_cmd.get_next_value(char_arr);
    if (success) ki = atof(char_arr);
    success = robot_cmd.get_next_value(char_arr);
    if (success) kd = atof(char_arr);

    // Do pid
    pid();
    // Send back data after pid
    for (int i = 0; i < ARR_SIZE; i++){
        tx_estring_value.clear();

        tx_estring_value.append(times[i]);
        tx_estring_value.append(",");
        tx_estring_value.append(tof[i]);
        tx_estring_value.append(",");
        tx_estring_value.append(pwm[i]);
        tx_characteristic_string.writeValue(tx_estring_value.c_str());
    }
    break;
}

Then, on the python side my message handler looked like,

outs = []
def readdata(uuid, bytearray):
    outs.append(list(map(float, ble.bytearray_to_string(bytearray).split(",")))) # params are comma separated

P control

I started out with just P control which just drives the motor input proportionally to the position error. Also, I set the sensor mode to short because I was always testing < 1.4m of distance and this would allow for the quickest measurments. I additionally added a hard stop of 5s while testing, so I didn’t think I needed to account for bluetooth disconnect (which wouldn’t work with this kind of loop).

void pid(){
    
    while (start_time + 10000 > millis()){ // run for 10s at max
        // PID calculations
        if (!get_sensor_data()){
            int idx1 = (cnt - 1) % ARR_SIZE;
            int idx2 = (cnt - 2) % ARR_SIZE;
            sensor2_[cnt % ARR_SIZE] = sensor2_[idx1] + (sensor2_[idx1] - sensor2_[idx2]) / (times[idx1] - times[idx2])  * (times[cnt % ARR_SIZE] - times[idx1]); // estimate
        }
        float error = ((float) sensor2_[cnt % ARR_SIZE]) - targetDistance;
        set_motor_speed(kp * error);
    }
}

void set_motor_speed(float pid_val){
  int motor_out1 = min(pid_val, 200);
  int motor_out2 = 0;
  
  if (pid_val < 0.0){
    motor_out2 = min(-pid_val, 200);
    motor_out1 = 0;
  }
  analogWrite(3, motor_out1);
  analogWrite(4, motor_out2);
  analogWrite(15, motor_out2);
  analogWrite(16, motor_out1);
}

int get_sensor_data()
{
  times[cnt % ARR_SIZE] = millis();
  if ( !sensor2.checkForDataReady()) return 0; 
  sensor2_[cnt % ARR_SIZE] = sensor2.getDistance();
  return 1;
}

Tuning

Tuning the controller was fairly straight forward, the only major issue I faced was that the PWM deadband which I resolved by just adding the minimum value of 45 to the motor outputs. Testing with Kp = 0.5,

tof data tof data car gif

Clearly 0.5 is too high leading to very high oscillations.

Next, I tried 0.05 which I found significantly reduced oscillations, but it also made the robot move significantly slower.

tof data tof data car gif

Speed discussion

Without extrapolation, my code would run at 50 Hz because the ToF sensor was a bottleneck. With the extrapolation (shown in the earlier code block), the loop runs at roughly 155 Hz so every 3 iterations I receive a new ToF reading. I think this causes a decent amount of the oscillation when the error is low because the simple linear extrapolation overshoots the target.

Discussion

I found that for this kind of linear control even P control produces a good controller. Also, in hindsight I think my minimum PWM setting was a little high which could have been another possible reason for getting the slight oscillations even with a Kp of 0.05.