/***********************************************************************************
* Thermostat_ATtinyCore.cpp
*
* Reads a 10k thermistor / NTC and a potentiometer input for desired temperature and controls a fan output.
*
* The program flow:
* 1. Read a 10k thermistor to determine the current temperature.
* 2. Read a potentiometer for the temperature threshold / the desired temperature.
* 3. Control the fan output (No PWM yet).
* 4. Use the internal led for signaling the current or desired temperature.
* 5. Sleep for 200 ms with Watchdog-Wakeup to optimize power consumption.
*
* If in INFO (and DEBUG) mode, a 115200 baud serial output is generated at the LED pin.
* For serial output, you need the Arduino library "ATtinySerialOut"
* and Print.cpp and TinySoftwareSerial.cpp must be removed from ATTinyCore.
*
* Tested with ATTinyCore and board settings: "ATtiny85 (Micronucleus / DigiSpark)"
* and "1MHz (no USB)" for "Clock (controls sketch only)" in the Arduino IDE!
*
* Copyright (C) 2024 Armin Joachimsmeyer
* Email: [email protected]
*
***********************************************************************************/
// WOKWI implementation: https://wokwi.com/projects/399385788134700033
#include <Arduino.h>
#include <avr/sleep.h> // required for sleep_enable()
#define VERSION "1.0.0"
#define WOKWI // Enable WOKWI debug output
//#define INFO // To see serial output with 115200 baud at PB1 - (Digispark) LED
//#define DEBUG // To see serial trace output with 115200 baud at PB1 - (Digispark) LED - only for development
// ATMEL ATTINY85 +-\/-+
// DO NOT USE: RESET/ADC0 (D5) PB5 1| |8 VCC 1.8 (ATtiny25V/45V/85V) 2.7 (25/45/85) - 5.5V
// USB+ ADC3 (D3) PB3 2| |7 PB2 (D2) INT0/ADC1 - Thermistor / NTC input
// Threshold in USB- ADC2 (D4) PB4 3| |6 PB1 (D1) PWM/MISO/DO/AIN1/OC0B/OC1A/PCINT1 - (Digispark) LED, TX Debug output (Serial Out)
// GND 4| |5 PB0 (D0) PWM/MOSI/OC0A/AIN0 - Relay / Mosfet output
// +----+
//
// PB3/USB+ and PB4/USB- :: are each connected to a 3.7 V Z-diode to GND and with a 68 ohm series resistor to the ATtiny pin.
// PB3/USB+ :: additionally has a 1.5K pullup to CPU VCC!
// PB4/USB- :: could be used for analog input if voltage is below 3.5V. If analog input is connected with a series resistor auf
// PB3/USB+ :: could be used for push button input. For analog input only if the pullup connection is changed from CPU V+ to USB V+.
#define MOSFET_OUT_PIN 0 // PB0
#define LED_PIN 1 // PB1 TX output
#if defined(WOKWI)
// Wokwi uses channels and not pin numbers for analogRead() :-(
#define TEMPERATURE_IN_CHANNEL 1 // Channel ADC1
#define TEMPERATURE_IN_PIN TEMPERATURE_IN_CHANNEL // must be ADC channel number for Wokwi
#define TEMPERATURE_THRESHOLD_IN_PIN 2 // must be ADC channel number for Wokwi
// Timing
#define DELAY_BETWEEN_TEPERATURE_OUTPUT_SECONDS 30 // Signal current temperature every minute
#define MINIMUM_ON_OFF_TIME_SECONDS 1
#else
#define TEMPERATURE_IN_CHANNEL 1 // Channel ADC1
#define TEMPERATURE_IN_PIN 2 // PB2
#define TEMPERATURE_THRESHOLD_IN_PIN 4 // PB4 Connected with a 10k series resistor to the potentiometer.
// Timing
#define DELAY_BETWEEN_TEPERATURE_OUTPUT_SECONDS 60 // Signal current temperature every minute
#define MINIMUM_ON_OFF_TIME_SECONDS 60
#endif
#if defined(WOKWI)
#include "TinyDebug.h"
#define Serial Debug // generate Wokwi Debug.print from our Serial.print
# if !defined(INFO)
#define INFO
# endif
#else
# if defined(INFO) || defined(DEBUG)
#define TX_PIN LED_PIN // Use the internal LED pin for serial output
#include "ATtinySerialOut.hpp" // Available as Arduino library "ATtinySerialOut"
# endif
#endif
//#include "ADCUtils.hpp" // must be after #include "ATtinySerialOut.hpp", because it uses Serial.*() on DEBUG
float getTemperatureOfNTC(uint8_t aTemperaturePin);
void handleFanOutput(uint8_t aCurrentTemperatureCelsius, uint8_t aThresholdTemperatureCelsius);
void handleLEDSignaling(uint8_t aCurrentTemperatureCelsius, uint8_t aThresholdTemperatureCelsius);
void signalTemperature(uint8_t aTemperature);
#define MILLIS_IN_ONE_SECOND 1000L
/*
* Static variables
*/
uint8_t sLastTemperatureCelsius = 0; // For display of temperature changes
unsigned long sLastMillisOfTemperatureSignaling = 0; // For implementation of the delay between temperature signaling
uint8_t sLastThresholdTemperatureCelsius = 0; // For display of threshold changes
unsigned long sLastMillisOfOutputChange = 0; // For implementation of the minimum on / off time
bool sFanIsActive = false; // Current state of the output
/*
* Helper macro for getting a macro definition as string
*/
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
void setup() {
// Set fan output to inactive
pinMode(MOSFET_OUT_PIN, OUTPUT);
digitalWrite(MOSFET_OUT_PIN, LOW);
// Initialize LED pin for serial output or plain LED
#if defined(INFO)
# if defined(WOKWI)
Debug.begin();
# else
initTXPin(); // Initialize the LED pin as an output for Serial.print
# endif
Serial.println(F("START " __FILE__ "\nVersion " VERSION " from " __DATE__ "")); // Just to know which program is running on my Arduino
Serial.println(
F(
"Read NTC voltage at pin " STR(TEMPERATURE_IN_PIN) " and threshold voltage at pin " STR(TEMPERATURE_THRESHOLD_IN_PIN)));
#else
pinMode(LED_PIN, OUTPUT); // otherwise pin is initialized by ATtinySerialOut
digitalWrite(LED_PIN, LOW);
#endif
// Enable sleep mode
sleep_enable();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
}
void loop() {
/*
* 1. Read a 10k thermistor to determine the current temperature.
*/
uint8_t tCurrentTemperatureCelsius = getTemperatureOfNTC(TEMPERATURE_IN_PIN);
/*
* 2. Read a potentiometer for the temperature threshold / the desired temperature.
*
* We have a series resistor at the potentiometer in order not tINFOattached Zener diode at pin 4.
* => we have a maximum reading of around 511.
* We map the potentiometer range to a threshold temperature of 20 to 50 degree celsius.
*/
uint16_t tThresholdRaw = analogRead(TEMPERATURE_THRESHOLD_IN_PIN);
uint8_t tThresholdTemperatureCelsius = map(tThresholdRaw, 0, 511, 20, 50);
#if defined(DEBUG)
Serial.print(F("Threshold temperature="));
Serial.print(tThresholdTemperatureCelsius);
Serial.println(F(" C"));
#endif
/*
* 3. Check if we must change state of fan output
*/
handleFanOutput(tCurrentTemperatureCelsius, tThresholdTemperatureCelsius);
/*
* 4. Use the internal led for signaling the desired or current temperature.
*/
handleLEDSignaling(tCurrentTemperatureCelsius, tThresholdTemperatureCelsius);
delay(1000);
}
/*
* Check if we must change state of fan output
*/
void handleFanOutput(uint8_t aCurrentTemperatureCelsius, uint8_t aThresholdTemperatureCelsius) {
/*
* Check if minimum on or off time has elapsed and set output on or off according to temperature difference
*/
bool tFanShouldBeActive = aCurrentTemperatureCelsius > aThresholdTemperatureCelsius;
if (sFanIsActive != tFanShouldBeActive) {
// check for minimum time gone
if (millis() - sLastMillisOfOutputChange > (MINIMUM_ON_OFF_TIME_SECONDS * MILLIS_IN_ONE_SECOND)) {
// minimum time elapsed here -> change state
sLastMillisOfOutputChange = millis();
digitalWrite(MOSFET_OUT_PIN, tFanShouldBeActive);
sFanIsActive = tFanShouldBeActive;
#if defined(INFO)
Serial.print(F("Change fan output to "));
Serial.println(tFanShouldBeActive);
#endif
}
}
}
/*
* Signal temperature by a blink code
* First blink the tens, then do a break of one second and then blink the ones.
*/
void signalTemperature(uint8_t aTemperature) {
#if defined(INFO)
Serial.print(F("Signal "));
Serial.print(aTemperature);
Serial.println(F(" C"));
#endif
for (uint8_t i = 0; i < aTemperature / 10; ++i) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(400);
}
delay(1000);
for (uint8_t i = 0; i < aTemperature % 10; ++i) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(400);
}
delay(2000);
}
/*
* 4. Use the internal led for signaling the desired or current temperature.
*/
void handleLEDSignaling(uint8_t aCurrentTemperatureCelsius, uint8_t aThresholdTemperatureCelsius) {
// check for changes in desired temperature
if (sLastThresholdTemperatureCelsius != aThresholdTemperatureCelsius) {
sLastThresholdTemperatureCelsius = aThresholdTemperatureCelsius;
sLastMillisOfTemperatureSignaling = millis(); // reset delay for signaling of current temperature
#if defined(INFO)
Serial.print(F("New desired temperature="));
Serial.print(aThresholdTemperatureCelsius);
Serial.println(F(" C"));
#endif
signalTemperature(aThresholdTemperatureCelsius);
}
// check for periodically signaling current temperature or if temperature changed more than 1 degree
if (((sLastTemperatureCelsius - aCurrentTemperatureCelsius) > 1)
|| (millis() - sLastMillisOfTemperatureSignaling > (DELAY_BETWEEN_TEPERATURE_OUTPUT_SECONDS * MILLIS_IN_ONE_SECOND))) {
sLastMillisOfTemperatureSignaling = millis();
sLastTemperatureCelsius = aCurrentTemperatureCelsius;
#if defined(INFO)
Serial.print(F("Current temperature="));
Serial.print(aCurrentTemperatureCelsius);
Serial.println(F(" C"));
#endif
signalTemperature(aCurrentTemperatureCelsius);
}
}
#define THERMISTOR_NOMINAL_RESISTANCE 10000 // The B25 resistance
#define SERIES_RESISTANCE 10000 // For use with 5V VCC reference
//#define DEFAULT_SERIES_RESISTANCE 22000.0 // Especially for use with 3.3 V supply and 1.1 V reference
#define DEFAULT_BETA_COEFFICIENT 3950 // B25. The bigger, the steeper for temperatures > nominal temperature and less steeper for temperatures < nominal
#define DEFAULT_NOMINAL_TEMPERATURE 25 // 25 degree is default
#define DEFAULT_SAMPLES 4
//#define CIRCUIT_SUPPLY_VOLTAGE_MILLIVOLT 3300 // 3.3V supply of R_NTC | R_series circuit
//#define FULL_ADC_READING_OF_SUPPLY_VOLTAGE ((CIRCUIT_SUPPLY_VOLTAGE_MILLIVOLT * 1023L) / 1100) // 1100 is reference voltage used for ADC reference (INTERNAL)
#define FULL_ADC_READING_OF_SUPPLY_VOLTAGE 1023 // if VCC is used as ADC reference (DEFAULT)
// https://en.wikipedia.org/wiki/Thermistor
// http://www.scynd.de/tutorials/arduino-tutorials/5-sensoren/5-1-temperatur-mit-10k%CF%89-ntc.html
// https://learn.adafruit.com/thermistor/using-a-thermistor - Uses also the simplified B parameter equation.
// https://www.ametherm.com/blog/thermistor/arduino-and-thermistors - calculator for S-H and ß model
float getTemperatureOfNTC(uint8_t aTemperaturePin) {
uint16_t tNTCRaw = analogRead(aTemperaturePin);
/*
* Formula raw reading to resistance: R_NTC / R_series = U_NTC / U_series = AD_reading / (1023 - AD_reading)
* R_NTC = R_series * (AD_reading / (1023 - AD_reading)) => 1 subtraction, 1 division and 1 multiplication
* R_NTC = R_series / ((1023 / AD_reading) - 1) => 1 subtraction and 2 divisions
*/
#if (SERIES_RESISTANCE == THERMISTOR_NOMINAL_RESISTANCE)
float tTemperature = ((float) tNTCRaw / (FULL_ADC_READING_OF_SUPPLY_VOLTAGE - tNTCRaw)); // (R/Ro)
#else
float tRNTCResistance = SERIES_RESISTANCE * ((float) tNTCRaw / (FULL_ADC_READING_OF_SUPPLY_VOLTAGE - tNTCRaw));
/*
* Apply Steinhart Hart equation, based on https://learn.adafruit.com/thermistor/using-a-thermistor
* We use the simplified B parameter equation
*/
float tTemperature = tRNTCResistance / THERMISTOR_NOMINAL_RESISTANCE; // (R/Ro)
#endif
tTemperature = log(tTemperature); // ln(R/Ro)
tTemperature /= DEFAULT_BETA_COEFFICIENT; // 1/B * ln(R/Ro)
tTemperature += 1.0 / (DEFAULT_NOMINAL_TEMPERATURE + 273.15); // + (1/To)
tTemperature = 1.0 / tTemperature; // Invert
tTemperature -= 273.15; // convert Kelvin to Celsius
#if defined(DEBUG)
Serial.print(F("NTCRaw="));
Serial.print(tNTCRaw);
#if (SERIES_RESISTANCE != THERMISTOR_NOMINAL_RESISTANCE)
Serial.print(F(", NTC="));
Serial.print(tRNTCResistance, 0);
Serial.print(F(" ohm, Temperature="));
#else
Serial.print(F(", Temperature="));
#endif
Serial.print(tTemperature, 1);
Serial.println(F(" C"));
#endif
return tTemperature;
}
Desired temperature
Output level
Temperatures