Дачные часы

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

Главный компонент — датчик MQ-7

Суть его работы проста. Нагревательный элемент разогревает подложку, которая вступая в химическую реакцию дает содержание окисида углерода. Про автономность системы при рабочем токе датчика в 150 мA можно забыть сразу, при этом разогреться ему нужно до 50С, иначе он показывает откровенную ерунду, так что вариант как с BME280, включать раз в минуту и быстро снимать показания не прокатит.

Теоретически можно настроить потенциометр который на плате, кинув на пищалку-оралку от ноги DA которая выдает +5V и получится весьма годная сигнализация задымления.

Но у меня завалялась куча всякого хлама из которого было решено сделать более или менее цивильный приборчик.

Мозг системы — Arduino UNO

Учитывая габариты UNO 75х50мм, и наличия аналога типа Nano 45×17мм, смысл использовать где либо UNO для меня непонятен, но если уж завалялась, то для этого проекта в самый раз пойдет.

Дисплей 7 сегментов на 4 цифры

Учитывая что нынче всё, от 4 цифровых табло до 20х4 LED дисплеев и даже дисплеев покруче работает по I2C, требующих припаять 4 провода, единственный смысл использовать старое табло — это отработка навыков пайки, ну и чтобы «добро» не пропадало.

Потенциометр как регулятор уровня предельного значения

Практика показала, что глючат они безбожно, что отчасти решается на уровне кода:

norm=int(500+analogRead(NORMpin)/2); //Максимум он может дать 1024, но ниже 500 нет смысла
if ((abs(norm-oldnorm)>10) || ((abs(norm-oldnorm)>1) && (UTS() - shownorm < 1)))
{
oldnorm=norm;
shownorm=UTS();//Показывать выставляемую норму
}
//На случай если потенциометр отвалился или его закоротило выставляем норму 720, которой достаточно чтобы не орать напрасно но и не проспать дым
if ((norm<510) || (norm>980)) {norm=720;}

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

Общая картинка

В 99.999% случаев задымления в доме нет, и цифры, которые дает MQ7 никому не нужны, а табло есть, и раз уж там 4 цифры то пилим из них часы, для которых явное дело нужен RTC модуль, а еще прикручиваем кнопку, которая в случае «беды» отключает оралку на 10 минут, как раз хватит проветрить, а в «мирное» время переключает дисплей с часиков на показания датчика.

Модуль реального времени

Как заявляет производитель, потребление модуля PCF8563 составляет 0.25мкА, то есть от батарейки должен жить в автономном режиме около 140 тысяч часов или почти 16 лет, что немаловажно так как время и дата устанавливается при заливке скрипта, а соответственно батарейку надо менять либо при включенном девайсе, что не гуд, либо перепрошивать после смены.

Зимнее и летнее время

Поскольку игрушка предполагается к использованию в стране где есть переход на зимнее и летнее время, то встал вопрос — либо вешать кнопки управления, в принципе поскольку кнопка на аналоговом входе то через резисторы 3 кнопки повесить не проблема, либо вообще по тупому перезаливать прошивку дважды в год, либо, что мне кажется интереснее, хранить как водится в базах данных время в UTC, и уже от UTC высчитывать зимнее сейчас время или летнее, накидывая для той же Болгарии либо +3, либо +2 к часикам.

Основная функция определения зимности или летности времени получилась довольно простая.

int WorS(DateTime DT)
{
//Ищем последнее воскресенье марта
DateTime Mar=DateTime(DT.year(),4,1,3-Winter,0,0); //Находим 1 апреля текущего года (3 часа ночи со сдвигом на UTC)
uint32_t MarUT=Mar.unixtime()-86400*Mar.dayOfWeek(); //Из unixtime 1 апреля вычитаем день недели которым он является
DateTime Oct=DateTime(DT.year(),11,1,4-Summer,0,0);
uint32_t OctUT=Oct.unixtime()-86400*Oct.dayOfWeek();
if (DT.unixtime()<MarUT) {return Winter;}
if (DT.unixtime()<OctUT) {return Summer;}
return Winter;
}

При установке даты и времени после заливки скрипта сдвигаем полученную дату до UTC (тут главное скрипт заливать не в ночь перевода часов).

if (SetClock==1)
{
DateTime DT=DateTime(__DATE__, __TIME__);
int GMT=WorS(DT);
DT=DateTime(DT.unixtime()-GMT*3600);
rtc.adjust(DT);
}

Ну а в функции показа часиков соответственно читать из RTC наш UTC, проверять Winter или Summer и сдвигать

void ShowTime()
{
DateTime now = rtc.now();
int GMT=WorS(now);
now=DateTime(now.unixtime()+GMT*3600);
//Показываем часики
}

Все millis() заменил на UTS (UnixTimeStamp)

Раз уж есть реальное время то там где не нужны интервалы в миллисекундах — проще проверки через millis() заменить на секунды реального времени, заменив millis() на проверку UTS.

uint32_t UTS()
{
return rtc.now().unixtime();
}

Часики мерцают

У меня подсажены глаза и со времен старых мониторов я на глаз ощущаю мерцание, которое некоторые не видят, но даже те кто не видят, на старых мониторах с настройкой в 60гц анальгин от головной боли глотали втрое чаще чем люди, сидевшие за монитором у которого 85гц.

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

Чтобы табло горело равномерно, в идеале нужно чтобы паузы были равными, и я пришел к варианту, что 4-5 мс нормально, но это означает, что время работы программы должно укладываться в эти самые 4-5 мс, из чего следует что программу «сельские часики» нужно оптимизировать по скорости, как бы смешно это не звучало на 16мГц процессоре для человека который на 286 играл в Дюну 🙂

Оптимизец. Шаг первый. Loop VS While

Фишка в том, что замена классической конструкции void loop() {} на void loop() {while (1) {…}} сокращает время обработки цикла на 20%, что при столь банальном решении весьма недурно, и остается лишь гадать чем эти 20% времени занимается loop()

Оптимизец. Шаг второй. AnalogRead()

Погуглив, нашел замечательную статью из которой следует что самая тормознутая функция — это AnalogRead(), отрабатывающая в среднем 112микросекунд, а в проекте весь обвес на аналоговых пинах, при этом чуть уронив точность замера можно парой команд ускорить этот процесс в 7-10 раз, на setup указав:

ADCSRA |= (1 << ADPS2); //Биту ADPS2 присваиваем единицу - коэффициент деления 16
ADCSRA &= ~ ((1 << ADPS1) | (1 << ADPS0)); //Битам ADPS1 и ADPS0 присваиваем нули

При таком варианте слегка «пострадает» замер показаний датчика MQ-7 который сам по себе не страдает точностью, «упадет» точность замера показания «потенциометра», ну и кнопка на аналоговом входе будет работать чуть грубее, что в общем не косяк от слова совсем, при этом даже если через резисторы навесить всё таки 3-4 кнопки.

Оптимизец. Шаг третий. Не опрашивать то что не нужно.

В изначальном коде была глобальная переменная uint32_t timing = 0; по которой с переходом на UTS опрос датчика MQ7 выполнялся раз в 5 секунд с условием if (UTS() — timing > 5) {value=analogRead(MQ7pin); timing = UTS();} что логично, так как дыма за 5 секунд едва ли серьезно может прибавиться или убавиться, да и даже если изменилось что-то в воздухе, датчик разнюхивает это не сразу.

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

int WorS(DateTime DT)
{
if (lasthour!=DT.hour()) //Если изменился час с прошлого расчета зима-лето
{
currentGMT=WorSCalc(DT);
lasthour=DT.hour();
}
return currentGMT;
}

int WorSCalc(DateTime DT)
{
//Ищем последнее воскресенье марта
DateTime Mar=DateTime(DT.year(),4,1,3-Winter,0,0); //Находим 1 апреля текущего года (3 часа ночи со сдвигом на UTC)
uint32_t MarUT=Mar.unixtime()-86400*Mar.dayOfWeek(); //Из unixtime 1 апреля вычитаем день недели которым он является
DateTime Oct=DateTime(DT.year(),11,1,4-Summer,0,0);
uint32_t OctUT=Oct.unixtime()-86400*Oct.dayOfWeek();
if (DT.unixtime()<MarUT) {return Winter;}
if (DT.unixtime()<OctUT) {return Summer;}
return Winter;
}

Бренчмарк делать лениво, но уверен, что банальное сравнение lasthour!=DT.hour() работает в 100500 раз быстрее чем вычисления внутри функции WorSCalc();

Запрос текущего времени тоже переписал, использовав millis()

uint32_t UTS()
{
if ((millis()-checkUTS<1000) || (checkUTS==0)) //Если это первый опрос или с момента первого прошло более секунды
{
savedUTS=rtc.now().unixtime();
checkUTS=millis();
}
return savedUTS;
}

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

To be continued

Продолжение и развитие этого проекта будет, так как хочется поиграть с цифровыми выходами, всё таки навесить несколько кнопок на один аналоговых вход, а так же отработать процедуру платного увеличения скорости реакции кнопки, ну типа чтобы если один раз нажал, то на минуту часы изменились, зажал подольше — часы начали быстрее меняться. А там может еще чего выплевет.