Understanding PID control

Hi,

I'd apprecieate some guidence on using a PID ontroller in a two wheeled balancing robot. I'm trying to learn a new skill but I think I'm missing something.

Would I be correct in saying that in a PID algorithm, if the error persisted over a period, the output should also continue to try and correct the error?

Put another way, I'm using an MCU605 to calculate the angle of my robot, which seems to be working. MY PID set-point is 0. When I change the angle of the MPU 6050 to say 20 deg (a error of 20), the PID algorithm breifly supplies a output to correct the error, then the output drops back to 0, even though the error is still 20.

I have set my arduino loop to sample every 100 millis, and I believe the PID algorithm is set accordingly.

So my question is should the PID output continue to be greater than 0 if my error continues to be greater than zero?

I probably need to go back to basics.

In the code below, I use a updateMovingAverage() method and strucure to remove some of the noise in the data. I've run that with a plotter and it seems solid.

For the PID algorithm I'm using br3ttb/PID @ ^1.2.1.

void setup() {
  Serial.begin(115200);
  while (!Serial)
    delay(10); // will pause Zero, Leonardo, etc until serial console opens

    // Try to initialize!
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
      delay(10);
    }
  }  
  Serial.println("MPU6050 Found!");
  mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
  
  maAngle = createMovingAverage(WINDOW_SIZE);
  //turn the PID on
  myPID.SetMode(AUTOMATIC);
  myPID.SetSampleTime(delaySize);
}

void loop() {
  
  currentTime = millis();
  if(currentTime - previousTime > delaySize){
    previousTime = currentTime;
    /* Get new sensor events with the readings */
    sensors_event_t a, g, temp;
    mpu.getEvent(&a, &g, &temp);
    
    float accYDrift=0;
    float accZDrift=0;

    float accY = a.acceleration.y + accYDrift;
    float accX = a.acceleration.x;
    float accZ = a.acceleration.z + accZDrift;  
    //float gyroX = g.gyro.x + gyroXdrift;
    float gyroX = g.gyro.roll;

    currentAngle = atan2(accY, accZ)*RAD_TO_DEG + ANGLE_OFFSET;
    float currentAngleAvg = updateMovingAverage(maAngle, currentAngle);
    Input =  currentAngleAvg;  
    myPID.Compute();
   

    Serial.print(" accY:");
    Serial.print(accY);
    Serial.print(" ,accZ:");
    Serial.print(accZ);
    Serial.print(" ,currentAngle:");
    Serial.print(currentAngle);
    Serial.print(" ,currentAngleAvg:");
    Serial.print(currentAngleAvg);
    Serial.print(" ,Input:");
    Serial.print(Input);
    Serial.print(" ,Output:");
    Serial.println(Output);
  }
}

Shouldn't the PID output be proportional to the size of the error ? Isn't that the 'P' in PID ?

1 Like

@UKHeliBob thank you for the quick reply. That was my understanding.

I forgot to mention in the code I am using the library "br3ttb/PID @ ^1.2.1" for the PID algorithm, which appears to be respectable.

I suspect my understanding of the "SampleTime" may be incorrect? I call the code every 100millis so I set the library "sampleTime" to match.
in a simple PID formula the sampletime has some bearing

correctionValue = Kp*(error) + Ki*(errorSum)*sampleTime - Kd*(currentAngle-prevAngle)/sampleTime;

I have no experience of that library and precious little experience of PID control in general

I have just built a n Elegoo Tumbller 2 wheel balancing robot and I fumbling my way through the code to understand how and why it works. The closest that I have come so far to the PID algorithm is to fiddle with some of the parameters

1 Like

Where do you initialize and setup the PID library and parameters?
Post your complete code.

I have moved to use a simple PID algorithm, taken from another balancing robot builder. It was failing for a while until I dropped the Ki value to zero, then the output responds to the movement and settles at rest as needed. The Ki value operates on the delaySize (increment time) and errorSum, so maybe something related to that isn't right.

I won't know if the code is going to respond well until I start putting the hardware together.

The "movingAverage" function is a bit complicated for my liking (I took it from the internet), I could probably refactor it at some point. I introduced a moving average and simple threshold filter because the responses from the MCU6050 were a bit noisy.

Any views on the code are welcome.

#include <Arduino.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include "math.h"

Adafruit_MPU6050 mpu;

#define Kp  8
#define Kd  0.1
#define Ki  0
#define targetAngle 0.0
#define WINDOW_SIZE 3

volatile float currentAngle, prevAngle=0, error, prevError=0, errorSum=0;
volatile int motorPower;
unsigned long currentTime, previousTime=0, delaySize=100;

#define FILTER_THRESHOLD 1
#define ANGLE_OFFSET -1.1

typedef struct {
    float *buffer;
    int size;
    int count;
    int index;
    float sum;
    float previousAvg;
} MovingAverage;

// Initialize the MovingAverage structure
MovingAverage* createMovingAverage(int size) {
    MovingAverage* ma = (MovingAverage*)malloc(sizeof(MovingAverage));
    ma->buffer = (float*)malloc(size * sizeof(float));
    ma->size = size;
    ma->count = 0;
    ma->index = 0;
    ma->sum = 0.0;
    ma->previousAvg = 100;
    return ma;
}

// Update the MovingAverage with a new value
float updateMovingAverage(MovingAverage* ma, float value) {
    int returnValue;
    
    if (ma->count < ma->size) {
        ma->buffer[ma->index] = value;
        ma->sum += value;
        ma->count++;
    } else {
        ma->sum -= ma->buffer[ma->index];
        ma->buffer[ma->index] = value;
        ma->sum += value;
    }
    ma->index = (ma->index + 1) % ma->size;
    
    float currentAvg = ma->sum / ma->count;
    float difference = ma->previousAvg - currentAvg;

    // filter out small differences
    if( difference > FILTER_THRESHOLD || difference < -FILTER_THRESHOLD){
      ma->previousAvg = currentAvg;
      returnValue =  currentAvg;
    }else{
      returnValue = ma->previousAvg;
    }    
    return returnValue;
}

MovingAverage* movingAverage;

void setup() {
  Serial.begin(115200);
  while (!Serial)
    delay(10); // will pause Zero, Leonardo, etc until serial console opens

    // Try to initialize!
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
      delay(10);
    }
  }  
  Serial.println("MPU6050 Found!");
  mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
  
  movingAverage = createMovingAverage(WINDOW_SIZE);
  
}

void loop() {
  
  currentTime = millis();
  if(currentTime - previousTime > delaySize){
    previousTime = currentTime;
    /* Get new sensor events with the readings */
    sensors_event_t a, g, temp;
    mpu.getEvent(&a, &g, &temp);
    
    float accYDrift=0;
    float accZDrift=0;

    float accY = a.acceleration.y + accYDrift;
    float accZ = a.acceleration.z + accZDrift;  

    currentAngle = atan2(accY, accZ)*RAD_TO_DEG + ANGLE_OFFSET;
    float currentAngleAvg = updateMovingAverage(movingAverage, currentAngle);   
    
   // --- PID code
    error = currentAngleAvg - targetAngle ;
    errorSum = errorSum + error;  
    errorSum = constrain(errorSum, -300, 300);

    //calculate output from P, I and D values
    motorPower = Kp*(error) + Ki*(errorSum)*delaySize - Kd*(currentAngleAvg-prevAngle)/delaySize;
    prevAngle = currentAngleAvg;

    Serial.print(" ,currentAngleAvg:");
    Serial.print(currentAngleAvg);
    Serial.print(" ,motorPower:");
    Serial.print(motorPower);
    Serial.print(" ,errorSum:");
    Serial.println(errorSum);
  }
}

Apart from the correct implementation of the algorithm the result depends solely on your settings of Kp, Ki and Kd. There exist tutorials on finding the optimal values. The libary AUTOMATIC mode may or may not find reasonable values. In your case I think that it doesn't find good values.

1 Like

@DrDiettrich thank you for the response.
I think you're right. I'm testing on a breadboard, which does not oscillate or respond to the output.
I think I'll move on to building the rest, then revisit the PID code once it's on a position to respond more representatively.
I'm yet to discover if I've made bad choices in motor, wheel size or battery!!

Yes, if your controlled system doesn’t respond to the output of the PID, then the PID can give garbage results: the output due to P component will be proportional, the I component will drive the output to one of the limits, and the D component will be zero. Tuning will be useless without connection to a controlled system.

1 Like

I use PID but have not gone beyond tuning a system written by someone else.

It seems 100 milliseconds is a long time to leave a robot to start falling over. I don't know what's typical in balancing robots, but the PID loops I exploit run thousands of times a second, not intuitively useful for physical systems that would have had almost no time to respond.

The running average seems like a bad idea, again speaking from no experience. A faster PID loop would be doing the same work, just eating the values and relying on the closed loop to achieve stability in the face of unstable inputs. As long as the average is, which you are implicitly assuming by taking an average.

A better filter if indeed one is beneficial might be a leaky integrator, the colloquial name for a kind of low pass filter, very easy to understand and imllement

   filteredValue = 0.9 * filteredValue + 0.1 * newReading;

The new filtered value is mostly the current filtered value with a little bit of the new reading factored in.

The 0.9 and 0.1 just have to add up to one. 0.7 and 0.3 would be more influenced by the newer readings and thus converge faster, at the expense of not filtering out as much of the undesired fluctuations.

It's one of my seven favorite concepts.

which links to a demo you can play with to get a feel for how it works and how the 0.1/0.9 decision effects things.

HTH

a7

1 Like

Many thanks for the response. I've not been looking at forums for a while.

I've been putting together the structural aspects of the robot lately and ignoring the coding part. I think I'm rubbish at creating Dupont cables, as I'm getting lots of wiring issues. I should probably solder up a perf board with everything on it, but I've not really done that before.

I'll move back to the coding soon. I'll try your filter suggestion, I like the idea. Although I also agree that I may not need a filter if the loop time is fast enough and my PID algorithm is working properly.

I think the MPU6050 library from Adafruit is nice and simple, but doesn't match any of the examples of other people, so I might also swap to a different library.

My thanks again.

The automatic /manual feature in the Arduino PID library doesn’t do automatic tuning, instead it does a bumpless transition between control with PID and manual control, like the manual/auto switch on a commercial controller. Many hand-crafted pids can’t switch smoothly.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.