Mercurial > ~darius > hgwebdir.cgi > tempctrl
diff tempctrl.c @ 41:5898fba6593c
Add temperature control.
- Split out console stuff to cons.[ch]. Set up stdio so we can use printf etc.
- Use \r\n as the line terminator consistently.
- Add OWGetTemp to get temperatures from a device.
- Load/save settings in EEPROM, defaults loaded from flash.
Nearly feature complete except you can't edit ROM IDs without a programming tool :)
(To be fixed)
Needs more testing.
author | darius@inchoate.localdomain |
---|---|
date | Sun, 06 Jul 2008 22:19:53 +0930 |
parents | |
children | 97ae82023d5b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tempctrl.c Sun Jul 06 22:19:53 2008 +0930 @@ -0,0 +1,544 @@ +/* + * Temperature control logic + * + * Copyright (c) 2008 + * Daniel O'Connor <darius@dons.net.au>. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <avr/interrupt.h> +#include <avr/pgmspace.h> +#include <avr/eeprom.h> +#include <util/crc16.h> + +#include "cons.h" +#include "1wire.h" +#include "tempctrl.h" + +/* Helpers for our number system */ +#define GETWHOLE(x) ((x) / 100) +#define GETFRAC(x) ((x) - (GETWHOLE(x) * 100)) + +typedef struct { + int32_t sec; + int32_t usec; +} time_t; + +/* Holds all the settings needed */ +typedef struct { + uint8_t fermenter_ROM[8]; + uint8_t fridge_ROM[8]; + uint8_t ambient_ROM[8]; + int16_t target_temp; + uint16_t hysteresis; + + /* How much to under/overshoot on heating/cooling */ + int16_t minheatovershoot; + int16_t mincoolovershoot; + + /* Minimum time the cooler can be on/off */ + int16_t mincoolontime; + int16_t mincoolofftime; + + /* Minimum time the heater can be on/off */ + int16_t minheatontime; + int16_t minheatofftime; + +#define TC_MODE_AUTO 'a' /* Automatic control */ +#define TC_MODE_HEAT 'h' /* Force heating */ +#define TC_MODE_COOL 'c' /* Force cooling */ +#define TC_MODE_IDLE 'i' /* Force idle */ +#define TC_MODE_NOTHING 'n' /* Do nothing (like idle but log nothing) */ + char mode; + + /* Bit patterns for various modes */ + uint8_t coolbits; + uint8_t heatbits; + uint8_t idlebits; + + /* Check/stale times */ + int16_t check_interval; + int16_t stale_factor; +} __attribute__((packed)) settings_t; + +/* Current settings in RAM */ +static settings_t settings; + +/* Our map of EEPROM */ +struct { + settings_t settings; + uint16_t crc; +} ee_area __attribute__((section(".eeprom"))); + +/* Defaults that are shoved into EEPROM if it isn't inited */ +const PROGMEM settings_t default_settings = { + .fermenter_ROM = { 0x10, 0xeb, 0x48, 0x21, 0x01, 0x08, 0x00, 0xdf }, + .fridge_ROM = { 0x10, 0xa6, 0x2a, 0xc4, 0x00, 0x08, 0x00, 0x11 }, + .ambient_ROM = { 0x10, 0x97, 0x1b, 0xfe, 0x00, 0x08, 0x00, 0xd1 }, + .target_temp = 1400, + .hysteresis = 100, + .minheatovershoot = 50, + .mincoolovershoot = -50, + .mincoolontime = 300, + .mincoolofftime = 600, + .minheatontime = 60, + .minheatofftime = 60, + .mode = TC_MODE_AUTO, + .coolbits = _BV(7), + .heatbits = _BV(6), + .idlebits = 0x00, + .check_interval = 10, + .stale_factor = 3 +}; + +/* Local variable declarations */ +volatile static time_t now; + +/* Local function prototypes */ +static int gettemp(const PROGMEM char *name, uint8_t *ROM, int16_t *temp, uint8_t last); +static void tempctrl_load_or_init_settings(void); +static void tempctrl_default_settings(void); +static void tempctrl_write_settings(void); +static void setstate(char state); +static const PROGMEM char*state2long(char s); + +/* + * tempctrl_init + * + * Setup timer, should be called with interrupts disabled. + * + */ +void +tempctrl_init(void) { + /* Setup timer */ + /* 16Mhz / 1024 = 15625 Hz / 125 = 125 Hz = IRQ every 8 ms */ + + /* CTC mode, no output on pin, Divide clock by 1024 */ + TCCR0 = _BV(WGM01)| _BV(CS02) | _BV(CS00); + + /* Compare with ... */ + OCR0 = 125; + + /* Enable interrupt for match on A */ + TIMSK = _BV(OCIE0); + + now.sec = 0; + now.usec = 0; + + tempctrl_load_or_init_settings(); +} + +/* + * Timer 0 Compare IRQ + * + * Update time counter + */ + +ISR(TIMER0_COMP_vect) { + now.usec += 8000; /* 1000000 * 1 / F_CPU / 1024 / 125 */ + while (now.usec > 1000000) { + now.usec -= 1000000; + now.sec++; + } +} + +/* + * tempctrl_update + * + * Should be called in a normal context, could run things that take a long time. + * (ie 1wire bus stuff) + * + */ +void +tempctrl_update(void) { + /* State variables */ + static int32_t checktime = 0; // Time of next check + static int32_t lastdata = 0; // Last time we got data + + static int16_t fermenter_temp = 0; // Fermenter temperature + static int16_t fridge_temp = 0; // Fridge temperature + static int16_t ambient_temp = 0; // Ambient temperature + // These are inited like this so we will still heat/cool when + // now < settings.minheatofftime + static int32_t lastheaton = -100000; // Last time the heater was on + static int32_t lastheatoff = -100000; // Last time the heater was off + static int32_t lastcoolon = -100000; // Last time the cooler was on + static int32_t lastcooloff = -100000; // Last time the cooler was off + static char currstate = 'i'; // Current state + + /* Temporary variables */ + int32_t t; + int16_t diff; + char nextstate; + int forced; + int stale; + + t = gettod(); + /* Time to check temperatures? */ + if (t < checktime) + return; + + checktime = t + settings.check_interval; + + /* Don't do any logging, just force idle and leave */ + if (settings.mode == TC_MODE_NOTHING) { + nextstate = 'i'; + goto setstate; + } + + /* Update our temperatures */ + printf_P(PSTR("Time: %ld, Target: %d.%02d, "), now.sec, GETWHOLE(settings.target_temp), + GETFRAC(settings.target_temp)); + + if (gettemp(PSTR("Fermenter"), settings.fermenter_ROM, &fermenter_temp, 0)) + lastdata = t; + + /* Check for stale data */ + if (lastdata + (settings.check_interval * settings.stale_factor) < t) + stale = 1; + else + stale = 0; + + gettemp(PSTR("Fridge"), settings.fridge_ROM, &fridge_temp, 0); + gettemp(PSTR("Ambient"), settings.ambient_ROM, &ambient_temp, 1); + + /* Default to remaining as we are */ + nextstate = '-'; + + /* Temperature diff, -ve => too cold, +ve => too warm */ + diff = fermenter_temp - settings.target_temp; + + switch (currstate) { + case 'i': + /* If we're idle then only heat or cool if the temperate difference is out of the + * hysteresis band + */ + if (abs(diff) > settings.hysteresis) { + if (diff < 0 && settings.minheatofftime + lastheatoff < t) + nextstate = 'h'; + else if (diff > 0 && settings.mincoolofftime + lastcooloff < t) + nextstate = 'c'; + } + break; + + case 'c': + /* Work out if we should go idle (based on min on time & overshoot) */ + if (diff + settings.mincoolovershoot < 0 && + settings.mincoolontime + lastcoolon < t) + nextstate = 'i'; + break; + + case 'h': + if (diff - settings.minheatovershoot > 0 && + settings.minheatontime + lastheaton < t) + nextstate = 'i'; + break; + + default: + printf_P(PSTR("\r\nUnknown state %c, going to idle\n"), currstate); + nextstate = 'i'; + break; + } + + /* Override if we have stale data */ + if (stale) + nextstate = 'i'; + + /* Handle state forcing */ + if (settings.mode != TC_MODE_AUTO) + forced = 1; + else + forced = 0; + + if (settings.mode == TC_MODE_IDLE) + nextstate = 'i'; + else if (settings.mode == TC_MODE_HEAT) + nextstate = 'h'; + else if (settings.mode == TC_MODE_COOL) + nextstate = 'c'; + + if (nextstate != '-') + currstate = nextstate; + + printf_P(PSTR(", State: %S, Flags: %S%S\r\n"), state2long(currstate), + forced ? PSTR("F") : PSTR(""), + stale ? PSTR("S") : PSTR("")); + + setstate: + setstate(currstate); +} + +/* + * Log a temperature & store it if valid + * + * Returns 1 if it was valid, 0 otherwise + */ +static int +gettemp(const PROGMEM char *name, uint8_t *ROM, int16_t *temp, uint8_t last) { + int16_t tmp; + + tmp = OWGetTemp(ROM); + printf_P(PSTR("%S: "), name); + if (tmp > OW_TEMP_BADVAL) { + printf_P(PSTR("%d.%02d%S"), GETWHOLE(tmp), GETFRAC(tmp), last ? PSTR("") : PSTR(", ")); + *temp = tmp; + return(1); + } else { + printf_P(PSTR("NA (%d)%S"), tmp, last ? PSTR("") : PSTR(", ")); + return(0); + } +} + +/* Return 'time of day' (really uptime) */ +int32_t +gettod(void) { + int32_t t; + + cli(); + t = now.sec; + sei(); + + return(t); +} + +/* Read the settings from EEPROM + * If the CRC fails then reload from flash + */ +static void +tempctrl_load_or_init_settings(void) { + uint8_t *dptr; + uint16_t crc, strcrc; + int i; + + crc = 0; + eeprom_busy_wait(); + eeprom_read_block(&settings, &ee_area.settings, sizeof(settings_t)); + strcrc = eeprom_read_word(&ee_area.crc); + + dptr = (uint8_t *)&settings; + + for (i = 0; i < sizeof(settings_t); i++) + crc = _crc16_update(crc, dptr[i]); + + /* All OK? */ + if (crc == strcrc) + return; + + printf_P(PSTR("CRC mismatch got 0x%04x vs 0x%04x, setting defaults\r\n"), crc, strcrc); + tempctrl_default_settings(); + tempctrl_write_settings(); +} + +/* Load in the defaults from flash */ +static void +tempctrl_default_settings(void) { + memcpy_P(&settings, &default_settings, sizeof(settings_t)); +} + +/* Write the current settings out to EEPROM */ +static void +tempctrl_write_settings(void) { + uint16_t crc; + uint8_t *dptr; + int i; + + eeprom_busy_wait(); + eeprom_write_block(&settings, &ee_area.settings, sizeof(settings_t)); + + dptr = (uint8_t *)&settings; + crc = 0; + for (i = 0; i < sizeof(settings_t); i++) + crc = _crc16_update(crc, dptr[i]); + + eeprom_write_word(&ee_area.crc, crc); +} + +/* Set the relays to match the desired state */ +static void +setstate(char state) { + switch (state) { + case 'c': + PORTC = settings.coolbits; + break; + + case 'h': + PORTC = settings.heatbits; + break; + + default: + printf_P(PSTR("Unknown state %c, setting idle\r\n"), state); + /* fallthrough */ + + case 'i': + PORTC = settings.idlebits; + break; + } +} + +/* Handle user command + * + */ +void +tempctrl_cmd(char *buf) { + char cmd[6]; + int16_t data; + int i; + + i = sscanf_P(buf, PSTR("tc %5s %d"), cmd, &data); + + if (i == 1) { + if (!strcasecmp_P(cmd, PSTR("help"))) { + printf_P(PSTR( + "tc help This help\r\n" + "tc save Save settings to EEPROM\r\n" + "tc load Load or default settings from EEPROM\r\n" + "tc dflt Load defaults from flash\r\n" + "tc list List current settings\r\n" + "tc mode [achin] Change control mode, must be one of\r\n" + " a Auto\r\n" + " c Always cool\r\n" + " h Always heat\r\n" + " i Always idle\r\n" + " n Like idle but don't log anything\r\n" + "\r\n" + "tc X Y Set X to Y where X is one of\r\n" + " targ Target temperature\r\n" + " hys Hysteresis range\r\n" + " mhov Minimum heat overshoot\r\n" + " mcov Minimum cool overshoot\r\n" + " mcon Minimum cool on time\r\n" + " mcoff Minimum cool off time\r\n" + " mhin Minimum heat on time\r\n" + " mhoff Minimum heat off time\r\n" + " Times are in seconds\r\n" + " Temperatures are in hundredths of degrees Celcius\r\n" + )); + return; + } + + if (!strcasecmp_P(cmd, PSTR("save"))) { + tempctrl_write_settings(); + return; + } + if (!strcasecmp_P(cmd, PSTR("load"))) { + tempctrl_load_or_init_settings(); + return; + } + if (!strcasecmp_P(cmd, PSTR("dflt"))) { + tempctrl_default_settings(); + return; + } + if (!strcasecmp_P(cmd, PSTR("list"))) { + printf_P(PSTR("Fermenter ROM ID %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\r\n" + "Fridge ROM ID %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\r\n" + "Ambient ROM ID %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\r\n" + "Target - %d, Hystersis - %d\r\n" + "Min heat overshoot - %d, Min cool overshoot - %d\r\n" + "Min cool on time - %d, Min cool off time - %d\r\n" + "Min heat on time - %d, Min heat off time -%d\r\n"), + settings.fermenter_ROM[0], settings.fermenter_ROM[1], settings.fermenter_ROM[2], settings.fermenter_ROM[3], + settings.fermenter_ROM[4], settings.fermenter_ROM[5], settings.fermenter_ROM[6], settings.fermenter_ROM[7], + settings.fridge_ROM[0], settings.fridge_ROM[1], settings.fridge_ROM[2], settings.fridge_ROM[3], + settings.fridge_ROM[4], settings.fridge_ROM[5], settings.fridge_ROM[6], settings.fridge_ROM[7], + settings.ambient_ROM[0], settings.ambient_ROM[1], settings.ambient_ROM[2], settings.ambient_ROM[3], + settings.ambient_ROM[4], settings.ambient_ROM[5], settings.ambient_ROM[6], settings.ambient_ROM[7], + settings.target_temp, settings.hysteresis, + settings.minheatovershoot, settings.mincoolovershoot, + settings.mincoolontime, settings.minheatontime, + settings.minheatontime, settings.minheatofftime); + return; + } + if (!strcasecmp_P(cmd, PSTR("mode"))) { + switch (buf[8]) { + case TC_MODE_AUTO: + case TC_MODE_HEAT: + case TC_MODE_COOL: + case TC_MODE_IDLE: + case TC_MODE_NOTHING: + settings.mode = buf[8]; + break; + + default: + printf_P(PSTR("Unknown mode character '%c'\r\n"), buf[8]); + break; + } + return; + } + + } + + if (i != 2) { + printf_P(PSTR("Unable to parse command\r\n")); + return; + } + + if (!strcasecmp_P(cmd, PSTR("targ"))) { + settings.target_temp = data; + } else if (!strcasecmp_P(cmd, PSTR("hys"))) { + settings.hysteresis = data; + } else if (!strcasecmp_P(cmd, PSTR("mhov"))) { + settings.minheatovershoot = data; + } else if (!strcasecmp_P(cmd, PSTR("mcov"))) { + settings.mincoolovershoot = data; + } else if (!strcasecmp_P(cmd, PSTR("mcon"))) { + settings.mincoolontime = data; + } else if (!strcasecmp_P(cmd, PSTR("mcoff"))) { + settings.mincoolofftime = data; + } else if (!strcasecmp_P(cmd, PSTR("mhon"))) { + settings.minheatontime = data; + } else if (!strcasecmp_P(cmd, PSTR("mhoff"))) { + settings.minheatofftime = data; + } else { + printf_P(PSTR("Unknown setting\r\n")); + return; + } +} + +static const PROGMEM char* +state2long(char s) { + switch (s) { + case 'i': + return PSTR("idle"); + break; + + case 'c': + return PSTR("cool"); + break; + + case 'h': + return PSTR("heat"); + break; + + case '-': + return PSTR("-"); + break; + + default: + return PSTR("unknown"); + break; + } +} +