Arduino PID Simulator


Introduction to Arduino PID Simulator

This project is an Arduino PID Simulator. Basically, the Arduino will send a process variable to a PID controller. In this case, the signal is 0 to 5v. Additionally, the Arduino reads the control variable from the PID controller, and simulates a real world process. Realize this is not a PID controller. This is a field device to simulate a process. I’ve written this project so simulate different types of processes.

You can adjust load, losses,, lag, delay, process time, and noise. Additionally, there is a setting to adjust the process time. Because some real loads might take a very long time to tune, this project has the ability to speed up those processes for the purpose of training. Unlike some software simulators, this project already has some inherent noise. This is due to both the resolution of the analog output, converting PWM to analog, and some small amount of line noise. This project is for the purpose of training and practice only.

What you Need

For this project, you will need a few components.

  • Arduino Board (or equivalent)
  • 3.9k Resistor
  • .1Uf Capacitor (The resistor and capacitor are to convert the PWM to analog)
  • A TM1638 Lock & Key unit. This is a display with 8 segments, 8 buttons, and 8 LED’s. Strobe connects to GPIO4, Clock to 6, and Data (DIO) to 7.
  • You also need some jumpers, and wire to connect things together.
  • A PID controller, such as a ControlLogix, SLC, or PLC-5 with Analog Input and Output ability (voltage)
  • A Breadboard

In my own project, I just used an Atmega328 processor with a 16MHz resonator. This saved some money vs. purchasing an Arduino board.

How the Arduino PID Simulator works

Compared to software simulators, this simulator needs a real voltage for the control variable. Keep in mind, your limit is 5v. If you plant to use a 10v output, simply set up a voltage divider. I’ve tried to keep this controller as simple as possible to make it easy to customize the code.

  • To begin with, the Arduino reads the CV from the controller.
  • This CV goes through an array of 100 elements.
  • At this time, the lag setting will choose which element of the array to use. A lower setting is faster. Likewise, a higher lag setting is slower.
  • Next, based on the CV, we’ll add 1 unit to the bulkpv variable for each percent of the CV per time unit.
  • On the other hand, loss and load will subtract from the bulkpv. This is a sliding scale. For example, if the PV is at 0, even though loss is 100, there will be no loss. This is because if there is no pressure or temperature, the loss is minimum. Likewise, the load works the same way.
  • At this point, the bulkpv variable goes into another array. The delay variable selects which element of this array to use. A lower delay will select a more recent PV to use.
  • After that, we inject noise into the PV. This is simply a random value based on the noise setting. For example, if the PV is 50, and the noise is 5, you will get a randomized value between 45 and 55 for the PV.
  • At this time, the program writes the PV to the analog output channel of the Arduino. Obviously, this will be the analog input of your PID controller.
  • ** The Test button allows you to inject a PV of 0 and PV of 100. This is to calibrate your scaling in the controller. In addition, you can change the process time. For example, a base setting of 60 could inject up to 100 units per minute into the bulkpv, and remove the same with combined loss and load at 100. If you press the test button again, you have the option to reset the Atmega328.
  • ** If you go to your Serial Monitor, you will see different values of the process. The Serial Monitor updates every 1 Second. Set it at 9600 baud.

Tuning the PID

To illustrate, you simply tune this PID like any other. As you cause disturbances, simply increase the controller gain until it becomes unstable. At this time, record the natural period of the oscillations (peak to peak). Next, cut the Kp in half. You will use the natural period to calculate the starting Integral and Derivative settings. Most of the time, however, you may not need derivative. It might, in fact, be detrimental to your process. Especially if you have a noisy process variable.

For information on tuning PID, check out the CotnrolLogix PID post. It’s important to realize that different controllers have different units for your gains. For example, if your controller uses minutes per repeat, the natural period itself is a good starting point for integral. Derivative would be 1/8th of that in minutes. For other units, you will have to convert the values to match your processor, and PID equation type.

Putting together the Arduino PID Simulator

Here, I am mounting the TM1638 display unit into a 3d printed box. I simply found a good TM1638 case and project box on thingiverse. You might use tinkercad to to put your own face on the box as I did in this case.

In this case, I just put an atmega328 onto prototype paper.

The code for the Arduino PID Simulator

Although I’m sure there are better ways to write the code, this seems to work well. I’m sure I will be changing the code, and updating this in the future. The code is simple, and should be easy to follow. Some of the variables are not currently in use anymore. I will remove them in the future if I decide not to use them.


// Written by Ricky Bryce with addition of TM1638Plus library;
// tested Kp=8; Ki = .5;  Kd=0 in ControlLogix;
int timeconstant = 250; //Resolution of process
int timeunits = 2; //Speed of Process (starting at 2)
int loadfactor = 400;
int resetproc = 0;
int resetPin = 12;

int serialvar = 0;
long averagetank = 0;
int mode = 0;
int prevmode = 0;
int load = 5;      // Starting 5
int lag = 5;       // Starting 5
int loss = 10;     // Starting 10
int pvnoise = 0;   // Starting 0
int pvdelay = 1;   //Starting 1
long controlvariable = 50;
long processvariable = 0;
int cvdisplay = 0;
int pvdisplay = 0;
long currentmillis = 0;
long prevmillis = 0;
uint8_t tmbuttons = 0;
int decbutton = 0;
bool modeflag = 0;
int modereq = 0;
int prevtmbuttons = 0;
long buttonrepeatmillis = 0;
char displayformat[13];
long serialmillis = 0;

// PID Variables
float actualpower = 0;
float powervelocity = 0;
float actualloss = 0;
float actualload = 0;
float bulkpv = 0;
float pvafterdelay = 0;
float cvafterdelay = 0;
float pvafternoise = 0;
float actualpv = 0;
long lagdelaymillis = 0;
long displaymillis = 0;
long calculationmillis = 0;
float pvdelayarray[101];
float cvdelayarray[101];
int teststep = 0;
int lockchannels = 0;
int maxpv = 150;




#include <TM1638plus.h>

// GPIO I/O pins on the Arduino connected to strobe, clock, data,
//pick on any I/O you want.
#define  STROBE_TM 4 // strobe = GPIO connected to strobe line of module
#define  CLOCK_TM 6  // clock = GPIO connected to clock line of module
#define  DIO_TM 7 // data = GPIO connected to data line of module
bool high_freq = false; //default false,, If using a high freq CPU > ~100 MHZ set to true.

//Constructor object (GPIO STB , GPIO CLOCK , GPIO DIO, use high freq MCU)
TM1638plus tm(STROBE_TM, CLOCK_TM , DIO_TM, high_freq);


void setup() {
  digitalWrite(resetPin, HIGH);
  delay(200);
  pinMode(resetPin, OUTPUT);
  Serial.begin(9600);
  delay(100);
  Serial.println("--Comms UP -- PID Simulation -- Reverse Acting--");
  tm.displayBegin();
  tm.brightness(1);
  tm.displayText("pid rdy ");
  delay(1000);

}

void(* resetFunc) (void) = 0;  //Set up reset ability.


void loop() {
  currentmillis = millis();

  tmbuttons = tm.readButtons();
  if (tmbuttons == 0) {
    decbutton = 0;
    prevmillis = currentmillis;
  }

  if (tmbuttons != prevtmbuttons) {
    switch (tmbuttons)
    {
      case 0: decbutton = 0; break;   // No Buttons Pressed
      case 1: decbutton = 1; break;   // Change Load
      case 2: decbutton = 2; break;   // Change Lag
      case 4: decbutton = 3; break;   // Change Loss
      case 8: decbutton = 4; break;   // Change Noise
      case 16: decbutton = 5; break;  // Change Delay
      case 32: decbutton = 6; break;  // Run Test
      case 64: decbutton = 7; break;  // Up Button
      case 128: decbutton = 8; break; // Down Button

    }
  } else {
    decbutton = 0;
  }


  prevtmbuttons = tmbuttons;


  //set up repeating up/down keys
  if (((currentmillis - prevmillis) > 1000) and ((tmbuttons == 64) or (tmbuttons == 128))) {

    if ((tmbuttons == 64) & ((currentmillis - buttonrepeatmillis) > 40)) {
      decbutton = 7;
      buttonrepeatmillis = currentmillis;
    }

    if ((tmbuttons == 128) & ((currentmillis - buttonrepeatmillis) > 40)) {
      decbutton = 8;
      buttonrepeatmillis = currentmillis;
    }
  } else {
    buttonrepeatmillis = currentmillis;
  }



  if ((decbutton > 0) & (decbutton < 7)) {
    mode = decbutton;
    if (mode != prevmode) {
      modeflag = 0;
      tm.setLED((mode - 1), 1);
      for (int x = 0; x < 7; x++) {
        if (x != (mode - 1)) {
          tm.setLED(x, 0);
        }
      }
      prevmode = mode;
    }
  }


  switch (mode)
  {
    case 0: modePID(); break;
    case 1: modeLoad(); break;
    case 2: modeLag(); break;
    case 3: modeLoss(); break;
    case 4: modeNoise(); break;
    case 5: modeDelay(); break;
    case 6: modeTest(); break;
  }
  if (mode == 0) {
    for (int x = 0; x < 7; x++) {
      tm.setLED(x, 0);
    }
  }


  if ((currentmillis - calculationmillis) > timeconstant) {
    CalculateLoad();
    CalculatePower();
    CalculateLoss();
    CalculateNoise();
    ProcessPID();
    WriteSerial();
    calculationmillis = currentmillis;
  }

  if ((currentmillis - lagdelaymillis) > (timeconstant / 100)) {
    CalculateLagDelay();

  }


}


//********** Modes **********//
void modeLoad () {
  int mymode = 1;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }

  sprintf(displayformat, "load %03d", load);
  tm.displayText(displayformat);
  if (decbutton == 7) {
    if (load < 100) {
      load ++;
    }
  } else if (decbutton == 8) {
    if (load > 0) {
      load --;
    }
  }

  if ((decbutton == mymode) & (modeflag == 1)) {
    modeflag = 0;
    mode = 0;  // Switch to PID Mode
    tm.setLED((mymode - 1), 0);

  }


}


void modeLag () {
  int mymode = 2;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }


  sprintf(displayformat, " lag %03d", lag);
  tm.displayText(displayformat);
  if (decbutton == 7) {
    if (lag < 100) {
      lag ++;
    }
  } else if (decbutton == 8) {
    if (lag > 0) {
      lag --;
    }
  }
  if ((decbutton == mymode) & (modeflag == 1)) {
    modeflag = 0;
    mode = 0;  // Switch to PID Mode
    tm.setLED((mymode - 1), 0);

  }
}



void modeLoss () {
  int mymode = 3;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }

  sprintf(displayformat, "loss %03d", loss);
  tm.displayText(displayformat);
  if (decbutton == 7) {
    if (loss < 100) {
      loss ++;
    }
  } else if (decbutton == 8) {
    if (loss > 0) {
      loss --;
    }
  }
  if ((decbutton == mymode) & (modeflag == 1)) {
    modeflag = 0;
    mode = 0;  // Switch to PID Mode
    tm.setLED((mymode - 1), 0);

  }
}


void modeNoise () {
  int mymode = 4;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }

  sprintf(displayformat, " nse %03d", pvnoise);
  tm.displayText(displayformat);
  if (decbutton == 7) {
    if (pvnoise < 100) {
      pvnoise ++;
    }
  } else if (decbutton == 8) {
    if (pvnoise > 0) {
      pvnoise --;
    }
  }
  if ((decbutton == mymode) & (modeflag == 1)) {
    modeflag = 0;
    mode = 0;  // Switch to PID Mode
    tm.setLED((mymode - 1), 0);

  }
}


void modeDelay () {
  int mymode = 5;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }

  sprintf(displayformat, " dly %03d", pvdelay);
  tm.displayText(displayformat);
  if (decbutton == 7) {
    if (pvdelay < 100) {
      pvdelay ++;
    }
  } else if (decbutton == 8) {
    if (pvdelay > 0) {
      pvdelay --;
    }
  }

  if ((decbutton == mymode) & (modeflag == 1)) {
    modeflag = 0;
    mode = 0;  // Switch to PID Mode
    tm.setLED((mymode - 1), 0);

  }
}

void modeTest () {
  int mymode = 6;
  tm.setLED((mymode - 1), 1);
  if (decbutton == 0) {
    modeflag = 1;

  }
  if (teststep == 0) {
    lockchannels = 1;
    processvariable = 0;
    sprintf(displayformat, "%03d  %03d", processvariable, controlvariable);
    tm.displayText(displayformat);
  }
  if (teststep == 1) {
    lockchannels = 1;
    processvariable = 100;
    sprintf(displayformat, "%03d  %03d", processvariable, controlvariable);
    tm.displayText(displayformat);
  }
  if (teststep == 2) {
    lockchannels = 0;

    sprintf(displayformat, "base %03d", timeunits);
    tm.displayText(displayformat);
    if (decbutton == 7) {
      if (timeunits < 999) {
        timeunits ++;
      }
    } else if (decbutton == 8) {
      if (timeunits > 0) {
        timeunits --;
      }
    }

  }

  if (teststep == 3) {
    lockchannels = 0;

    sprintf(displayformat, "reset %2d", resetproc);
    tm.displayText(displayformat);
    if (decbutton == 7) {
      if (resetproc < 1) {
        resetproc ++;
      }
    } else if (decbutton == 8) {
      if (resetproc > 0) {
        resetproc --;
      }
    }

  }


  if (teststep == 4) {
    if (resetproc == 1) {
      Serial.println("Resetting");
      delay(10);
      digitalWrite(resetPin, LOW); 
    } else {
      resetproc = 0;
    }

    teststep = 0;
    mode = 0;
    modeflag = 0;
    lockchannels = 0;
    tm.setLED((mymode - 1), 0);

  }


  if ((decbutton == mymode) & (modeflag == 1) & (teststep < 4)) {
    teststep ++;
  }


}


void modePID () {

  if ((currentmillis - displaymillis) > 500) {
    pvdisplay = processvariable;
    cvdisplay = controlvariable;
    sprintf(displayformat, "%03d  %03d", pvdisplay, cvdisplay);
    tm.displayText(displayformat);
    displaymillis = currentmillis;
  }
}

//********** Calculations **********//

void CalculateLoad() {

  if (actualload < load) {
    actualload ++;
  } else if (actualload > load) {
    actualload --;
  }
  bulkpv = bulkpv - ((actualload) / ((1000 / timeconstant) * timeunits)); // Max Load = 100/minute

  if (bulkpv > 149) {
    bulkpv = 149;
  } else if (bulkpv < 0) {
    bulkpv = 0;
  }

  if ((bulkpv < 149) and (bulkpv > 0)) {
    bulkpv = bulkpv;
  } else {
    bulkpv = 0;
  }

}



void CalculatePower() {

  // No Velocity  (powervelocity)

  actualpower = ((controlvariable) / ((1000.0 / timeconstant) * timeunits)); //Add 100 BTU Per Minute

  bulkpv = bulkpv + actualpower;
  if (bulkpv > 149) {
    bulkpv = 149;
  } else if (bulkpv < 0) {
    bulkpv = 0;
  }
}



void CalculateLoss() {
  bulkpv = bulkpv - ((loss) / ((1000 / timeconstant) * timeunits)); // Max Losso = 100/minute
}


void CalculateLagDelay() {

  for (int y = 100; y >= 0; y--) {
    cvdelayarray[y] = cvdelayarray[y - 1];
  }
  cvdelayarray[0] = controlvariable;
  cvafterdelay = cvdelayarray[lag];


  for (int x = 100; x >= 0; x--) {
    pvdelayarray[x] = pvdelayarray[x - 1];
  }
  pvdelayarray[0] = bulkpv;
  pvafterdelay = pvdelayarray[pvdelay];

}

void CalculateNoise() {
  pvafternoise = pvafterdelay + random(-pvnoise, pvnoise);
}

void ProcessPID() {
  if (lockchannels == 0) {
    if (pvdelay == 0) {
      processvariable = pvafternoise;
    } else if (pvdelay > 0) {
      processvariable = pvafternoise;
    }
    if (processvariable > 149) {
      processvariable = 149;
    } else if (processvariable < 0) {
      processvariable = 0;
    }
  }

  analogWrite(9, map(processvariable, 0, 150, 0, 255));
  controlvariable = map(analogRead(A0), 0, 1024, 0, 100);
}

void WriteSerial() {
  if (currentmillis - serialmillis > 2000) {
    Serial.print(F("Actual Load (from load): "));
    Serial.println(actualload);
    //Serial.print(F("Actual Power (from lag): "));
    //Serial.println(actualpower);
    //Serial.print(F("Power Velocity (from lag): "));
    //Serial.println(powervelocity);
    Serial.print(F("Process Variable: "));
    Serial.println(processvariable);
    Serial.print(F("Control Variable: "));
    Serial.println(controlvariable);
    Serial.print(F("BulkPV: "));
    Serial.println(bulkpv);
    Serial.print(F("Mode: "));
    Serial.println(mode);
    Serial.print(F("Loss: "));
    Serial.println(loss);
    Serial.print(F("Lag: "));
    Serial.println(lag);
    Serial.print(F("Noise: "));
    Serial.println(pvnoise);
    Serial.print(F("Delay: "));
    Serial.println(pvdelay);
    Serial.println(F("---------------------------------------------------"));
    serialmillis = currentmillis;

  }
}

For more information, check out the main page at https://bryceautomation.com!

— Ricky Bryce

For Customized automation training, visit my employer's website at http://atifortraining.com!

Leave a comment

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