PID demonstration RIG Part 1: Software

Introduction

In the PID regulator article I mentioned that I had an upcoming project utilising a PI regulator, this isn’t it, but when I was writing that article I though to myself that there must be a simple way of demonstrating visually how the different parts of a regulator effect the system as a whole. Moreover it made me think of a course I took in regulator theory, specifically how PID regulators work. In that course we were divided into teams and each team got to build, and program, a Quad Copter from scratch. That is to say, we started from a blank page of nothing and wrote the entire firmware for the Quad Copter in C code. Trust me when I say that this is not an optimal way of building a Quad Copter.

But the course served to illustrate a couple of things really well.

  • It gave us a crash course in regulators for embedded systems (Micro Controllers).
  • It was an extremely unstable system to work with, in other words, our regulator needed to work or we would fail miserably.
  • The Quad Copter was a visual way of demonstrating the effect of the regulator parameters.

I won’t document a Quad Copter build in this article but the theories used in this build could be used for it and the build will demonstrate the same things that the Quad Copter project did for us.

Material

  • Clear acrylic tube
  • Ping pong ball
  • 12V Computer case fan
  • IR distance sensor
  • LCD Display
  • Microcontroller (I will use an arduino UNO but anyone should be fine as long as you know how to program it).
  • Rotary encoder
  • Push button (My encoder doesn’t have an integrated switch)
  • Mosfet (For PWM control of the fan).

The System

Looking at the bill of materials it isn’t that hard to figure out what I’m building. The ping pong ball goes inside the acrylic tube, the fan is attached to the bottom, and the distance sensor gives the feedback (or process variable) to the micro controller of the distance to the ball. So the idea here is that we will set the distance to the ping pong ball in centimetres and the regulator in the microcontroller will set the fan speed based on the feedback from the distance sensor.

I will also program a simple HMI (Human Machine Interface) on the LCD where we can adjust both the setpoint and the PID parameters, Kp, Ki, and Kd. The reason I wanted an unstable system for the demonstration is partly as I said in the introduction, it forces us to have a well functioning regulator, but also to have a reason to use the derivative part of a PID regulator.

Programming

If you want to download the source code click here.

I messed around with the program of and on for about two days but most of that time was spent getting the HMI to work as I wanted. I admittedly have a lot more experience working with industrial HMI: s like PanelView and SIMATIC panels where everything is done graphically, dragging and dropping. So when I have to “write the graphics” I get my head in a knot for while because I do it so rarely. In contrast, the PID routine of the program is around 20 lines of code, that took 5 min to write.

The variables are quite basic, and I mostly use integers. The exceptions are the mapping routine for the distance sensor where I use floating point maths and the variables used in the derivative calculations. Generally you should avoid floating point maths because it slows the processor down a lot but the processor still executes the program fast enough for our purposes.

I also use one array to store a lookup table for converting the analog value of the distance sensor to centimetres. Now you might ask why I didn’t use the map function or a mathematics formula for this, and I would have if the value had been linear. I could probably have used some advanced second degree maths function to get a reasonable translation from V to cm, but this would have taken up a lot of processing time and slowed the system down even more than the floating point equations so it was easier to create a lookup table. The values are close enough for our purposes.

Void Setup

void setup()
{

  pinMode (MenuButton, INPUT);
  pinMode (MenuMinus, INPUT);
  pinMode (MenuPlus, INPUT);

  lcd.setBacklightPin(BACKLIGHT_PIN, POSITIVE);
  lcd.setBacklight(HIGH);

  lcd.begin(16, 2);   //Rows and colums of the LCD
  lcd.setCursor(3, 0);
  lcd.print ("Off Hours");
  lcd.setCursor(2, 1);
  lcd.print("Engineering");
  delay (3000);
  lcd.clear();
  lcd.print ("Sp");
  lcd.setCursor(4, 0);
  lcd.print ("Kp");
  lcd.setCursor(8, 0);
  lcd.print("Ki");
  lcd.setCursor(12, 0);
  lcd.print("Kd");


  Serial.begin (38400);


}

The first interesting bit is really in the setup routine, if you have used arduino IDE (or other c/c++ compilers) before you should recognize most of it. But I want you to look at the information I write to the LCD. The first text is just s greeting, it doesn’t really matter, but after clearing the screen (lcd.clear();) I write Sp, Kp, Ki, and Kd as text to the display. After this I don’t clear the display anywhere in the code, I only clear specific characters, if you clear the screen in the main loop the screen will flicker and I want to avoid this.

Void Loop

Mapping of a non linear value

  Pv = analogRead(A0);  //Mapping the distance sensor to read in centimeters
  Pv = map (Pv, 166 , 606 , 82 , 297);


  //The for loop goes into the lookup table, PvMapArray, and checks which two numbers the Pv is between,
  //then we linearize the curve between the two values, use the straight line equation to get the value in centimeter
  //and write it to the PvMapped variable.

  for (int i = 0; i < sizeof(PvMapArray) - 2; i = i + 2) {
    if ((Pv >= PvMapArray[i]) && (Pv <= PvMapArray[i + 2])) {
      PvMapped = PvMapArray[i + 1] - (((PvMapArray[i + 1] - PvMapArray[i + 3]) * (Pv - PvMapArray[i])) / (PvMapArray[i + 2] - PvMapArray[i]));
      break;
    }
  }

The mapping routine has three steps, had the value been linear it would only have needed the first two. The analogRead and map function.

The analogRead gives us a digitized value between 0 and 1023, 0V is 0 and 5.0V is 1023, or close enough. But our sensor only gives a value between 0.3 and 3.1 volts, and since the lookup table is based on the voltage readings we need to translate the value to volts, or in this case centivolts. I know, weird unit, but the size is appropriate and easy to work with and it gives me the output in centimetres after mapping. If you look at the map function in the code snippet 82= 0.82V and 297=2.97V, these are the actual physical values measured with a multimeter on the signal line of the sensor. I got all the other values for the lookup table from the data sheet.

The for loop

The next part is a bit more complicated but not to much.

The for loop functions as a search routine, the loop executes once for each voltage value in the array. The if condition only executes if the feedback, Pv, is within the range of the lookup table. If it is, the following equation executes and writes the distance in cm to the PvMapped variable.

Equation that linearize the value from the distance sensor for the PID regulator.

It is kind of hard to see the equation when it is written in code so the above figure is the formula cleaned up for our viewing pleasure.

HMI or How a simple state machine is programmed

A state machine is a device which can be in one of a set number of stable conditions depending on its previous condition and on the present values of its inputs. This is an excellent way to program a simple HMI for an embedded system.

  MenuButtonState = digitalRead (MenuButton);
  MenuPlusState = digitalRead (MenuPlus);
  MenuMinusState = digitalRead (MenuMinus);

  if (MenuButtonState != lastButtonState) {
    if (MenuButtonState == HIGH) {
      HMICounter++;
    }
  }

  if (HMICounter > 3) {
    HMICounter = 0;
  }


  if ((MenuPlusState != lastPlusState) && (HMICounter == 0)) {
    if (MenuMinusState != lastPlusState) {
      spCount++;
    } else {
      spCount--;
    }
  }

The HMICounter is used to determine which state the machine should be in, and which value of the PID regulator should increase or decrease based on the input from the encoder. The encoder is attached to the MenuPlus and MenuMinus inputs. We do a digitalRead on the inputs and store the value in the State variables, we could do a read directly in the code as well but if we do several reads in the program loop in a larger program it might lead to unexpected behaviour so I think it’s good practice to read it once and store it in a variable.

How to read on a high flank

The if statements are used to check if there has been a state change on the inputs since the last program cycle, this function is the same for checking the button input and the encoder pulses. Depending on if there has been a change on the MenuButton we increase the HMICounter to change the state of the HMI but only till it reaches a value of 4 then we reset it to 0 and start over again.

If the program detects a change on the encoder inputs one of 4 things happens, based on which of the 4 states the HMI routine is in. The 4 states are for increasing or decreasing Sp, Kp, Ki, and Kd values, and we have an if statement for each. You can do this in several different ways but for smallish programs I think this is the simplest and most straightforward way to do it.

Keeping track of the encoder

Before you ask, the XxCount variables (in the figure it is SpCount) is there because the encoder I had laying around has the annoying function of outputting 2 pulses per indent of the knob. So I store the actual pulses in an intermediate variable that is then divided by 2 to give the value of Sp, Kp, Ki, and Kd.

PID routine

Here comes the moment you’ve been waiting for, the PID part of the code in all its glory:

  //PID Calculation, these are explained more in-depth in my article "Software based PID regulators" 
  //on offhoursengineering.com
  Error = Sp - PvMapped;
  P_Part = (Kp * Sp) * 0.1;

  SampleTime = millis() - Time;                 //For simple systems the sample time is not necessary,
  Integral = (Error * SampleTime) + Integral;   //here I use it to increase the I part of the regulator
  I_Part = (Ki * Integral) * 0.00001;           //Scaling to get an appropriately sized part
  Time = millis();

  Derivative = Error - Old_Error;
  meanDerivative = ((meanOldDerivative * 49) + Derivative) / 50;            //Filtration of the Derivative, an exponential mean filter taking the last 50 values into account
  D_Part = Kd * meanDerivative;

  if (Sp <= 10) {                                  //The integral equation is ignored if Sp is below 10cm
    Integral = 0;
  }

  Speed = P_Part + I_Part + D_Part;

  if (Speed >= 255) {         //The max value the analog write function can handle is 255
    Speed = 255;
  }

  analogWrite (fan, Speed);


Simple, right? I won’t go into to much detail, I explain the different parts more in depth in the Software Based PID Regulators post but a couple of things are worth pointing out. The if statement in the figure is my way of preventing integral wind-up. If the Set Point is below 10 we simply reset the integral to zero. I use the analogWrite Function to generate a PWM signal and it accepts values between 0 and 255 so I limit the values from the PID routine to work with the analogWrite function.

Filtration of the Derivative

One thing I don’t mention in the regulator article is filtration of the process variable, the distance sensor in my case. Any signal will have some noise, some distortions, and in the case of a regulator we don’t want to regulate the system based on the noise. To prevent this I implemented a simple but effective exponentially weighted mean filter in the code. Stay tuned for a separate article on simple filtration of signals. This filter is really only necessary for the D part of the regulator since the values are so small that the distortions would have a huge impact on the behavior of the regulator.

Serial print and writing to the LCD

The last part of the code starts like this:

  if (LoopCounter == 1000) {      //I don't need to write to the serial port or LCD every program cycle so I only do it every 1000 loops

    Serial.print(Sp);             //Printing the relevant values over the serial port for debugging purposes
    Serial.print("\t");
    Serial.print(PvMapped);
    Serial.print("\t");
    Serial.print(Speed);
    Serial.print("\t");
    Serial.print(P_Part);
    Serial.print("\t");
    Serial.print(I_Part);
    Serial.print("\t");
    Serial.print(D_Part);
    Serial.print("\t");
    Serial.print(meanDerivative);
    Serial.print("\t");
    Serial.print(Error);
    Serial.print("\t");
    Serial.print(Integral);
    Serial.print("\t");
    Serial.println(HMICounter);

    LoopCounter = 0;

And I don’t think it needs much of an explanation. We print the most relevant variables over the serial port for debugging purposes, just as storing the digitalRead values in a variable this is a good habit to have. Being able to see which values update and when makes it a lot easier to troubleshoot your code. The loopCounter variable is there to dictate when we write data to the serial port and I2C bus, because it is not necessary to do it every program cycle. This lowers the load on processor and speeds up program execution. As it is set now the program writes data every thousand loops.

The final piece of the puzzle

    lcd.cursor();
    lcd.home();
    lcd.setCursor(0, 1);

    lcd.print (Sp);
    lcd.setCursor(4, 1);
    lcd.print (int(Kp));
    lcd.setCursor(8, 1);
    lcd.print(int (Ki));
    lcd.setCursor(12, 1);
    lcd.print(int (Kd));

    if (Sp < 10) {
      lcd.setCursor (1, 1);
      lcd.print (" ");
    }
    if (Kp < 10) {
      lcd.setCursor (5, 1);
      lcd.print (" ");
    }
    if (Ki < 10) {
      lcd.setCursor (9, 1);
      lcd.print (" ");
    }
    if (Kd < 10) {
      lcd.setCursor (13, 1);
      lcd.print (" ");
    }


    if (HMICounter == 0) {        //Moves the cursor on he LCD so it indicates the value we are changing.
      lcd.setCursor(0, 1);
    } else if (HMICounter == 1) {
      lcd.setCursor (4, 1);
    } else if (HMICounter == 2) {
      lcd.setCursor(8, 1);
    } else if (HMICounter == 3) {
      lcd.setCursor(12, 1);
    }
  }

  if (spCount >= 110) {       //Preventing the values from being larger than i can display on the LCD,
    spCount = 110;
  }
  if (kpCount >= 198) {
    kpCount = 198;
  }
  if (kiCount >= 198) {
    kiCount = 198;
  }
  if (kdCount >= 198) {
    kdCount = 198;
  }

  lastPlusState = MenuPlusState;      //At last we store all the variables we need for the next program loop
  lastButtonState = MenuButtonState;
  Old_Error = Error;
  meanOldDerivative = meanDerivative;

}

We end the program by printing the values of Sp, Kp, Ki, and Kd to the LCD. Remember how I said that I don’t want to clear the entire LCD each program loop? The way to clear a specific character on this type of LCD is printing a blank space, notice the space between the “ “. Because a space contains something, in comparison to nothing. Here we use it to clear the second number on the LCD when the values decrease to below 10.

The values on the LCD

To indicate which state the HMI is in, which value we can change, the set cursor function moves the cursor on the LCD based on the state. The maximum value of Sp is limited to about 80cm based on the distance sensors maximum range but in reality it will never go above 50cm because the acrylic tube is only 50cm long. A small overflow function resets the XxCount variables so the maximum value of all the PID Gains and Sp is 99.

The last four lines are used to store all the information that is needed in the next program cycle.

Conclusion

I went into way more detail about the programming than I had planned, but I figure if I do it know I won’t have to explain it later if there are any questions. And I like writing about this stuff, I think i understand concepts better if I try to get others to understand.

I had originally planned to to write this as one article, but since I am passed 2000 words and I have barely gotten past the programming part of the project I will split it into 2 parts.

And again, here is the download link for the source code if you missed it the first time:

Download PID source code

Till next time

Posted in All, Projects.

Leave a Reply

Your email address will not be published. Required fields are marked *