HVAC thermostat on an Arduino Uno
A two-stage heat and cool thermostat on an Uno: W1, W2, Y1, Y2 and G on LEDs, compressor minimum on and off times, fan delays and a heat and cool interlock (a low-voltage teaching model, not a real 24 VAC thermostat). The whole build, an Arduino Uno and 15 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 50 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $36. On Mokxi they're free.
What you'll learn
- Sensors
- School
- Reading a knob with an analog pin
- Choosing and reading a resistor
- How an LED lights and why it needs a resistor
- Difficulty: Advanced
- Time: About 50 minutes
- Board: Arduino Uno R3
- Parts: 17
How it works
A two-stage heat and cool thermostat on an Uno: W1, W2, Y1, Y2 and G on LEDs, compressor minimum on and off times, fan delays and a heat and cool interlock (a low-voltage teaching model, not a real 24 VAC thermostat).
Arduino Uno R3 runs the sketch below. The circuit around it: Temperature sensor, DS18B20, Slide switch, Potentiometer, Resistor, 220 Ω, LED, red, LED, blue. pinMode sets PIN_MODE_BIT0 as an input with its pull-up on. digitalWrite drives outs[i] LOW. digitalRead reads whether PIN_MODE_BIT1 is HIGH or LOW. analogRead measures the voltage on PIN_HEAT_KNOB as a number. Serial.begin opens the serial port at 115200 baud.
Parts list
17 parts, plus the jumper wires. They retail for about $36; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × Temperature sensor, DS18B20
- 2 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
- 2 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
- 5 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 2 × LED, redIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 2 × LED, blueIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Resistor, 220 Ω (5) pin 1 | Arduino Uno R3 pin 10 |
| Resistor, 220 Ω (4) pin 1 | Arduino Uno R3 pin 9 |
| Resistor, 220 Ω (3) pin 1 | Arduino Uno R3 pin 8 |
| Resistor, 220 Ω (2) pin 1 | Arduino Uno R3 pin 7 |
| Resistor, 220 Ω (1) pin 1 | Arduino Uno R3 pin 6 |
| Slide switch (2) pin 2 | Arduino Uno R3 pin 5 |
| Slide switch (1) pin 2 | Arduino Uno R3 pin 4 |
| Temperature sensor, DS18B20 pin DQ | Arduino Uno R3 pin 2 |
| Temperature sensor, DS18B20 pin VDD | Arduino Uno R3 pin 5V |
| Potentiometer (1) pin 3 | Arduino Uno R3 pin 5V |
| Potentiometer (2) pin 3 | Arduino Uno R3 pin 5V |
| Temperature sensor, DS18B20 pin GND | Arduino Uno R3 pin GND |
| Slide switch (1) pin 3 | Arduino Uno R3 pin GND |
| Slide switch (2) pin 3 | Arduino Uno R3 pin GND |
| Potentiometer (1) pin 1 | Arduino Uno R3 pin GND |
| Potentiometer (2) pin 1 | Arduino Uno R3 pin GND |
| LED, red (1) pin C | Arduino Uno R3 pin GND |
| LED, red (2) pin C | Arduino Uno R3 pin GND |
| LED, blue (1) pin C | Arduino Uno R3 pin GND |
| LED, blue (2) pin C | Arduino Uno R3 pin GND |
| LED, green pin C | Arduino Uno R3 pin GND |
| Potentiometer (1) pin 2 | Arduino Uno R3 pin A0 |
| Potentiometer (2) pin 2 | Arduino Uno R3 pin A1 |
| LED, red (1) pin A | Resistor, 220 Ω (1) pin 2 |
| LED, red (2) pin A | Resistor, 220 Ω (2) pin 2 |
| LED, blue (1) pin A | Resistor, 220 Ω (3) pin 2 |
| LED, blue (2) pin A | Resistor, 220 Ω (4) pin 2 |
| LED, green pin A | Resistor, 220 Ω (5) pin 2 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// HVAC thermostat: a two-stage heat and cool thermostat that behaves like a
// real one, with the compressor protected and the fan on its delays.
//
// THIS IS A TEACHING MODEL. A real thermostat switches 24 VAC with relays or
// triacs between R (the 24 V hot from the transformer) and the call wires,
// with C as the transformer's common. Here the calls are five LEDs on 5 V
// logic, and none of that 24 VAC wiring is modeled beyond using its names.
// Never connect this to a real furnace, air conditioner or heat pump.
//
// pin 2 DS18B20 data (4.7 kohm pull-up to 5 V), the room temperature
// pin 4 mode switch, bit 0 (switch to GND = 1)
// pin 5 mode switch, bit 1 (switch to GND = 1)
// A0 heat setpoint knob 55 to 80 F
// A1 cool setpoint knob 65 to 90 F
// pin 6 W1 heat, first stage (red LED)
// pin 7 W2 heat, second stage (red LED)
// pin 8 Y1 cool, compressor (blue LED)
// pin 9 Y2 cool, second stage (blue LED)
// pin 10 G fan (green LED)
//
// MODE (bit 1, bit 0): 00 OFF 01 HEAT 10 COOL 11 AUTO
//
// STAGES, in whole degrees F around the setpoint
// Heat W1 on at setpoint - 1, off at the setpoint (1 F of hysteresis).
// W2 on when the gap grows to 3 F, or when W1 has run past the stage
// timer and the room is still 1 F short; W2 off again at 0.5 F short.
// Cool Y1 on at setpoint + 1, off at the setpoint.
// Y2 on at +3 F, or after the stage timer with the room still +1 F;
// off again at +0.5 F.
// AUTO uses both setpoints; the cool setpoint is held at least 3 F above
// the heat setpoint so the two can never chase each other.
//
// COMPRESSOR PROTECTION (Y1 and Y2)
// Minimum off time: after the compressor stops, Y1 will not start again
// until the time is up, and also not for that long after power-up. A call
// that wants it sooner just waits.
// Minimum on time: once Y1 starts it runs at least that long, even if the
// room is satisfied or the mode is switched away.
//
// FAN (G)
// Cooling: G comes on with the Y1 call and runs on for the cooling fan
// off-delay after Y1 drops, to blow the last of the cold off the coil.
// Heating: G waits for the heating fan on-delay after W1, so the heat
// exchanger warms up before the blower pushes air through it, and drops
// with the call. (A real furnace runs its own off-delay as well.)
//
// INTERLOCK
// Heat and cool are never called together. W1 will not start while Y1 or
// Y2 is on, and Y1 will not start while W1 or W2 is on, so a changeover
// (a mode switch from COOL to HEAT, say) waits for the first side to stop.
//
// SCALED TIME
// The real times are a 3 minute minimum off, a 2 minute minimum on, a 60 s
// cooling fan off-delay, a 30 s heating fan on-delay and a 10 minute stage
// timer. So that a simulation shows them in seconds, every one is divided
// by SCALE (20): 9 s, 6 s, 3 s, 1.5 s and 30 s here. Set SCALE to 1 for the
// real times.
//
// If the sensor is missing the thermostat calls for nothing (a fault line is
// printed), and the compressor still honors its minimum on time.
#include <OneWire.h>
#include <DallasTemperature.h>
const int PIN_SENSOR = 2;
const int PIN_MODE_BIT0 = 4;
const int PIN_MODE_BIT1 = 5;
const int PIN_W1 = 6;
const int PIN_W2 = 7;
const int PIN_Y1 = 8;
const int PIN_Y2 = 9;
const int PIN_G = 10;
const int PIN_HEAT_KNOB = A0;
const int PIN_COOL_KNOB = A1;
const unsigned long SCALE = 20;
const unsigned long MIN_OFF_MS = 180000UL / SCALE;
const unsigned long MIN_ON_MS = 120000UL / SCALE;
const unsigned long COOL_FAN_OFF_MS = 60000UL / SCALE;
const unsigned long HEAT_FAN_ON_MS = 30000UL / SCALE;
const unsigned long STAGE2_TIMER_MS = 600000UL / SCALE;
// Temperatures are whole tenths of a degree F.
const int STAGE1_BAND = 10;
const int STAGE2_BAND = 30;
const int STAGE2_RELEASE = 5;
const int AUTO_GAP = 30;
enum Mode { OFF, HEAT, COOL, AUTO };
const char *const MODE_NAME[] = {"OFF", "HEAT", "COOL", "AUTO"};
OneWire oneWire(PIN_SENSOR);
DallasTemperature sensors(&oneWire);
DeviceAddress room;
bool haveSensor = false;
bool w1 = false, w2 = false, y1 = false, y2 = false, g = false;
unsigned long w1On = 0, y1On = 0;
unsigned long y1OffAt = 0; // 0 at power-up: the minimum off time runs from boot
unsigned long coolFanUntil = 0;
bool holdW1 = false, holdY1Start = false, holdY1Stop = false;
int tempTenths = 700;
bool tempOk = false;
bool converting = false;
unsigned long requestedAt = 0;
unsigned long lastStatus = 0;
int heatSp = 70, coolSp = 75;
int coolEff = 75; // the cool setpoint in use (AUTO keeps it 3 F above heat)
void printTenths(int t) {
Serial.print(t / 10);
Serial.print('.');
Serial.print(t % 10);
}
void drive(int pin, bool &state, bool next, const char *name) {
if (state == next) return;
state = next;
digitalWrite(pin, next ? HIGH : LOW);
Serial.print(name);
Serial.print(next ? " on T " : " off T ");
printTenths(tempTenths);
Serial.println("F");
}
// Print a hold line once, when the wait begins.
void note(bool &flag, bool holding, const char *text, unsigned long msLeft) {
if (holding && !flag) {
Serial.print("HOLD ");
Serial.print(text);
if (msLeft > 0) {
Serial.print(" (");
Serial.print((msLeft + 999) / 1000);
Serial.print(" s left)");
}
Serial.println();
}
flag = holding;
}
void control(int mode) {
unsigned long now = millis();
int hsp = heatSp * 10;
int csp = coolSp * 10;
if (mode == AUTO && csp < hsp + AUTO_GAP) csp = hsp + AUTO_GAP;
coolEff = csp / 10;
int t = tempTenths;
bool heatMode = tempOk && (mode == HEAT || mode == AUTO);
bool coolMode = tempOk && (mode == COOL || mode == AUTO);
// Heat: W1 first, so a changeover releases the heat side before cool asks.
bool wantW1 = heatMode && (w1 ? t < hsp : t <= hsp - STAGE1_BAND);
bool holding = false;
if (wantW1 && !w1) {
if (y1 || y2) {
holding = true;
} else {
drive(PIN_W1, w1, true, "W1");
w1On = now;
}
} else if (!wantW1 && w1) {
drive(PIN_W2, w2, false, "W2");
drive(PIN_W1, w1, false, "W1");
}
note(holdW1, holding, "W1: cooling is still on", 0);
bool wantW2 = w1 && wantW1 &&
(w2 ? t < hsp - STAGE2_RELEASE
: (t <= hsp - STAGE2_BAND ||
(now - w1On >= STAGE2_TIMER_MS && t <= hsp - STAGE1_BAND)));
drive(PIN_W2, w2, wantW2, "W2");
// Cool: Y1 is the compressor contactor.
bool wantY1 = coolMode && (y1 ? t > csp : t >= csp + STAGE1_BAND);
holding = false;
if (wantY1 && !y1) {
if (w1 || w2) {
holding = true;
} else if (now - y1OffAt < MIN_OFF_MS) {
note(holdY1Start, true, "Y1: compressor minimum off", MIN_OFF_MS - (now - y1OffAt));
holding = true;
} else {
drive(PIN_Y1, y1, true, "Y1");
y1On = now;
}
}
if (!(wantY1 && !y1)) holdY1Start = false;
if (!wantY1 && y1) {
if (now - y1On < MIN_ON_MS) {
note(holdY1Stop, true, "Y1: compressor minimum on", MIN_ON_MS - (now - y1On));
} else {
holdY1Stop = false;
drive(PIN_Y2, y2, false, "Y2");
drive(PIN_Y1, y1, false, "Y1");
y1OffAt = now;
}
} else {
holdY1Stop = false;
}
bool wantY2 = y1 && wantY1 &&
(y2 ? t > csp + STAGE2_RELEASE
: (t >= csp + STAGE2_BAND ||
(now - y1On >= STAGE2_TIMER_MS && t >= csp + STAGE1_BAND)));
drive(PIN_Y2, y2, wantY2, "Y2");
// Fan: with cooling at once and for the off-delay after; for heat, after
// the on-delay.
if (y1) coolFanUntil = now + COOL_FAN_OFF_MS;
bool fan = y1 || (long)(coolFanUntil - now) > 0 || (w1 && now - w1On >= HEAT_FAN_ON_MS);
drive(PIN_G, g, fan, "G");
}
void readSensor() {
if (!haveSensor) return;
if (!converting) {
sensors.requestTemperatures();
requestedAt = millis();
converting = true;
} else if (millis() - requestedAt >= 800) {
converting = false;
long raw = sensors.getTemp(room);
if (raw == DEVICE_DISCONNECTED_RAW) {
if (tempOk) Serial.println("FAULT: sensor not answering, no calls");
tempOk = false;
} else {
// raw is 1/128 C; F = C * 1.8 + 32, in tenths.
tempTenths = (int)((raw * 18 + (raw < 0 ? -64 : 64)) / 128) + 320;
tempOk = true;
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_MODE_BIT0, INPUT_PULLUP);
pinMode(PIN_MODE_BIT1, INPUT_PULLUP);
const int outs[] = {PIN_W1, PIN_W2, PIN_Y1, PIN_Y2, PIN_G};
for (int i = 0; i < 5; i++) {
pinMode(outs[i], OUTPUT);
digitalWrite(outs[i], LOW);
}
Serial.println("Mokxi Arduino Uno: HVAC thermostat (teaching model, 20x faster timers)");
sensors.begin();
sensors.setWaitForConversion(false);
haveSensor = sensors.getAddress(room, 0);
if (!haveSensor) Serial.println("FAULT: no DS18B20 on pin 2");
Serial.println("compressor lockout after power-up: Y1 waits for the minimum off time");
}
void loop() {
int mode = (digitalRead(PIN_MODE_BIT1) == LOW ? 2 : 0) | (digitalRead(PIN_MODE_BIT0) == LOW ? 1 : 0);
heatSp = 55 + (analogRead(PIN_HEAT_KNOB) * 25L + 511) / 1023;
coolSp = 65 + (analogRead(PIN_COOL_KNOB) * 25L + 511) / 1023;
readSensor();
control(mode);
if (millis() - lastStatus >= 1000) {
lastStatus = millis();
Serial.print("T ");
printTenths(tempTenths);
Serial.print("F ");
Serial.print(MODE_NAME[mode]);
Serial.print(" heat ");
Serial.print(heatSp);
Serial.print(" cool ");
Serial.print(coolEff);
Serial.print(" W1 ");
Serial.print(w1);
Serial.print(" W2 ");
Serial.print(w2);
Serial.print(" Y1 ");
Serial.print(y1);
Serial.print(" Y2 ");
Serial.print(y2);
Serial.print(" G ");
Serial.println(g);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
Serial.begin(115200);
pinMode(PIN_MODE_BIT0, INPUT_PULLUP);
pinMode(PIN_MODE_BIT1, INPUT_PULLUP);
const int outs[] = {PIN_W1, PIN_W2, PIN_Y1, PIN_Y2, PIN_G};
for (int i = 0; i < 5; i++) {
pinMode(outs[i], OUTPUT);
digitalWrite(outs[i], LOW);
}
Serial.println("Mokxi Arduino Uno: HVAC thermostat (teaching model, 20x faster timers)");
sensors.begin();
sensors.setWaitForConversion(false);
haveSensor = sensors.getAddress(room, 0);
if (!haveSensor) Serial.println("FAULT: no DS18B20 on pin 2");
Serial.println("compressor lockout after power-up: Y1 waits for the minimum off time");
}loop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
int mode = (digitalRead(PIN_MODE_BIT1) == LOW ? 2 : 0) | (digitalRead(PIN_MODE_BIT0) == LOW ? 1 : 0);
heatSp = 55 + (analogRead(PIN_HEAT_KNOB) * 25L + 511) / 1023;
coolSp = 65 + (analogRead(PIN_COOL_KNOB) * 25L + 511) / 1023;
readSensor();
control(mode);
if (millis() - lastStatus >= 1000) {
lastStatus = millis();
Serial.print("T ");
printTenths(tempTenths);
Serial.print("F ");
Serial.print(MODE_NAME[mode]);
Serial.print(" heat ");
Serial.print(heatSp);
Serial.print(" cool ");
Serial.print(coolEff);
Serial.print(" W1 ");
Serial.print(w1);
Serial.print(" W2 ");
Serial.print(w2);
Serial.print(" Y1 ");
Serial.print(y1);
Serial.print(" Y2 ");
Serial.print(y2);
Serial.print(" G ");
Serial.println(g);
}
// ... 1 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The sensor reads -127 C or no device is found. Temperature sensor
Put a 4.7 k resistor between the data line and VCC. Check the pinout, since swapping VCC and GND can overheat the sensor. Then confirm the data pin matches your code. - It always shows 85 C. Temperature sensor
85 C is the power on value, meaning the conversion has not finished or the supply is weak. Wait for the conversion (up to 750 ms at 12 bit) before reading. - The input reads random values in one position. Slide switch
A switch only connects or disconnects. Add a pull-up or pull-down resistor, or use INPUT_PULLUP, so the pin has a defined level when the switch is open. - The load never turns on when the switch is flipped. Slide switch
Use the center (common) leg and one outer leg. The unused outer leg goes nowhere. Check that the switch is in series with the load. - The reading jumps around or only covers part of the range. Potentiometer
Wire the two outer legs to 5 V and ground and the middle leg (the wiper) to an analog pin. Using the wrong two legs gives a dead or partial range. - Turning the knob does nothing, or the value is stuck at 0 or 1023. Potentiometer
Check that the wiper goes to an analog input (A0 to A5 on an Uno), not a digital pin. Make sure both ends are connected, since a floating end gives noise. - The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure. - A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.