ugokanain
Published © CC BY-NC

Tankless Water Heater temperature limiter

Control to limit the temperature of a tankless WH to prevent triggering the limit switch

IntermediateShowcase (no instructions)2,023
Tankless Water Heater temperature limiter

Things used in this project

Hardware components

Wire, Hook Up
Wire, Hook Up
probably should be ok, but it would be better to use high temperature wire (90°C+). Either teflon or fiber glass. I used one with 3 wires. Also you need high temperature tape. If you can't find one, self-fusing tape (silicone) would work fine as long as you make sure the tape is well placed.
×1
Arduino Leonardo
Arduino Leonardo
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

screen_shot_2020-09-22_at_19_57_28_6RQEY5fJHH.png

The output (led) should be pin 13 (not 3). Also you don't need the resistors, since the heater already has those in it's controller

Code

Temperature limiter

C/C++
It has the code for a lcd shield, you can remove that if you don’t use it.

VtoC converts the voltage from the ADC to a temperature reading. I used the lcd on the Arduino to display the voltage and filmed the values with the temperature displayed by the heater. This is for T2, the output thermistor. Then I curve fitted the data into the function you see here. A fourth degree polynomial was enough for the temperature range I needed. The units should matter and you can use Rankine, Kelvin or Fahrenheit as yours, just adjust the constants throughout the program. You probably want to insure that the temperature is more accurate around the range it activates (max_temp to about max_temp -15°C

Since the heater is supplied by solar panels, the input temperature ranged from cold to about 60°C. You can try to adjust the temperature of T1 (first few lines of the loop function) to match T1 better. However, if you can’t (because it doesn’t change much), you can remove T1 from the program. It basically is a feature for efficiency and safety. It prevents the heater from turning on when the input temperature is high.

To improve measurements and reduce reading artifacts, the code averages the last half second voltages using a stack (the PushPop class template). Allows to only have the data from the last window long number of points (you can change the size using the window constant)

The heater control is done by sending a pulse to the flow sensor input. The Arduino configures a PWM oscillator to send a signal. The pwm… functions set this up. A frequency of 22 Hz was chosen to insure the heater wouldn’t shut-off thinking there wasn’t enough flow. The Arduino starts or stops the signal to control the heater.

There is a delay of 5s to give the time for the water from solar panels time to reach the water heater. If you don’t like this, you can remove it. It also gives time to check that the flow sensor didn’t glitch and send a spurious pulse. The Arduino won’t wake up if it doesn’t detect a flow of water. Either mine is defective or the water pipe caused it to pulse.

The program tries to predict the trend of the temperature doing a curve fit on the data from the stack. It does this trying to get ahead of the heater’s time constant to prevent it from either overheating or get ahead of the time it takes to turn on. Not perfect, and you can still feel the heater turning on and off by the range of temperature in the water.

You might want to adjust the heater’s temperature control to be good enough for most cases (mine is at 51°C) at full flow. With low flow the temperature can reach 60–70°C (depending on how fast the temperature changes, or how restricted is the flow). So don’t try to set the temperature on the heater too high, or it will be turning on and off too often.

The sleepy code puts the Arduino into sleep mode, to save energy
#include <avr/sleep.h>
#include <Arduino.h>
#include <curveFitting.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_pinIO.h> // Arduino pin i/o class header

PROGMEM const bool debug = false;

PROGMEM const int min_flow = 3;
PROGMEM const int max_temp = 69;
PROGMEM const int window = 23;
PROGMEM const int pred_w = 6;
PROGMEM const int order = 2;
const uint8_t flowsensor = 2;
const uint8_t t1_pin = A1;
const uint8_t t2_pin = A2;

// lcd values
const int rs = 8, en = 9, db4 = 4, db5 = 5, db6 = 6, db7 = 7; // for all other devices
hd44780_pinIO lcd(rs, en, db4, db5, db6, db7);
const int BACKLIGHT_PIN = 10;
const int OFF = 0;
const int ON = 1;

#define SafeBLon() pinMode(BACKLIGHT_PIN, INPUT)

void SafeBLoff() {
  digitalWrite(BACKLIGHT_PIN, LOW);
  delay(500);
  pinMode(BACKLIGHT_PIN, OUTPUT);
}


#define PWM366 10   //    366 Hz
#define PWM183 11   //    183 Hz
#define PWM91  12   //     91 Hz
#define PWM45  13   //     45 Hz
#define PWM22  14   //     22 Hz
#define PWM11  15   //     11 Hz

// Direct PWM change variables
#define PWM13       OCR4A


double coeffs[3];
double y[window];
double x[window];

byte adcsra_save = ADCSRA;
byte prr0_save = 0x00;

int t1 = 0;
int t2 = 0;
int t1t = 0;
int t2t = 0;
int t1sum = 0;
int t2sum = 0;
int t1avg = 0;
int t2avg = 0;
int t2last = 0;
int last_zero = 0;
double a = 0;
double m = 0;
double pred = 0;
double pred0 = 0;
unsigned int n = 0;
unsigned long currentTime = 0;
unsigned long cloopTime = 0;
unsigned long lastPulse = 0;
unsigned int seq = 0;
volatile unsigned int count = 0;
unsigned int liters = 0;
String str = "";

char format1[] = "F% 3d a% 4s m% 4s";
char format2[] = "T% 3d P% 4s Q% 4s";


void toLCD (const char* format, int a, double b, double c, int line) {
  static char chr[16] = "";
  static char outstr[6] = "";
  static char outstr1[6] = "";

  dtostrf(b, 3, 1, outstr);
  dtostrf(c, 3, 1, outstr1);

  lcd.setCursor(0, line);

  sprintf(chr, format, a, outstr, outstr1);
  lcd.print(chr);
}

void putLCD (char* format, int line) {
  if (line == 0) {
    format1[0] = format;
    format2[0] = 'T';
  } else {
    format1[0] = 'F';
    format2[0] = format;
  }

}
template <class DATATYPE, unsigned char SIZE> class PushPop {
  private:
    const unsigned char m_Size = SIZE - 1;
    DATATYPE m_Array[SIZE];
    int m_Index;
  public:
    PushPop() {
      m_Index = -1;
    }
    void Zero() {
      m_Index = -1;
      for (int i = 0; i < m_Size; i++) {
        m_Array[i] = 210;
      }
    }
    void Push(DATATYPE what) {
      if (m_Index >= m_Size) {
        Pop();
      }
      m_Index = min(m_Size, m_Index++);
      m_Array[m_Index] = what;
    }
    DATATYPE Pop() {
      for (int i = 0; i < m_Index; i++) {
        m_Array[i] = m_Array[i + 1];
      }
      m_Index--;
      return m_Array[m_Index];
    }
    DATATYPE Walk(int i) {
      return m_Array[i];
    }
    DATATYPE Last() {
      return m_Array[m_Index];
    }
    void printpp() {
      for (int i = 0; i <= m_Index; i++) {
        Serial.print(m_Array[i]);
        Serial.print('\t');
      }
      Serial.println();
    }
    DATATYPE avg(int n) {
      DATATYPE sum = 0;
      if (n == 0) {
        n = m_Index;
      }
      int i;
      for (i = m_Index - n ; i <= m_Index; i++) {
        sum = sum + m_Array[i];
      }
      return sum / (n + 1);
    }
    void getmat(DATATYPE mat[]) {
      for (int i = 0; i <= m_Index; i++) {
        mat[i] = m_Array[i];
      }
    }
    int getsize() {
      return m_Index;
    }

    double slope(int n) {
      if (n == 0) {
        n = m_Index;
      }
      n = m_Index - n;
      return (  m_Array[m_Index] - m_Array[n] ) / m_Index;
    }
};

// Configure the PWM clock
// The argument is one of the 7 previously defined modes
void pwm613configure()
{
  // TCCR4A configuration
  TCCR4A = 0;

  // TCCR4B configuration
  TCCR4B = 0;

  // TCCR4C configuration
  TCCR4C = 0;

  // TCCR4D configuration
  TCCR4D = 0;

  TCCR4E |= (1 << ENHC4);

  // PLL Configuration
  // Use 96MHz / 2 = 48MHz
  PLLFRQ = (PLLFRQ & 0xCF) | 0x30;
  // PLLFRQ=(PLLFRQ&0xCF)|0x10; // Will double all frequencies

  // Terminal count for Timer 4 PWM
  OCR4C = 255;
}

// Set PWM to D13 (Timer4 A)
// Argument is PWM between 0 and 255
void pwmSet13()
{
  if (TCCR4B == 0) {
    TCCR4B = PWM91;
    OCR4A = 255; // Set PWM value
    DDRC |= 1 << 7; // Set Output Mode C7
    TCCR4A = 0x82; // Activate channel A
  }
  delay(10);
  if (debug) {
    Serial.println("pwmset");
  }
}

void pwmStop13()
{
  TCCR4B = 0;
  digitalWrite(13, LOW);
  delay(10);

  if (liters < min_flow) {
    seq = 0;
  }
}

void flow() {
  count++;
}

void wake() {
  sleep_disable();
  detachInterrupt(digitalPinToInterrupt(flowsensor));
}

int VtoC (int temp) {
  double f = 1.81176e-10;
  f = f * temp;
  f = f - 5.03596e-7;
  f = f * temp;
  f = f + 0.0005340536;
  f = f * temp;
  f = f - 0.33203628;
  f = f * temp;
  f = f + 110.4503125;
  if (f > 100 or f < 0 ) {
    f = 0;
  }
  return (int(f));
}


PushPop<double, window> t2s;

void setup() {
  if (debug) {
    Serial.begin(9600);
    delay(3000);
    Serial.println("Starting…");

  }

  analogReference((int)DEFAULT);

  lcd.begin(16, 2);              // Initialize LCD for 16 character x 2 line operation

  lcd.clear();

  pwm613configure();
  pwmStop13();
  pinMode(flowsensor, INPUT);
  pinMode(t1_pin, INPUT);
  pinMode(t2_pin, INPUT);

  delay(3000);

  lcd.setCursor(0, 0);
  lcd.print("Starting...");

  SafeBLon();

  attachInterrupt(digitalPinToInterrupt(flowsensor), flow, RISING);
  sei(); // Enable interrupts
  currentTime = millis();
  cloopTime = currentTime;
  lastPulse = currentTime;
  t1sum = 0;
  t2sum = 0;
  n = 0;
  t2last = 0;

  for (int i = 0; i < window; i++) {
    x[i] = i;
  }

}

void loop() {

new_cycle:
  currentTime = millis();

  yield();

  delay(15);
  t1 = analogRead(t1_pin);
  t1 = 1.11 * t1 - 120.2;

  delay(25);
  t2 = analogRead(t2_pin);

  t1sum = t1sum + t1;
  t2sum = t2sum + t2;
  n++;

  t1avg = t1sum / n;
  t2avg = t2sum / n;

  t1t = VtoC(t1avg);
  t2t = VtoC(t2avg);

  if ((liters <= min_flow) or (t2t > max_temp)) {
    if (debug) {
      Serial.println("immediate stop");
    }

    pwmStop13();
  }

  if (currentTime >= (cloopTime + 500)) {

  //  analogReference((int)DEFAULT);
    
    t2s.Push(t2t);

    cloopTime = currentTime; // Updates cloopTime
    // Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min.
    //     liters = (count / 7.5 ); // (Pulse frequency ) / 7.5Q = flowrate in L
    noInterrupts();
    liters = count;
    count = 0; // Reset Counter
    interrupts();

    //    liters = 12;

    if (liters > min_flow) {
      seq++;
      lastPulse = currentTime;
      last_zero = 0;
    } else {
      if (last_zero > 2) {
        seq = 0;
      }
      last_zero++;
    }
    if (debug) {
      str = "liters: " + String(liters) + " t1t: " + String(t1t) + " t2t: " + String(t2t) + " seq: " + String(seq) + " n: " + String(n);
      Serial.println(str);
    }

	  t1sum = 0;
    t2sum = 0;
    n = 0;

    if ((liters <= min_flow) or (t2t > max_temp + 2) or (t1t >= 48)) {
      if (debug) {
        Serial.println("stop");
        Serial.println("seq " + String(seq));
      }
      pwmStop13();
    } else if (seq >= 5) {
      t2s.getmat(y);
      if (fitCurve(order, t2s.getsize(), x, y, sizeof(coeffs) / sizeof(double), coeffs) == 0 and !isnan(coeffs[0])) {
        pred  = (coeffs[0] * (window + pred_w) + coeffs[1]) * (window + pred_w) + coeffs[2];
        pred0 = (coeffs[0] * (window + 1) + coeffs[1]) * (window + 1) + coeffs[2];
        a = t2s.avg(10);
        m = t2s.slope(4) * 2;


        if (max(a, t2t) > (max_temp)) {
           if (debug) {
              Serial.println("    +Powering off");
            }
            putLCD ('M', 1);
            pwmStop13();
        } else if (min(a, t2t) < (max_temp - 12)) {
          if (debug) {
              Serial.println("    -Powering on");
            }
            putLCD ('m', 0);
            pwmSet13();
        } else if (m > 0.2) {
          if (max(a, t2t) > (max_temp -10) and pred > (max_temp -5) and t2t < (pred0 + 1) and (pred0 + 1) < pred and pred > (a + 2)) {
            if (debug) {
              Serial.println("    Powering off");
            }
            putLCD ('.', 1);
            pwmStop13();
          } else if (min(a, t2t) < (max_temp -3) and pred < (max_temp -8) and t2t > pred0 and pred0 > (pred + 1) ) {
            if (debug) {
              Serial.println("    Powering on");
            }
            putLCD ('.', 0);
            pwmSet13();
          }
        } else if  (m <= -0.1) {
          if (max(a, t2t) > (max_temp - 10) and pred > (max_temp -5) and t2t < (pred0 + 1) and (pred0 + 1) < pred) {
            if (debug) {
              Serial.println("    *Powering off");
            }
            putLCD (':', 1);
            pwmStop13();
          } else if (max(a, t2t) >= (max_temp -10) and t2t <= max_temp and pred < (max_temp -5) and a >= (pred + 6) and t2t > pred0 and pred0 > (pred + 1)) {
            if (debug) {
              Serial.println("    *Powering on");
            }
            putLCD (':', 0);
            pwmSet13();
          } else if ((t2t + 1) < a and t2t < max_temp and a >= (pred + 3) and t2t > pred0 and pred0 > (pred + 1)) {
            if (debug) {
              Serial.println("    *Powering on *");
            }
            putLCD ('-', 0);
            pwmSet13();
          }
        } else {
          if ((a <= t2t and t2t > (max_temp -4) and pred >= (a + 2) and pred0 < pred) or (t2t > (max_temp - 2))) {
            if (debug) {
              Serial.println("    **Powering off");
            }
            putLCD ('*', 1);
            pwmStop13();
          } else if (t2t <= (max_temp -4) and t2t < a and (pred + 2) < a and pred0 >= pred) {
            if (debug) {
              Serial.println("    **Powering on");
            }
            putLCD ('*', 0);
            pwmSet13();
          }
        }
      }
    }

	toLCD (format1, liters, a, m, 0);
	toLCD (format2, t2t, pred, pred0, 1);

    if ((currentTime >= (lastPulse + 300000)) and (liters < min_flow)) {
sleepy:
      if (debug) {
        Serial.println("sleepy");
      }
      SafeBLoff();
      delay(500);
      noInterrupts();
      detachInterrupt(digitalPinToInterrupt(flowsensor));
      set_sleep_mode(SLEEP_MODE_PWR_DOWN);
      sleep_enable();
      attachInterrupt(digitalPinToInterrupt(flowsensor), wake, RISING);
      interrupts();

      adcsra_save = ADCSRA;
      prr0_save = PRR0;

      pwmStop13();

      ADCSRA &= ~(1 << ADEN);    // Disable ADC
      PRR0 = 0xFF;   // Power down functions

      sleep_mode();
      sleep_disable();
      PRR0 = 0x00; //modules on
      ADCSRA |= (1 << ADEN); // adc on

      ADCSRA = adcsra_save; // stop power reduction
      PRR0 = prr0_save;


      noInterrupts();
      attachInterrupt(digitalPinToInterrupt(flowsensor), flow, RISING);
      interrupts();

      currentTime = millis();
      cloopTime = currentTime;
      int times = 0;
      count = 0;
      liters = 0;
      int i = 0;
      t2s.Zero();

      for (i = 0; i < 6; i++) {
        while (currentTime < (cloopTime + 1000)) {
          yield();
          t2s.Push(VtoC(analogRead(t2_pin)));
          delay(25);
          currentTime = millis();
        }
        if (count > 0) {
          noInterrupts();
          liters = liters + count;
          count = 0; // Reset Counter
          interrupts();
          times = times + 1;
        }
        if ((liters > 50) and (times > 2)) {
          break;
        }
        cloopTime = currentTime;
      }

      if ((times < 5) and (liters < 30)) {
        goto sleepy;
      }

      seq = times;

      currentTime = millis();
      lastPulse = currentTime;

      SafeBLon();
      delay(100);
      t1sum = 0;
      t2sum = 0;
      n = 0;

    }
  }
}

Credits

ugokanain
0 projects • 0 followers

Comments