Way to test control loop without using it in real life

I am trying to use a (discrete) I-PD controller to regulate the temperature of a pan I will be using to homebrew. However I would like to test my controller calculations and code, without having to fire up my heater and finding out 5 times in a row that I did something wrong. Is there a simulation I can use to test my code?

Code for reference

#include <Arduino.h>

#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 2
#define RELAY 3
#define PERIODMS 2000
#define PWMVALUE 50

#define RESET 0

// Start value = 20 degrees
// Endvalue = 93.5 degrees
// DeltaTemp (DT) = 93.5 - 20 = 73.5
// T1 = time at .735*DT = (0.283*73.5) = time at DT == 20+20.80 => T = 20+20.80 = 40.80
// T1 = 691.690550
// T2 = time at (0.632*73.5) = tijd time at DT == 20+46.45 => T = 20+46.45=66.45
// T2 = 1426.1424958
// Tsys = 1.5(T2-T1) = 1101.6 s
// Theta = T2-Tsys = 324.5 s
// Ksys = 93.5/50 = 1.87 degC/%
#define Tsys    1101.6
#define Theta   324.5
#define Ksys    1.87

// Cohen-coon
#define Kc ((Tsys/(Ksys*Theta))*((Theta/(4.0*Tsys)) +(4.0/3.0)))
#define Ti (Theta*(32.0*Tsys+6.0*Theta)/(13.0*Tsys+8.0*Theta))
#define Td (Theta*(4.0*Tsys)/(2*Theta+11*Tsys))

#define Ts (0.1875*Theta)

// #define q0 (Kc * ( 1 + (Ts/(2 * Ti)) + (Td/Ts)))
// #define q1 (Kc * (-1 + (Ts/(2 * Ti)) - (2*Td/Ts)))
// #define q2 (Kc * (Td / Ts))

OneWire oneWire(ONE_WIRE_BUS);

DallasTemperature sensors(&oneWire);

uint64_t startTime;
uint16_t flipTime;
uint8_t setPoint;

void setPWM(uint8_t pwm) { // in %
if (pwm > 100) {
pwm = 100;
}
flipTime = PERIODMS*pwm/100;
}

void setSetpoint(uint8_t setpoint) {
if (setpoint > 95) {
setpoint = 95;
}
setPoint = setpoint;
}

float getSensorTemp(bool report) {
static float currentTemp = 20, lastTemp = 20;
sensors.requestTemperatures();
currentTemp = sensors.getTempCByIndex(0);
if(currentTemp == -127)
currentTemp = lastTemp;
if(currentTemp != lastTemp){
lastTemp = currentTemp;
uint32_t secondsPassed = (millis() - startTime)/1000;
Serial.print(secondsPassed);
Serial.print('.');
Serial.print(uint32_t(millis()-startTime-secondsPassed));
Serial.print("; ");
Serial.println(currentTemp);
}
return currentTemp;
}

uint16_t control(float e0, float y0) {
// u0 = output(t-0), u1 = output(t-1)
// e = error, y = process output (degrees)
static int32_t u0, u1;
static float e1, e2, y1, y2;

u0 = u1 + Kc * (-y0 + y1 + (Ts/Ti) * e0 + (Td/Ts) * (-y0 + 2 * y1 - y2));
if(u0 > 100) {
u0 = 100;
} else if(u0 < 0) {
u0 = 0;
}

u1 = u0;
e2 = e1;
e1 = e0;

return (uint16_t)u0;
}

void setup(void) {
pinMode(RELAY, OUTPUT);
digitalWrite(RELAY, LOW);
while(RESET);

Serial.begin(9600);
sensors.setWaitForConversion(0);
sensors.begin();
delay(1000);

// Start heating
digitalWrite(RELAY, HIGH);
startTime = millis();

setSetpoint(80);
setPWM(100);
}

void loop(void) {
float currentTemp;
static uint64_t lastPeriod = 0, lastControl = 0;

do {
currentTemp = getSensorTemp(true);

// Control function if passed sampletime
if(millis() >= lastControl + Ts) {
control(setPoint - currentTemp, currentTemp);
lastControl = millis();
}

// PWM Calculations
if((millis() >= lastPeriod + flipTime) && pinState) {
digitalWrite(RELAY, LOW);
pinState = LOW;
}
if(millis() >= lastPeriod + PERIODMS) {
lastPeriod = millis();
digitalWrite(RELAY, HIGH);
pinState = HIGH;
}

} while (currentTemp < 95);
digitalWrite(RELAY, LOW);
while(1)
getSensorTemp(true);
}
$$`$$
• You can write your own! Most of this code will run on your computer just as well. But you'll need to change the parts that talk to the sensors and the pins. And make fake time - put millis++ in each loop or something. Or copy your algorithm over to your favourite other computer language like Python Sep 21 '20 at 18:40
• Realistically you're going to spend more time trying to accurately simulate the system than it would take to just make the real one safe to test. One of the things you'll probably want to do is get good temperature logging going ultimately to a graph, so you can tell how it is performing; then do things like add a bunch of cold water to the pot (or whatever is being heated) Sep 21 '20 at 19:04
• connect a lightbulb instead of the heater Sep 21 '20 at 19:27
• @jsotola great idea, thanks! Although the time constant of a lightbulb is too short for my sensor (200ms) reducing the time constant from 20 minutes down to a minute really solves my problem. I’d mark this as the solution Sep 21 '20 at 19:31