Built-in project · Arduino Uno R3

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.

The circuit runs here once the page has loaded.

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.

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 pinConnects to
Resistor, 220 Ω (5) pin 1Arduino Uno R3 pin 10
Resistor, 220 Ω (4) pin 1Arduino Uno R3 pin 9
Resistor, 220 Ω (3) pin 1Arduino Uno R3 pin 8
Resistor, 220 Ω (2) pin 1Arduino Uno R3 pin 7
Resistor, 220 Ω (1) pin 1Arduino Uno R3 pin 6
Slide switch (2) pin 2Arduino Uno R3 pin 5
Slide switch (1) pin 2Arduino Uno R3 pin 4
Temperature sensor, DS18B20 pin DQArduino Uno R3 pin 2
Temperature sensor, DS18B20 pin VDDArduino Uno R3 pin 5V
Potentiometer (1) pin 3Arduino Uno R3 pin 5V
Potentiometer (2) pin 3Arduino Uno R3 pin 5V
Temperature sensor, DS18B20 pin GNDArduino Uno R3 pin GND
Slide switch (1) pin 3Arduino Uno R3 pin GND
Slide switch (2) pin 3Arduino Uno R3 pin GND
Potentiometer (1) pin 1Arduino Uno R3 pin GND
Potentiometer (2) pin 1Arduino Uno R3 pin GND
LED, red (1) pin CArduino Uno R3 pin GND
LED, red (2) pin CArduino Uno R3 pin GND
LED, blue (1) pin CArduino Uno R3 pin GND
LED, blue (2) pin CArduino Uno R3 pin GND
LED, green pin CArduino Uno R3 pin GND
Potentiometer (1) pin 2Arduino Uno R3 pin A0
Potentiometer (2) pin 2Arduino Uno R3 pin A1
LED, red (1) pin AResistor, 220 Ω (1) pin 2
LED, red (2) pin AResistor, 220 Ω (2) pin 2
LED, blue (1) pin AResistor, 220 Ω (3) pin 2
LED, blue (2) pin AResistor, 220 Ω (4) pin 2
LED, green pin AResistor, 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.

sketch.ino · Arduino Uno R3
// 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 editor

Troubleshooting

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.