Lab 6 is a step further from Lab 5, which utilizes the PID controller to maintain the stunt car facing a desired orientation (keep a fixed Yaw angle). The robot should be
turning into the desired Yaw angle when received a new command via BLE.
You can click on the image to view it in a larger window.
This lab introduces hands-on experience about tuning a orientational PID controller to control a car's yaw to be maintained to the setpoint (which may change while running), utilizing data sent via BLE for debugging.
Code on the Stunt Car: The orientational PID controller is wrapped in the function "PID_ROT()". Once the orientational PID controller is enabled, it will try to maintain the orientation to the setpoint and store Yaw data accumulated from the Gyroscope for 10 seconds. When reaching the time limit, the PID controller will shutdown itself and apply the break to stop the car. After that, function sendIMU() will be called to send the collected data
(including time stamps, Yaw angles, motor PWM counts, desired Yaw setpoints) via BLE. When exiting the orientational PID controller, parameters will be reset so that experiments can be conducted without rebooting the board.
void loop() {
// only move when ble connected.
BLEDevice central = BLE.central();
if(central){
digitalWrite(LED_BUILTIN, HIGH);
Serial.print("Connected to: ");
Serial.println(central.address());
while(central.connected()){
read_data(); // get the robot command and related messages from BLE
// wheel control and PID based on ToF sensors
// ---- check ToF data ----
tofUpdate();
if(printDebug){
printDS();
}
// ---- PID to refresh the speedL and speedR according to ToF readings
PID_FB();
// ---- PID to control the rotation (yaw)
PID_ROT();
// ---- wheel control ----
wheelCtrl1(speedL, speedR);
// ---- send data over BLE
if(startSend){
sendToF();
}
if(startSendIMU){
sendIMU();
}
}
// if not connected to central
digitalWrite(LED_BUILTIN, LOW);
wheelBrkCtrl(100); // will clear the speedL and speedR.
delay(10);
}
}
ble.start_notify(ble.uuid['RX_STRING'], rx_handler_IMU)
ble.send_command(CMD.SET_TARGET_YAW, "0")
ble.send_command(CMD.PID_ROT_ON, "")
time.sleep(5) # wait for 5 secs.
ble.send_command(CMD.SET_TARGET_YAW, "90")
ble.send_command(CMD.SET_I_ROT, "0")
void yawCalibrate(){
float time = micros(), time_p = micros(), dt = 0.0;
float yaw = 0.0;
Serial.println("Calibrating yaw drifting, place car still for 15 seconds.");
while(1){
if(micros() - time > 20000000.0){
break;
}
dt = (micros() - time_p) / 1000000.0;
time_p = micros();
if(myICM.dataReady()){
myICM.getAGMT();
/* Computation variables */
yaw = yaw + dt * myICM.gyrZ(); // should be compensated to counter drifting
}
}
driftYaw = yaw / ((time_p - time) / 1000000.0);
Serial.print("driftYaw is: ");
Serial.println(driftYaw);
// Serial.println(yaw);
}
As a feedback controller, the Proportional-Integral-Derivative controller (PID controller) takes the error between desired control goal and the actual status as the input. The math expression
is listed below.
\(
u(t) = K_p e(t) + K_i \int_{0}^t e(\tau) d\tau + K_d \frac{d e(t)}{d t}
\)
This mermaid flowchart may help explain the data stream of a PID controller:
Range Discussion: the range of Gyroscope can be specified if we use the sparkfun ICM20948 Library. Since in this lab I am using the Pololu Library, the range of IMU seems not configurable. Instead of discussing the range,
it is worth mentioning that the Gyroscope will have a 1.2~1.3 deg/sec drift if we do not add any bias to the Z axis. Despite I have substracted a bias manually, the Pololu library indeed provides a built-in bias config API:
//Gyro Bias
ICM_20948_Status_e setBiasGyroX(int32_t newValue);
ICM_20948_Status_e setBiasGyroY(int32_t newValue);
ICM_20948_Status_e setBiasGyroZ(int32_t newValue);
ICM_20948_Status_e getBiasGyroX(int32_t* bias);
ICM_20948_Status_e getBiasGyroY(int32_t* bias);
ICM_20948_Status_e getBiasGyroZ(int32_t* bias);
//Accel Bias
ICM_20948_Status_e setBiasAccelX(int32_t newValue);
ICM_20948_Status_e setBiasAccelY(int32_t newValue);
ICM_20948_Status_e setBiasAccelZ(int32_t newValue);
ICM_20948_Status_e getBiasAccelX(int32_t* bias);
ICM_20948_Status_e getBiasAccelY(int32_t* bias);
ICM_20948_Status_e getBiasAccelZ(int32_t* bias);
//CPass Bias
ICM_20948_Status_e setBiasCPassX(int32_t newValue);
ICM_20948_Status_e setBiasCPassY(int32_t newValue);
ICM_20948_Status_e setBiasCPassZ(int32_t newValue);
ICM_20948_Status_e getBiasCPassX(int32_t* bias);
ICM_20948_Status_e getBiasCPassY(int32_t* bias);
ICM_20948_Status_e getBiasCPassZ(int32_t* bias);
The code implementation of the orientational PID controller is shown below:
void PID_ROT(){
static float lastError = 0.0;
static float lastTime = micros();
float dt = (micros() - lastTime) / 1000000.0;
lastTime = micros();
static float yaw = 0.0; // degrees
static float errorI = 0.0;
static unsigned int idxStorage = 0, startTime = micros();
// ---- update IMU reading ----
if(PID_ROT_IS_ON){
if(myICM.dataReady()){
myICM.getAGMT();
yaw = yaw + dt * myICM.gyrZ() - driftYaw * dt; // substracted bias manually.
// PID
float errorD = 0.0;
float error = desiredYaw - yaw;
errorI = errorI + error * dt;
errorD = error - lastError;
lastError = error;
Serial.print(yaw);
Serial.print(" ");
speedR = cutoffPWM(Kp_r * error + Ki_r * errorI + Kd_r * errorD);
speedL = -speedR;
Serial.print(speedL);
Serial.print(" ");
Serial.println(speedR);
// store data into the arrays.
if(startTime == 0){
startTime = micros();
}
if(micros() - startTime < maxDuration * 1000 && idxStorage < maxIMUAmount){ // store collected data while it's within time limit.
itoa(millis(), imuStamps[idxStorage], 10);
itoa(int(speedL), imuMotorSpeeds[idxStorage], 10);
itoa(int(yaw), imuStorage[idxStorage], 10);
itoa(int(desiredYaw), imuSetpoint[idxStorage], 10);
idxStorage += 1;
}
else{ // if the duration exceeds 10s or the data length exceeds the size of array, shut down the PID controller.
actualAmount = idxStorage;
PID_ROT_IS_ON = false;
wheelBrkCtrl(10);
}
}
}
else if(yaw != 0.0){ // if the PID should be shut down, reset the static vars here.
yaw = 0.0;
errorI = 0.0;
speedL = 0.0;
speedR = 0.0;
lastError = 0.0;
idxStorage = 0;
startTime = 0;
startSendIMU = true;
}
}
Wind-up in the PID controller refers to the phenomenon when the integration error accumulates over time, it dominates over the all 3 error inputs and causes slow response and fluctuations to the system.
When the car approaches to the setpoint, the control output should slowly decrease (the motor speed should slowly decrease). But a large integrational term will make the sum of P/I/D terms still very large,
the decreasing rate of P/D terms is subtle compared with the increasing rate of the I term. Thus the motor will actually speed up instead of slowing down.
In the orientational PID controller, if the integrated error is exploding, the car will rotate at a very fast speed for a long time, and unlikely to stop and converge again. Thus we can cutoff the integrated error
within a maximum value of 10 degrees.
float maxErrorI = 10.0; // in degrees.
float cutoffErrorI(float ErrorI){
if(ErrorI > maxErrorI){
ErrorI = maxErrorI;
}
return ErrorI;
}
THE END