Lab 6

Prelab

I created two commands: “START_ORI_PID” and “STOP_ORI_PID” which I could send over bluetooth to command the robot to start and stop the orientation PID loop. These commands would turn the flag “run_ori_pid” true or false. The orientation PID loop sits in the main loop and runs if run_ori_pid = true.

case START_PID: {
  //reset variables/indeces = 0
  ...
  run_ori_pid = true;
  break;
}

case STOP_PID: {
  run_ori_pid = false;
  stopDriving();
  break;
}

I also created a command “CHANGE_ORI_GAIN” to be able to tune the PID gains on the fly over bluetooth rather than uploading code to the board every time I wanted to change a gain value. This included the PID gains as well as alpha for the derivative term low-pass filter. This made the PID tuning process a lot more efficient.

The command “SET_ORIENTATION” sets the target setpoint angle to the angle given as an input.

case CHANGE_GAIN: {
  float new_Kp; float new_Ki; float new_Kd; float new_alpha;
          
  success = robot_cmd.get_next_value(new_Kp);
  if(!success)
    return;

  success = robot_cmd.get_next_value(new_Ki);
  if(!success)
    return;

  success = robot_cmd.get_next_value(new_Kd);
  if(!success)
    return;
  
  success = robot_cmd.get_next_value(new_alpha);
  if(!success)
    return;

  Kp_ori = new_Kp;
  Ki_ori = new_Ki;
  Kd_ori = new_Kd;
  alpha_ori = new_alpha;

  break;
}

I also didn’t want the PID loop to automatically send the data every time it finished while I repetively tuned the gains, so I created separate commands to do that as well:

case SEND_PID_DATA_ORI: {
  sendpidData_Ori();
  break;
}

void sendpidData_Ori() {
  for (int j = 0; j < PIDindex_ori; j++) { 
    tx_estring_value.clear();
    //char time_str[20];
    //sprintf(time_str, "%lu", pidtime_arr[j]);
    float pid_time = (float) ori_pidtime[j]/1000 - (float) startTime; //convert to ms
    tx_estring_value.append(pid_time);
    tx_estring_value.append(" | ");        
    tx_estring_value.append(ori_pidorient[j]);
    tx_estring_value.append(" | "); 
    tx_estring_value.append(ori_pwm_arr[j]);
    tx_characteristic_string.writeValue(tx_estring_value.c_str());
    delay(5);
  }
}

PID Control for Orientation

For practical purposes I set constraints on the speed. If the PID loop wanted to give a control input greater than the maximum PWM of 250, it would set it at the max PWM. As we found in lab 4, the robot would not be able move at low PWM values, so I set a minimum turning speed at 190 PWM. If the resulting control input from the PID controllers was above the zero threshold, but less than the minimum speed, it would set the control input to the minimum speed so that the robot could actually move. If the control input was lower than a the zero threshold (100 PWM), it would set the control input to zero, since a low enough control input was effectively zero and wouldn’t really turn the robot. I put a low-pass filter on the the derivative term to reduce noise, and tuned the alpha value.

float ori_pid(float targetOrient) {
  float pwm;
  unsigned long current_time = micros();
  float dt = (current_time - prev_time) / 1e6; // Convert microsec to sec
  
  float error = yaw-targetOrient;
  
  // Set target reciprocal at 180 degrees offset
  float pid_ori_recip = targetOrient - (targetOrient < 0 ? -1 : 1) * 180;

  if (abs(error) > 180.0) {
      // Prevent sudden jumps in angle when crossing the +/- 180 boundary
      error += (pid_ori_recip - yaw) / abs(pid_ori_recip - yaw) * 360.0;
  }

  float deriv = (error - prev_error_ori)/dt;
  float filter_deriv = alpha_ori * deriv + (1 - alpha_ori) * prev_deriv_ori;
  prev_deriv_ori = filter_deriv;

  pwm = Kp_ori*error + Ki_ori*sumerror_ori + Kd_ori*filter_deriv;

  // Speed constraints
  if(abs(pwm) > maxTurnSpeed){
    pwm=copysignf(maxTurnSpeed, pwm);
  } 
  else if (abs(pwm) < zero_turn_thresh){
    pwm=0;
  }
  else if(abs(pwm) < minTurnSpeed){
    pwm=copysignf(minTurnSpeed, pwm);
  }
  
  prev_error_ori = error;  // Update previous error for next iteration
  prev_time = current_time;
  return pwm;
}

PID Loop

Synthesizing the parts above, the below code shows the PID loop within the main loop. It first collects the ToF sensor data if available, or else extrapolates a distance estimate based on previous sensor data. Then the PID controller calculates the control input which is then used to control the motors.

if(run_ori_pid){
  float ori_pwm;

  if (myICM.dataReady()) {
    getOrientation();
  }

  ori_pwm = ori_pid(TARGET_ORIENT);

  // collect data
  if(PIDindex_ori < PID_SAMPLES){
    ori_pidtime[PIDindex_ori] = micros(); // store micros
    ori_pidorient[PIDindex_ori] = yaw;
    ori_pwm_arr[PIDindex_ori] = ori_pwm;
  }
  PIDindex_ori++;

  drivePointTurn(ori_pwm);              
}

PID Gain Discussion

I started with tuning Kp (proportional gain), setting the other two gains to 0. Sometimes it wouldn’t reach the setpoint angle, so I increased the Ki (integral gain term). When it was too high, it would cause the robot to overshoot and oscillate a lot or go unstable, so I would increase the Kd (derivative gain).

Final Results

My final gain values are: Kp = 2, Ki = 1.5, Kd = 1, as well as alpha = 0.1.

Disturbance Correction

Below shows the response from the car when disturbed from its target angle. The orientation is the angle given in degrees.

Setpoint Changes

Below shows the car rotating to each setpoint angle I give: 45 deg, 90 deg, 135 deg, 180 deg, and 225 deg.

Resources Used:

Back to Home