I wrote an example PID simulation for the PID_RT library. In the sketch code, it simulates a heater similar to the extruder block on a 3D printer so you can experiment with a PID with standardized, (simulated) hardware and see how it responds.
I'm not discussing how to tune the parameters, since that depends greatly on your system and is well covered elsewhere in places specific to your system.
I'm trying to demonstrate that:
- The default
kP=2, kI=5, kD=1
PID parameters do respond to a standard test case (a simulated 3d printer heater block). - That the Arduino can run code to simulate its own external system so you can do software development before the hardware is complete.
Since the code is in Rob Tillart's PID_RT repository as an example, it will show up in your Arduino IDE as File/Examples/PID_RT/PID_simulated_heater after you've installed the PID_RT library.
The code is in github and is copied here:
//
// FILE: PID_simulated_heater.ino
// AUTHOR: drf5n (based on basic example)
// PURPOSE: demo
//
// This simulates a 20W heater block driven by the PID
// Wokwi https://wokwi.com/projects/356437164264235009
//
// https://github.com/RobTillaart/PID_RT/issues/5
#include "PID_RT.h"
PID_RT PID;
const int PWM_PIN = 3; // UNO PWM pin
int op = 0;
float input = 0;
void setup()
{
Serial.begin(115200);
Serial.println(__FILE__);
PID.setPoint(125);
PID.setOutputRange(0, 255); // PWM range
PID.setInterval(50);
PID.setK(2, 5, 1);
PID.start();
op = analogRead(A0);
}
void loop()
{
float heaterWatts = ((int)op)*20.0/255; // 20W heater
float blockTemp = simPlant(heaterWatts); // simulate heating
input = blockTemp;
// input = analogRead(A0);
if (PID.compute(input))
{
op = PID.getOutput();
analogWrite(PWM_PIN, op);
// Serial.print(PID.getInput());
// Serial.print('\t');
// Serial.println(op);
}
report();
}
void report(void){
static uint32_t last = 0;
const int interval = 1000;
if (millis() - last > interval){
last += interval;
// Serial.print(millis()/1000.0);
Serial.print(PID.getSetPoint());
Serial.print(' ');
Serial.print(input);
Serial.print(' ');
Serial.println(op);
}
}
float simPlant(float Q){ // heat input in W (or J/s)
// simulate a 1x1x2cm aluminum block with a heater and passive ambient cooling
// next line "C is not used", fails in arduino-build-ci for ESP32
// float C = 237; // W/mK thermal conduction coefficient for Al
float h = 5; // W/m2K thermal convection coefficient for Al passive
float Cps = 0.89; // J/g°C
float area = 1e-4; // m2 area for convection
float mass = 10; // g
float Tamb = 25; // °C
static float T = Tamb; // °C
static uint32_t last = 0; // last call
uint32_t interval = 100; // milliseconds
if(millis() - last >= interval){
last += interval;
T = T + Q * interval / 1000 / mass / Cps - (T - Tamb) * area * h;
}
return T;
}
// -- END OF FILE --
The difference between other PID example is the code that simulates the process that the PID is controlling. The code that simulates the heater block is these lines and this function:
void loop()
{
...
float heaterWatts = ((int)op)*20.0/255; // 20W heater
float blockTemp = simPlant(heaterWatts); // simulate heating
...
}
...
float simPlant(float Q){ // heat input in W (or J/s)
// simulate a 1x1x2cm aluminum block with a heater and passive ambient cooling
// next line "C is not used", fails in arduino-build-ci for ESP32
// float C = 237; // W/mK thermal conduction coefficient for Al
float h = 5; // W/m2K thermal convection coefficient for Al passive
float Cps = 0.89; // J/g°C
float area = 1e-4; // m2 area for convection
float mass = 10; // g
float Tamb = 25; // °C
static float T = Tamb; // °C
static uint32_t last = 0; // last call
uint32_t interval = 100; // milliseconds
if(millis() - last >= interval){
last += interval;
T = T + Q * interval / 1000 / mass / Cps - (T - Tamb) * area * h;
}
return T;
}
...which takes the 0-255 output of the PID on PWM pin 3, converts it into the watts of heating that a MOSFET driver and cartridge heater would produce. It then takes those watts as input to a radiant heat transfer model to transform it into the temperature of the block, which the PID can take as its input.
There's a Wokwi simulation of it here:
...and an expansion on it here to give a user-adjustable setpoint:
// FILE: PID_simulated_heater_pot.ino
// AUTHOR: drf5n (based on basic example)
// PURPOSE: demo
//
// This simulates a 20W heater block driven by the PID
// Vary the setpoint with the Pot, and watch the heater drive the temperature up
//
// Code at https://wokwi.com/projects/357374218559137793
//
// Based on
// Wokwi https://wokwi.com/projects/356437164264235009
//
// https://github.com/RobTillaart/PID_RT/issues/5
//
#include "PID_RT.h" // https://github.com/RobTillaart/PID_RT
PID_RT PID; // https://github.com/RobTillaart/PID_RT
const int PWM_PIN = 3; // UNO PWM pin
const int INPUT_PIN = -1; // Analog pin for Input (set <0 for simulation)
const int SETPOINT_PIN = A1; // Analog pin for Setpoint Potentiometer
const int SETPOINT_INDICATOR = 6; // PWM pin for indicating setpoint
const int INPUT_INDICATOR = 5; // PWM pin for indicating Input
int op = 0;
float input = 0;
void setup()
{
Serial.begin(115200);
Serial.println(__FILE__);
if (SETPOINT_INDICATOR >= 0) pinMode(SETPOINT_INDICATOR, OUTPUT);
if (INPUT_INDICATOR >= 0) pinMode(INPUT_INDICATOR, OUTPUT);
PID.setPoint(125);
PID.setOutputRange(0, 255); // PWM range
PID.setInterval(50);
PID.setK(2, 5, 1);
PID.start();
Serial.println("Setpoint Input Output");
op = analogRead(A0);
}
void loop()
{
float heaterWatts = ((int)op) * 20.0 / 255; // 20W heater
if (INPUT_PIN > 0 ) {
input = analogRead(INPUT_PIN);
} else {
float blockTemp = simPlant(heaterWatts); // simulate heating
input = blockTemp; // read input from simulated heater block
}
if (PID.compute(input))
{
op = PID.getOutput();
analogWrite(PWM_PIN, op);
// Serial.print(PID.getInput());
// Serial.print('\t');
// Serial.println(op);
PID.setPoint(analogRead(SETPOINT_PIN) / 4); // Read setpoint from potentiometer
if (INPUT_INDICATOR >= 0) analogWrite(INPUT_INDICATOR, input);
if (SETPOINT_INDICATOR >= 0) analogWrite(SETPOINT_INDICATOR, PID.getSetPoint());
}
report();
}
void report(void) {
static uint32_t last = 0;
const int interval = 1000;
if (millis() - last > interval) {
last += interval;
// Serial.print(millis()/1000.0);
Serial.print(PID.getSetPoint());
Serial.print(' ');
Serial.print(input);
Serial.print(' ');
Serial.println(op);
}
}
float simPlant(float Q) { // heat input in W (or J/s)
// simulate a 1x1x2cm aluminum block with a heater and passive ambient cooling
float C = 237; // W/mK thermal conduction coefficient for Al
float h = 5 ; // W/m2K thermal convection coefficient for Al passive
float Cps = 0.89; // J/g°C
float area = 1e-4; // m2 area for convection
float mass = 10 ; // g
float Tamb = 25; // °C
static float T = Tamb; // °C
static uint32_t last = 0;
uint32_t interval = 100; // ms
if (millis() - last >= interval) {
last += interval;
T = T + Q * interval / 1000 / mass / Cps - (T - Tamb) * area * h;
}
return T;
}
// -- END OF FILE --
In the Wokwi simulation, you can move the slide potentiometer up and down to adjust the SETPOINT of the PID controller. The graph icon in the lower right toggles between serial monitor or serial plotter mode, and you can see the SETPOINT, INPUT and OUTPUT of the PID as traces or as a table of values.
You could edit this line of code to adjust the PID parameters kP, kI, and kD to change the behavior.
void setup()
{
...
PID.setK(2, 5, 1);
...
}
The units of the parameters are:
- kP: PWM_output_counts/°Cerror
- kI: PWM_output_counts/(°Cerror *sec)
- kI: PWM_output_counts/(°Cerror / sec)
Where the PID controller uses these parameters to compute a 0-255 PWM setting to drive the (simulated) 20W cartridge heater.
The (2,5,1) setting is a very common set of PID parameters due to historical reasons: It is given in all sorts of examples. The Kp, Ki, and Kd parameters really should be customized to the system you are trying to control with a PID. The tuning, and even selection of the PID algorithm, is very dependent on the physical process you are trying to control with the PID.
A heater block is very different than a quadcopter, levitating magnet, or cruise control, but PID can help control any of them.