Compare commits

...

4 Commits

Author SHA1 Message Date
Rodrigo Arias Mallo
8502ee3c5c Implement PID thremostat controller
For now only the proportional (Kp) and derivative (Kd) components are
used, the integral term is 0.
2025-11-02 19:24:02 +01:00
Rodrigo Arias Mallo
678f16111b Add thermostat and heater control 2025-11-02 19:24:02 +01:00
Rodrigo Arias Mallo
3ea6ff0e14 Merge makefile rules for tests 2025-11-02 19:24:02 +01:00
Rodrigo Arias Mallo
d59dec50a8 Implement led control in another module 2025-11-02 19:24:02 +01:00
15 changed files with 544 additions and 74 deletions

3
barista/.gitignore vendored
View File

@ -1,3 +1,4 @@
build/
test_ntc
misc/
*.test
*.o

View File

@ -16,6 +16,10 @@ SRC=barista.ino \
overheat.c \
overheat.h
TESTS=ntc.test \
overheat.test \
led.test
# For host test programs
CPPFLAGS=-I.
LIBS=-lm
@ -31,15 +35,15 @@ upload: $(HEX)
serial:
picocom -b 9600 --lower-rts --lower-dtr /dev/ttyUSB0 --imap lfcrlf
test: test_ntc test_overheat
test: ntc.test overheat.test led.test
test_ntc: test/test_ntc.o ntc.o
%.test: test/test_%.o test/compat.o %.o
gcc $^ -o $@ $(LIBS)
test_overheat: test/test_overheat.o overheat.o
thermostat.test: test/test_thermostat.o test/compat.o thermostat.o overheat.o heater.o
gcc $^ -o $@ $(LIBS)
clean:
rm -f test/test_ntc.o ntc.o
rm -f $(TESTS) test/*.o *.o
.PHONY: test all clean

View File

@ -3,6 +3,9 @@
#include "ntc.h"
#include "overheat.h"
#include "led.h"
#include "heater.h"
#include "thermostat.h"
#include "pinout.h"
#include <avr/wdt.h>
@ -12,8 +15,7 @@ enum logic {
};
#define DEBOUNCE_TIME 20L /* ms */
#define TEMP_MIN 40.0f /* C */
#define TEMP_MAX 50.0f /* C */
#define TEMP_HOT 75.0f /* C */
#define LED_MIN_VALUE 0
@ -50,7 +52,6 @@ enum buzz_state {
BUZZ_ACTIVE,
};
int button_pin[MAX_BTN] = {
[BTN_ON] = PIN_POWER_ON,
[BTN_HOT] = PIN_HOT,
@ -90,6 +91,12 @@ struct state {
struct overheat overheat;
unsigned long overheat_t0;
struct led red_led;
struct led green_led;
struct heater heater;
struct thermostat thermostat;
} g_st;
int read_input(int pin)
@ -187,27 +194,20 @@ void proc_buttons(struct state *state, const struct input *input)
int red_min = 50;
int red_state = red_min;
unsigned long brewing_max_time = 3000UL; /* 3 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 = 10000UL; /* 10 seconds */
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 = 300000UL; /* 300 seconds */
void proc_machine(struct state *st)
{
static unsigned long last_print_t = 0;
float temp = st->ntc_T;
int on = (st->btn[BTN_ON].state == RELEASED);
int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
Serial.print("t=");
Serial.print(millis());
Serial.print(" state=");
Serial.print(st->mstate);
Serial.print(" on=");
Serial.print(on);
Serial.print(" temp=");
Serial.print(temp);
Serial.println(" C");
unsigned long t = millis();
/* If the machine is overheating */
if (overheat_panic(&st->overheat)) {
@ -235,7 +235,7 @@ void proc_machine(struct state *st)
Serial.println("heating");
}
} else if (st->mstate == HEATING) {
if (temp > TEMP_MAX) {
if (temp > TEMP_HOT) {
st->mstate = HOT;
st->hot_t0 = millis();
st->buzz_state = BUZZ_HEY;
@ -313,64 +313,85 @@ void do_proc(struct state *st, const struct input *input)
void
output_leds(const struct state *st)
{
static int r = 0;
static int g = 0;
unsigned long t = millis();
if (st->mstate == HEATING || st->mstate == COOLING) {
analogWrite(PIN_LED_RED, r);
setled(PIN_LED_GREEN, 0);
if (r >= 255)
r = 0;
else
r += 5;
if (st->mstate == SLEEPING) {
led_off(&st->red_led);
led_off(&st->green_led);
} else if (st->mstate == HEATING) {
led_off(&st->red_led);
led_pattern(&st->green_led, t, 1000UL, "000123456789abcdefff");
} else if (st->mstate == COOLING) {
led_off(&st->red_led);
led_pattern(&st->green_led, t, 1000UL, "fffedcba987654321000");
} else if (st->mstate == HOT) {
setled(PIN_LED_RED, 0);
setled(PIN_LED_GREEN, 1);
r = 0;
led_off(&st->red_led);
led_on(&st->green_led);
} else if (st->mstate == PANIC_OVERHEAT) {
setled(PIN_LED_RED, 1);
setled(PIN_LED_GREEN, 0);
led_pattern(&st->red_led, t, 3000UL, "f0f0f0000000");
led_pattern(&st->green_led, t, 3000UL, "000000f0f0f0");
} else if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) {
setled(PIN_LED_RED, 0);
analogWrite(PIN_LED_GREEN, g);
if (g >= 255)
g = 0;
else
g += 5;
led_off(&st->red_led);
led_pattern(&st->green_led, millis(), 2000UL, "0123456789abcdefedcba9876543210");
} else {
setled(PIN_LED_RED, 0);
setled(PIN_LED_GREEN, 0);
r = 0;
led_off(&st->red_led);
led_off(&st->green_led);
}
analogWrite(PIN_LED_RED, led_level(&st->red_led, t));
analogWrite(PIN_LED_GREEN, led_level(&st->green_led, t));
}
void
output_heater(const struct state *st)
output_relays(const struct state *st)
{
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT) {
if (st->ntc_T < TEMP_MIN)
relay(PIN_HEAT, ON);
else if (st->ntc_T > TEMP_MAX)
relay(PIN_HEAT, OFF);
} else {
unsigned long t = millis();
/* First configure thermostate */
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT)
thermostat_set(&st->thermostat, TEMP_HOT+5.0);
else
thermostat_off(&st->thermostat);
/* Then update heater state from thermostate */
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 */
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()

70
barista/heater.c Normal file
View File

@ -0,0 +1,70 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "heater.h"
#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, float duty)
{
unsigned long dt_on = duty * HEATER_MAX;
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->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;
}
int
heater_state(struct heater *h, unsigned long t_ms)
{
if (h->st == HEATER_OFF)
return 0;
/* Switch state if current time exceeds time limit
* in the current state */
/* FIXME: Integer overflow can cause the heater to turn on forever */
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;
}
}
if (!changed)
break;
}
if (h->cycle == CYCLE_ON)
return 1;
return 0;
}

39
barista/heater.h Normal file
View File

@ -0,0 +1,39 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef BARISTA_HEATER_H
#define BARISTA_HEATER_H
#ifdef __cplusplus
extern "C" {
#endif
enum heater_state {
HEATER_OFF = 0,
HEATER_ON,
};
enum heater_cycle {
CYCLE_ON = 0,
CYCLE_OFF,
};
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);
#ifdef __cplusplus
}
#endif
#endif /* BARISTA_HEATER_H */

71
barista/led.c Normal file
View File

@ -0,0 +1,71 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "led.h"
#include <string.h>
void led_on(struct led *led)
{
led->mode = LED_ON;
}
void led_off(struct led *led)
{
led->mode = LED_OFF;
}
void led_pattern(struct led *led, unsigned long t_ms, unsigned long period_ms, const char *pattern)
{
int n = strlen(pattern);
unsigned long step_ms = period_ms / n;
/* Don't change the current state */
if (led->mode == LED_PATTERN &&
led->step_ms == step_ms &&
led->pattern == pattern)
return;
led->mode = LED_PATTERN;
led->pat_i = 0;
led->pat_n = n;
led->pattern = pattern;
led->step_ms = step_ms;
led->t_ms = t_ms;
}
/* Return led level brightness in [0, 255] at current time */
int led_level(struct led *led, unsigned long t_ms)
{
if (led->mode == LED_OFF)
return 0;
if (led->mode == LED_ON)
return 255;
if (led->mode == LED_PATTERN) {
while (led->t_ms + led->step_ms < t_ms) {
led->t_ms += led->step_ms;
led->pat_i++;
if (led->pat_i >= led->pat_n)
led->pat_i = 0;
}
int c = led->pattern[led->pat_i];
int level;
if (c >= '0' && c <= '9')
level = 17 * (c - '0');
else
level = 17 * (10 + (c - 'a'));
if (level < 0)
level = 0;
else if (level > 255)
level = 255;
return level;
}
/* Unknown mode, turn off */
return 0;
}

35
barista/led.h Normal file
View File

@ -0,0 +1,35 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef BARISTA_LED_H
#define BARISTA_LED_H
#ifdef __cplusplus
extern "C" {
#endif
enum led_mode {
LED_OFF = 0,
LED_ON,
LED_PATTERN,
};
struct led {
enum led_mode mode;
const char *pattern;
int pat_i;
int pat_n;
unsigned long step_ms;
unsigned long t_ms;
};
void led_on(struct led *led);
void led_off(struct led *led);
void led_pattern(struct led *led, unsigned long t_ms, unsigned long period_ms, const char *pattern);
int led_level(struct led *led, unsigned long t_ms);
#ifdef __cplusplus
}
#endif
#endif /* BARISTA_LED_H */

View File

@ -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;

View File

@ -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

13
barista/test/compat.c Normal file
View File

@ -0,0 +1,13 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "compat.h"
#include <time.h>
unsigned long millis(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000UL + ts.tv_nsec / 1000000UL;
}

17
barista/test/compat.h Normal file
View File

@ -0,0 +1,17 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef BARISTA_COMPAT_H
#define BARISTA_COMPAT_H
#ifdef __cplusplus
extern "C" {
#endif
unsigned long millis(void);
#ifdef __cplusplus
}
#endif
#endif /* BARISTA_COMPAT_H */

42
barista/test/test_led.c Normal file
View File

@ -0,0 +1,42 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include <stdio.h>
#include "led.h"
#include "compat.h"
int main(void)
{
struct led led;
unsigned long t0 = millis();
unsigned long step = 1000UL;
led_pattern(&led, t0, step, "000123456789abcdefff");
int last_level = -1;
unsigned long last_t = millis();
while (1) {
unsigned long t = millis();
if (t - t0 >= 10000UL)
break;
if (t - last_t < 50UL)
continue;
int level = led_level(&led, millis());
printf("|");
for (int i = 0; i < 256; i+=4) {
if (i < level)
printf("=");
else
printf(" ");
}
printf("|\n");
last_level = level;
last_t = t;
}
return 0;
}

View 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;
}

64
barista/thermostat.c Normal file
View File

@ -0,0 +1,64 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#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)
{
if (th->st == THERMOSTAT_ON && th->temp_target == temp_target)
return;
th->st = THERMOSTAT_ON;
th->temp_target = temp_target;
th->temp_min = temp_target - DELTA_LOW;
th->temp_max = temp_target + DELTA_HIGH;
th->on = 1;
}
void
thermostat_off(struct thermostat *th)
{
th->st = THERMOSTAT_OFF;
th->on = 0;
}
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.0;
return pid(th->temp_target, T, dT_dt);
}

32
barista/thermostat.h Normal file
View File

@ -0,0 +1,32 @@
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef BARISTA_THERMOSTAT_H
#define BARISTA_THERMOSTAT_H
#ifdef __cplusplus
extern "C" {
#endif
enum thermostat_state {
THERMOSTAT_OFF = 0,
THERMOSTAT_ON,
};
struct thermostat {
enum thermostat_state st;
float temp_target;
float temp_min;
float temp_max;
int on;
};
void thermostat_set(struct thermostat *th, float temp_target);
void thermostat_off(struct thermostat *th);
float thermostat_state(struct thermostat *th, float T, float dT_dt);
#ifdef __cplusplus
}
#endif
#endif /* BARISTA_THERMOSTAT_H */