/ arduino

Arduino thermostat for a 'passive' 24VAC HVAC system, Part 3


The overall circuitry for the thermostat is relatively simple, containing six major areas of concern.

  • The Arduino UNO
  • The thermistor duct temperature sensor
  • The TMP36 room temperature sensor
  • The relay module
  • The duct damper
  • The 24VAC power to the duct damper controller

Here's a simplified diagram of how it's all connected (updated 26 March 2017 due to missing connection from center lead on TMP36):

Thermostat wiring diagram

I've represented the damper controller as a motor, which is a gross understatement of what the damper controller actually is. In particular, note that there is no 'open' vs. 'close' capability in the diagram. Just assume that when the relay is 'off', the damper closes and when the relay is 'on' the damper opens.

The duct thermistor is the two-wire thermistor at the top of the diagram, whereas the TMP36 room temperature sensor can be seen on the right side of the breadboard.

I'm also only showing a two relay module, although as I mentioned in Part 2, I actually used a four relay module.

Here's what the assembled version actually looks like:



One consideration that I had to make was with respect to which voltages to use, and where. The relay module requires 5V to operate. While I could have used the same 5V for both the relay module and the rest of the circuits, in the past I found this to be problematic due to current fluctuations and other noise -- especially when trying to deal with temperature sensors. So, instead, I opted to make use of the available 3.3V has the reference for the temperature circuits.

This was relatively easy to wire up. I connected the power side of the breadboard to the 3.3V Arduino UNO power pin, and also connected the AREF analog reference pin to the same power. When doing so, it is necessary to tell the Ardunio that it has an external analog reference, which is easy to do in setup().

void setup() {

Reading Temperatures

Reading the room temperature is the easier of two temperature readings, thanks to the design of the TMP36. However, because the TMP36 is being powered by 3.3V instead of 5V, we need to make sure the temperature calculations are correct.

But, beyond that, it's not safe to hardcode the operating voltage in to the formulas, anyway, even if you know you've wired 3.3V or 5V. Voltage within an Arduino fluctuates -- in large part due to the fact that the voltage coming into the Arduino fluctuates. When you connect your Arduino via USB to do debugging, it's highly unlikely that your PC is sending a perfectly clean 5V.

As a result, I like to use the readVcc() function which you can find all over the web. It attempts to read the current voltage in the Arduino. This seems to work well with the UNO; your mileage may vary.

int readVcc()
// Calculate current Vcc in mV from the 1.1V reference voltage
  long result;

  // Read 1.1V reference against AVcc
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA, ADSC));
  result = ADCL;
  result |= ADCH << 8;
  result = 1126400L / result; // Back-calculate AVcc in mV

  return (result);

With that little bit of magic at our disposal, reading the room temperature from the TMP36 is straightforward, assuming it is connected to the pin defined by TMPPIN.

float getRoomTemp()
  float vcc = readVcc() / 1000.0f;

  float reading = analogRead(TMPPIN);
  float voltage = (reading / 1024.0) * vcc;

  return (voltage - .5) * 100 + .5;

This returns the room temperature in Centigrade.

For the duct temperature, things are a little bit more complicated. Recall the we constructed a voltage divider using the thermistor in the duct and a resistor. To convert the analog input reading from the voltage divider into a temperature, we use the Steinhart-Hart equation. However, when dealing with analog inputs, it's common for any individual spot reading to be noisy. As such, it's often worthwhile to make several readings and average them.

Assuming that we'll take NUMSAMPLES samples to calculate the average, that the voltage divider is connected to analog input pin THERMISTORPIN, the nominal resistance (at 25°C) of the thermistor is THERMISTORNOMINAL, the resistor completing the voltage divider has a resistance of SERIESRESISTOR, and the b coefficient of the Steinhart-Hart question for the thermistor is BCOEFFICIENT, the duct temperature can be measured this way:

float getDuctTemp()
  // reading the duct termistor can be noisy
  // so grab a few samples and average them
  uint8_t i;
  float average;

  // take N samples in a row, with a slight delay
  for (i = 0; i < NUMSAMPLES; i++) {
    samples[i] = analogRead(THERMISTORPIN);

  // average all the samples out
  average = 0;
  for (i = 0; i < NUMSAMPLES; i++) {
    average += samples[i];
  average /= NUMSAMPLES;
  if (average < minread) minread = average;
  if (average > maxread) maxread = average;

  // convert the value to resistance
  average = 1023 / average - 1;
  average = SERIESRESISTOR / average;

  // compute the temperature via Steinhart-Hart equation
  float steinhart;
  steinhart = average / THERMISTORNOMINAL;     // (R/Ro)
  steinhart = log(steinhart);                  // ln(R/Ro)
  steinhart /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
  steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
  steinhart = 1.0 / steinhart;                 // Invert
  steinhart -= 273.15;                         // convert from K to C

  return steinhart;

Controlling the Damper

Opening and closing the damper is extremely simple. The damper has two inputs -- Rc and Ro -- which, when supplied with 24VAC, close or open the damper, respectively. As we are using a relay which has both a normally open and a normally closed output, it's simply a matter of either turning the relay on or off two switch between these two states. As such, assuming the relay is controlled by the output pin DAMPER_RELAY_PIN, the damper closing and opening code is extremely simple:

void closeDamper()
  digitalWrite(DAMPER_RELAY_PIN, HIGH);

void openDamper()
  digitalWrite(DAMPER_RELAY_PIN, LOW);

Next Step

Armed with a reliable way to read the two temperatures (room and duct) and a way to open and close the damper, the next step is to implement the logic for determining what to do and when to do it.