Implement PID thremostat controller
For now only the proportional (Kp) and derivative (Kd) components are used, the integral term is 0.
This commit is contained in:
parent
678f16111b
commit
8502ee3c5c
@ -40,6 +40,9 @@ test: ntc.test overheat.test led.test
|
||||
%.test: test/test_%.o test/compat.o %.o
|
||||
gcc $^ -o $@ $(LIBS)
|
||||
|
||||
thermostat.test: test/test_thermostat.o test/compat.o thermostat.o overheat.o heater.o
|
||||
gcc $^ -o $@ $(LIBS)
|
||||
|
||||
clean:
|
||||
rm -f $(TESTS) test/*.o *.o
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ enum logic {
|
||||
};
|
||||
|
||||
#define DEBOUNCE_TIME 20L /* ms */
|
||||
#define TEMP_HOT 70.0f /* C */
|
||||
#define TEMP_HOT 75.0f /* C */
|
||||
|
||||
#define LED_MIN_VALUE 0
|
||||
|
||||
@ -198,7 +198,7 @@ unsigned long brewing_max_time = 30000UL; /* 30 seconds */
|
||||
unsigned long cooling_time = 3000UL; /* 3 seconds */
|
||||
unsigned long overheat_time = 10000UL; /* 10 seconds */
|
||||
unsigned long max_heating_time = 60000UL; /* 60 seconds */
|
||||
unsigned long max_idle_time = 120000UL; /* 120 seconds */
|
||||
unsigned long max_idle_time = 300000UL; /* 300 seconds */
|
||||
|
||||
void proc_machine(struct state *st)
|
||||
{
|
||||
@ -209,15 +209,6 @@ void proc_machine(struct state *st)
|
||||
int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
|
||||
unsigned long t = millis();
|
||||
|
||||
if (t - last_print_t > 100) {
|
||||
Serial.print(t);
|
||||
Serial.print(" ");
|
||||
Serial.print(st->mstate);
|
||||
Serial.print(" ");
|
||||
Serial.println(temp);
|
||||
last_print_t = t;
|
||||
}
|
||||
|
||||
/* If the machine is overheating */
|
||||
if (overheat_panic(&st->overheat)) {
|
||||
st->mstate = PANIC_OVERHEAT;
|
||||
@ -352,43 +343,55 @@ output_leds(const struct state *st)
|
||||
}
|
||||
|
||||
void
|
||||
output_heater(const struct state *st)
|
||||
output_relays(const struct state *st)
|
||||
{
|
||||
unsigned long t = millis();
|
||||
|
||||
/* First configure thermostate */
|
||||
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT)
|
||||
thermostat_set(&st->thermostat, TEMP_HOT);
|
||||
thermostat_set(&st->thermostat, TEMP_HOT+5.0);
|
||||
else
|
||||
thermostat_off(&st->thermostat);
|
||||
|
||||
/* Then update heater state from thermostate */
|
||||
if (thermostat_state(&st->thermostat, st->ntc_T))
|
||||
heater_on(&st->heater, t);
|
||||
else
|
||||
heater_off(&st->heater);
|
||||
float T = st->ntc_T;
|
||||
float dT_dt = overheat_speed(&st->overheat);
|
||||
float u = thermostat_state(&st->thermostat, T, dT_dt);
|
||||
heater_on(&st->heater, t, u);
|
||||
|
||||
/* Then switch relays accordingly */
|
||||
if (heater_state(&st->heater, t))
|
||||
int heater_relay_st = heater_state(&st->heater, t);
|
||||
if (heater_relay_st)
|
||||
relay(PIN_HEAT, ON);
|
||||
else
|
||||
relay(PIN_HEAT, OFF);
|
||||
}
|
||||
|
||||
void
|
||||
output_pump(const struct state *st)
|
||||
{
|
||||
int pump_relay_st = 0;
|
||||
if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD)
|
||||
pump_relay_st = 1;
|
||||
|
||||
if (pump_relay_st)
|
||||
relay(PIN_PUMP, ON);
|
||||
else
|
||||
relay(PIN_PUMP, OFF);
|
||||
|
||||
Serial.print(t);
|
||||
Serial.print(" ");
|
||||
Serial.print(st->mstate);
|
||||
Serial.print(" ");
|
||||
Serial.print(T);
|
||||
Serial.print(" ");
|
||||
Serial.print(u);
|
||||
Serial.print(" ");
|
||||
Serial.print(heater_relay_st);
|
||||
Serial.print(" ");
|
||||
Serial.println(pump_relay_st);
|
||||
}
|
||||
|
||||
void do_output(const struct state *st)
|
||||
{
|
||||
output_leds(st);
|
||||
output_heater(st);
|
||||
output_pump(st);
|
||||
output_relays(st);
|
||||
}
|
||||
|
||||
void setup()
|
||||
|
||||
@ -3,25 +3,34 @@
|
||||
|
||||
#include "heater.h"
|
||||
|
||||
#define HEATER_T_ON 500UL /* ms */
|
||||
#define HEATER_T_OFF 5000UL /* ms */
|
||||
#define HEATER_MIN 500UL /* ms */
|
||||
#define HEATER_MAX 2000UL /* ms */
|
||||
#define HEATER_PERIOD 5000UL /* ms */
|
||||
|
||||
void
|
||||
heater_on(struct heater *h, unsigned long t_ms)
|
||||
heater_on(struct heater *h, unsigned long t_ms, float duty)
|
||||
{
|
||||
if (h->st == HEATER_ON)
|
||||
return;
|
||||
unsigned long dt_on = duty * HEATER_MAX;
|
||||
|
||||
h->next_t = t_ms + HEATER_T_ON;
|
||||
if (dt_on < HEATER_MIN)
|
||||
dt_on = 0;
|
||||
else if (dt_on > HEATER_MAX)
|
||||
dt_on = HEATER_MAX;
|
||||
|
||||
h->next_on_dt = dt_on;
|
||||
|
||||
if (h->st == HEATER_OFF) {
|
||||
h->st = HEATER_ON;
|
||||
h->turn_on = 1;
|
||||
h->t0_on = t_ms;
|
||||
h->t0_off = t_ms + h->next_on_dt;
|
||||
h->cycle = CYCLE_ON;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
heater_off(struct heater *h)
|
||||
{
|
||||
h->st = HEATER_OFF;
|
||||
h->turn_on = 0;
|
||||
}
|
||||
|
||||
int
|
||||
@ -33,15 +42,29 @@ heater_state(struct heater *h, unsigned long t_ms)
|
||||
/* Switch state if current time exceeds time limit
|
||||
* in the current state */
|
||||
/* FIXME: Integer overflow can cause the heater to turn on forever */
|
||||
if (t_ms > h->next_t) {
|
||||
if (h->turn_on) {
|
||||
h->turn_on = 0;
|
||||
h->next_t = t_ms + HEATER_T_OFF;
|
||||
} else {
|
||||
h->turn_on = 1;
|
||||
h->next_t = t_ms + HEATER_T_ON;
|
||||
while (1) {
|
||||
int changed = 0;
|
||||
if (h->cycle == CYCLE_ON) {
|
||||
if (t_ms >= h->t0_off) {
|
||||
h->cycle = CYCLE_OFF;
|
||||
h->t0_on += HEATER_PERIOD;
|
||||
changed = 1;
|
||||
}
|
||||
} else if (h->cycle == CYCLE_OFF) {
|
||||
if (t_ms >= h->t0_on) {
|
||||
/* Compute current cycle t0_off */
|
||||
h->cycle = CYCLE_ON;
|
||||
h->t0_off = h->t0_on + h->next_on_dt;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return h->turn_on;
|
||||
if (!changed)
|
||||
break;
|
||||
}
|
||||
|
||||
if (h->cycle == CYCLE_ON)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -13,13 +13,22 @@ enum heater_state {
|
||||
HEATER_ON,
|
||||
};
|
||||
|
||||
struct heater {
|
||||
enum heater_state st;
|
||||
int turn_on;
|
||||
unsigned long next_t; /* in ms */
|
||||
enum heater_cycle {
|
||||
CYCLE_ON = 0,
|
||||
CYCLE_OFF,
|
||||
};
|
||||
|
||||
void heater_on(struct heater *h, unsigned long t_ms);
|
||||
struct heater {
|
||||
enum heater_state st;
|
||||
enum heater_cycle cycle;
|
||||
unsigned long t0_on; /* current cycle time on */
|
||||
unsigned long t0_off; /* current cycle time off */
|
||||
|
||||
/* Next cycle */
|
||||
unsigned long next_on_dt; /* in ms */
|
||||
};
|
||||
|
||||
void heater_on(struct heater *h, unsigned long t_ms, float duty);
|
||||
void heater_off(struct heater *h);
|
||||
int heater_state(struct heater *h, unsigned long t_ms);
|
||||
|
||||
|
||||
@ -18,22 +18,25 @@ overheat_input(struct overheat *o, unsigned long t_ms, float temp)
|
||||
if (delta < OVH_INTERVAL)
|
||||
return;
|
||||
|
||||
o->temp[o->next++] = temp;
|
||||
/* If already go n samples, recompute delta and speed */
|
||||
if (o->n == OVH_NSAMPLES) {
|
||||
float last_T = o->temp[o->next];
|
||||
float last_t = o->t[o->next];
|
||||
float dt = (float) (t_ms - last_t) * 1e-3;
|
||||
o->delta = temp - last_T;
|
||||
o->speed = o->delta / dt;
|
||||
}
|
||||
|
||||
/* Add the new sample */
|
||||
o->temp[o->next] = temp;
|
||||
o->t[o->next] = t_ms;
|
||||
o->next++;
|
||||
if (o->next >= OVH_NSAMPLES)
|
||||
o->next = 0;
|
||||
if (o->n < OVH_NSAMPLES)
|
||||
o->n++;
|
||||
|
||||
/* Recompute state */
|
||||
float tmin = 10000.0, tmax = 0.0;
|
||||
for (int i = 0; i < o->n; i++) {
|
||||
if (o->temp[i] < tmin)
|
||||
tmin = o->temp[i];
|
||||
if (o->temp[i] > tmax)
|
||||
tmax = o->temp[i];
|
||||
}
|
||||
|
||||
o->delta = tmax - tmin;
|
||||
o->last_time = t_ms;
|
||||
}
|
||||
|
||||
float
|
||||
@ -42,10 +45,16 @@ overheat_delta(struct overheat *o)
|
||||
return o->delta;
|
||||
}
|
||||
|
||||
float
|
||||
overheat_speed(struct overheat *o)
|
||||
{
|
||||
return o->speed;
|
||||
}
|
||||
|
||||
int
|
||||
overheat_panic(struct overheat *o)
|
||||
{
|
||||
if (o->delta > OVH_THRESHOLD)
|
||||
if (o->speed > OVH_THRESHOLD)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
|
||||
@ -10,19 +10,22 @@ extern "C" {
|
||||
|
||||
#define OVH_NSAMPLES 20
|
||||
#define OVH_INTERVAL 200 /* ms */
|
||||
#define OVH_THRESHOLD 2.0 /* °C */
|
||||
#define OVH_THRESHOLD 2.0 /* °C / s */
|
||||
|
||||
struct overheat {
|
||||
unsigned long last_time;
|
||||
float temp[OVH_NSAMPLES];
|
||||
long unsigned t[OVH_NSAMPLES];
|
||||
int next;
|
||||
int n;
|
||||
float delta;
|
||||
float speed;
|
||||
};
|
||||
|
||||
void overheat_init(struct overheat *o);
|
||||
void overheat_input(struct overheat *o, unsigned long t_ms, float temp);
|
||||
float overheat_delta(struct overheat *o);
|
||||
float overheat_speed(struct overheat *o);
|
||||
int overheat_panic(struct overheat *o);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
49
barista/test/test_thermostat.c
Normal file
49
barista/test/test_thermostat.c
Normal file
@ -0,0 +1,49 @@
|
||||
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
|
||||
#include <stdio.h>
|
||||
#include "overheat.h"
|
||||
#include "thermostat.h"
|
||||
#include "heater.h"
|
||||
|
||||
/* Read a CSV from the stdin in the format
|
||||
* <time_in_seconds> <state> <temp_in_C>
|
||||
* skipping the first row (header).
|
||||
*
|
||||
* Outputs overheat state. */
|
||||
|
||||
#define MAX_LINE 1024
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char buf[MAX_LINE];
|
||||
fgets(buf, MAX_LINE, stdin);
|
||||
|
||||
float t, temp;
|
||||
int st;
|
||||
|
||||
struct overheat ovh = { 0 };
|
||||
struct thermostat th = { 0 };
|
||||
struct heater heater = { 0 };
|
||||
|
||||
overheat_init(&ovh);
|
||||
|
||||
for (int i = 0; scanf("%f %d %f", &t, &st, &temp) == 3; i++) {
|
||||
if (i == 0)
|
||||
thermostat_set(&th, 60.0);
|
||||
|
||||
unsigned long t_ms = t * 1000;
|
||||
|
||||
overheat_input(&ovh, t_ms, temp);
|
||||
float delta = overheat_delta(&ovh);
|
||||
float speed = overheat_speed(&ovh);
|
||||
float u = thermostat_state(&th, temp, speed);
|
||||
heater_on(&heater, t_ms, u);
|
||||
int h = heater_state(&heater, t_ms);
|
||||
int panic = overheat_panic(&ovh);
|
||||
|
||||
printf("%8.3f %2d %6.2f %6.1f %8.3f %8.3f %d %s\n", t, st, temp, delta, speed, u, h, panic ? "PANIC" : "OK");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -3,9 +3,12 @@
|
||||
|
||||
#include "thermostat.h"
|
||||
|
||||
#define TEMP_MIN 35.0 /* °C */
|
||||
#define DELTA_LOW 1.0
|
||||
#define DELTA_HIGH 1.0
|
||||
|
||||
#define T_ERR_MIN 35.0 /* °C */
|
||||
|
||||
void
|
||||
thermostat_set(struct thermostat *th, float temp_target)
|
||||
{
|
||||
@ -26,16 +29,36 @@ thermostat_off(struct thermostat *th)
|
||||
th->on = 0;
|
||||
}
|
||||
|
||||
int
|
||||
thermostat_state(struct thermostat *th, float temp)
|
||||
static float
|
||||
pid(float T0, float T, float dT_dt)
|
||||
{
|
||||
float err_min = 2.0;
|
||||
|
||||
/* The rate of change of error is the same as the temperature, as they
|
||||
* are only offset by a mostly constant value */
|
||||
float derr_dt = dT_dt;
|
||||
|
||||
if ((T0 - T) < err_min)
|
||||
return 0.0;
|
||||
|
||||
float Kp = 1.0 / 20.0;
|
||||
float Kd = - 1.0 / 3.0;
|
||||
float u = Kp * (T0 - T) + Kd * dT_dt;
|
||||
|
||||
if (u < 0.0)
|
||||
u = 0.0;
|
||||
else if (u > 1.0)
|
||||
u = 1.0;
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
/* Return a value in [0, 1] to set the heater duty cycle */
|
||||
float
|
||||
thermostat_state(struct thermostat *th, float T, float dT_dt)
|
||||
{
|
||||
if (th->st == THERMOSTAT_OFF)
|
||||
return 0;
|
||||
return 0.0;
|
||||
|
||||
if (th->on && temp > th->temp_max)
|
||||
th->on = 0;
|
||||
else if (!th->on && temp < th->temp_min)
|
||||
th->on = 1;
|
||||
|
||||
return th->on;
|
||||
return pid(th->temp_target, T, dT_dt);
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ struct thermostat {
|
||||
|
||||
void thermostat_set(struct thermostat *th, float temp_target);
|
||||
void thermostat_off(struct thermostat *th);
|
||||
int thermostat_state(struct thermostat *th, float temp);
|
||||
float thermostat_state(struct thermostat *th, float T, float dT_dt);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user