Управление МК Arduino циркуляционным насосом для полотенцесушителя

Возникла необходимость установки циркуляционного насоса в полотенцесушителе, поскольку через некоторое время после ремонта горячая вода перестала самотёком заходить в него. Был куплен сам насос и смонтирован куда надо, с физикой всё в порядке. А вот с менеджментом и экономикой стало как-то грустно: даже заявленные 25 Вт мощности насоса выливаются в 219 кВт*ч потребления за год, или, если в деньгах - примерно в 800 рублей добавочных расходов. Уменьшить энергопотребление можно различными путями: 1. Заменить насос на менее производительный и маломощный; 2. запускать насос эпизодически, по мере надобности (остывания полотенцесушителей). С первым пунктом всё сложно: чем ниже энергопотребление, тем выше цены. Второй пункт можно реализовать при помощи механических, электронных таймеров (300-700 рублей) или термореле ( >2000 рублей). Бытовые таймеры страдают либо грубым квантованием времени (шаг 15 минут в суточных механических) или недостаточным количеством программ (порядка 20 в электронных), ну а готовые термореле дороговаты и требуют перенастройки при изменении температуры теплоносителя.

В связи с вышесказанным родилась хотелка самонастраиваемого устройства регулировки работы насоса. Предъявляемые требования: низкое энергопотребление, автоматическая адаптация к температуре теплоносителя, самонастройка длительности времени работы/простоя.

Для реализации использовал МК семейства Arduino (Пробовал как Mega, так и Nano), цифровой термодатчик Dallas DS18xxx, резистор на 4,7кОм, реле рабочим напряжением 5В и коммутируемым 220В (я брал твердотельное OMRON), и блок питания на 5В постоянного тока. Разумеется, нужны ещё провода, пара клеммников и коробка, куда будет уложено всё богатство. Схема творения не сохранилась, но способы подключения реле и термодатчиков широко описаны в примерах на различных сайтах, ничего нового изобретено не было. Термодатчик был размещён на выходящей из полотенцесушителя трубе, ближе к магистрали.

Принцип работы.

При включении МК включается насос, начинает измеряться температура. Насос работает, пока растёт температура на выходной трубе. При прекращении роста температуры отсчитывается wait_before_pump_off секунд и насос отключается. Далее МК ожидает падения температуры на hysteresis градусов, после чего насос включается и цикл повторяется. Если по какой-то причине (изначально недостаточная температура теплоносителя, хорошая теплоизоляция полотенцесушителя, ошибочное значение параметров) температура не может снизиться на величину гистерезиса, контроллер отсчитывает max_state_time_counter секунд и включает насос. Эти три параметра влияют на соотношение и длительность интервалов вкл/выкл. На практике достигнуто соотношение работы/отдыха на уровне порядка 1/3,5 при данных значениях параметров.

Код скетча:

#include <OneWire.h>

struct TempRelay {
int PumpAddress; //Пин выхода на силовое реле
bool Heating; //Cостояние реле вкл/выкл
byte addr[8]; //Адрес датчика Dallas
byte type_s; //Тип датичка Dallas
int time_counter; //Время работы насоса
int state_time_counter; //Время нахождения в текущем состоянии
int higher_temp; //Максимально достигнутая температура * 16
};

int wait_before_pump_off = 60; // Время работы насоса после достижения максимальной температуры трубы
int hysteresis = 5*16; // Градусы Цельсия, умноженные на 16 (приведение к raw-формату dallas). Падение температуры для повторного включения насоса.
int max_state_time_counter = 3600; //Максимальное время нахождения в одном из состояний, в секундах.
int loop_delay_calculator_counter = 0;
int loop_timer = 0; // Время выполнения одного прохода функции loop. Для вычисления задержки.
int loop_first_time_check = 0;

TempRelay TR;
OneWire ds(2); // Работать с датчиками Dallas на пине 2 (нужен резистор на 4.7 кОм)


void setup(void) {
int i = 0;
Serial.println("Init...");
Serial.begin(9600);
TR = {4, false, {0,0,0,0,0,0,0,0},0, 0, 0, -55*16};
pinMode(TR.PumpAddress, OUTPUT);

ds.write(0x60);
int sensors = 0;
while (ds.search(TR.addr)){
sensors++;
};
ds.reset_search();
if (!sensors) {
Serial.println("No DS18xxx sensors.");
}
else
{
Serial.print("Sensors on the wire: ");
Serial.println(sensors);

Serial.print("HW id:");
for( i = 0; i < 8; i++) {
Serial.write(' ');
Serial.print(TR.addr[i], HEX);
}
if (OneWire::crc8(TR.addr, 7) != TR.addr[7]) {
Serial.println("CRC is not valid! Bad wire?");
return;
}
Serial.println();
}
// the first ROM byte indicates which chip
switch (TR.addr[0]) {
case 0x10:
Serial.println(" Chip = DS18S20"); // or old DS1820
TR.type_s = 1;
break;
case 0x28:
Serial.println(" Chip = DS18B20");
TR.type_s = 0;
break;
case 0x22:
Serial.println(" Chip = DS1822");
TR.type_s = 0;
break;
default:
Serial.println("Device is not a DS18x20 family device.");
return;
}
int16_t raw = read_dallas(&ds, TR.addr);
delay(1000); // Дать время на формирование результата
raw = read_dallas(&ds, TR.addr);
pump_on(&TR);
pump_diag(&TR, raw);
Serial.println("Init complete...");
}


void loop() {
//Засечка времени начала первого прохода
if (loop_delay_calculator_counter == 0)
loop_first_time_check = millis();

loop_delay_calculator_counter++;

int16_t raw = 0;
raw = read_dallas(&ds, TR.addr);


TR.state_time_counter++;
if (TR.Heating) {
if (TR.higher_temp < raw){
TR.higher_temp = raw;
TR.time_counter = 0;
}
else
TR.time_counter++;
if(TR.time_counter > wait_before_pump_off || TR.state_time_counter > max_state_time_counter){
pump_diag(&TR, (float)raw);
pump_off(&TR);
}
}
else
{
if (raw + hysteresis < TR.higher_temp || TR.state_time_counter > max_state_time_counter){
pump_diag(&TR, (float)raw);
pump_on(&TR);
}
}

if (loop_delay_calculator_counter > 100)
{
loop_delay_calculator_counter--;
delay(1000-loop_timer); //Подгоняем продолжительность цикла к 1 секунде. Приблизительно. Особого практического смысла эта фича не имеет, можно просто поставить задержку в 1 сек.
}
else
{
if (loop_delay_calculator_counter == 100) // Считаем среднее время выполнения одного цикла после прохождения 100 циклов
loop_timer = (millis() - loop_first_time_check) / 100;
}
TR.state_time_counter--;
TR.time_counter = 0;
}

int16_t read_dallas(OneWire *ds, byte *address){
byte data[12];
byte present = 0;
ds->reset();
ds->select(address);
// при паразитном питании нужна задержка.
ds->write(0x44, 1); // start conversion, with parasite power on at the end
// delay(1000); // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.

present = ds->reset();
ds->select(address);
ds->write(0xBE);

// Serial.print(" Data = ");
// Serial.print(present, HEX);
// Serial.print(" ");
for (byte i = 0; i < 9; i++) { // we need 9 bytes
data[i] = ds->read();
// Serial.print(data[i], HEX);
// Serial.print(" ");
}

/* float celsius;
celsius = (float)raw_temp(TR.type_s, data) / 16.0;
Serial.print("Temperature = ");
Serial.print(celsius);
Serial.println(" Celsius");

*/
// Serial.print(" CRC=");
// Serial.print(OneWire::crc8(data, 8), HEX);
// Serial.println();
return raw_temp(TR.type_s, data);
}

void pump_diag(TempRelay *no, float raw){
float celsius = raw / 16.0;
Serial.print("Reached temperature = ");
Serial.print(celsius);
Serial.println(" Celsius");

Serial.print("Pump was ");
Serial.print(no->state_time_counter);
Serial.print(" sec. ");
Serial.println(no->Heating?"on":"off");
}

void pump_off(TempRelay *no){
digitalWrite(no->PumpAddress, HIGH); // почему в nano - HIGH, а в mega LOW?
no->Heating = false;
no->state_time_counter = 0;
}

void pump_on(TempRelay *no){
digitalWrite(no->PumpAddress, LOW); // почему в nano - LOW, а в mega HIGH?
no->Heating = true;
no->time_counter = 0;
no->higher_temp = 0;
no->state_time_counter = 0;
}

int16_t raw_temp(int type_s, byte *data)
{
// Convert the data to actual temperature
// because the result is a 16 bit signed integer, it should
// be stored to an "int16_t" type, which is always 16 bits
// even when compiled on a 32 bit processor.
int16_t raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// "count remain" gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
}
} else {
byte cfg = (data[4] & 0x60);
// at lower res, the low bits are undefined, so let's zero them
if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
//// default is 12 bit resolution, 750 ms conversion time
}
return raw;
}