Алгоритм регулировки температуры с учетом инерционности

rjlexx
Offline
Зарегистрирован: 28.01.2015

Всем привет! Пытаюсь реализовать регулятор температуры в комнате. Имею следующие компоненты:

1. Нормально открытый сервопривод на батарее. При подаче на него напряжения, батарея перекрывается в течение 3-5 минут.

2. Датчик температуры в другом конце комнаты

Сейчас алгоритм регулировки такой:

Каждые 30 сек проверяется температура в комнате.

Если температура выше 24,5 градусов и сервопривод открыт, то подаю напряжение на сервопривод(клапан закрывается)

Если температура ниже 24,3 градусов и сервопривод закрыт, то снимаю напряжение с сервопривода(клапан открывается)

Очевдно, что из-за длительного(3-5 мин) переходного процесса сервопривода, появляется инерционность. И температура скачет в горазжо больших пределах.

Посоветуйте, пожалуйста, алгоритм регулировки с учетом этой инерционности. Присматриваюсь к методу ПИД регулятора. Но, там обычно ругулируется мощность нагревателя. А сервопривод можно только включить или выключить и опять же, инерционность.

И возможно ли вообще это сделать с текущим набором компонентов или надо обязательно еще измерять температуру батареи?

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Релейный ПИД с периодом 20 минут.
А чем не устраивают обычные термоголовки?
Я понимаю если бы у вас был водяной теплый пол, там да, много гемора из-за инерционности.

rjlexx
Offline
Зарегистрирован: 28.01.2015

trembo пишет:
Релейный ПИД с периодом 20 минут. А чем не устраивают обычные термоголовки? Я понимаю если бы у вас был водяной теплый пол, там да, много гемора из-за инерционности.

У меня стояла изначально обычная термоголовка с ручной регулировкой температуры. Хочется сделать возможным удаленое регулирование температуры и ее автоматическое снижание при отсутствии дома.

Пойду штудировать про релейный ПИД. А 20 мин не слишком большой период?

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Чтобы клапана успевали шевелиться.
Можете и 5 минут поставить.

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Готовые с таймером двадцатку стоят....
http://www.santehnet.lv/index.php?l=2&c=206&p=24072

Не знаю сколько это стоит, но ещё:

https://ibud.ua/ru/p120624-c2395-termogolovka-herz-etk-elektronnaya

rjlexx
Offline
Зарегистрирован: 28.01.2015

Да, анализировал рынок на наличие готовых вариантов. Те которые с таймером, обычно не позволяют ими управлять удаленно. А дома я появляюсь не по расписанию. Могу раньше придти, могу позже. Те варианты, которые с удаленным управлением, стоят больше 100 баксов. А сервопривод стоил дешелве 500р. Вот и решил попробовать с его помощью сделать самостоятельно.

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Тогда серва со шкивом раза в два большем диаметра термоголовки.
Тросовый привод. Даже нить капроновая. Надежно зафиксировать от проворота.
У неё 180, на головке 360 (если надо).
Откалибровать один раз температура-градус поворота.
Плюсы - память состояния без питания.
Простейшее управление.
Минусы - городить конструктив.
Девятиграммовая не справится....

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

rjlexx, во-первых, непонятно, почему сервопривод можно только включить и выключить. Его, вообще-то, можно повернуть на любой угол.

Если все-таки состояния только два, то посмотрите, как регулируют мощность - там точно так же, как правило, только два состояния, и для регулировки температуры изменяется отношение времен во включенном и выключенном состоянии. 

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Нет, там не настоящий сервопривод,
а штука называемая сантехниками сервопривод.
Ну да бог им судья....
Внутри привод с воском.
Типа амортизатора.
Как в автотермостате или смесителе с термостатом.
Воск греют, он расширяется и нажимает на клапан.
Греют керамическим позистором, как в глюгане,
мощьность в прогретом состоянии 3-5 Ватт.
Поэтому и время на открытие- закрытие несколько минут.
Обычно стоят на рейке тёплого пола.

rjlexx
Offline
Зарегистрирован: 28.01.2015

trembo пишет:
Тогда серва со шкивом

Как раз минус в громоздкости и оттолкнул от этой затеи.

Пробую сейчас на основе примера PID_RelayOutput из библиотеки Arduino PID сделать ругулировку. Там как раз на выходе идет регулировка времени нахождения в открытом\закрытом состоянии. 

Вы очень развернуто ответили про сантехнический сервопривод:) Спасибо! Именно для теплых полов он и предназначается. А так как в комнатном отоплении используется аналогичный клапан, решил попробовать использовать этот сервопривод. Вот такой у меня: https://www.aliexpress.com/item/230v-NO-thermal-actuator-electric-valve-for-underfloor-heating-radiator-valve-control-normally-open/32806771466.html?spm=a2g0s.9042311.0.0.AKYJXW

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011
rjlexx
Offline
Зарегистрирован: 28.01.2015

Да, я рассматривал этот регулятор. Выше писал по какой причине он мне показался неподходящим.

bwn
Offline
Зарегистрирован: 25.08.2014

rjlexx пишет:

У меня стояла изначально обычная термоголовка с ручной регулировкой температуры. Хочется сделать возможным удаленое регулирование температуры и ее автоматическое снижание при отсутствии дома.

Честно говоря, при использовании газового радиаторного отопления смысла немного, клапана будете 10 лет окупать. Проще температуру или скорость подачи теплоносителя изменять, да и то, нужно ли? Какую то нормальную экономию наблюдал на эл.теплых полах, там таймера недельные стоят с вариациями включения. Газ провел, сперва поигрался с головками, а потом бросил, не стоят 200-300 рублей экономии этой возни. ИМХО.

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Именно так.
Если в котле его нет то лучше сделать погодный регулятор.
А вот там тогда и таймер добавлять.
Термоголовки на тёплый пол отключил в первый же год.
Котёл конденсатный с погодником.
По таймеру на ночь на 2 градуса снижается температура.

rjlexx
Offline
Зарегистрирован: 28.01.2015

У меня квартира в МКД. Оплата отопления по счетчику. До этого стояла механическая термоголовка. Я понижал на ней температуру, когда уходил на работу и повышал температуру, когда возвращался домой. Неудобство в том, что приходится это каждый раз делать вручную и при возвращении квартира некоторое время прогревается.

С сервоприводом планирую сделать следующую логику:

1. Когда никого нет дома, МК будет автоматически снижать температуру.

2. На пути домой буду вручную отправлять с телефона команду на прогрев квартиры или автоматизирую это завязавшись, например, на приближение телефона на определенное расстояние к дому.

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Ну так и я о том же!
Снижайте температуру носителя.
Термоголовки будут ограничивать максимум,
а минимумом будете занимать сами.

bwn
Offline
Зарегистрирован: 25.08.2014

trembo, не, это нам домовладельцам хорошо. Не знаю как у Вас в многоквартирных, а у нас опа. У кого входящая, тот зажаривается, у кого выход, зубами лязгает. Где счетчики калорий, думаю жарятся все и просто взять и снизить не выйдет.
Самое прикольное, у мамки квартира 69кв.м, у жены 48кв.м, ценники на отопление раза в полтора-два выше моих на 100кв. Это при том, что квартиры не угловые и с улицей одна стена контачит, физическая загадка однако.))))

rjlexx
Offline
Зарегистрирован: 28.01.2015

Если не получится с сервой на парафине добиться поддержания стабильной температуры, то буду уже городить управлвление механической головкой.

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Есть ещё шаровой трехвходовой клапан с моторчиком.
Открывается-закрывается за 2-3 секунды. 12 Вольт.
Но стОит как готовый цифровой термостат.
https://m.ebay.com/itm/3-way-motorized-ball-valve-DN15-reduce-port-T-por...

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Ну какой нафик трехходовый. Есть стандартное решение "термоэлектрическая головка", электроуправляемый аналог батарейной термоголовки. Установочные места - один в один, с батареи крутилку снял и выбросил, на ее место эту самую термоэлектрическую головки поставил. Воткнул в розетку - открылась, вынул - закрылась. В разрыв поставил два реле последовательно, одно термо, тругое - таймер. Ну или от большого ума ардуино с релюшкой.

b707
Offline
Зарегистрирован: 26.05.2017

и ради чего это все в квартире, в которой почти постоянно кто-то есть?? - смысл есть, когда люди уезжают на несколько дней.  А понижать температуру на 5 часов днем, а вечером поднимать обратно - ну прикиньте, какая экономия будет. Думаю. что и 200 рублей в месяц вряд ли...

Потратьте лучше эти силы и опыт не на экономию каких-то копеек, а чтоб заработать побольше...

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

Всем привет!

Разбираюсь с сантехническим парафиновым "сервоприводом" (Valtec VT. ТЕ 3040)

Задача - чтобы "сервопривод" нажимал на шток трехходового клапана (Valtec VT.MR03.N) и на выходе получалась необходимая температура подачи в коллектор тёплых полов (частный дом, большая теплоёмкость, надоело крутить термоголовку по погоде).

Упрощенно - "сервопривод" давит на шток клапана. Надавил сильно - перекрыл подачу горячей воды полностью. Надавил средне - пошла тёпленькая водичка. Отпустил вообще - пошла очень горячая вода в пол (плохо). То есть нужно балансировать на грани открытия клапана (насколько это получится).

Интересная задачка, короче. Пока строился - всё руки не доходили. Специально взял "сервопривод" на 220 вольт - чтобы легко управлять через релейный модуль ардуино.

Сваял вариант для отладки:

- ардуино Uno с ethernet-шилдом подключена к домашнему роутеру

- раз в 10 секунд измеряет температуру коллектора с помощью ds18b20

- отправляет температуру get-запросом на сервер в инете. на сервере лежит php-скрипт, записывает температуру в БД (для анализа) и отвечает ардуинке, что делать с сервоприводом (подать ток на него или нет)

- ардуино по ответу web-сервера включает или выключает реле

Загвоздка в том, что парафин долго разогревается (и долго остывает). Зато потом в разогретом парафине шток очень резко двигается и температура воды скачкообразно растёт (ведь смесительный клапан открывается на 100%).

Для пробного алгоритма ввел два параметра - сколько секунд разогревать парафин и сколько остужать. Хочется найти такие параметры, чтобы температура на выходе была более стабильна. Подбираю параметры и вот что получается.

Увеличиваю время нагрева - клапан успевает открыться полностью и в коллектор успевает зайти вода +50° (нехорошо). Уменьшаю на несколько секунд - клапан вообще не открывается. Вот только что подобрал параметры близкие к "равновесию" - греем парафин 109 сек., охлаждаем 300 сек.

Продолжаю изучать.

Вот алгоритм работы "сервопривода" из мануала:

 

Прошу высказываться, если этот вопрос интересен. Всю жизнь программирую, мне пока в удовольствие разбираться :) Если в этой теме неуместно - могу создать отдельную.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Ну только так у Воды большая теплоемкость, а толкать туда парафин и при температуре когда он застывает.

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

PavelTLT пишет:

Прошу высказываться, если этот вопрос интересен. Всю жизнь программирую, мне пока в удовольствие разбираться :) Если в этой теме неуместно - могу создать отдельную.

Уважаемый, люди намного умнее нас с Вами, давно придумали ПИД алгоритм. Я сомневаюсь, что Вы (или кто-то) его прямо сейчас опровергнут. Если не читали - почитайте, очень полезное занятие.

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

И, кстати, при большой инерционности интегальная и дифференциальная составляющая не имеет смысла. И остается только пропорциональная составляющая...

ВН
Offline
Зарегистрирован: 25.02.2016

судя по графику производителя этакий пульсирующий режим работы заложен здесь изначально.

Если есть желание попытаться этот клапан открывать не полностью, а частично, то нужно по меньшей мере представлять физику его работы и уже исходя из этого, пытаться там что-то мастрячить.

т.е. попробуйте описать свою задачу сначала с т.з. физики. 

 

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019
qwone пишет:
у Воды большая теплоемкость, а толкать туда парафин и при температуре когда он застывает.
Ну что вы, я не толкаю парафин в воду :)
Видимо, слишком широко описал задачу и вас запутал. 
Давайте пока ограничимся первым шагом - есть типа "электротолкатель". Подаём напряжение - из него потихоньку выдвигается шток на 3 мм. Перестаём подавать - шток потихоньку заползает обратно.
Вот этим "электротолкателем" я и пытаюсь управлять. Заменить его на изделие другого типа не могу - это определенный стандарт в управлении водяными клапанами (упрощенно).
 
Спросите, причем здесь парафин? Внутри "электротолкателя" находится небольшая ёмкость (пишут по-разному, то ли с твердотельным армопарафином, то ли с толуолом, не суть, я упрощенно написал "парафин"). При подаче напряжения эта ёмкость нагревается, рабочее тело ("парафин") расширяется и толкает шток. Пропадает напряжение - рабочее тело потихоньку остывает и шток возвращается в исходное положение.
 
Хорошо объяснил?
 
 
mykaida пишет:
Уважаемый, люди намного умнее нас с Вами, давно придумали ПИД алгоритм. Я сомневаюсь, что Вы (или кто-то) его прямо сейчас опровергнут. Если не читали - почитайте, очень полезное занятие.

Уважаемый, конечно читал про ПИД. Использовать его собираюсь на следующем этапе. А сейчас разбираюсь с самим "сервоприводом" (или "электротолкателем", если так удобнее называть). Чтобы лучше представлять работу "электротолкателя" при подборе ПИД-коэффициентов.

ВН пишет:
судя по графику производителя этакий пульсирующий режим работы заложен здесь изначально.

Верно! Но я подобрал такое время воздействия на "электротолкатель", чтобы шток выходил не полностью. Изобразил зеленым цветом. Шток не успевает выйти целиком, а мне это и нужно.

После обкатки (подбираю на реально работающей системе) можно будет подобрать t-max -  время воздействия, которое запрещено превышать при управлении через ПИД-алгоритм (для избежания выхода штока целиком).

ВН пишет:
Если есть желание попытаться этот клапан открывать не полностью, а частично, то нужно по меньшей мере представлять физику его работы и уже исходя из этого, пытаться там что-то мастрячить.

Выше расписал физику его работы. 

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

Это вы так думаете, а торговцы знают что это не так.

Как правило, электрические приводы
имеют встроенную спираль,
при получении сигнала она нагревается
и расширяется -
тем самым приводя в движение клапан.

http://heiz.ru/articles/119.html

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

trembo пишет:
Это вы так думаете, а торговцы знают что это не так.

Я паспорт изделия практически цитировал.

Изделие по вашей ссылке тоже имеет паспорт производителя Herz, где сказано "Термопривод принимает положение благодаря электрически нагреваемому расширительному элементу."

Ну да ладно, какой бы ни был материал рабочего тела (металл, жидкость в рабочем диапазоне t или жидкость/твердое тело в рабочем диапазоне t) - алгоритм работы "электротолкателя" будет одинаковый.

Подали напряжение - ничего не происходит. Ждём нагрева рабочего тела. Подождали - рабочее тело расширилось, вытолкнуло шток.

И наоборот. Согласны?

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

PavelTLT пишет:

Задача - чтобы "сервопривод" нажимал на шток трехходового клапана (Valtec VT.MR03.N) и на выходе получалась необходимая температура подачи в коллектор тёплых полов (частный дом, большая теплоёмкость, надоело крутить термоголовку по погоде).

Уважаемый, у меня была такая же проблема и я ее успешно решил. Изначально я тоже решил пойти путем микроконтроллеров, но слава богу бросил эту мысль. Решил все много проще - поставил на подачу механический терморегулятор с выносным датчиком температуры. Датчик температуры прикрутил к трубе обратки. И все - не зависит от электричества, сбоев программы и т.д.

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

mykaida пишет:
Решил все много проще - поставил на подачу механический терморегулятор с выносным датчиком температуры. Датчик температуры прикрутил к трубе обратки. И все - не зависит от электричества, сбоев программы и т.д.

У меня тоже 4 года стоял механический терморегулятор с выносным датчиком. Давно хотел автоматизации и вот сейчас занимаюсь этим. 

 

ВН
Offline
Зарегистрирован: 25.02.2016

PavelTLT пишет:
Ну да ладно, какой бы ни был материал рабочего тела (металл, жидкость в рабочем диапазоне t или жидкость/твердое тело в рабочем диапазоне t) - алгоритм работы "электротолкателя" будет одинаковый.

И наоборот. Согласны?

в общем - верно, а в частности - не верно.

на данный момент не вполне понятно, за счет какого расширения работает ентот парафин.

При затвердевании большинство веществ уменьшают свой объем, причем это совсем другой коэф, чем при нагревании- охлаждении того- же вещества, но в жидкой фазе.

И теплоемкостей при фазовом переходе аж 3, для тврдоей фазы, самого фазового прехода + еще и жидкой фазы.

Пока ясно одно, частота опроса датчика Т должна быть выше, а управление на симмисторе.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Глючноватая у Вас система. По факту желательно поставить датчик температуры для измерения температуры электропарафина. И по этой температуре следить за уровнем открытия прикрытия клапана. А дальше задача состоит в регулировании этой температуры для управления всей системы. Конечно эту трепанацию можно сделать только на одном регуляторе и получить эмпирическую зависимость. Длина выдвинутого штока -(полное время  ШИМ ,время вклШИМ, время выкл ШИМ). Но даже это не внесет полную ясность работу всей вашей системы, так как это все же электроклапан и "закон Ома для водяной системы " не работает. И вам придется индивидуально регулировать каждый клапан(темепературу регулирования армопарафина каждого) для вменяемой работы всей вашей системы.
Вопрос из зала: а почему не поставили тогда изначально термодатчики.
Ответ : Так это все капитализм. Защита рынков сбыта.  У фирм производителей есть разработки типовых систем . И вот для каждого терморегулятора вычисленые типовые настройки регулирования, которые и зашиты в контролере, который скорее всего и продается по конским ценам. И работу которого вы вычисляете.  Так что или покупайте контроллер или ставьте термометры в каждый клапан.

ПС: Но может быть такая картина что эти клапаны работают в режиме открыл/закрыл . А промежуточных значений не предусмотрено производителем. 

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

qwone пишет:
Но может быть такая картина что эти клапаны работают в режиме открыл/закрыл . А промежуточных значений не предусмотрено производителем.

Так и есть.

Но шток двигается плавно, поэтому подбором времени открытия/закрытия уже удалось ограничить выдвижение штока. Если добавить автоматическую подстройку времени открытия/закрытия, то получится интерфейс для управления этим "электротолкателем". Потом добавить ПИД-алгоритм и получить желаемое.

А пока сделал прямое управление ПИД-алгоритмом, подбираю коэффициенты и анализирую результаты.

На графиках выше оси X - температура. Ниже оси X - результат функции ПИД (для анализа). Если ПИД()>0, тогда подаю напряжение на "электротолкатель", иначе отключаю.

Включать питание до того, как температура упадёт уже получилось (коэффициентом Д=0.1). А вот выключать питание "электротолкателя" раньше, чем температура начнет расти - вот это задачка... Температура не растет - значит П не срабатывает, И не наблюдает ошибку... Вся надежда на подбор Д-коэффициента.

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

ВН пишет:
При затвердевании большинство веществ уменьшают свой объем, причем это совсем другой коэф, чем при нагревании- охлаждении того- же вещества, но в жидкой фазе.

И теплоемкостей при фазовом переходе аж 3, для тврдоей фазы, самого фазового прехода + еще и жидкой фазы.

Согласен. Но для нужд алгоритма, полагаю, будет достаточно знаний, полученных опытным путём. 

Ведь точных параметров (время нагрева, остывания) быть не может - они зависят от температуры рабочего тела перед началом воздействия. Полагаю, придется придумывать какой-то алгоритм самообучения этим параметрам с автокоррекцией каждые Х-минут. Здорово, правда? (с) :)

ВН пишет:
Пока ясно одно, частота опроса датчика Т должна быть выше, а управление на симмисторе.

Частота пока 3 секунды. Симистор - для надежности, чтобы ресурс реле не убивать, да? Пока для отладки хотел на имеющихся твердотельных реле поиграться.

ВН
Offline
Зарегистрирован: 25.02.2016

PavelTLT пишет:
Частота пока 3 секунды. Симистор - для надежности, чтобы ресурс реле не убивать, да? 

да.

Вы просто сформулируйте себе текущую задачу в вашей постанове.

т.е. вы хотите фактически зафиксировать какую-то температуру парафина, когда нагревается он принудительно, а охлаждается - самостоятельно.

Вообще, зачем все эти танцы с бубном, если теплоемкость пола или комнаты очень велика и кормите вы свои радиаторы стаб температурой или пульсирующим потоком разниы особо нет. Просто датчики для этих случаев должны стоять в разным местах.  

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

Итак, вчерашний день наблюдал за работой под управлением ПИД-алгоритма и подбирал коэффициенты. Результаты не очень понравились, есть скачки температуры до максимальной. Нужно самообучение (обратная связь). Всё фигня, Миша, давай по-новой (c) :)

Искал практику по управлению термоприводом, который долго разогревается и остывает.

Нашёл интересную тему! Там человек управляет похожим термоприводом с помощью ШИМ обычной схемой. При этом промежуточное положение штока поддерживается годами!

Сегодня проснулся в 6 утра и решил реализовать ШИМ-алгоритм (пока "на коленке", с той же ардуино и твердотельным реле, периодичность целых 3 секунды).

Результат даже в таких условиях очень понравился! За 5 часов работы на уставке 32°С было всего пара скачков температуры на пару градусов! При том, что система реально работала и температура приходящей к смесительному клапану воды менялась! Я очень доволен. Средняя температура выходящей воды за 5 часов составила 31.9°С. 

Теперь про алгоритм:

- Изначально ширина импульса "электротолкатель включен" 10 секунд. "Выключен" - 40 секунд.

- Каждые 120 секунд проверяем температуру выходящей воды. Если t° больше нужной - уменьшаем ширину импульса "включен" на 1 сек. Когда t° упало ниже желаемой - увеличиваем ширину импульса "включен" на 1 сек.

После пары часов изучения графиков ввёл пару хитростей:

1) Если t° и так падает - не нужно уменьшать ширину импульсов, нужно просто подождать (с ростом - то же самое)

2) Если t° стала выше нормы - сразу же нужно однократно уменьшить ширину импульса (а не ждать пока сработает цикл "каждые 120 сек")

3) Ввел гистерезис (пока поставил 0.3°С), чтобы система срабатывала еще до достижения заданной t°

 

Результатом, повторюсь, уже очень доволен. Реальный гистерезис этого "умного термостата" составил около +/- 0.7°С. Дом весь день работает на этом термостате. Пробовал дестабилизировать систему (увеличил ширину импульса) - постепенно клапан опять прикрылся до нужного уровня.

Продолжу наблюдения.

Когда отлажу коэффициенты и алгоритм (вдруг еще какие хитрости нужно добавить) - перепишу всё на arduino, пусть автономно управляет. Чтобы я через облачную БД только настройки контроллеру передавал. 

И можно будет переходить к следующему этапу.

 

ВН пишет:
Вы просто сформулируйте себе текущую задачу в вашей постанове.
 
Хочу управлять температурой выходящей жидкости из смесительного узла с некоторой точностью (+/- градус, например). Для этого нужно поддерживать какой-то промежуточный выход штока "электротолкателя".
 
Первичный анализ показал, что это возможно. Достаточно подобрать временные задержки и сделать алгоритм с обратной связью.
Сегодняшние эксперименты показали, что алгоритм неплохо работает.
 
ВН пишет:
Вообще, зачем все эти танцы с бубном, если теплоемкость пола или комнаты очень велика и кормите вы свои радиаторы стаб температурой или пульсирующим потоком разниы особо нет. Просто датчики для этих случаев должны стоять в разным местах. 
 
Сейчас делаю только для нужд тёплого пола. Металлопластиковым трубам и самой стяжке вредно подавать высокую температуру "скачками". На комфорте это тоже может сказаться. Я лучше сделаю нормальное, плавное управление температурой потока.
 
ВН
Offline
Зарегистрирован: 25.02.2016

ок, сейчас вы стали двигаться в верном направлении

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019

Всем привет!

Подобрал параметры, отладил алгоритм. Пару недель уже работает автономно на реальной системе.

Температуру держит стабильно даже на первой скорости насоса (при этом вода медленнее доходит до термодатчика и он соответственно дольше реагирует)

 

Переписал всё на arduino. Самостоятельно лезет в облачную БД за настройками, хранит их у себя в EEPROM (на случай отсутствия интернета), раз в 30 секунд скидывает в облачную БД показания температурных датчиков (четыре в доме, один на улице, один на трубе подачи отопительной воды в пол).

Может еще будут небольшие доработки (проверять контрольную сумму настроек из облачной БД - вдруг какой-то сбой чтения).

Но в целом задача №1 выполнена. Arduino самостоятельно управляет электромеханическим клапаном Valtec, который накручен на трехходовый клапан смесительного узла. Кому надо - можете пользоваться кодом. Особо не чистил от отладочных "костылей", поэтому извиняйте.

PavelTLT
PavelTLT аватар
Offline
Зарегистрирован: 09.02.2019
// нужно ввести CRC настроек. а то каким-то образом on_delay_step стал 5 вместо 0.25
// это неправильное значение даже сохранилось в EEPROM

#include <Ethernet.h>
#include <OneWire.h>
#include <EEPROM.h>
#include <MemoryFree.h>
//#include <utility/w5100.h>

typedef uint8_t SensorAddress[8]; // определяем тип данных для адреса одного датчика

// настройки ethernet
byte mac[] = {0x1E, 0x2D, 0xBE, 0xEF, 0xFE, 0xED};
IPAddress ip(192, 168, 1, 177);
IPAddress myDns(8, 8, 8, 8);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
char server[] = "www.xxxxxxxx.ru";
EthernetClient client;


#define RELAY_PIN  2      // номер выхода,подключенного к реле

OneWire ds1(7); // Создаем объект OneWire для шины 1-Wire на 7-пине
OneWire ds2(8); // Создаем объект OneWire для шины 1-Wire на 7-пине
OneWire ds3(6); // Создаем объект OneWire для шины 1-Wire на 7-пине
OneWire ds4(9); // Создаем объект OneWire для шины 1-Wire на 7-пине
byte temper_data[2]; // Место для значения температуры

// настройки по умолчанию ШИМ-алгоритма управления термоприводом
float temp_ideal=30; // целевая температура по умолчанию
float temp_gist=0.35; // гистерезис. греть/охлаждать перестаем на 0.3 градуса раньше
float on_delay_default=10;  // 10 секунд работаем (питание на клапан), 40 секунд отдыхаем
float sum_delay=50; // общее время работы с клапаном 50 секунд. если будем дольше включать - то уменьшим время выключения
float temp_delay=120; //  каждые 120 секунд определяем - надо ли изменить продолжительность питания/отдыха
float diff_delay=120; //  динамику изменения температуры: t 2x сек назад, t 1x сек назад, t сейчас
float on_delay_step=0.5; //  шаг изменения ширины импульса (за раз изменяем на 1 секунду)
float max_temp=50; //  если температура выше критической - сбрасываем настройку $on_delay
float max_on_delay=20; //  не греем клапан дольше 20 сек
// инициализируем переменные для работы ШИМ-алгоритма управления термоприводом
float temp_now=0; // температура в этот конкретный момент
float temp_old=0; // старая температура
unsigned long temp_old_time=0; // момент сохранения старой температуры в temp_old

float temp_before1=0; // температура temp_delay секунд назад
float temp_before2=0; // температура 2 х temp_delay секунд назад
float on_delay=0; // время работы с клапаном
int target_direction=0; //  направление к целевой температуре. +1 значит хотим повысить, -1 значит хотим понизить. 0 - ничего не хотим
int alarm_frost=0; //  один раз при перегреве снижаем $on_delay
int hot_stopper=0; //  один раз если температура резко снижается больше 1.5 градусов за 4 минуты - увеличиваем $on_delay (пытаемся замедлить снижение, чтобы клапан не закрылся целиком)
int grow_chance_count=0; //  счетчик шансов на рост. когда температура сама растет - просто ждем, вдруг дорастёт сама
                         //  но если 3 раза подряд роста до целевой температуры не произошло - значит нужно
                         //  усилить воздействие (ширину импульса)
int freeze_counter=0;    // пока идёт обратный отсчет freeze_counter (до нуля) мы не пытаемся влиять на клапан, а просто наблюдаем (кроме аварийных случаев)
bool relay_status=false;
String info=""; // для каждого сеанса связи с сервером собираем всякую отладочную инфу и отправляем.
float temp_outdoor=0; // температура на улице
float temp_home1=0; // температура дома
float temp_home2=0; // температура дома
float temp_home3=0; // температура дома
float temp_home4=0; // температура дома

long bigtimer; // таймер для контроля периодичности выполнения команд. будем раз в секунду добавлять +1. если будет расходиться с реальным временем - значит где-то зависания.
int need_grow_timer=0; // таймер "сколько секунд подряд нужен рост". запускаем как только температура ниже уставки. обнуляем как дойдет до уставки или выше.
                     // если (таймер > 10 x temp_delay && on_delay < on_delay_default) тогда on_delay = on_delay_default 
                     // для случаев когда после сильного падения on_delay (например, до нуля) замучаешься ждать роста
                     // ведь с шагом 0.25 необходимо 12 / 0.25 * 2мин = 96 минут

int FindStrInArray(char *mArray,int mlength, String marker) {
 int posi=-1;
 //Serial.println("marker:"+marker);
 //Serial.print("size:");
 //Serial.println(marker.length());
 for(int i=0;i<mlength-marker.length();i++)
  {
   int f=0;
   //Serial.print("pos:"); Serial.print(i); Serial.print(" symbol:"); Serial.println(mArray[i]);
   for(int j=0;j<marker.length()-1;j++)
    {
     if (mArray[i+j]==marker[j]) 
      { 
       f=1; 
       //Serial.print("совпал символ на поз"); Serial.print(i+j); Serial.print(" маркер:"); Serial.print(marker[j]);  Serial.print(" symbol:"); Serial.println(mArray[i+j]); 
      }
     else 
      {
       if (f==1) f=0;
       break; 
      }
    } 
   if (f==1) 
    {  
     posi=i; 
     break; 
    }
  } 
 return(posi); 
}

String GetStrFromArrayBetweenMarkers(char *mArray,int ArrayLength,String marker1, String marker2) {
 int posi1,posi2;
 posi1=FindStrInArray(mArray,ArrayLength,marker1);
 if (posi1==-1) { return("error1"); }
 posi2=FindStrInArray(mArray,ArrayLength,marker2);
 if (posi2==-1) return("error2");

 String res="";
 for (int i=posi1+marker1.length(); i<posi2; i++)
  res=res+mArray[i];

 return(res);
}

float StringToFloat(String value) {
 float floatVar;
 char floatbufVar[32];
 value.toCharArray(floatbufVar,sizeof(floatbufVar));
 floatVar=atof(floatbufVar);  
 return(floatVar);
}

// this method makes a HTTP connection to the server:
bool httpRequest(String request,String request2,bool fOnlySend) {
  //client.setTimeout(2000);
  // close any connection before send a new request.
  // This will free the socket on the WiFi shield
  client.stop();

  // if there's a successful connection:
  if (client.connect(server, 80)) {
    Serial.println("connecting...");
    // send the HTTP GET request:
    client.print("GET /");
    client.print(request);
    client.print(request2);
    client.println(" HTTP/1.1");
    client.println("Host: www.xxxxxxxxxxx.ru");
    client.println("User-Agent: arduino-ethernet");
    client.println("Connection: close");
    client.println();
    return(true);
  } else {
    // if you couldn't make a connection:
    Serial.println("connection failed");
    return(false);
  }
 if (fOnlySend) client.stop(); 
}

float HttpGet() {
 char mGet[200];
 int mGetSize=0; int f=1; char c2=' '; bool f_start=false;
 //client.setTimeout(2000);
 long startOfHttpResponseTime = millis();
 while ((long)(millis() - startOfHttpResponseTime) < 1000) {
  if (client.available()) 
   {
    char c = client.read(); 
    if (!f_start) {
     if (c2!='P'||c!='H') {
      c2=c;
      continue;
     }
     f_start=true;
    } 
    Serial.print(c);
    mGet[mGetSize]=c;
    
    mGetSize++;
    if (mGetSize>sizeof(mGet)-1) break;
   }
  else break;
 }
 //client.stop();
 //Serial.print("buffer size:");
 //Serial.println(mGetSize);
 //Serial.println(mGet);
 if (mGetSize==0) return(-1);
 //Serial.println(mGetSize);

 // получаем настройки с сервера
 String res;
 
 Serial.println(temp_ideal);
 float last_temp_ideal=temp_ideal;
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st1%","en1%"); if (res!="") temp_ideal=StringToFloat(res);
 // если на сервере изменили temp_ideal - то нужно срочно начать реагировать
 if (last_temp_ideal!=temp_ideal) { on_delay=on_delay_default; freeze_counter=0; }
 
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st2%","en2%"); if (res!="") temp_gist=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st3%","en3%"); if (res!="") sum_delay=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st4%","en4%"); if (res!="") temp_delay=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st5%","en5%"); if (res!="") diff_delay=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st6%","en6%"); if (res!="") on_delay_step=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st7%","en7%"); if (res!="") max_temp=StringToFloat(res);
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st8%","en8%"); if (res!="") max_on_delay=StringToFloat(res);

 float last_on_delay_default=on_delay_default;
 res=GetStrFromArrayBetweenMarkers(mGet,mGetSize,"st9%","en9%"); if (res!="") on_delay_default=StringToFloat(res);
 // если на сервере изменили on_delay по умолчанию - то нужно обновить on_delay. вдруг удаленное управление при аварийной ситуации
 if (last_on_delay_default!=on_delay_default) { on_delay=on_delay_default; info=info+"def_on_delay_updated,"; }

 WriteParamsToEEPROM(); // полученные настройки запишем в ПЗУ

 Serial.println(temp_ideal);
 Serial.println(temp_gist);
 Serial.println(on_delay_step);

 Serial.println("загрузили настройки");
 //Serial.println(res);
 //Serial.println(resfloat);
 float resfloat=1;
 return(resfloat);
}

float GetParamsFromDB() {
 Serial.println("подключаемся к серверу");
 if (httpRequest("ard.php?action=get_params","",false)) { // успешно отправили запрос
  Serial.println("запрашиваем настройки");
  delay(500);
  Serial.println(HttpGet());
  Serial.println(sum_delay);
 }
}
 
void EEPROM_float_write(int addr, float val) // запись в ЕЕПРОМ
{
  byte *x = (byte *)&val;
  for(byte i = 0; i < 4; i++) {
   if (EEPROM.read(i+addr)!=x[i]) EEPROM.write(i+addr, x[i]); // записываем только если изменилось - ведь ячейки ПЗУ не вечны
  }
}
 
float EEPROM_float_read(int addr) // чтение из ЕЕПРОМ
{   
  byte x[4];
  for(byte i = 0; i < 4; i++) x[i] = EEPROM.read(i+addr);
  float *y = (float *)&x;
  return y[0];
}

void ReadParamsFromEEPROM() // читаем все настройки из ПЗУ при старте
{
 // по адресу 0 находится флаг. =1 можно читать. =255 не читать. если ПЗУ пустое (новая ардуино) то по адресу 0 будет лежать 255.
 if (EEPROM.read(0)!=1) return;
 
 float val;
 val=-1; val=EEPROM_float_read(0*4+1); if (val!=-1) { temp_ideal=val; Serial.println(val); }
 val=-1; val=EEPROM_float_read(1*4+1); if (val!=-1) temp_gist=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(2*4+1); if (val!=-1) sum_delay=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(3*4+1); if (val!=-1) temp_delay=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(4*4+1); if (val!=-1) diff_delay=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(5*4+1); if (val!=-1) on_delay_step=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(6*4+1); if (val!=-1) max_temp=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(7*4+1); if (val!=-1) max_on_delay=val;
 Serial.println(val);
 val=-1; val=EEPROM_float_read(8*4+1); if (val!=-1) on_delay_default=val;
 Serial.println(val);
}

void WriteParamsToEEPROM() // сохраняем все настройки в ПЗУ
{
 EEPROM_float_write(0*4+1,temp_ideal);
 EEPROM_float_write(1*4+1,temp_gist);
 EEPROM_float_write(2*4+1,sum_delay);
 EEPROM_float_write(3*4+1,temp_delay);
 EEPROM_float_write(4*4+1,diff_delay);
 EEPROM_float_write(5*4+1,on_delay_step);
 EEPROM_float_write(6*4+1,max_temp);
 EEPROM_float_write(7*4+1,max_on_delay);
 EEPROM_float_write(8*4+1,on_delay_default);
 
 if (EEPROM.read(0)!=1) EEPROM.write(0, 1); // ставим флаг = 1 (можно читать настройки из EEPROM )
}

void initEthernet() {
 Serial.println("инициализируем ethernet");
 // start the Ethernet connection using a fixed IP address and DNS server:
 //W5100.setRetransmissionCount(2);
 Ethernet.begin(mac, ip, myDns,gateway,subnet);
 //Ethernet.begin(mac);
 // print the Ethernet board/shield's IP address:
 Serial.print("My IP address: ");
 Serial.println(Ethernet.localIP());
}

int8_t sensorAmount(OneWire *ds) {
 SensorAddress sAddress;
 (ds)->reset_search();
 int8_t sensors = 0;
 while ((ds)->search(sAddress)) // получили адрес датчика    
  if ( (ds)->crc8(sAddress,7)==sAddress[7] ) {
   info=info+";+"; 
   for (int i=0;i<8;i++)
    info=info+String(sAddress[i])+":";
   info=info+";"; 
   sensors++;
  }
 return sensors;
}

void setup() {
 bigtimer=0;
 pinMode(RELAY_PIN, OUTPUT);
 digitalWrite(RELAY_PIN, HIGH); // на всякий случай сразу выключаем реле

 // start serial port:
 Serial.begin(9600);
 while (!Serial) {
   ; // wait for serial port to connect. Needed for native USB port only
 }

 Serial.print("free memory:"); Serial.println(freeMemory());

 ReadParamsFromEEPROM(); // загружаем параметры из ПЗУ (пока с инетом не соединились надо работать по старым настройкам
 on_delay=on_delay_default; // начинаем работать по настройкам. если в ПЗУ сбой - по настройкам из шапки программы.
 
 // при старте инициализируем ethernet и загружаем настройки с сервера
 delay(1000); // give the ethernet module time to boot up:
 initEthernet();
 GetParamsFromDB();

 // нужно узнать адреса всех подключенных датчиков
 info=""; 

 // переменные датчика температуры
 {
 SensorAddress sAddress;
 int8_t sensors = 0;
 //OneWire ds1(7); // Создаем объект OneWire для шины 1-Wire на 7-пине
 //OneWire ds2(8); // Создаем объект OneWire для шины 1-Wire на 8-пине
 //OneWire ds3(6); // Создаем объект OneWire для шины 1-Wire на 6-пине
 
 ds1.reset_search();
 while (ds1.search(sAddress)) // получили адрес датчика    
  if ( ds1.crc8(sAddress,7)==sAddress[7] ) {
   info=info+";+"; 
   for (int i=0;i<8;i++)
    info=info+String(sAddress[i])+":";
   info=info+";"; 
   sensors++;
  }
 info=info+"Wire1count:"+String(sensors);
 httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true);
 info="";
 delay(2000);

 ds2.reset_search();
 sensors = 0;
 while (ds2.search(sAddress)) // получили адрес датчика    
  if ( ds2.crc8(sAddress,7)==sAddress[7] ) {
   info=info+";+"; 
   for (int i=0;i<8;i++)
    info=info+String(sAddress[i])+":";
   info=info+";"; 
   sensors++;
  }
 info=info+"Wire2count:"+String(sensors);
 httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true);
 info="";
 delay(2000);

 ds3.reset_search();
 sensors = 0;
 while (ds3.search(sAddress)) // получили адрес датчика    
  if ( ds3.crc8(sAddress,7)==sAddress[7] ) {
   info=info+";+"; 
   for (int i=0;i<8;i++)
    info=info+String(sAddress[i])+":";
   info=info+";"; 
   sensors++;
  }
 info=info+"Wire3count:"+String(sensors);
 httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true  );
 info="";
 delay(2000);

 ds4.reset_search();
 sensors = 0;
 while (ds4.search(sAddress)) // получили адрес датчика    
  if ( ds4.crc8(sAddress,7)==sAddress[7] ) {
   info=info+";+"; 
   for (int i=0;i<8;i++)
    info=info+String(sAddress[i])+":";
   info=info+";"; 
   sensors++;
  }
 info=info+"Wire4count:"+String(sensors);
 httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true  );
 delay(2000);

 // проверяем, сколько символов помещается в буфер GET-запроса
 //delay(2000);
 //httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=",String(freeMemory()), true);
 //delay(2000);
 //httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=33&info=A","", true);
 //delay(2000);
 //httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=34","&info=A", true);
 //delay(2000);

 info="";
 delay(2000);

// info="";
// int SensorCount=sensorAmount(&ds1); Serial.print("Wire1 - total devices found: "+String(SensorCount)); info=info+"Wire1count:"+String(SensorCount);
// httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true);
// info="";
// delay(2000);
// SensorCount=sensorAmount(&ds2); Serial.print("Wire2 - total devices found: "+String(SensorCount)); info=info+"Wire2count:"+String(SensorCount);
// httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true);
// info="";
// delay(2000);
// SensorCount=sensorAmount(&ds3); Serial.print("Wire3 - total devices found: "+String(SensorCount)); info=info+"Wire3count:"+String(SensorCount);
// httpRequest("ard.php?sensor_count=-1&sensor_num=3&sensor_3=2&info=",info , true);
// info="";
// delay(2000);


 Serial.print("free memory:"); Serial.println(freeMemory());
 }
}

void loop() {
 // 1 - даём импульсы ШИМ на термопривод (реле через пин7)
 perSHIM(on_delay*1000,sum_delay*1000-on_delay*1000);
 // 2 - каждые X секунды измеряем температуру
 perReadTemperature(3000);
 // 3 - каждые X секунд отправляем температуру на сервер (для анализа лога)
 perSendTemp(30000);
 // 4 - каждые 5 минут переинициализируем ethernet (вдруг обрыв или глюки) и загружаем настройки с сервера (вдруг изменились?)
 perInitEthernet(300000);
 // 5 - каждую секунду вызываем функцию анализа и принятия решения
 perAnalyze(1000);
 //Serial.print("free memory:"); Serial.println(freeMemory());
}

void perSHIM(unsigned long interval_on,unsigned long interval_off){
 static unsigned long prevTime = 0; // время когда последний раз переключали реле
 unsigned long interval;
 if (relay_status) interval=interval_off; // реле включено, значит отсчитываем миллисекунды до выключения реле
 else interval=interval_on; // реле выключено, значит отсчитываем миллисекунды до включения реле
 if(millis() - prevTime > interval ) {
  Serial.println("--");
  prevTime = millis();  //
  if (relay_status)
   {
    Serial.println("off");
    //digitalWrite(LED_BUILTIN,HIGH); 
    digitalWrite(RELAY_PIN,LOW); 
   } 
  else
   {
    Serial.println("on");
    //digitalWrite(LED_BUILTIN,LOW); 
    digitalWrite(RELAY_PIN,HIGH); 
   } 
  relay_status=!relay_status; 
 }
} 

void perReadTemperature(unsigned long interval){
 static unsigned long prevTime = 0; // время когда последний раз читали температуру
 if(millis() - prevTime > interval ) {
  prevTime = millis();  //

  float temperature;
  int LowByte,HighByte,TReading,SignBit;

  ds1.reset(); // Теперь готовимся получить значение измеренной температуры
  ds1.write(0xCC); 
  ds1.write(0xBE); // Просим передать нам значение регистров со значением температуры
  // Получаем и считываем ответ
  temper_data[0]=ds1.read(); 
  temper_data[1]=ds1.read(); 
  //temperature =  ((temper_data[1] << 8) | temper_data[0]) * 0.0625;
  LowByte = temper_data[0];
  HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) // если значение отрицательное
   {
    TReading = (TReading ^ 0xffff) + 1; 
   }
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) // если значение отрицательное
   temperature=-temperature;
  //temperature=5;
  
  //LowByte = temper_data[0];
  //HighByte = temper_data[1];  
  //temperature =  ((temper_data[1] << 8) | temper_data[0]) * 0.0625;
  //TReading = (temper_data[1] << 8) + temper_data[0]; // склеиваем
  //SignBit = TReading & 0x8000;  // test most sig bit
  //if (SignBit) // negative
  // TReading = (TReading ^ 0xffff) + 1; // 2 comp
  //temperature = TReading * 0.0625;    // multiply by (100 * 0.0625) or 6.25  
  //if (SignBit) temperature=-temperature;
  // Формируем итоговое значение: 
  //    - сперва "склеиваем" значение, 
  //    - затем умножаем его на коэффициент, соответсвующий разрешающей способности (для 12 бит по умолчанию - это 0,0625)
  //float temperature =  ((temper_data[1] << 8) | temper_data[0]) * 0.0625;
  // Выводим полученное значение температуры в монитор порта
  //Serial.println("подача ТП:"+String(temperature));
  temp_now=temperature;

  ds2.reset(); // Теперь готовимся получить значение измеренной температуры
  ds2.write(0xCC); 
  ds2.write(0xBE); // Просим передать нам значение регистров со значением температуры
  temper_data[0]=ds2.read(); 
  temper_data[1]=ds2.read(); 
  LowByte = temper_data[0];
  HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) // если значение отрицательное
   {
    TReading = (TReading ^ 0xffff) + 1; 
   }
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) // если значение отрицательное
   temperature=temperature*-1;
  //temperature=5;
  temp_outdoor=temperature;


//; 40:255:64:139:33:23:4:48:;; 40:255:198:229:33:23:4:226:;; 40:255:153:141:33:23:3:23:;Wire3count:3
  SensorAddress sAddress;
  sAddress[0]=40; sAddress[1]=255; sAddress[2]=64; sAddress[3]=139; sAddress[4]=33; sAddress[5]=23; sAddress[6]=4; sAddress[7]=48;
  ds3.reset(); // Теперь готовимся получить значение измеренной температуры
  ds3.select(sAddress); 
  ds3.write(0xBE); // Просим передать нам значение регистров со значением температуры
  temper_data[0]=ds3.read(); 
  temper_data[1]=ds3.read(); 
  LowByte = temper_data[0]; HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) TReading = (TReading ^ 0xffff) + 1; // если значение отрицательное
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) temperature=-temperature; // если значение отрицательное   
  temp_home1=temperature;

  sAddress[0]=40; sAddress[1]=255; sAddress[2]=198; sAddress[3]=229; sAddress[4]=33; sAddress[5]=23; sAddress[6]=4; sAddress[7]=226;
  ds3.reset(); // Теперь готовимся получить значение измеренной температуры
  ds3.select(sAddress); 
  ds3.write(0xBE); // Просим передать нам значение регистров со значением температуры
  temper_data[0]=ds3.read(); 
  temper_data[1]=ds3.read(); 
  LowByte = temper_data[0]; HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) TReading = (TReading ^ 0xffff) + 1; // если значение отрицательное
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) temperature=-temperature; // если значение отрицательное   
  temp_home2=temperature;
  
  sAddress[0]=40; sAddress[1]=255; sAddress[2]=153; sAddress[3]=141; sAddress[4]=33; sAddress[5]=23; sAddress[6]=3; sAddress[7]=23;
  ds3.reset(); // Теперь готовимся получить значение измеренной температуры
  ds3.select(sAddress); 
  ds3.write(0xBE); // Просим передать нам значение регистров со значением температуры
  temper_data[0]=ds3.read(); 
  temper_data[1]=ds3.read(); 
  LowByte = temper_data[0]; HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) TReading = (TReading ^ 0xffff) + 1; // если значение отрицательное
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) temperature=-temperature; // если значение отрицательное   
  temp_home3=temperature;

  ds4.reset(); // Теперь готовимся получить значение измеренной температуры
  ds4.write(0xCC); 
  ds4.write(0xBE); // Просим передать нам значение регистров со значением температуры
  temper_data[0]=ds4.read(); 
  temper_data[1]=ds4.read(); 
  LowByte = temper_data[0]; HighByte = temper_data[1];
  TReading = (HighByte << 8) | LowByte;
  SignBit = TReading & 0x8000;  // проверяем значение в самом старшем бите
  if (SignBit) TReading = (TReading ^ 0xffff) + 1; // если значение отрицательное
  temperature=float(TReading)*0.0625;    // умножаем на (100 * 0.0625) или 6.25
  if (SignBit) temperature=-temperature; // если значение отрицательное   
  temp_home4=temperature;

  ds1.reset(); // Теперь готовимся получить значение измеренной температуры
  ds1.write(0xCC); // Даем датчику DS18b20 команду пропустить поиск по адресу. В нашем случае только одно устрйоство 
  ds1.write(0x44); // Даем датчику DS18b20 команду измерить температуру. Само значение температуры мы еще не получаем - датчик его положит во внутреннюю память
  ds2.reset(); // Теперь готовимся получить значение измеренной температуры
  ds2.write(0xCC); // Даем датчику DS18b20 команду пропустить поиск по адресу. В нашем случае только одно устрйоство 
  ds2.write(0x44); // Даем датчику DS18b20 команду измерить температуру. Само значение температуры мы еще не получаем - датчик его положит во внутреннюю память
  ds3.reset(); // Теперь готовимся получить значение измеренной температуры
  ds3.write(0xCC); // Даем датчику DS18b20 команду пропустить поиск по адресу. Пусть все датчики измеряют
  ds3.write(0x44); // Даем датчику DS18b20 команду измерить температуру. Само значение температуры мы еще не получаем - датчик его положит во внутреннюю память
  ds4.reset(); // Теперь готовимся получить значение измеренной температуры
  ds4.write(0xCC); // Даем датчику DS18b20 команду пропустить поиск по адресу. Пусть все датчики измеряют
  ds4.write(0x44); // Даем датчику DS18b20 команду измерить температуру. Само значение температуры мы еще не получаем - датчик его положит во внутреннюю память
  // Микросхема измеряет температуру, а мы пока занимаемся другими делами
 }
} 

void perSendTemp(unsigned long interval){
 static unsigned long prevTime = 0; // время когда последний раз 
 if(millis() - prevTime > interval ) {
  prevTime = millis();  //
  Serial.println("подключаемся к серверу");
  client.stop();

  // if there's a successful connection:
  if (client.connect(server, 80)) {
    Serial.println("connecting...");
    // send the HTTP GET request:
    client.print("GET /");
    client.print("ard.php?sensor_count=7");
    client.print("&sensor_1=");
    client.print(temp_now);
    client.print("&sensor_2=");
    client.print(on_delay);
    client.print("&sensor_3=");
    client.print(temp_outdoor);
    client.print("&sensor_4=");
    client.print(temp_home1);
    client.print("&sensor_5=");
    client.print(temp_home2);
    client.print("&sensor_6=");
    client.print(temp_home3);
    client.print("&sensor_7=");
    client.print(temp_home4);
    client.print("&info=");
    client.print(info);
    client.println(" HTTP/1.1");
    client.println("Host: www.xxxxxxx.ru");
    client.println("User-Agent: arduino-ethernet");
    client.println("Connection: close");
    client.println();
  } else {
    // if you couldn't make a connection:
    Serial.println("connection failed");
  }
  info="";
 }
} 

void perInitEthernet(unsigned long interval){
 static unsigned long prevTime = 0; // время когда последний раз 
 if(millis() - prevTime > interval ) {
  prevTime = millis();  //
  initEthernet();
  GetParamsFromDB();
 }
} 

void perAnalyze(unsigned long interval){
 static unsigned long prevTime = 0; // время когда последний раз 
 
 if(millis() - prevTime > interval ) {
  prevTime = millis();  //
  /////////////////////////////////////////////////////////////////
  // обрабатываем
  /////////////////////////////////////////////////////////////////
  bigtimer++;
 
  if (temp_old_time==0) {
   temp_old=temp_now; // через temp_delay секунд эта температура станет полезна
   temp_old_time=millis();
  }
  else if (millis()-temp_old_time>diff_delay*1000) { // этот момент настал
    temp_before2=temp_before1;
    temp_before1=temp_old;
    temp_old_time=0;
  }
  
  // управляем воздействием в зависимости от текущей температуры
    
  if (freeze_counter>0) { // между точками воздействия реагируем на аварийные ситуации 
   freeze_counter--;
   // вдруг мы пытаемся нагревать, а температура превысила уставку?
   // нужно срочно остудить на одну ступень, а не ждать кучу времени ($freeze_counter) пока перегреется
   // но только один раз
   if (temp_now>temp_ideal+temp_gist && target_direction==1 && alarm_frost==0) {
    target_direction=-1;
    grow_chance_count=0; // обнуляем счетчик шансов на прогрев (перегрелись, куда еще :)      
    alarm_frost++;
    on_delay=on_delay-on_delay_step;  
   } 

   if (hot_stopper<1 && temp_before2>temp_before1 && temp_before1>temp_now && temp_before2-temp_now>1.5) { // резкое падение нужно один раз замедлить
    on_delay=on_delay+on_delay_step;  
    hot_stopper++;
   } 

  } 
  else {
   // противоаварийный сброс настроек, если температура выше критической 4 минуты подряд
   if (temp_now>max_temp && temp_before1>max_temp && temp_before2>max_temp) 
    { on_delay=on_delay_default; }
   // если температура выше критической и on_delay на максимуме - сбрасываем настройки, что-то резко изменилось
   if (temp_now>max_temp && on_delay>=(max_on_delay-on_delay_step)) 
    { on_delay=on_delay_default; }

   // а если температура = установке $temp_ideal, тогда вообще ничего делать не нужно
   // тогда остановим $freeze_counter, пока температура не выйдет за пределы

   if (temp_now<(temp_ideal-temp_gist)) { // если температура ниже заданной
    need_grow_timer++; // таймер "сколько секунд подряд нужен рост". запускаем как только температура ниже уставки. обнуляем как дойдет до уставки или выше.
                     // если (таймер > 10 x temp_delay && on_delay < on_delay_default) тогда on_delay = on_delay_default 
                     // для случаев когда после сильного падения on_delay (например, до нуля) замучаешься ждать роста
                     // ведь с шагом 0.25 необходимо 12 / 0.25 * 2мин = 96 минут
    if (need_grow_timer > 10*temp_delay && on_delay < on_delay_default) on_delay=on_delay_default;
    // нужно сильнее открывать клапан (увеличить on_delay)
    hot_stopper=0; // обнуляем счетчик попыток замедлить падение температуры
    target_direction=1; 
    // минуточку, может быть температура уже растёт? зачем еще сильнее открывать клапан?
    if (temp_before2<temp_before1 && temp_before1<temp_now ) 
     {
      if (grow_chance_count<4) grow_chance_count++; // температура и так растет. даём шанс
      else if (temp_now-temp_before2>=1.5) //  шансы на рост уже исчерпаны. но вдруг температура уже растёт хорошими темпами?
       ; // растёт хорошо! 1.5 градуса за 4 минуты, будем ждать сколько потребуется!
      else // рост слишком медленный, увеличиваем ширину импульса на термопривод
       on_delay=on_delay+on_delay_step; 
     }
    else  
     {
      on_delay=on_delay+on_delay_step;
      // если до уставки осталось более 3 градусов - ускоряем разогрев (увеличим on_delay еще на ступень on_delay_step)
      if ( (temp_ideal-temp_gist)-temp_now > 3 ) on_delay=on_delay+on_delay_step;
     } 
    if (on_delay>max_on_delay) on_delay=max_on_delay; // не позволяем подавать питания на термопривод импульсами дольше чем max_on_delay

    freeze_counter=temp_delay*2; // термопривод долго реагирует, не торопимся
   }
   else if (temp_now>(temp_ideal+temp_gist)) { // нужно слабее открывать клапан (увеличить on_delay)
    target_direction=-1;
    need_grow_timer=0; // таймер "сколько секунд подряд нужен рост". запускаем как только температура ниже уставки. обнуляем как дойдет до уставки или выше.
    alarm_frost=0; // обнуляем счетчик аварийных охлаждений
    grow_chance_count=0; // обнуляем счетчик шансов на прогрев (перегрелись, куда еще :)
    // минуточку, может быть температура уже падает? зачем еще сильнее прикрывать клапан?
    if (temp_before2>temp_before1 && temp_before1>temp_now) // уже падает
     { // если падает и мы недалеко от целевой температуры - лучше немного подождать, чем вызвать резкое неконтролируемое падение
      if ( (temp_now-(temp_ideal+temp_gist))<4 ) { on_delay=on_delay; } // просто ждём 

      // хотя температура и падает, но у нас сильный перегрев (на 10 градусов) - нужно прикрыть клапан
      if ( (temp_before2-(temp_ideal+temp_gist))>10&&
           (temp_before1-(temp_ideal+temp_gist))>10&&
           (temp_now-(temp_ideal+temp_gist))>10 )
            { // горячее на 10 градусов уже 4 минуты
              // нужно дважды уменьшить ширину импульса
              if (on_delay<on_delay_step*3) on_delay=on_delay_step; else { on_delay=on_delay-2*on_delay_step; }
            }
     }
    else // температура не падает, уменьшаем ширину импульса на термопривод
     if (on_delay<on_delay_step*2) on_delay=on_delay_step; else { on_delay=on_delay-on_delay_step; }
    freeze_counter=temp_delay; 
   }
   else { // температура в пределах гистерезиса
    target_direction=0; // никуда не дергаемся, тепло, хорошо :)
    need_grow_timer=0; // таймер "сколько секунд подряд нужен рост". запускаем как только температура ниже уставки. обнуляем как дойдет до уставки или выше.
    alarm_frost=0; // обнуляем счетчик аварийных охлаждений
    grow_chance_count=0; // обнуляем счетчик шансов на прогрев (вышли на режим, всё норм)
    hot_stopper=0; // обнуляем счетчик попыток замедлить падение температуры
   }
  }

  /////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////
 }
} 



 

mistert83
mistert83 аватар
Offline
Зарегистрирован: 24.01.2021

Здравствуйте. 

Собираюсь сделать терморегулятор  радиаторов в доме посредством управления сервоприводами, установленными на коллекторе. 

Есть положительный опыт построения  ПИД терморегулятора для управления газовым котлом по протоколу OpenTherm. Там ESP8266 на основе полученных данных вычислял значение ПИД от 0 до 100, которое фактически задавало температуру теплоносителя от 30 до 85 градусов. Подбором коэффициентов удалось добиться точности поддержания температуры в комнате  0,3 градуса.

Теперь задача следующая. Котёл будет поддерживать постоянную температуру теплоносителя, а регулировка температуры в помещениях должна производиться посредством управления сервоприводами на коллекторе радиаторов. 

Правильно ли я понимаю, что Ваш алгоритм представляет значение ПИД в период ШИМ ? То есть если ПИД=70, то 7 минут клапан открыт а 3 минуты закрыт, или например ПИД=20, тогда 2 минуты клапан открыт, а 8 минут закрыт. Это я так грубо привёл пример для собственного понимания.