Данная статья планируется в двух частях. Первая часть - программирование и сборка контроллера, вторая часть - сборка и тестирование системы в целом. Понятно, что пасмурный февраль за окном не позволит проверить основной летний режим, и вообще не располагает к работе на бане (в смысле на крыше), но испытать сам контроллер с некоторой имитацией и допусками всё же можно. Для этого у нас есть холодильник и кастрюлька с кипятком. Но будем последовательными.
Представьте себе летний день. Вы смотрите в небо и не видите сферу Дайсона. Наше солнце щедро раздает свою энергию направо и налево (а ещё и вверх и вниз). Нерационально, надо собрать хоть немного. Пока ваш сосед поднимает на опоры огромную бочку, стоически заполняет её из шланга, также гордо сливает цветущую водорослями жидкость каждую осень - вы решили подружиться с солнцем ещё ближе и технологичней. Сегодня поговорим о разработке "переводчика" с солнечного на человеческий - контроллера для солнечного коллектора. Это устройство будет поддерживать оптимальную температуру воды для банного душа, используя солнечную энергию.
Внимание, температура везде будет указана в градусах Цельсия, чтобы не писать каждый раз.
Изначальными причинами были только хорошо аргументированные "лень ходить из бани в дом, чтобы помыться", "не хочу бочку выше головы", "хочу хороший душ в бане" и т.д. Чтобы читатели были в курсе - баня мобильная, режим работы - финская парная, но возможно поставить душевую кабину в предбаннике. Расстояние до дома метров 15, прокладывать дополнительную сеть ГВС экономически оказывается не выгодным.
Из определения солнечного коллектора в Википедии кажется, что сложного ничего нет. На самом деле, как и везде, есть нюансы. Заводские не всегда устраивают по своей функциональности (да, у такого простого определения ещё и функциональность есть), а самостоятельно сделать ещё сложнее, чем заработать на него.
Так зачем же делать самому? - тоже нюансы есть. Заводские контроллеры имеют функции, связанные управлением отопительными устройствами (типа котла) и кучу временных и неактуальных параметров; более дешёвые варианты вообще на семисегментных индикаторах и, как следствие, имеют проблемы с информативностью. Понятно, что когда возникнет ошибка, сходу расшифровать код на таком дисплее не получится, а инструкцию уже скурили физически. Также есть нюансы с применением в системах под давлением и возможным перегревом. На закуску - не забываем о легионелле - это анаэробная бактерия, живущая в воде и почве. Заводские системы по умолчанию замкнуты, а стоячая вода плюс органика плюс комфортная температура... можно не продолжать.
Достоинства предлагаемого устройства - проточная система с запиткой от водопровода и таким же давлением - аналог квартирного бойлера, но с более сложным управлением. Проточная вода, даже не будучи прогретой, росту живности не способствует. При желании можно активировать нагреватель и насос вручную. Качество водопроводной воды где-то соответствует ГОСТ, где-то нет, вплоть до анекдотов. Фильтр на ввод холодной воды надо ставить в любом случае, ведь даже питьевая вода - это не химически чистая, бережём коллектор от накипи.
Есть и ещё плюсы: полностью русский язык, минимум сокращений. В этом хорошо помогает дисплей 2004, он ещё и выключается сам через 30 секунд. Кроме этого ещё есть интуитивно понятный интерфейс и минимальное участие пользователя в настройке (включил и сразу забыл, даже кнопок не тискал). В предыдущем предложении я немного слукавил: всё-таки температуру и дельту лучше настроить по собственным ощущениям типа холодно-тёпленько или горячо-вообще ппц; режим холодно-вообще ппц - не реализован, так как считается экономически не выгодным. Температура и дельта настраивается с точностью до градуса, но это уже детали, пробуем то всё равно на ощупь. В контроллере нет часов, при работе от солнца они совершенно бесполезны и только отвлекают своей низкой точностью. Есть реализация WatchDog и таймера для ручного управления насосом (защита от дурости со стороны как контроллера, так и пользователя).
Устройство рассчитано на работу от сети холодного водоснабжения, можно использовать бойлер любого типа - что прямого, что косвенного нагрева, главное со встроенным ТЭНом. Но не всё так просто - придётся врезать датчик уровня воды в бойлер. Это чтобы не случилось катастрофы из серии "Ой, забыл подающий кран открыть". Да, защита от забывчивости – наше всё! А ещё в контроллере реализован зимний режим в виде принудительной циркуляции с подогревом бойлером, мы же для умеренного климата делаем. Это скорее защита от "лень снег почистить". Хотя этой зимой в Витебской области солнца меньше, чем у кота Матроскина молока, да и снега тоже. Вообще, умеренный климат не позволяет устройству эффективно работать зимой. Если зимы и дальше будут такими пасмурными, то всё-таки лучше не мучить электросчётчик и не плодить легионеллу в режиме антизамерзания, а систему выключить и слить (проточный бойлер - не огромная бочка, болота не будет). По умолчанию все режимы переключаются автоматически и ничего незнакомого не содержат. Есть ещё различные экраны настройки и управления. Сбросим всё под спойлер, дабы не тратить место на странице и остатки терпения читателя.
Собственно, сам спойлер. Внимание, много букв
Устройство содержит 3 кнопки: туда, сюда, ввод. Есть ещё "включить подсветку", но на работу она не влияет. Также есть большая кнопка "включить питание 220В", на работу влияет самым прямым образом, но её можно не описывать, так как нажимается не часто.
Основной интерфейс состоит из шести экранов, переключаемых кнопками туда и сюда и двух независимых экранов:
Главное меню, в котором показываются температуры коллектора, бойлера, установленная температура. В нижней строке показывается, включен ли насос и на какую скорость, включен ли нагреватель, текущий режим работы и значение дельты. Это информационный экран, тискать ввод здесь не имеет смысла.
Установка температуры бойлера (там так и написано, от 30 до 90). Вроде всё понятно. Удерживаем ввод 2 секунды, появляются стрелочки в нижней строке. Кнопками туда-сюда выставляем нужную температуру. Снова удерживаем ввод - стрелочки исчезают, значение записано. Мы герои, дальше будет не сложнее.
Установка дельты (тоже так и написано, от 2 до 20). Как и в прошлый раз - удерживаем ввод 2 секунды, появляются стрелочки в нижней строке. Кнопками туда-сюда выставляем нужное значение дельты. Снова удерживаем ввод - стрелочки исчезают, значение записано, ничего нового. Для справки: дельта температуры для данного коллектора - максимально допустимое отличие в меньшую сторону температуры бойлера от её установленного значения. Данный диапазон поддерживается циркуляционным насосом и тепловыми трубками в коллекторе.
Насос (ручной режим) - экран ручного управления насосом. Здесь показывается его текущий статус (выключен или скорость в виде разного числа квадратов), текущий режим (Авто, выключен, низкая или высокая скорость). Для принудительного изменения нажимаем ввод до получения нужного режима. По умолчанию, конечно же, Авто, но если решили поменять - защита от забывчивости встроена - в пользовательском режиме насос работает не более 20 минут, затем переходит в автоматический режим.
-
ТЭН (ручной режим) - экран ручного управления нагревателем. Показания аналогичны насосу, отличия только в меньшем количестве возможных вариантов (авто, выключен, включен). Изменяем также нажатиями кнопки ввод. Защита от забывчивости также есть - бойлер греется до 90 градусов, затем переходит в автоматический режим. При температуре бойлера ниже 30 градусов ручное управление им выключено (ночью спать надо, а если зима - тем более не лезть в алгоритм). Это не баг а запланированная фича, строки с 1170 по 1174. Вот они:
// Запрет запуска в ручном режиме для нагревателя if (heaterManualModeActive && (boilerTemp <= 30 || boilerTemp >= 90)) { heaterManualModeActive = false; // Возвращаемся в автоматический режим heaterManualMode = 2; // Возвращаем в автоматический режим }
Настройка датчика - экран настройки датчика PT100 (это тот, что в коллекторе). На плате всего 3 датчика: один PT100 в коллекторе , второй DS18B20 - в бойлере, третий DS18B20 для возможной калибровки первого PT100. Вот здесь важное замечание: если Вы купили готовый модуль MAX31865 с датчиком - эта калибровка вообще не нужна, уже всё настроено. Если решили спаять самостоятельно - вот тут и сравните значение с DS18B20. По завершении калибровки третий датчик DS18B20 снимаем (вообще снимаем и уносим домой): в коллекторе при температуре до 150 градусов (6 атм. давление) он проживёт не долго, контроллер больше нигде его не опрашивает. Аварийные экраны, недоступные для ручного включения:
Экран ошибок, указывающий проблемы с датчиками коллектора и бойлера, а также отсутствие воды в бойлере.
Экран перегрева, указывающий на перегрев всей системы (и коллектора и бойлера) и выводящий температуру бойлера. Относится к отдельному режиму "Перегрев". В режимах аварии ручное управление системой полностью блокировано, и вообще отключен доступ к каким либо функциям. Это защита от человеческого фактора. Нельзя запускать систему без датчиков и воды, нельзя играться с насосом и нагревателем при перегреве. Система справится, задача пользователя - не мешать, но подключить датчики всё же придётся. Теперь рассмотрим основные режимы работы (переключаются автоматически, участие пользователя не требуется).
Режим работы Лето: гоняем воду циркуляционным насосом из бойлера в коллектор на высокой скорости, включение и выключение по дельте, стандарт короче.
Режим работы Ночь - всё выключено, спим.
Режим работы Зима - греем воду бойлером и (или) гоняем на низкой скорости в коллектор. Зависит от того, кому холодно. Внимание! Режим энергетически не эффективен, отсутствует проточность, есть опасность роста легионеллы. Если солнечных дней не планируется (как в этом году в Витебской области) - настоятельно рекомендуется выключить систему и всё слить.
Режим работы Перегрев - гоняем воду на низкой скорости и (или) открываем клапаны сброса газа и подачи холодной воды в бойлер, зависит от того, кому жарче. Если перегрето всё (у нас здесь тропики) - открываем все клапаны и гоним горячую воду в коллектор на низкой скорости для аварийного испарения, снижая давление в бойлере и освобождая пространство для холодной воды; дополнительно блокируем ручное управление и вообще все другие экраны, ибо нефиг лезть. Также, если всё перегрето, выводим отдельный экран, указывающий на высокую температуру бойлера (краник подачи холодной воды открыть не забыли?). Вообще режим перегрева более широкий. Если перегрет только коллектор (более 95 градусов) - отключаем показ режима в четвёртой строке и гоним воду на высокой скорости из бойлера, охлаждая за счёт его массивности. Если перегрет только бойлер (по закону подлости) - снова отключаем показ режима в четвёртой строке и гоним воду на той же высокой скорости в холодный коллектор, охлаждая за счёт высокой площади теплообмена. Если перегрето всё - см. начало абзаца со слов "у нас здесь тропики".
После изучения всех нюансов можно и программу написать. Полный и наконец-то финальный её текст будет в самом конце под спойлером. Так как я не программист (ни по работе, ни по образованию), воспользуюсь для отдельных моментов нейросетями. Хотелось бы кнопочку "сделать всё красиво", но увы, до разума им - как до Луны через пояс Койпера. Здесь я распишу основные принятые решения и небольшую историю дебага, как программного, так и аппаратного. Для затравки - сразу ничего не включилось, даже подсветка. Шикарное начало, можно методичку писать. К счастью, окончание первого этапа вполне успешное: устройство заработало и ждёт лета.
Фото в студию!
Что же нам нужно? Не так и много, хватит и десяти пунктов:
Руки с коэффициентом кривизны ниже среднего.
Не пить спиртное хотя бы неделю, а лучше две (в смысле две недели, а не две бутылки).
Уровень терпения выше среднего (вспоминаем старый карандашный мем со слоном "в случае припадка ярости - вырвать, скомкать, швырнуть под ноги и топтать, топтать...") - так вот, этого не надо.
Способность не материться громко вслух при каждой неудаче, а хотя бы тихонечко шептать ругательства под нос. Соседи будут благодарны, да и карма, как у меня, не пострадает.
Чай, кофе, или другой напиток, поддерживающий бодрость духа и остроту ума. Но не спиртное! (мы же помним пункт про неделю-две, правда?)
Отсутствие боязни "чёрного экрана терминала", он просто устал и отдыхает.
Готовность к тому, что отладка кода займет намного больше времени, чем его написание. Такова суровая правда.
Режим "невидимка" для всех отвлекающих факторов. Социальные сети, мессенджеры, котики в интернете - все это зло! Только работа, только хардкор!
Вера в светлое будущее и в то, что "оно заработает!" Оно же заработает?...
Умение помнить конечную цель, не отвлекаясь на менее важные функции. Помним с чего начали? Ну, значит всё готово.
Тот самый бородатый мем со слоном
Скрывать не буду, консультировался на электронном портале Kazus.ru. Да простят меня админы, тема тут: https://kazus.ru/forums/showthread.php?t=123055
Что я не могу предоставить - печатную плату. Её предыдущую версию сильно (но объективно) раскритиковали, и я понял, что знаний немного не хватает. Здесь я приведу пару её фотографий, Прошу относиться как к макетной.
Так как устройство не планируется, как коммерческое, сократим потери нервных клеток и используем ардуино нано от доброго китайского дядюшки. Только прошу не злословить сразу, плюсы в этом решении тоже есть. Устройство сложное, сразу сходу написать да без бага не выйдет. Разобьём на мелкие функциональные участки, это сильно поможет наращивать функционал до требуемого. На первом этапе - ничего сверх, получаем показания датчиков, используя стандартные библиотеки OneWire, DallasTemperature, Adafruit_MAX31865, выводим на экран. По сути, это и будет основным экраном.
Скрывать не буду, консультировался на электронном портале Kazus.ru. Да простят меня админы, тема тут: https://kazus.ru/forums/showthread.php?t=123055
Что я не могу предоставить - печатную плату. Её предыдущую версию сильно (но объективно) раскритиковали, и я понял, что знаний немного не хватает. Здесь я приведу пару её фотографий, Прошу относиться как к макетной.Так как устройство не планируется, как коммерческое, сократим потери нервных клеток и используем ардуино нано от доброго китайского дядюшки. Только прошу не злословить сразу, плюсы в этом решении тоже есть. Устройство сложное, сразу сходу написать да без бага не выйдет. Разобьём на мелкие функциональные участки, это сильно поможет наращивать функционал до требуемого. На первом этапе - ничего сверх, получаем показания датчиков, используя стандартные библиотеки OneWire, DallasTemperature, Adafruit_MAX31865, выводим на экран. По сути, это и будет основным экраном.
// Функция для считывания температуры с PT100
void readPT100Temperature() {
uint16_t rtd = pt100.readRTD();
float ratio = rtd;
ratio /= 32768;
float resistance = RREF * ratio;
// Вычисляем температуру по формуле для PT100
pt100Temp = (resistance - 100) / 0.385; // Коэффициент 0.385 для PT100
pt100Temp += calibrationOffset; // Добавление калибровочного смещения
// Проверка на ошибки
uint8_t fault = pt100.readFault();
if (fault) {
// Обработка ошибок
pt100.clearFault();
pt100Temp = NAN; // Установить ошибку температуры
}
}
// Считывание температуры с PT100
readPT100Temperature();
// Считывание температуры с датчика коллектора DS18B20 (для калибровки)
sensor1.requestTemperatures();
collectorTemp = sensor1.getTempCByIndex(0);
// Считывание температуры с датчика бойлера DS18B20
sensor2.requestTemperatures();
boilerTemp = sensor2.getTempCByIndex(0);
// float collectorTempCalib = sensor1.getTempCByIndex(0); // Только для калибровки
А вот тут и первый баг. Как оказалось (по крайней мере в моём случае), дисплей на пинах 0 и 1 ардуины (RX и TX) не хочет работать, о чём PROTEUS - ни слова (может порт помер?) в итоге пошло по схеме LiquidCrystal lcd(4, 5, 3, 2, 6, 7). Да,пришлось кинуть проводами, плату переделывать лень (мы с ней боремся, но она наше всё).
Что лучше: PT1000 или PT100? Есть нюансы. PT-1000 лучше на длинной линии; если верить рекламе, может дать чуть выше точность. Что выбрать? - PT100 - он был первым в списке и сопротивления линии до 25 Ом при длине до 12 м нам за глаза хватит. В программе строки с PT1000 есть, но закомментированы. Не забываем о другом калибровочном резисторе, там тоже есть.
Как-то я сказал, что и подсветка тоже не включилась - был непропай СМД резистора R16. Признаюсь, было желание сделать управление подсветкой от контроллера, но в итоге поставил старую добрую NE555. Зато настраивать яркость - сама радость и голова на подборе режима не болит. Участие пользователя в работе устройства должно быть минимальным, дисплей выключаем через 30 секунд, если кнопки не нажаты. Пару раз за сезон подсветить его можно и врукопашную по отдельной кнопке.
unsigned long displayTimeout = 30000;
и раз уж связалить со временем - сразу сделаем 20 минут для ручного режима насоса
#define PUMP_MANUAL_TIMEOUT 1200000
Пришло время нарисовать дополнительные символы, в том числе Й (из таблицы мне не нравится) и добавить экранов. Не пишу здесь, действительно много букв, в конце сброшу целиком. Переключать экраны будем кнопками, также ими редактируем некоторые параметры.
Спойлер: Всего-то 3 кнопки, а тут.....
// Обработка нажатия кнопки "Вниз"
if (digitalRead(BUTTON_DOWN) == HIGH) {
lastButtonPressTime = millis();
if (!displayOn) {
lcd.display();
displayOn = true;
} else {
if (editingMode) {
// В режиме редактирования уменьшаем значение параметра
if (selectedParameter == 0) {
setBoilerTemp--;
if (setBoilerTemp < MIN_TEMP) setBoilerTemp = MIN_TEMP;
} else if (selectedParameter == 1) {
deltaTemp--;
if (deltaTemp < MIN_DELTA) deltaTemp = MIN_DELTA;
}
} else {
// В обычном режиме переключаем экраны
currentScreen++;
if (currentScreen > 5) currentScreen = 0;
updateDisplay(); // Обновляем экран при смене меню
}
}
}
// Обработка нажатия кнопки "Ввод"
if (digitalRead(BUTTON_ENTER) == HIGH && !buttonEnterPressed) {
lastButtonPressTime = millis();
buttonEnterPressed = true; // Устанавливаем флаг нажатия
if (currentScreen == 3) { // Экран насоса
if (!pumpManualModeActive) {
pumpManualModeActive = true; // Включаем ручной режим
manualPumpControlStartTime = millis(); // Запоминаем время начала ручного режима
}
manualPumpMode++;
if (manualPumpMode > 3) {
manualPumpMode = 0; // Циклически переключаем режимы: 0 - выключен, 1 - низкая скорость, 2 - высокая скорость, 3 - авто
}
updateDisplay(); // Обновляем экран для отображения текущего режима насоса
}
if (currentScreen == 4) { // Экран нагревателя
if (!heaterManualModeActive) {
heaterManualModeActive = true; // Включаем ручной режим
}
heaterManualMode++;
if (heaterManualMode > 2) {
heaterManualMode = 0; // Циклически переключаем режимы: 0 - выключен, 1 - включен, 2 - авто
}
updateDisplay(); // Обновляем экран для отображения текущего режима нагревателя
}
// Обработка на экранах редактирования (1 и 2)
if (currentScreen == 1 || currentScreen == 2) {
if (!editingMode) {
// Включаем режим редактирования
editingMode = true;
selectedParameter = (currentScreen == 1) ? 0 : 1;
} else {
// Выходим из режима редактирования
editingMode = false;
// Если изменения не были внесены, не сохраняем в EEPROM
if (EEPROM.read(0) != setBoilerTemp) {
EEPROM.write(0, setBoilerTemp); // Сохраняем значение температуры в EEPROM
}
if (EEPROM.read(1) != deltaTemp) {
EEPROM.write(1, deltaTemp); // Сохраняем значение дельты в EEPROM
}
updateDisplay(); // Обновляем экран после выхода из режима редактирования
}
}
}
// Если кнопка отпущена, сбрасываем флаг нажатия
if (digitalRead(BUTTON_ENTER) == LOW) {
buttonEnterPressed = false; // Сбрасываем флаг нажатия кнопки 'Ввод'
}
// Обновляй время последнего нажатия кнопки
lastDebounceTime = millis();
}
}
Теперь добавим ещё экран ошибок и перегрева, заблокируем управление до устранения причин (аппаратных, а не пользователя).Теперь добавим ещё экран ошибок и перегрева, заблокируем управление до устранения причин (аппаратных, а не пользователя).
Теперь добавим ещё экран ошибок и перегрева, заблокируем управление до устранения причин (аппаратных, а не пользователя).
// Проверка на ошибки датчиков
collectorSensorError = (collectorTemp == DEVICE_DISCONNECTED_C || collectorTemp == -127.0);
boilerSensorError = (boilerTemp == DEVICE_DISCONNECTED_C || boilerTemp == -127.0);
// Проверка на ошибки PT100
if (isnan(pt100Temp) || pt100Temp < -100 || pt100Temp > 400) {
PT100Error = true;
} else {
PT100Error = false;
}
// Экран перегрева
// !!!! ПЕРЕГРЕВ !!!!
// ТЕМПЕРАТУРА БОЙЛЕРА
if (overheat) {
lcd.setCursor(0, 0);
lcd.print(""); // далее длинная фраза и грозим пальчиком
....
return; // Прерываем выполнение остальных экранов, если есть перегрев
}
// Экран ошибок. Ошибки:
if (boilerSensorError || PT100Error || waterLevelError) {
lcd.setCursor(0, 0);
lcd.print(""); //тоже грозим пальчиком
....
// Датчик коллектора
if (PT100Error) {
lcd.setCursor(0, 1);
lcd.print(""); //и здесь тоже
....
}
// Датчик бойлера
if (boilerSensorError) {
lcd.setCursor(0, 2);
lcd.print(""); //и здесь
....
}
// Нет воды в бойлере
if (waterLevelError) {
lcd.setCursor(0, 3);
lcd.print(""); //какие-то мы грозные сегодня
....
}
return; // Прерываем выполнение остальных экранов, если есть ошибка
}
Всё это малоинтересно, а вот что действительно может вызвать споры - так это режимы работы и диапазоны переключения. Понятно, что при давлении в 6 атмосфер температура кипения сместится к отметке 150 градусов, что опасно по определению. Если пользователь захочет включить душ - вскипание произойдёт мгновенно. Ахтунг? - как бы не так. Писали ранее под спойлером в режимах, но можно кратко повторить. Да, коллектор может нагреться от души, но привязано всё к пределу 95 градусов. Охладить тонкие тепловые трубки мы можем принудительно с помощью массивного бойлера, а если и он нагрелся до 95 градусов - открываем клапан сброса из коллектора на кровлю (а потом в водосток и на землю). Вода там или уже газ (дай Бог такое лето) - из душа пойдёт максимум кипяток (и то, если кран подачи холодной воды закрыт), что гораздо безопаснее. Техническое задание составили, ладно можно теперь сделать
Переключатель режимов
// То есть, сначала переключатель, а потом спойлер
// Переключение режимов работы
void updateMode() {
// Переключение в режим перегрева, если температура коллектора или бойлера >= 95°C
if ((pt100Temp >= 95 || boilerTemp >= 95) && currentMode != OVERHEAT) {
currentMode = OVERHEAT; // Переключаемся на режим перегрева
}
// Возвращаемся в нормальные режимы, если температуры упали ниже 90°C
else if (currentMode == OVERHEAT && pt100Temp < 95 && boilerTemp < 90) {
currentMode = NIGHT; // Возвращаемся в летний режим (или другой, в зависимости от условий)
}
// Логика зимнего режима
else if ((pt100Temp <= WINTER_TEMP && currentMode != WINTER) || boilerTemp <= WINTER_TEMP) {
currentMode = WINTER; // Переключаемся на зимний режим
}
// Переход из зимнего режима, если условия для других режимов выполнены
else if (currentMode == WINTER && pt100Temp >= 10 && boilerTemp >= 10) {
if ((boilerTemp + NIGHT_TEMP_DIFF >= pt100Temp && boilerTemp >= 30)) {
currentMode = NIGHT; // Переключаемся на ночной режим
} else if (pt100Temp >= 30 && (pt100Temp > boilerTemp + NIGHT_TEMP_DIFF)) {
currentMode = SUMMER; // Включаем летний режим
}
}
// Переключение между режимами ночь и лето, если не зима
else if (currentMode != WINTER && currentMode != OVERHEAT) {
if ((boilerTemp + NIGHT_TEMP_DIFF >= pt100Temp && boilerTemp >= 30)) {
currentMode = NIGHT; // Ночной режим
} else if (pt100Temp >= 30 && (pt100Temp > boilerTemp + NIGHT_TEMP_DIFF)) {
currentMode = SUMMER; // Летний режим
}
}
}
Маловато, не хватает чего-то. Совершенно верно, нужна ещё основная логика всей системы. И вот тут действительноМаловато, не хватает чего-то. Совершенно верно, нужна ещё основная логика всей системы. И вот тут действительно
Маловато, не хватает чего-то. Совершенно верно, нужна ещё основная логика всей системы. И вот тут действительно
Перечислим самые весёлые баги, без них не обошлось.
Например:
Дисплей выключается через 30 секунд, если кнопка не нажата, а экраны ошибок и перегрева блокируют все ручные функции управления. Что будет, если дисплей включен не был (а он выключен по умолчанию)? Правильно, он вообще ничего не покажет, и выключать питание не имеет смысла.
Некорректное переключение реле первой и второй скорости насоса (могли включиться одновременно), словил несколько коллизий с переходами Зима-Ночь и Лето-Зима. Да, коллектор на улице может сильно остыть при горячем бойлере.
Не включение нагревателя при снижении температуры и не выключение при её повышении.
Режим перегрева (когда всё перегрето) внезапно включил нагрев бойлера ТЭНом. Proteus иногда ругают, но тут всё-же лучше в симуляции. Надёжным решением в итоге для 2, 3, 4 (и вообще для всего) в итоге стало жёсткое управление реле на if.
-
Перепутал переменные collectorTemp и pt100Temp (первая для калибровки, вторая рабочая). Долго голову ломал. GPT не помог, у него всё красиво...зараза.
Вот мы и добрались до конца первой части. Как и обещал, схема и исходный код под спойлерами.
Исходный код. Длиииинный
#include <LiquidCrystal.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>
#include <SPI.h>
#include <Adafruit_MAX31865.h>
#include <avr/wdt.h>
// Подключение дисплея
LiquidCrystal lcd(4, 5, 3, 2, 6, 7);
// Пины для датчиков DS18B20
#define ONE_WIRE_BUS1 0 // Датчик коллектора (используется только для калибровки)
#define ONE_WIRE_BUS2 1 // Датчик бойлера
OneWire oneWire1(ONE_WIRE_BUS1);
OneWire oneWire2(ONE_WIRE_BUS2);
DallasTemperature sensor1(&oneWire1); // Датчик коллектора (для калибровки)
DallasTemperature sensor2(&oneWire2); // Датчик бойлера
// Настройки
#define DEFAULT_BOILER_TEMP 60
#define DEFAULT_DELTA 8
#define MIN_TEMP 30
#define MAX_TEMP 90
#define MIN_DELTA 2
#define MAX_DELTA 20
#define MAX_PUMP_MANUAL_HOURS 8
#define PUMP_MANUAL_TIMEOUT 1200000 // 20 минут в миллисекундах
//#define PUMP_MANUAL_TIMEOUT 20000 // 20 секунд в миллисекундах, для теста
// Пины подключения MAX31865 (PT100)
#define MAX31865_CS A3
#define MAX31865_DI A1
#define MAX31865_DO A0
#define MAX31865_CLK A2
// Инициализация модуля MAX31865
Adafruit_MAX31865 pt100 = Adafruit_MAX31865(MAX31865_CS, MAX31865_DI, MAX31865_DO, MAX31865_CLK);
// Используем 4-х проводный PT100
#define RREF 430.0 // Сопротивление калибровочного резистора для PT100
// #define RREF 4300 // Сопротивление калибровочного резистора для PT1000
// Номинальное сопротивление (при 0 градусов цельсия) 100 ом для PT100, 1000 ом для PT1000
#define PT100_TYPE 100 // Для PT100
// Калибровочный коэффициент // Для PT100
float calibrationOffset = 0;
// Переменные для состояний и таймеров
unsigned long lastDebounceTime = 0; // Таймер для дребезга кнопок
unsigned long debounceDelay = 50; // Задержка для антидребезга
unsigned long lastButtonPressTime = 0;
unsigned long displayTimeout = 30000; // Таймаут для отключения дисплея (30 сек)
bool displayOn = true; // Флаг состояния дисплея
unsigned long pumpManualStartTime = 0;
unsigned long heaterManualStartTime = 0;
bool pumpManualMode = false;
bool manualControlScreenActive = false; // Флаг для ручного управления насосом и ТЭНом
bool errorScreenActive = false; // Флаг для экрана ошибок
bool buttonEnterPressed = false; // Флаг для отслеживания нажатия кнопки "Ввод"
bool waterLevelError = false; // Флаг ошибки уровня воды
// bool testHang = false; // Флаг для теста зависания
// Переменные для ручного управления насосом
bool pumpManualModeActive = false;
unsigned long manualPumpControlStartTime = 0;
int manualPumpMode = 3; // 0 - выключен, 1 - низкая скорость, 2 - высокая скорость, 3 - авто
// Переменные для ручного управления нагревателем
bool heaterManualModeActive = false; // Флаг для отслеживания состояния ручного режима
int heaterManualMode = 2; // 0 - Выключен, 1 - Включен, 2 - Авто
// Определяем состояния для режима редактирования
bool editingMode = false; // Режим редактирования
int selectedParameter = 0; // 0 - температура, 1 - дельта
// Переменные для режимов работы
enum Mode { SUMMER, WINTER, NIGHT, OVERHEAT }; // Лето, Зима, Ночь, Перегрев
Mode currentMode = SUMMER; // По умолчанию режим "Лето"
// Температурные границы для режимов
#define WINTER_TEMP 5 // Порог для включения режима "Зима"
#define NIGHT_TEMP_DIFF 5 // Разница температур для включения режима "Ночь"
// Переменные для отслеживания ошибок
bool collectorSensorError = false;
bool boilerSensorError = false;
bool PT100Error = false;
bool overheat = false; // Переменные для отслеживания состояния перегрева
// Переменные для текущего положения в меню
int currentScreen = 0; // 0 - Основной экран, 1 - Экран насоса, 2 - Экран ТЭНа, 3 - Экран настройки датчика;
// Переменные для хранения предыдущих значений
float prevcollectorTemp = -999.0;
float prevboilerTemp = -999.0;
int prevSetBoilerTemp = -999;
int prevDelta = -999;
int lastDelta = -999; // Инициализируем переменную для хранения предыдущего значения дельты
float pt100Temp = 0.0;
// Пины реле
#define RELAY_LOW_SPEED 11 // Низкая скорость
#define RELAY_HIGH_SPEED 12 //Высокая скорость
#define RELAY_HEATER 13 // Нагреватель
#define RELAY_OVERHEAT A4 // Перегрев
// Пины кнопок
#define BUTTON_DOWN 8 // Вниз
#define BUTTON_ENTER 9 // Ввод
#define BUTTON_UP 10 // Вверх
#define WATER_LEVEL_PIN A5 // Пин датчика уровня воды
// Переменные для температур и настроек
float collectorTemp = 0.0;
float boilerTemp = 0.0;
const int tolerance = 1; // Допустимое отклонение
int setBoilerTemp = EEPROM.read(0); // Установленная температура бойлера (из EEPROM)
int deltaTemp = EEPROM.read(1); // Дельта для включения насоса (из EEPROM)
// Символы для отображения на экране
byte degreeSymbol[8] = { // Символ не задействован, байт 0
0b11000,
0b11000,
0b00000,
0b00000,
0b00000,
0b00000,
0b00000,
0b00000
};
byte pumponSymbol[8] = { // Символ включенного насоса, байт 1
0b11111,
0b00100,
0b01110,
0b11111,
0b11111,
0b01110,
0b00100,
0b11111
};
byte pumpoffSymbol[8] = { // Символ выключенного насоса, байт 2
0b11111,
0b00100,
0b01010,
0b10001,
0b10001,
0b01010,
0b00100,
0b11111
};
byte heateronSymbol[8] = { // Символ включенного нагревателя, байт 3
0b01110,
0b10001,
0b10101,
0b11111,
0b11111,
0b11111,
0b11111,
0b11111
};
byte heateroffSymbol[8] = { //Символ выключенного нагревателя, байт 4
0b01110,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001
};
byte deltaSymbol[8] = { // Символ дельта, байт 5
0b00011,
0b00011,
0b00101,
0b00101,
0b01001,
0b01001,
0b10001,
0b11111
};
byte razdelSymbol[8] = { // Символ разделения, байт 6
0b00100,
0b00100,
0b00100,
0b00100,
0b00100,
0b00100,
0b00100,
0b00100
};
byte ikratkoeSymbol[8] = { // Символ Й, байт 7
0b10101,
0b10001,
0b10011,
0b10101,
0b11001,
0b10001,
0b10001,
0b10001
};
// Инициализация
void setup() {
// Инициализация WDT (сторожевой таймер)
if (MCUSR & (1 << WDRF)) {
MCUSR &= ~(1 << WDRF); // Очищаем флаг Watchdog Reset
}
wdt_enable(WDTO_8S); // Устанавливаем WDT с тайм-аутом 8 секунд
// Настройка пинов
pinMode(RELAY_LOW_SPEED, OUTPUT);
pinMode(RELAY_HIGH_SPEED, OUTPUT);
pinMode(RELAY_HEATER, OUTPUT);
pinMode(RELAY_OVERHEAT, OUTPUT);
pinMode(BUTTON_DOWN, INPUT);
pinMode(BUTTON_ENTER, INPUT);
pinMode(BUTTON_UP, INPUT);
pinMode(WATER_LEVEL_PIN, INPUT);
// Инициализация дисплея
lcd.begin(20, 4); // Подключение 20x4 LCD-дисплея
lcd.createChar(0, degreeSymbol); // Символ градуса
lcd.createChar(1, pumponSymbol); // Включенный насос
lcd.createChar(2, pumpoffSymbol); // Выключенный насос
lcd.createChar(3, heateronSymbol); // Включенный нагреватель
lcd.createChar(4, heateroffSymbol); // Выключенный нагреватель
lcd.createChar(5, deltaSymbol); // Дельта
lcd.createChar(6, razdelSymbol); // Разделение
lcd.createChar(7, ikratkoeSymbol); // Й
lcd.display(); // Включение дисплея
// Загрузка настроек из EEPROM
setBoilerTemp = EEPROM.read(0);
deltaTemp = EEPROM.read(1);
if (setBoilerTemp < MIN_TEMP || setBoilerTemp > MAX_TEMP) {
setBoilerTemp = DEFAULT_BOILER_TEMP; // Устанавливаем значение по умолчанию, если данные некорректны
}
// Проверка значений
setBoilerTemp = constrain(setBoilerTemp, MIN_TEMP, MAX_TEMP);
deltaTemp = constrain(deltaTemp, MIN_DELTA, MAX_DELTA);
if (deltaTemp < MIN_DELTA || deltaTemp > MAX_DELTA) {
deltaTemp = MIN_DELTA; // Устанавливаем значение по умолчанию для дельты
}
// Инициализация датчиков температуры
sensor1.begin();
sensor2.begin();
// Инициализация модуля MAX31865
pt100.begin(MAX31865_4WIRE);
delay(1000); // Задержка для стабильности
// Приветственное сообщение"Солнечный коллектор" "Инициализация..."
lcd.setCursor(0, 0);
lcd.print("Co");
lcd.print(char(187)); // л
lcd.print(char(189)); // н
lcd.print("e");
lcd.print(char(192)); // ч
lcd.print(char(189)); // н
lcd.print(char(195)); // ы
lcd.print(char(166)); // й
lcd.print(" ");
lcd.print(char(186)); // к
lcd.print("o");
lcd.print(char(187)); // л
lcd.print(char(187)); // л
lcd.print("e");
lcd.print(char(186)); // к
lcd.print(char(191)); // т
lcd.print("o");
lcd.print("p");
lcd.setCursor(0, 1);
lcd.print(char(165)); // И
lcd.print(char(189)); // н
lcd.print(char(184)); // и
lcd.print(char(229)); // ц
lcd.print(char(184)); // и
lcd.print("a");
lcd.print(char(187)); // л
lcd.print(char(184)); // и
lcd.print(char(183)); // з
lcd.print("a");
lcd.print(char(229)); // ц
lcd.print(char(184)); // и
lcd.print(char(199)); // я
lcd.print("...");
delay(1000);
lcd.clear();
}
// Основной цикл (loop):
// Получение текущих данных с датчиков.
// Обработка нажатий кнопок и смены экранов.
// Вызов функции обновления дисплея.
// Вызов функции controlSystem() для управления реле.
// Функция для считывания температуры с PT100
void readPT100Temperature() {
uint16_t rtd = pt100.readRTD();
float ratio = rtd;
ratio /= 32768;
float resistance = RREF * ratio;
// Вычисляем температуру по формуле для PT100
pt100Temp = (resistance - 100) / 0.385; // Коэффициент 0.385 для PT100
pt100Temp += calibrationOffset; // Добавление калибровочного смещения
// Проверка на ошибки
uint8_t fault = pt100.readFault();
if (fault) {
// Обработка ошибок
pt100.clearFault();
pt100Temp = NAN; // Установить ошибку температуры
}
}
void checkWaterLevel() {
// Считываем состояние датчика уровня воды
if (digitalRead(WATER_LEVEL_PIN) == HIGH) {
if (!waterLevelError) {
waterLevelError = true; // Устанавливаем флаг ошибки уровня воды
}
} else {
if (waterLevelError) {
waterLevelError = false; // Сбрасываем флаг ошибки уровня воды
}
}
}
void loop() {
wdt_reset(); // Сброс Watchdog Timer
unsigned long currentMillis = millis();
// Проверка уровня воды
checkWaterLevel();
// Искусственное зависание для теста WDT
// if (testHang) {
// while (true) {
// Симуляция зависания - WDT сработает и перезапустит контроллер
// }
// }
// Считывание температуры с PT100
readPT100Temperature();
// Считывание температуры с датчика коллектора DS18B20 (для калибровки)
sensor1.requestTemperatures();
collectorTemp = sensor1.getTempCByIndex(0);
// Считывание температуры с датчика бойлера DS18B20
sensor2.requestTemperatures();
boilerTemp = sensor2.getTempCByIndex(0);
// float collectorTempCalib = sensor1.getTempCByIndex(0); // Только для калибровки
// Проверка и обновление режима работы системы
updateMode();
// Проверка времени без нажатий и отключение дисплея
if (currentMillis - lastButtonPressTime >= displayTimeout && displayOn && !overheat) {
lcd.noDisplay();
displayOn = false;
} else if (pt100Temp >= 95 && boilerTemp >= 95) {
lcd.display();
displayOn = true;
}
// Обработка нажатий кнопок и обновление дисплея
handleButtons();
if (displayOn) {
updateDisplay();
}
// Вызов функции управления системой (контроль реле)
if (!pumpManualMode) {
// Автоматическое управление насосом
controlSystem();
}
// Для теста: включение зависания через 20 секунд
// if (currentMillis > 20000) {
// testHang = true; // Активируем зависание
// }
}
// Обработка кнопок
void handleButtons() {
// Игнорируем нажатия кнопок при перегреве
if (overheat) {
return;
}
// Антидребезг
if ((millis() - lastDebounceTime) > debounceDelay) {
// Обработка нажатия кнопки "Вверх"
if (digitalRead(BUTTON_UP) == HIGH) {
lastButtonPressTime = millis();
if (!displayOn) {
lcd.display();
displayOn = true;
} else {
if (editingMode) {
// В режиме редактирования увеличиваем значение параметра
if (selectedParameter == 0) {
setBoilerTemp++;
if (setBoilerTemp > MAX_TEMP) setBoilerTemp = MAX_TEMP;
} else if (selectedParameter == 1) {
deltaTemp++;
if (deltaTemp > MAX_DELTA) deltaTemp = MAX_DELTA;
}
} else {
// В обычном режиме переключаем экраны
currentScreen--;
if (currentScreen < 0) currentScreen = 5;
updateDisplay(); // Обновляем экран при смене меню
}
}
}
// Обработка нажатия кнопки "Вниз"
if (digitalRead(BUTTON_DOWN) == HIGH) {
lastButtonPressTime = millis();
if (!displayOn) {
lcd.display();
displayOn = true;
} else {
if (editingMode) {
// В режиме редактирования уменьшаем значение параметра
if (selectedParameter == 0) {
setBoilerTemp--;
if (setBoilerTemp < MIN_TEMP) setBoilerTemp = MIN_TEMP;
} else if (selectedParameter == 1) {
deltaTemp--;
if (deltaTemp < MIN_DELTA) deltaTemp = MIN_DELTA;
}
} else {
// В обычном режиме переключаем экраны
currentScreen++;
if (currentScreen > 5) currentScreen = 0;
updateDisplay(); // Обновляем экран при смене меню
}
}
}
// Обработка нажатия кнопки "Ввод"
if (digitalRead(BUTTON_ENTER) == HIGH && !buttonEnterPressed) {
lastButtonPressTime = millis();
buttonEnterPressed = true; // Устанавливаем флаг нажатия
if (currentScreen == 3) { // Экран насоса
if (!pumpManualModeActive) {
pumpManualModeActive = true; // Включаем ручной режим
manualPumpControlStartTime = millis(); // Запоминаем время начала ручного режима
}
manualPumpMode++;
if (manualPumpMode > 3) {
manualPumpMode = 0; // Циклически переключаем режимы: 0 - выключен, 1 - низкая скорость, 2 - высокая скорость, 3 - авто
}
updateDisplay(); // Обновляем экран для отображения текущего режима насоса
}
if (currentScreen == 4) { // Экран нагревателя
if (!heaterManualModeActive) {
heaterManualModeActive = true; // Включаем ручной режим
}
heaterManualMode++;
if (heaterManualMode > 2) {
heaterManualMode = 0; // Циклически переключаем режимы: 0 - выключен, 1 - включен, 2 - авто
}
updateDisplay(); // Обновляем экран для отображения текущего режима нагревателя
}
// Обработка на экранах редактирования (1 и 2)
if (currentScreen == 1 || currentScreen == 2) {
if (!editingMode) {
// Включаем режим редактирования
editingMode = true;
selectedParameter = (currentScreen == 1) ? 0 : 1;
} else {
// Выходим из режима редактирования
editingMode = false;
// Если изменения не были внесены, не сохраняем в EEPROM
if (EEPROM.read(0) != setBoilerTemp) {
EEPROM.write(0, setBoilerTemp); // Сохраняем значение температуры в EEPROM
}
if (EEPROM.read(1) != deltaTemp) {
EEPROM.write(1, deltaTemp); // Сохраняем значение дельты в EEPROM
}
updateDisplay(); // Обновляем экран после выхода из режима редактирования
}
}
}
// Если кнопка отпущена, сбрасываем флаг нажатия
if (digitalRead(BUTTON_ENTER) == LOW) {
buttonEnterPressed = false; // Сбрасываем флаг нажатия кнопки 'Ввод'
}
// Обновляй время последнего нажатия кнопки
lastDebounceTime = millis();
}
}
// Функция обновления дисплея
void updateDisplay() {
lcd.clear();
// Обновление режима работы системы
updateMode();
// Проверка на ошибки датчиков
collectorSensorError = (collectorTemp == DEVICE_DISCONNECTED_C || collectorTemp == -127.0);
boilerSensorError = (boilerTemp == DEVICE_DISCONNECTED_C || boilerTemp == -127.0);
// Проверка на ошибки PT100
if (isnan(pt100Temp) || pt100Temp < -100 || pt100Temp > 400) {
PT100Error = true;
} else {
PT100Error = false;
}
// Экран перегрева
// !!!! ПЕРЕГРЕВ !!!!
// ТЕМПЕРАТУРА БОЙЛЕРА
if (overheat) {
lcd.setCursor(0, 0);
lcd.print(" !!!! ");
lcd.print(char(168)); // П
lcd.print("EPE");
lcd.print(char(161)); // Г
lcd.print("PEB");
lcd.print(" !!!! ");
lcd.setCursor(0, 1);
lcd.print("TEM");
lcd.print(char(168)); // П
lcd.print("EPAT");
lcd.print(char(169)); // У
lcd.print("PA ");
lcd.print(char(160)); // Б
lcd.print("O");
//lcd.setCursor(14, 1);
lcd.write(byte(7)); // Й
lcd.print(char(167)); // Л
lcd.print("EPA");
lcd.setCursor(7, 2);
lcd.print(boilerTemp, 1);
lcd.print(char(223)); // Символ градуса
lcd.print(" C");
return; // Прерываем выполнение остальных экранов, если есть перегрев
}
// Экран ошибок. Ошибки:
if (boilerSensorError || PT100Error || waterLevelError) {
lcd.setCursor(0, 0);
lcd.print("O");
lcd.print(char(193)); // ш
lcd.print(char(184)); // и
lcd.print(char(178)); // б
lcd.print(char(186)); // к
lcd.print(char(184)); // и
lcd.print(":");
// Датчик коллектора
if (PT100Error) {
lcd.setCursor(0, 1);
lcd.print(char(224)); // Д
lcd.print("a");
lcd.print(char(191)); // т
lcd.print(char(192)); // ч
lcd.print(char(184)); // и
lcd.print(char(186)); // к
lcd.print(" ");
lcd.print(char(186)); // к
lcd.print("o");
lcd.print(char(187)); // л
lcd.print(char(187)); // л
lcd.print("e");
lcd.print(char(186)); // к
lcd.print(char(191)); // т
lcd.print("opa");
}
// Датчик бойлера
if (boilerSensorError) {
lcd.setCursor(0, 2);
lcd.print(char(224)); // Д
lcd.print("a");
lcd.print(char(191)); // т
lcd.print(char(192)); // ч
lcd.print(char(184)); // и
lcd.print(char(186)); // к
lcd.print(" ");
lcd.print(char(178)); // б
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(char(187)); // л
lcd.print("epa");
}
// Нет воды в бойлере
if (waterLevelError) {
lcd.setCursor(0, 3);
lcd.print("He");
lcd.print(char(191)); // т
lcd.print(" ");
lcd.print(char(179)); // в
lcd.print("o");
lcd.print(char(227)); // д
lcd.print(char(195)); // ы
lcd.print(" ");
lcd.print(char(179)); // в
lcd.print(" ");
lcd.print(char(178)); // б
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(char(187)); // л
lcd.print("epe");
}
return; // Прерываем выполнение остальных экранов, если есть ошибка
}
// Основной экран
if (currentScreen == 0) {
// t коллектора
lcd.setCursor(0, 0);
lcd.print("t ");
lcd.print(char(186)); // к
lcd.print("o");
lcd.print(char(187)); // л
lcd.print(char(187)); // л
lcd.print("e");
lcd.print(char(186)); // к
lcd.print(char(191)); // т
lcd.print("opa");
lcd.setCursor(14, 0);
lcd.write(byte(6));
lcd.setCursor(15, 0);
lcd.print(pt100Temp, 1);
lcd.setCursor(18, 0);
lcd.print(char(223)); // Символ градуса
lcd.setCursor(19, 0);
lcd.print("C");
lcd.setCursor(0, 1);
// t бойлера
lcd.print("t ");
lcd.print(char(178)); // б
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(char(187)); // л
lcd.print("epa");
lcd.setCursor(14, 1);
lcd.write(byte(6));
lcd.print(boilerTemp, 0);
lcd.setCursor(18, 1);
lcd.print(char(223)); // Символ градуса
lcd.setCursor(19, 1);
lcd.print("C");
lcd.setCursor(0, 2);
// t установлено
lcd.print("t ");
lcd.print(char(121)); // у
lcd.print("c");
lcd.print(char(191)); // т
lcd.print("a");
lcd.print(char(189)); // н
lcd.print("o");
lcd.print(char(179)); // в
lcd.print(char(187)); // л
lcd.print("e");
lcd.print(char(189)); // н
lcd.print("o");
lcd.setCursor(14, 2);
lcd.write(byte(6));
lcd.print(setBoilerTemp);
lcd.setCursor(18, 2);
lcd.print(char(223)); // Символ градуса
lcd.setCursor(19, 2);
lcd.print("C");
// Строка 4 - Статус насоса, ТЭНа, режим работы, дельта
lcd.setCursor(0, 3);
if (digitalRead(RELAY_LOW_SPEED) == HIGH) {
lcd.write(byte(1)); // Символ включенного насоса
lcd.print(char(201)); // >>
} else if (digitalRead(RELAY_HIGH_SPEED) == HIGH) {
lcd.write(byte(1)); // Символ включенного насоса
lcd.print(char(201)); // >>
lcd.print(char(201)); // >>
} else {
lcd.write(byte(2)); // Символ выключенного насоса
}
lcd.setCursor(5, 3);
if (digitalRead(RELAY_HEATER) == HIGH) {
lcd.write(byte(3)); // Символ включенногоТЭНа
} else {
lcd.write(byte(4)); // Символ вылюченногоТЭНа
}
switch (currentMode) {
// Режим работы Лето
case SUMMER:
lcd.setCursor(7, 3);
lcd.print(char(167)); // Л
lcd.print("e");
lcd.print(char(191)); // т
lcd.print("o");
break;
//Режим работы Зима
case WINTER:
lcd.setCursor(7, 3);
lcd.print(char(164)); // З
lcd.print(char(184)); // и
lcd.print(char(188)); // м
lcd.print("a");
break;
//Режим работы Ночь
case NIGHT:
lcd.setCursor(7, 3);
lcd.print("Ho");
lcd.print(char(192)); // ч
lcd.print(char(196)); // ь
break;
}
lcd.setCursor(13, 3);
lcd.write(byte(5)); // Символ дельта
lcd.setCursor(15, 3);
lcd.print(deltaTemp); // Дельта температуры
lcd.setCursor(18, 3);
lcd.print(char(223)); // Символ градуса
lcd.print("C");
// Экран установки температуры бойлера
// Установка
// температуры бойлера
} else if (currentScreen == 1) {
lcd.setCursor(0, 0);
lcd.print(char(169)); // У
lcd.print("c");
lcd.print(char(191)); // т
lcd.print("a");
lcd.print(char(189)); // н
lcd.print("o");
lcd.print(char(179)); // в
lcd.print(char(186)); // к
lcd.print("a");
lcd.setCursor(0, 1);
lcd.print(char(191)); // т
lcd.print("e");
lcd.print(char(188)); // м
lcd.print(char(190)); // п
lcd.print("epa");
lcd.print(char(191)); // т
lcd.print(char(121)); // у
lcd.print("p");
lcd.print(char(195)); // ы
lcd.print(" ");
lcd.print(char(178)); // б
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(char(187)); // л
lcd.print("epa");
lcd.setCursor(0, 2);
lcd.print("30...90");
lcd.setCursor(7, 2);
lcd.print(char(223)); // Символ градуса
lcd.setCursor(9, 2);
lcd.print("C");
if (editingMode) {
lcd.setCursor(5, 3);
lcd.print("< ");
lcd.setCursor(10, 3);
lcd.print(" >");
lcd.setCursor(7, 3);
lcd.print(setBoilerTemp);
} else {
lcd.setCursor(7, 3);
lcd.print(setBoilerTemp);
}
// Экран установки дельты
// Установка дельты
} else if (currentScreen == 2) {
lcd.setCursor(0, 0);
lcd.print(char(169)); // У
lcd.print("c");
lcd.print(char(191)); // т
lcd.print("a");
lcd.print(char(189)); // н
lcd.print("o");
lcd.print(char(179)); // в
lcd.print(char(186)); // к
lcd.print("a ");
lcd.print(char(227)); // д
lcd.print("e");
lcd.print(char(187)); // л
lcd.print(char(196)); // ь
lcd.print(char(191)); // т
lcd.print(char(195)); // ы
lcd.setCursor(0, 1);
lcd.print("2...20");
lcd.setCursor(7, 1);
lcd.print(char(223)); // Символ градуса
lcd.setCursor(9, 1);
lcd.print("C");
lcd.setCursor(0, 2);
lcd.print(char(168)); // П
lcd.print("o ");
lcd.print(char(121)); // у
lcd.print(char(188)); // м
lcd.print("o");
lcd.print(char(187)); // л
lcd.print(char(192)); // ч
lcd.print("a");
lcd.print(char(189)); // н
lcd.print(char(184)); // и
lcd.print(char(198)); // ю
lcd.print(" 8");
if (editingMode) {
lcd.setCursor(5, 3);
lcd.print("< ");
lcd.setCursor(10, 3);
lcd.print(" >");
lcd.setCursor(7, 3);
lcd.print(deltaTemp);
} else {
lcd.setCursor(7, 3);
lcd.print(deltaTemp);
}
// Экран ручного управления насосом
// Насос (ручной режим)
} else if (currentScreen == 3) {
lcd.setCursor(0, 0);
lcd.print("Hacoc (p");
lcd.print(char(121)); // у
lcd.print(char(192)); // ч
lcd.print(char(189)); // н
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(" pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
lcd.print(")");
//Статус
lcd.setCursor(0, 1);
lcd.print("C");
lcd.print(char(191)); // т
lcd.print("a");
lcd.print(char(191)); // т
lcd.print(char(121)); // у
lcd.print("c: ");
//Режим
lcd.setCursor(0, 2);
lcd.print("Pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
lcd.print(":");
// Ввод-сменить режим
lcd.setCursor(0, 3);
lcd.print("B");
lcd.print(char(179)); // в
lcd.print("o");
lcd.print(char(227)); // д
lcd.print("-");
lcd.print("c");
lcd.print(char(188)); // м
lcd.print("e");
lcd.print(char(189)); // н
lcd.print(char(184)); // и
lcd.print(char(191)); // т
lcd.print(char(196)); // ь
lcd.print(" pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
// Низкая скорость / Высокая скорость / Выключен / Авто
lcd.setCursor(8, 1);
if (digitalRead(RELAY_LOW_SPEED) == HIGH) {
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
} else if (digitalRead(RELAY_HIGH_SPEED) == HIGH) {
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
lcd.print(char(255)); // "квадрат"
} else {
lcd.print("B");
lcd.print(char(195)); // ы
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
}
// Ручной режим работы насоса
lcd.setCursor(8, 2);
if (manualPumpMode == 0) {
lcd.print("B");
lcd.print(char(195)); // ы
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
} else if (manualPumpMode == 1) {
lcd.print("H");
lcd.print(char(184)); // и
lcd.print(char(183)); // з
lcd.print(char(186)); // к
lcd.print("a");
lcd.print(char(199)); // я
lcd.print(" c");
lcd.print(char(186)); // к
lcd.print(".");
} else if (manualPumpMode == 2) {
lcd.print("B");
lcd.print(char(195)); // ы
lcd.print("co");
lcd.print(char(186)); // к
lcd.print("a");
lcd.print(char(199)); // я
lcd.print(" c");
lcd.print(char(186)); // к
lcd.print(".");
} else if (manualPumpMode == 3) {
lcd.print("A");
lcd.print(char(179)); // в
lcd.print(char(191)); // т
lcd.print("o");
}
// Экран ручного управления нагревателем
// ТЭН (ручной режим)
} else if (currentScreen == 4) {
lcd.setCursor(0, 0);
lcd.print("T");
lcd.print(char(175)); // Э
lcd.print("H (p");
lcd.print(char(121)); // у
lcd.print(char(192)); // ч
lcd.print(char(189)); // н
lcd.print("o");
lcd.print(char(166)); // й
lcd.print(" pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
lcd.print(")");
//Статус
lcd.setCursor(0, 1);
lcd.print("C");
lcd.print(char(191)); // т
lcd.print("a");
lcd.print(char(191)); // т
lcd.print(char(121)); // у
lcd.print("c: ");
//Режим
lcd.setCursor(0, 2);
lcd.print("Pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
lcd.print(":");
// Ввод-сменить режим
lcd.setCursor(0, 3);
lcd.print("B");
lcd.print(char(179)); // в
lcd.print("o");
lcd.print(char(227)); // д
lcd.print("-");
lcd.print("c");
lcd.print(char(188)); // м
lcd.print("e");
lcd.print(char(189)); // н
lcd.print(char(184)); // и
lcd.print(char(191)); // т
lcd.print(char(196)); // ь
lcd.print(" pe");
lcd.print(char(182)); // ж
lcd.print(char(184)); // и
lcd.print(char(188)); // м
// Включен / выключен
lcd.setCursor(8, 1);
if (digitalRead(RELAY_HEATER) == HIGH) {
lcd.print("B");
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
} else {
lcd.print("B");
lcd.print(char(195)); // ы
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
}
// Ручной режим работы нагревателя
lcd.setCursor(8, 2);
if (heaterManualMode == 0) {
lcd.print("B");
lcd.print(char(195)); // ы
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
} else if (heaterManualMode == 1) {
lcd.print("B");
lcd.print(char(186)); // к
lcd.print(char(187)); // л
lcd.print(char(198)); // ю
lcd.print(char(192)); // ч
lcd.print("e");
lcd.print(char(189)); // н
} else if (heaterManualMode == 2) {
lcd.print("A");
lcd.print(char(179)); // в
lcd.print(char(191)); // т
lcd.print("o");
}
// Экран настройки датчика коллектора
// Настройка датчика
} else if (currentScreen == 5) {
lcd.setCursor(0, 0);
lcd.print("Hac");
lcd.print(char(191)); // т
lcd.print("po");
lcd.print(char(166)); // й
lcd.print(char(186)); // к
lcd.print("a ");
lcd.print(char(227)); // д
lcd.print("a");
lcd.print(char(191)); // т
lcd.print(char(192)); // ч
lcd.print(char(184)); // и
lcd.print(char(186)); // к
lcd.print("a");
// Калибровочный датчик
lcd.setCursor(0, 2);
lcd.print("DS18B20: ");
if (collectorTemp == -127.0) {
lcd.print("Error");
} else {
lcd.print(collectorTemp, 1);
lcd.write(223); // Символ градуса
lcd.print(" C");
}
lcd.setCursor(0, 3);
lcd.print("PT100: ");
lcd.print(pt100Temp, 1);
lcd.write(223); // Символ градуса
lcd.print(" C");
}
}
// Переключение режимов работы
void updateMode() {
// Переключение в режим перегрева, если температура коллектора или бойлера >= 95°C
if ((pt100Temp >= 95 || boilerTemp >= 95) && currentMode != OVERHEAT) {
currentMode = OVERHEAT; // Переключаемся на режим перегрева
}
// Возвращаемся в нормальные режимы, если температуры упали ниже 90°C
else if (currentMode == OVERHEAT && pt100Temp < 95 && boilerTemp < 90) {
currentMode = NIGHT; // Возвращаемся в летний режим (или другой, в зависимости от условий)
}
// Логика зимнего режима
else if ((pt100Temp <= WINTER_TEMP && currentMode != WINTER) || boilerTemp <= WINTER_TEMP) {
currentMode = WINTER; // Переключаемся на зимний режим
}
// Переход из зимнего режима, если условия для других режимов выполнены
else if (currentMode == WINTER && pt100Temp >= 10 && boilerTemp >= 10) {
if ((boilerTemp + NIGHT_TEMP_DIFF >= pt100Temp && boilerTemp >= 30)) {
currentMode = NIGHT; // Переключаемся на ночной режим
} else if (pt100Temp >= 30 && (pt100Temp > boilerTemp + NIGHT_TEMP_DIFF)) {
currentMode = SUMMER; // Включаем летний режим
}
}
// Переключение между режимами ночь и лето, если не зима
else if (currentMode != WINTER && currentMode != OVERHEAT) {
if ((boilerTemp + NIGHT_TEMP_DIFF >= pt100Temp && boilerTemp >= 30)) {
currentMode = NIGHT; // Ночной режим
} else if (pt100Temp >= 30 && (pt100Temp > boilerTemp + NIGHT_TEMP_DIFF)) {
currentMode = SUMMER; // Летний режим
}
}
}
// Основная логика работы системы
void controlSystem() {
// Проверка на ошибки датчиков
if (boilerSensorError || PT100Error || waterLevelError) {
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_HIGH_SPEED, LOW);
digitalWrite(RELAY_HEATER, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Выключаем систему при ошибке датчиков
return; // Прерываем выполнение дальнейшей логики
}
unsigned long currentMillis = millis();
// Проверка времени работы в ручном режиме для насоса
if (pumpManualModeActive && (currentMillis - manualPumpControlStartTime >= PUMP_MANUAL_TIMEOUT)) {
pumpManualModeActive = false; // Возвращаемся в автоматический режим по истечении времени
manualPumpMode = 3; // Возвращаем в автоматический режим
}
// Запрет запуска в ручном режиме для нагревателя
if (heaterManualModeActive && (boilerTemp <= 30 || boilerTemp >= 90)) {
heaterManualModeActive = false; // Возвращаемся в автоматический режим
heaterManualMode = 2; // Возвращаем в автоматический режим
}
// Ручное управление насосом
if (pumpManualModeActive) {
switch (manualPumpMode) {
case 0: // Выключено
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_HIGH_SPEED, LOW);
break;
case 1: // Низкая скорость
digitalWrite(RELAY_LOW_SPEED, HIGH);
digitalWrite(RELAY_HIGH_SPEED, LOW);
break;
case 2: // Высокая скорость
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_HIGH_SPEED, HIGH);
break;
case 3: // Автоматический режим
pumpManualModeActive = false; // Выходим из ручного режима
break;
}
}
// Ручное управление нагревателем
if (heaterManualModeActive) {
switch (heaterManualMode) {
case 0: // Выключено
digitalWrite(RELAY_HEATER, LOW); // Выключаем нагреватель
break;
case 1: // Включено
digitalWrite(RELAY_HEATER, HIGH); // Включаем нагреватель
break;
case 2: // Автоматический режим
heaterManualModeActive = false; // Выходим из ручного режима
break;
}
}
// Автоматическое управление насосом
if (!pumpManualModeActive) {
if (currentMode == SUMMER) {
if (pt100Temp >= 30 && (pt100Temp > boilerTemp + NIGHT_TEMP_DIFF && setBoilerTemp >= boilerTemp + deltaTemp)) {
digitalWrite(RELAY_HIGH_SPEED, HIGH); // Включаем насос на высокой скорости
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Перегрева нет
} else if (boilerTemp >= setBoilerTemp - tolerance) {
digitalWrite(RELAY_HIGH_SPEED, LOW); // Выключаем насос
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Перегрева нет
} else if (boilerTemp < setBoilerTemp - tolerance && pt100Temp <= boilerTemp + NIGHT_TEMP_DIFF) {
digitalWrite(RELAY_HIGH_SPEED, LOW); // Отключаем насос
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Перегрева нет
}
} else if (currentMode == WINTER) {
// Управление насосом для предотвращения замерзания коллектора
if (pt100Temp <= 5) {
digitalWrite(RELAY_LOW_SPEED, HIGH); // Включаем насос на низкой скорости
digitalWrite(RELAY_HIGH_SPEED, LOW);
} else if (pt100Temp >= 10) {
digitalWrite(RELAY_LOW_SPEED, LOW); // Отключаем насос
digitalWrite(RELAY_HIGH_SPEED, LOW);
updateMode(); // Проверяем возможность смены режима
}
} else if (currentMode == NIGHT) {
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_HIGH_SPEED, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Перегрева нет
} else if (currentMode == OVERHEAT) {
if (pt100Temp >= 95 && boilerTemp < 95) {
digitalWrite(RELAY_HIGH_SPEED, HIGH); // Включаем насос на высокой скорости для охлаждения
digitalWrite(RELAY_LOW_SPEED, LOW);
digitalWrite(RELAY_OVERHEAT, LOW); // Реле перегрева выключено
overheat = false; // Снимаем флаг перегрева
} else if (pt100Temp >= 95 && boilerTemp >= 95) {
digitalWrite(RELAY_OVERHEAT, HIGH); // Включаем реле перегрева
digitalWrite(RELAY_HIGH_SPEED, LOW); // Останавливаем насос на высокой скорости
digitalWrite(RELAY_LOW_SPEED, HIGH); // Включаем насос на низкой скорости для циркуляции
overheat = true; // Устанавливаем флаг перегрева
} else if (pt100Temp < 95 && boilerTemp >= 95) {
digitalWrite(RELAY_HIGH_SPEED, HIGH); // Включаем насос на высокой скорости
digitalWrite(RELAY_OVERHEAT, LOW); // Отключаем реле перегрева
digitalWrite(RELAY_LOW_SPEED, LOW);
overheat = false; // Снимаем флаг перегрева
} else if (boilerTemp <= 90) {
digitalWrite(RELAY_OVERHEAT, LOW); // Отключаем реле перегрева
digitalWrite(RELAY_HIGH_SPEED, LOW); // Останавливаем насос
digitalWrite(RELAY_LOW_SPEED, LOW); // Останавливаем насос
overheat = false; // Снимаем флаг перегрева
} else if (boilerTemp == DEVICE_DISCONNECTED_C || boilerTemp == -127.0 || isnan(pt100Temp) || pt100Temp < -100 || pt100Temp > 400) {
digitalWrite(RELAY_OVERHEAT, LOW); // Отключаем реле перегрева
digitalWrite(RELAY_HIGH_SPEED, LOW); // Останавливаем насос
digitalWrite(RELAY_LOW_SPEED, LOW); // Останавливаем насос
overheat = false; // Снимаем флаг перегрева
}
}
}
// Автоматическое управление нагревателем
if (!heaterManualModeActive) {
if (currentMode == WINTER) {
// Управление нагревателем бойлера
if (boilerTemp < 5) {
digitalWrite(RELAY_HEATER, HIGH); // Включаем нагреватель
} else if (boilerTemp >= 30) {
digitalWrite(RELAY_HEATER, LOW); // Выключаем нагреватель
}
} else if (currentMode == SUMMER || currentMode == NIGHT) {
digitalWrite(RELAY_HEATER, LOW); // Выключаем нагреватель
} else if (currentMode == OVERHEAT) {
digitalWrite(RELAY_HEATER, LOW); // Выключаем нагреватель
}
}
}
Схема устройства для симуляции, открывать в новом окне, а то ничего не видно
Схема устройства для трассировки, открывать в новом окне, а то тоже ничего не видно
Отправил страницу на вёрстку, и тут вдруг подумал...тоже иногда бывает...а ведь алгоритм ректификационной колонны ну очень похож. Стоит ли делать, или не надо контрольного выстрела в печень? Как и жена очень просит не делать ей кофе-машину.
Комментарии (12)
Inn235
03.02.2025 16:15Не хватает самого интересного: какой коллектор предлагается использовать, где он будет расположен, что будете с ним делать зимой, что бы уберечь от перегрева, будете ли сливать на зиму воду?
kuzzdra
03.02.2025 16:15Не хватает самого интересного: какой коллектор предлагается использовать
Походу, да. Тот самый, сферический, для вакуума :( В тепловых машинах самое интересное - теплообменник, а его и нет.
Vassilij Автор
03.02.2025 16:15Согласен, данные о самом коллекторе пока отсутствуют, ввиду его физического отсутствия. Но пока планируется на тонких алюминиевых трубках. Стоит ли применять медный - не уверен. Обкатать решение я смогу только летом, и то не сразу, там и другие нюансы есть. Тогда смогу написать и вторую статью. По поводу бабаха при перегреве овер 100 для всего (если автоматика померла или магнитный клапан коллектора забит) - не, клапан ещё один с завода есть на бойлере. Но замечание верное, стоит вывести от его пипки трубку на улицу, на всякий.
renat85
Не видя то чем управляет эта штука можно сказать что сильно заморочено. Если система проточная почему просто не поставить аварийный клапан сброса давления на линию? Просто, дешево, надежно. Опять же, в случае если система не справится со своим охлаждением, банально температуры сред сравняются, а солнце продолжит дальше жарить коллектор и по дальнейшей "прожарке" и коллектор и бойлер овер 100, ждем бабах? Ибо автоматика сама что-то должна будет сделать...
randomsimplenumber
Солнечный коллектор, овер 100? риалли? О_о
Няп все значительно проще. Теплоизолированный бак, коллектор, кран Температура коллектора выше чем бака - кран открыт, иначе закрыт. Циркуляция - можно естественную, можно с насосом.
renat85
Ну если коллектор слеплен на коленке из спичек и арахисовой пасты то наверное овер 100 не про него. Нормальные решения на вакуумных трубках в легкую выведут на такую температуру без циркуляции при хорошей инсоляции.
randomsimplenumber
Вся конструкция собрана именно на коленке, и лишь коллектор на вакуумных трубках ;)
Расскажите подробнее плз. Это что то типа холодильника наоборот? Тогда достаточно не включать компрессор.
3epka
Странно что вас удивляет возможность кипения солнечного коллектора. У знакомого сделана специальная маркиза, которая автоматически закрывает тенью коллектор в случае опасной температуры (точно не знаю как он это реализовал). Также слышал что ставят просто радиатор в такой системе, где-то с северной стороны в тени - тоже на этот случай.
Вот кстати в статье оба варианта и рассмотрены https://1stsunflower.com/ru/Как-решить-проблему-перегрева-солнечного-коллектора%3F-id3036412.html
randomsimplenumber
Статья выглядит слишком нейросетевой, евпочя
Isiirk
Больше похоже на переводчик...
randomsimplenumber
Одно другому не помеха ;) Ни слова конкретики, ни одной цифры, и на картинке гидроакк вместо расширительного бака. Не верю.