/********************************************************
QuickPID simulated 1D heater Example
Reading simulated analog input 0 to control analog PWM output 3
********************************************************/
// This simulates a 5W heater block driven by the PID
// Vary the setpoint with the Pot, and watch the heater drive the temperature up
//
// Simulation at https://wokwi.com/projects/359818835102393345
//
// Based on
// PID_v1 sim at https://wokwi.com/projects/359088752027305985
// Wokwi https://wokwi.com/projects/357374218559137793
// Wokwi https://wokwi.com/projects/356437164264235009
//#include "PID_v1.h" // https://github.com/br3ttb/Arduino-PID-Library
#define HACK_QPID_PE_MULT 1
#include "QuickPID.h" // https://github.com/Dlloydev/Arduino-PID-Library
// For 1D heat transfer model:
#include <BasicLinearAlgebra.h> // https://github.com/tomstewart89/BasicLinearAlgebra
#include <sTune.h> // https://github.com/Dlloydev/sTune
//Define Variables we'll be connecting to
float Setpoint, Input, Output;
//Specify the links and initial tuning parameters
float Kp = 0.524, Ki = 0.05, Kd = 0.5; // подобранные коф. PID NoOvershoot_PID
//float Kp = 0.227, Ki = 0.045, Kd = 0.0; // подобранные коф. PID ZN_PID for ele_sensor=1
//float Kp = 23.202, Ki = 0.056, Kd = 0.222; // ZN_PID for ele_sensor=1
//double Kp = 20, Ki = .01, Kd = 10; // works reasonably with sim heater block fo 220deg
//double Kp = 25.5, Ki = 0.001, Kd = 0; // +/-10°proportional band
//float Kp = 255, Ki = 0.001, Kd = 0; // works reasonably with sim heater block
//double Kp = 255, Ki = 0.0, Kd = 0; // +/-1° proportional band works reasonably with sim heater block
//double Kp = 10000, Ki = 0.0, Kd = 0.0; // bang-bang
//double Kp = 2, Ki = 0.0, Kd = 0.0; // P-only
//double Kp = 2, Ki = 5, Kd = 1; // commonly used defaults
//PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, P_ON_E, DIRECT);
//Specify PID links
QuickPID myPID(&Input, &Output, &Setpoint);
// expose temperature vector to other routines
const int n_ele = 5; //
static BLA::Matrix<n_ele> x; //initial
const int ele_sensor = 1; //0 номер мат.модели нагревательного элемента (см.README) от 0 до 5 0..(n_ele-1)
const int PWM_PIN = 3; // UNO PWM pin for Output
const int FAN_PIN = 5; // UNO PWM pin for Fan Output
const int INPUT_PIN = -1; //A0; //-1; // Analog pin for Input (set <0 for simulation)
const int AUTOMATIC_PIN = 8; // Pin for controlling manual/auto mode, NO
const int OVERRIDE_PIN = 12; // Pin for integral override, NO
const int PLUS_PIN = 4; // Pin for integral override, NO
const int MINUS_PIN = 7; // Pin for integral override, NO
const int STUNE_PIN = 11; // Pin for initiating sTune
const int SHIFT_PIN = 2;
#include <LiquidCrystal_I2C.h>
#define I2C_ADDR 0x27
#define LCD_COLUMNS 20
#define LCD_LINES 4
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
enum MODES {MANUAL_MODE = 0, AUTO_MODE = 1, TUNE_MODE = 2} mode = MANUAL_MODE;
const char * modes[] = {"Manual", "Auto ", "Tuning"};
float incValue; // for editing params
uint16_t control_time = 50000U; // us on/off cycle for softpwm
float dispKp = 0; // These functions query the pid for interal values.
float dispKi = 0; // they were created mainly for the pid front-end,
float dispKd = 0;
//sTune tuner = sTune(&Input, &Output, tuner.ZN_PID, tuner.direct5T, tuner.printALL);
sTune tuner = sTune(&Input, &Output, tuner.NoOvershoot_PID, tuner.direct5T, tuner.printALL);
/* ZN_PID directIP serialOFF
DampedOsc_PID direct5T printALL
NoOvershoot_PID reverseIP printSUMMARY
CohenCoon_PID reverse5T printDEBUG
Mixed_PID
ZN_PI
DampedOsc_PI
NoOvershoot_PI
CohenCoon_PI
Mixed_PI
*/
// end of globals
void setup() {
Serial.begin(115200);
Serial.println(__FILE__);
//apply PID gains
myPID.SetTunings(Kp, Ki, Kd);
myPID.SetOutputLimits(-4, 255); // -4 for fan to increase cooling для вентилятора для увеличения охлаждения
pinMode(OVERRIDE_PIN, INPUT_PULLUP);
pinMode(AUTOMATIC_PIN, INPUT_PULLUP);
pinMode(MINUS_PIN, INPUT_PULLUP);
pinMode(PLUS_PIN, INPUT_PULLUP);
pinMode(SHIFT_PIN, INPUT_PULLUP);
pinMode(PWM_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(STUNE_PIN, INPUT_PULLUP);
Setpoint = 110; // уставка температуры
incValue = 1; // начальное приращение уставки температуры
//turn the PID on
if (digitalRead(AUTOMATIC_PIN) == HIGH) {
myPID.SetMode(myPID.Control::automatic);
mode = AUTO_MODE;
}
else {
myPID.SetMode(myPID.Control::manual);
mode = MANUAL_MODE;
}
myPID.SetMode(digitalRead(AUTOMATIC_PIN) == HIGH ? myPID.Control::automatic : myPID.Control::manual);
if (INPUT_PIN > 0) { // Actual pin or simulated heater?
Input = analogRead(INPUT_PIN);
}
else {
Input = simPlant1D(0.0, 1.0); // simulate heating
}
lcd.init();
lcd.backlight();
Serial.println("Setpoint Input Output Integral");
}
void loop() { // ------------------- основной цикл программы --------------------
// gather Input from INPUT_PIN or simulated block
float heaterWatts = Output * 5.0 / 255; // 5W heater
//float heaterWatts = (digitalRead(PWM_PIN)==HIGH) * 5.0 ; // 5W heater
if (INPUT_PIN > 0 ) { // Actual pin or simulated heater?
Input = analogRead(INPUT_PIN);
}
else {
float blockTemp = simPlant1D(heaterWatts, Output > 0 ? 1 : 1 - Output); // simulate heating
Input = blockTemp; // read input from simulated heater block
}
if (myPID.Compute() || myPID.GetMode() == (uint8_t)myPID.Control::automatic); {
//Output = (int)Output; // Recognize that the output as used is integer
analogWrite(PWM_PIN, Output >= 0 ? Output : 0); // Heater
analogWrite(FAN_PIN, Output < 0 ? -Output * 255 / 4.0 : 0); // Fan
}
// duty cycle or analogWrite above
//softPwm(PWM_PIN,Output >= 0 ? Output : 0,control_time);
doButtons();
static uint32_t lastButton = 0;
if (myPID.GetMode() == (uint8_t)myPID.Control::manual && millis() - lastButton > 250) {
if (digitalRead(STUNE_PIN) == LOW) { // initiate sTune
mode = 2; // stune
lcd.clear();
lcd.setCursor(0, 2); lcd.print("sTune: see serial");
delay(1000);
sTuneRunner();
mode = MANUAL_MODE;
}
}
report();
reportLCD();
} // ------------------- конец основного цикла программы --------------------
// -------------------------- подпрограммы ----------------------------------
// ------------------------- управление выводами в режиме PWM ---------------
int softPwm(byte pin, float val, uint32_t control_time_us) {
static uint32_t last = 0;
uint32_t now = micros();
if (now - last >= control_time_us) {
if (val > 0)digitalWrite(pin, HIGH);
last = now;
}
if (now - last >= (val * control_time_us) / 255) {
digitalWrite(pin, LOW);
}
return now - last < (val * control_time_us) / 255;
} // ---------------------- конец управления выводами в режиме PWM -------------
void doButtons(void) { // ------------------------ работа с кнопками ----------
uint32_t now = millis();
static uint32_t shiftMs = 0, minusMs = 0, plusMs = 0;
static byte lastShift = HIGH, lastMinus = HIGH, lastPlus = HIGH;
byte shift = digitalRead(SHIFT_PIN);
byte minus = digitalRead(MINUS_PIN);
byte plus = digitalRead(PLUS_PIN);
if (digitalRead(OVERRIDE_PIN) == LOW) mySetIntegral(&myPID, 0); // integral override
if ((digitalRead(AUTOMATIC_PIN) == HIGH != mode == AUTO_MODE) && mode != TUNE_MODE) {
switch (digitalRead(AUTOMATIC_PIN)) {
case HIGH:
mode = AUTO_MODE;
myPID.SetMode(myPID.Control::automatic);
break;
case LOW:
mode = MANUAL_MODE;
myPID.SetMode(myPID.Control::manual);
break;
}
}
if (plus == LOW && now - plusMs > 250) {
if (mode == MANUAL_MODE) {
Output += incValue;
} else if (mode == AUTO_MODE) {
Setpoint += incValue;
}
plusMs = now;
}
if (minus == LOW && now - minusMs > 250) {
if (mode == MANUAL_MODE) {
Output -= incValue;
} else if (mode == AUTO_MODE) {
Setpoint -= incValue;
}
minusMs = now;
}
if (shift == LOW && now - shiftMs > 250) {
incValue *= 10;
if (incValue > 100) {
incValue = 0.01;
}
shiftMs = now;
}
} //--------------------------------------- конец работы с кнопками ------------
void report(void) { // ---------------------------- вывод в COM Port -----------
static uint32_t last = 0;
const int interval = 500; // 250
if (millis() - last > interval) {
last += interval;
// Serial.print(millis()/1000.0);
Serial.print("SP:"); Serial.print(Setpoint);
Serial.print(" PV:"); Serial.print(Input);
Serial.print(" CV:"); Serial.print(Output);
Serial.print(" Int:"); Serial.print(myPID.outputSum);
Serial.print(' ');
for (int i = 0; i < n_ele; ++i) {
Serial.print("x_i"); Serial.print(i); Serial.print(':'); Serial.print(x(i), 3); Serial.print(' ');
}
Serial.println();
}
} // ---------------------------- конец вывода в COM Port ----------------------
void reportLCD(void) { // --------------- вывод на экран ------------------------
static uint32_t last = 0;
const int interval = 250;
if (millis() - last > interval) {
last += interval;
// Serial.print(millis()/1000.0);
// lcd.clear();
dispKp = myPID.GetKp();
dispKi = myPID.GetKi();
dispKd = myPID.GetKd();
lcd.setCursor(0, 0); lcd.print("PV :"); lcd.print(Input, 2); lcd.print(" ");
lcd.setCursor(11, 0); lcd.print(modes[mode]); lcd.print(" ");
lcd.setCursor(0, 1); lcd.print("SP :"); lcd.print(Setpoint, 2); lcd.print(" ");
lcd.setCursor(11, 1); lcd.print("+"); lcd.print(incValue, incValue >= 1 ? 0 : incValue >= 0.1 ? 1 : 2);
lcd.print(" ");
lcd.setCursor(0, 2); lcd.print("Int:"); lcd.print(myPID.outputSum, 2); lcd.print(" ");
lcd.setCursor(11, 2); lcd.print("CV:"); lcd.print(Output, 2); lcd.print(" ");
lcd.setCursor(0, 3); lcd.print("P:"); lcd.print(dispKp, 1); lcd.print(" ");
lcd.setCursor(7, 3); lcd.print("I:"); lcd.print(dispKi, 1); lcd.print(" ");
lcd.setCursor(14, 3); lcd.print("D:"); lcd.print(dispKd, 1); lcd.print(" ");
}
} // // --------------- конец вывода на экран ----------------------------------
// -------------------- мат модель нагревательного блока ----------------------
// моделируем алюминиевый блок размером 1x1x2 см с нагревателем и пассивным охлаждением окружающей среды
// C = 237 - коэффициент теплопроводности Вт/мК для Al алюминия
// Использование 1D-модели теплодиффузионного стержня
float simPlant1D(float Q, float hfactor) { // heat input in W (or J/s)
// simulate a 1x1x2cm aluminum block with a heater and passive ambient cooling
// C = 237 - W/mK thermal conduction coefficient for Al
// Using a 1D heat diffusion rod model
// Still 0D See https://wokwi.com/projects/359193529297155073 for
// what needs to go in here
using namespace BLA;
const int n_force = 2;
// setup for three-element 1D heat transfer model
// expose temperature vector to other routines
// move these to global if you want other routines to see them:
// const int n_ele = 5;
// static BLA::Matrix<n_ele> x; // initial
static BLA::Matrix<n_force> u; // Forcings at ends Watts
static BLA::Matrix<n_ele, 2> G; // map from forcings to elements, dT=f(W)
static BLA::Matrix<n_ele, n_ele> Fa ; // transition matrix
float h = 5; // W/m2K thermal convection coefficient for Al passive
float Cps = 0.89; // J/g°C
float C_tcond = 237; // W/mK коэффициент теплопроводности для Al (алюминий)
float Tamb = 100; // начальная температура °C
float rho = 2.710e6; // kg/m^3 density Al: 2,710kg/m3 плотность алюминия
float rod_dia = 0.02; // диаметр блока m
float rod_len = 0.10; // длина блока m
float area = pow(rod_dia, 2) * PI / 4 + 0 * rod_len / 3 * PI * rod_dia; // площадь рассеивания блока m2 area for convection
float mass = rod_len * pow(rod_dia, 2) * PI / 4 * rho ; // вес блока g
static float T = Tamb; // °C
static uint32_t last = 0;
uint32_t interval = 10; // ms
float dt = interval / 1000.0;
static bool oneShot = true;
if (millis() - last >= interval) {
last += interval;
if (oneShot == true) {
oneShot = false;
for (int i = 0; i < n_ele; ++i) {
x(i) = Tamb;
if (i > 0) { // conduction to the left
Fa(i, i - 1) = C_tcond * pow(rod_dia, 2) * PI / 4 / (rod_len / n_ele) / (mass / n_ele);
Fa(i, i) -= Fa(i, i - 1);
}
if (i < n_ele - 1) { // conduction to the right
Fa(i, i + 1) = C_tcond * pow(rod_dia, 2) * PI / 4 / (rod_len / n_ele) / (mass / n_ele);
Fa(i, i) -= Fa(i, i + 1);
}
//Fa = Fa * 1.0; // *20 extra conductive сверхпроводящий
G(0, 0) = Cps;
G(n_ele - 1, 1) = Cps;
u(0) = 0;
u(n_force - 1) = 0;
//Serial << "Fa:" << Fa << '\n';
//Serial << "G:" << G << '\n';
Serial.print("Oneshot");
}
} // end oneShot
// 0-dimensional heat transfer
//T = T + Q * interval / 1000 / mass / Cps - (T - Tamb) * area * h;
// 1-D explicit:
u(0) = ((Tamb - x(0)) * area * h * hfactor) + Q;
u(1) = (Tamb - x(2)) * area * h * hfactor;
x += (Fa * x + G * u) * dt ;
//T = x(n_ele - 1); // end element (most lag) Harder to control
T = x(ele_sensor); // element with heater (least lag)
}
return T;
} // -------------------- конец мат модели нагревательного блока ----------------------
void mySetIntegral(QuickPID * ptrPID, float value ) {
// per
uint8_t saveMode = ptrPID->GetMode();
ptrPID->SetMode(myPID.Control::manual);
Output = value;
ptrPID->SetMode(saveMode);
}
// ---------------------- подбор коэф. PID регулятора ---------------------------------
void sTuneRunner() { // блокирующая функция
float inputSpan = 255, outputSpan = 255;
float outputStart = Output;
float outputStep = Output + 2; //+2
float testTimeSec = 300;
float settleTimeSec = 10;
float samples = 300;
float tempLimit = 255;
tuner.Configure(inputSpan, outputSpan, outputStart, outputStep, testTimeSec, settleTimeSec, samples);
tuner.SetEmergencyStop(tempLimit);
int armed = true;
while (armed) {
float outputSpan = 255;
int relayPin = PWM_PIN;
float heaterWatts = Output * 5.0 / 255; // 5W heater
Input = simPlant1D(heaterWatts, Output > 0 ? 1 : 1 - Output); // simulate heating
// float sTune::softPwm(const uint8_t relayPin, float input, float output, float setpoint, uint32_t windowSize, uint8_t debounce)
tuner.softPwm(relayPin, Input, Output, 0, outputSpan, 1);
int junk = tuner.Run();
switch (junk) { // does the step
case tuner.sample: // active once per sample during test
// Serial.print(".");
// void sTune::plotter(float input, float output, float setpoint, float outputScale, uint8_t everyNth) {
//tuner.plotter(Input, Output, Setpoint, 1, 3); // ... outScale, every nth
reportLCD();
//report();
break;
case tuner.tunings: // active just once when sTune is done
Serial.println("sTune Tunings");
// Update values from sTun results
tuner.GetAutoTunings(&Kp, &Ki, &Kd); // sketch variables updated by sTune
myPID.SetTunings(Kp, Ki, Kd); // update PID with the new tunings
//lcd.setCursor(0, 2); lcd.print("Kp:"); lcd.print(Kp, 2); lcd.print(" Ki:"); lcd.print(Ki, 3);
lcd.setCursor(0, 3); lcd.print("P:"); lcd.print(Kp, 1); lcd.print(" ");
lcd.setCursor(7, 3); lcd.print("I:"); lcd.print(Ki, 1); lcd.print(" ");
lcd.setCursor(14, 3); lcd.print("D:"); lcd.print(Kd, 1); lcd.print(" ");
delay(2000);
Output = 0; // turn off
tuner.SetTuningMethod(tuner.TuningMethod::DampedOsc_PID);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::NoOvershoot_PID);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::CohenCoon_PID);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::Mixed_PID);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::ZN_PI);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::DampedOsc_PI);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::NoOvershoot_PI);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::CohenCoon_PI);
tuner.printTunings();
tuner.SetTuningMethod(tuner.TuningMethod::Mixed_PI);
tuner.printTunings();
// tuner.GetAutoTunings(&Kp, &Ki, &Kd); // sketch variables updated by sTune
// myPID.SetOutputLimits(0, outputSpan);
// myPID.SetSampleTimeUs(outputSpan * 1000 - 1);
// myPID.SetMode(myPID.Control::automatic); // the PID is turned on
// myPID.SetProportionalMode(myPID.pMode::pOnMeas);
// myPID.SetAntiWindupMode(myPID.iAwMode::iAwClamp);
// myPID.SetTunings(Kp, Ki, Kd); // update PID with the new tunings
armed = false;
break;
case tuner.runPid: // active once per sample after tunings
Serial.println("sTune Run");
armed = false; // end function
break;
default:
// Serial.print("default tuner.???");
;
}
}
} // ---------------------- конец подбора коэф. PID регулятора ---------------------------------
/*
float rnorm(float mean, float stdev) {
// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
static byte state = 0;
static float F1,F2;
switch (state) {
case 0:
F1 = sqrt(-2 * log(random()));
F2 = 2 * PI * random();
state = 1;
return mean + stdev * F1 * cos(F2);
break;
case 1:
return mean + stdev * F1 * sin(F2);
state = 0;
break;
}
}
*/PID_v1 with Heater Simulation,
with integrator examination and override
Manual/0/Auto
Integral
Clear
sTune
PWM
HEATER
PWM
FAN
Auto/(Manual: Minus Plus Shift)