289 lines
6.9 KiB
C
289 lines
6.9 KiB
C
#include "common.h"
|
|
#include "bus/uart.h"
|
|
#include "bus/mosfet.h"
|
|
#include "bus/pwm.h"
|
|
#include "bus/i2c.h"
|
|
|
|
#include <avr/interrupt.h>
|
|
|
|
#define DEADBAND 2.0f
|
|
#define HYSTERESIS_C 0.5f
|
|
#define HYSTERESIS_H 0.5f
|
|
|
|
// https://en.wikipedia.org/wiki/Bang%E2%80%93bang_control
|
|
// https://support.75f.io/hc/en-us/articles/360044956794-Deadband-and-Hysteresis
|
|
// https://www.elotech.de/fileadmin/user_upload/PID-Regelungsgrundlagen.pdf
|
|
// https://thomasfermi.github.io/Algorithms-for-Automated-Driving/Control/PID.html
|
|
// https://www.ni.com/en/shop/labview/pid-theory-explained.html
|
|
// https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller
|
|
// https://ctms.engin.umich.edu/CTMS/index.php?example=Introduction§ion=ControlPID
|
|
// https://www.youtube.com/watch?v=wkfEZmsQqiA&list=PLn8PRpmsu08pQBgjxYFXSsODEF3Jqmm-y
|
|
|
|
enum state_e
|
|
{
|
|
S_IDLE,
|
|
S_HEAT_UP,
|
|
S_COOL_DOWN,
|
|
S_DEHUMIDIFY
|
|
};
|
|
|
|
static enum state_e state;
|
|
static float temp, temp_target;
|
|
static float dewp, dewp_target;
|
|
static float rhum;
|
|
|
|
static int Init(void);
|
|
static void Update(void);
|
|
static void SetTarget(float t, float td);
|
|
static void FetchSensorValues(void);
|
|
|
|
static int Init(void)
|
|
{
|
|
mem_block_t mem;
|
|
|
|
state = S_IDLE;
|
|
|
|
// MOSFETS control things like the heating element
|
|
// so they are the highest priority to initialize
|
|
// to a default state via MOS_Init().
|
|
|
|
MOS_Init();
|
|
|
|
// The serial interface is required for output
|
|
// functions like Info() and Error() and it uses
|
|
// IRQs, so we need to initialize it as soon as
|
|
// possible and make sure to enable interrupts.
|
|
|
|
UART_Init();
|
|
sei();
|
|
|
|
Info("Initializing...");
|
|
|
|
// The watchdog timer is clocked from a separate
|
|
// on-chip oscillator which runs at 1 MHz. Eight
|
|
// different clock cycle periods can be selected
|
|
// to determine the reset period. If the reset
|
|
// period expires, the chip resets and executes
|
|
// from the reset vector.
|
|
|
|
// The update loop must call WDT_Reset to reset
|
|
// the timer, kind of like a dead man's switch
|
|
// allowing us to detect infinite loops and any
|
|
// other error that halts execution.
|
|
|
|
if (WDT_HasTriggered()) {
|
|
Info("Unexpected system reset.");
|
|
}
|
|
|
|
WDT_Enable();
|
|
WDT_SetTimeoutFlag(WDT2000); // 2 seconds
|
|
|
|
// See if there are persistent target settings
|
|
// stored in EEPROM and load them. Some sanity
|
|
// checking must be done before using those.
|
|
|
|
if (MEM_Read(&mem)) { // Any valid block found?
|
|
Info("Found persistent configuration in EEPROM!");
|
|
Info("Using targets TEMP=%.2fC, DEWP=%.2fC.",
|
|
mem.temp, mem.dewp);
|
|
|
|
temp_target = mem.temp;
|
|
dewp_target = mem.dewp;
|
|
}
|
|
|
|
// mem.temp = 20.50f;
|
|
// mem.dewp = 10.25f;
|
|
// MEM_Write(&mem);
|
|
// MEM_Dump();
|
|
// MEM_Free();
|
|
|
|
// There is a possiblity to use interrupt signals
|
|
// for I2C communication but only as one large
|
|
// branching routine for the whole I2C system.
|
|
// The blocking approach used right now is fine.
|
|
|
|
I2C_Init();
|
|
PWM_Init();
|
|
|
|
MOS_Enable(MOS03); // Lights
|
|
// MOS_Enable(MOS01); // Peltier
|
|
// MOS_Disable(MOS02); // Heating
|
|
|
|
PWM_SetValue(FAN01, 50); // Fan Peltier Hot side
|
|
PWM_SetValue(FAN02, 50); // Fan Peltier Cold Side
|
|
PWM_SetValue(FAN03, 50); // Fan Heating
|
|
|
|
// The I2C_SetChannel command changes the channel
|
|
// setting of the PCA9546 I2C multiplexer. Any
|
|
// command after it will be sent to the device
|
|
// listening on that channel.
|
|
|
|
I2C_SetChannel(AHT01);
|
|
I2C_AHT20_Init();
|
|
|
|
I2C_SetChannel(AHT02);
|
|
I2C_AHT20_Init();
|
|
|
|
I2C_SetChannel(AHT03);
|
|
I2C_AHT20_Init();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void Update(void)
|
|
{
|
|
char ch;
|
|
cmd_t cmd;
|
|
float terr;
|
|
float derr;
|
|
|
|
// Parse serial commands
|
|
while ((ch = UART_Getc()) >= 0) {
|
|
if (!CMD_Parse(ch, &cmd)) {
|
|
continue;
|
|
}
|
|
switch(cmd.type) {
|
|
case T_SET: // Set new persistent target
|
|
SetTarget(cmd.args[0], cmd.args[1]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get latest sensor values
|
|
FetchSensorValues();
|
|
|
|
// Two-step (bang-bang) controller testing
|
|
|
|
// TODO: Sanity check setpoint!
|
|
// TODO: Check thermistor for overheating!
|
|
// TODO: Implement control stages based on hysteresis
|
|
// TODO: PID control for fan pulse-width modulation?
|
|
|
|
// The dead band represents the lower and upper limits
|
|
// of the error between which the controller doesn't
|
|
// react.
|
|
|
|
state = S_IDLE;
|
|
terr = temp - temp_target;
|
|
derr = dewp - dewp_target;
|
|
|
|
if (terr < -DEADBAND / 2) {
|
|
state = S_HEAT_UP;
|
|
}
|
|
|
|
if (terr > DEADBAND / 2) {
|
|
state = S_COOL_DOWN;
|
|
}
|
|
|
|
switch (state) {
|
|
case S_IDLE:
|
|
MOS_Disable(MOS01);
|
|
MOS_Disable(MOS02);
|
|
PWM_SetValue(FAN01, 20);
|
|
PWM_SetValue(FAN02, 20);
|
|
PWM_SetValue(FAN03, 20);
|
|
break;
|
|
case S_HEAT_UP:
|
|
MOS_Enable(MOS02);
|
|
MOS_Disable(MOS01);
|
|
PWM_SetValue(FAN01, 20);
|
|
PWM_SetValue(FAN02, 60);
|
|
PWM_SetValue(FAN03, 60);
|
|
break;
|
|
case S_COOL_DOWN:
|
|
MOS_Enable(MOS01);
|
|
MOS_Disable(MOS02);
|
|
PWM_SetValue(FAN01, 60);
|
|
PWM_SetValue(FAN02, 60);
|
|
PWM_SetValue(FAN03, 60);
|
|
break;
|
|
case S_DEHUMIDIFY:
|
|
UNUSED(derr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void SetTarget(float t, float td)
|
|
{
|
|
mem_block_t mem;
|
|
|
|
Print("\r\n");
|
|
Info("Updating target configuration:");
|
|
Info("Setting temperature to %.2fC.", t);
|
|
Info("Setting dewpoint to %.2fC.", td);
|
|
|
|
// Even with wear leveling there is a finite number
|
|
// of EEPROM write cycles so we should always check
|
|
// for redundant values. This could be handled by
|
|
// the underlying memory implementation.
|
|
|
|
if (MEM_Read(&mem)) {
|
|
if (t == mem.temp && td == mem.dewp) {
|
|
return; // Nothing to do
|
|
}
|
|
}
|
|
|
|
// Update current targets
|
|
mem.temp = temp_target = t;
|
|
mem.dewp = dewp_target = td;
|
|
|
|
// Store in EEPROM
|
|
MEM_Write(&mem);
|
|
}
|
|
|
|
static void FetchSensorValues(void)
|
|
{
|
|
word raw;
|
|
float t[6], rh[3], td[3];
|
|
|
|
Print("\r\n");
|
|
Info("Fetching sensor values...");
|
|
|
|
I2C_SetChannel(AHT01);
|
|
I2C_AHT20_Read(&t[0], &rh[0]);
|
|
I2C_SetChannel(AHT02);
|
|
I2C_AHT20_Read(&t[1], &rh[1]);
|
|
I2C_SetChannel(AHT03);
|
|
I2C_AHT20_Read(&t[2], &rh[2]);
|
|
|
|
raw = I2C_ADS1115_ReadRaw(ADS01);
|
|
t[3] = SteinhartHart(Resistance(raw));
|
|
raw = I2C_ADS1115_ReadRaw(ADS02);
|
|
t[4] = SteinhartHart(Resistance(raw));
|
|
raw = I2C_ADS1115_ReadRaw(ADS03);
|
|
t[5] = SteinhartHart(Resistance(raw));
|
|
|
|
td[0] = Dewpoint(t[0], rh[0]);
|
|
td[1] = Dewpoint(t[1], rh[1]);
|
|
td[2] = Dewpoint(t[2], rh[2]);
|
|
|
|
temp = (t[0] + t[1] + t[2]) / 3;
|
|
rhum = (rh[0] + rh[1] + rh[2]) / 3;
|
|
dewp = (td[0] + td[1] + td[2]) / 3;
|
|
|
|
Info("T1=%.2fC, TD1=%.2fC, RH1=%.2f%%, NT1=%.2fC", t[0], td[0], rh[0], t[3]);
|
|
Info("T2=%.2fC, TD2=%.2fC, RH2=%.2f%%, NT2=%.2fC", t[1], td[1], rh[1], t[4]);
|
|
Info("T3=%.2fC, TD3=%.2fC, RH3=%.2f%%, NT3=%.2fC", t[2], td[2], rh[2], t[5]);
|
|
Info("T_AVG=%.2fC, TD_AVG=%.2fC, RH_AVG=%.2f%%", temp, dewp, rhum);
|
|
Info("T_TAR=%.2fC, TD_TAR=%.2fC", temp_target, dewp_target);
|
|
Info("STATE=%s", (state == S_IDLE) ? "S_IDLE" :
|
|
(state == S_HEAT_UP) ? "S_HEAT_UP" :
|
|
(state == S_COOL_DOWN) ? "S_COOL_DOWN" :
|
|
(state == S_DEHUMIDIFY) ? "S_DEHUMIDIFY" :
|
|
"UNKNOWN");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
Init();
|
|
|
|
for (;;) {
|
|
WDT_Reset();
|
|
Update();
|
|
WDT_Reset();
|
|
Sleep(1000);
|
|
}
|
|
|
|
return 0;
|
|
}
|