Я тут наступил на какое-то серийное количество грабель с ардуиной, хочу поделиться и предостеречь.
У меня был простой проект как раз для маленькой ардуины. Можно было решить и без неё, но хотелось чисто из спортивного интереса задействовать микроконтроллер.
Свет в ванной. Как было:
Светодиодные ленты (12В) запитаны через ATX-блок питания с перемычкой между зелёной жилой и землёй, имитирующей включение со стороны компьютера.
Для включения света при нажатии кнопки у входа питание 12В поступало на стандартное реле таймера FC-32, реле подавало 220В на блок питания.
Упомянутый таймер это готовый модуль, который имеет выбор 8 интервалов включения: около 1 секунды в самом коротком (от 0.13 до 1.3) и десятки минут в самом длинном (389-3700 секунд). Точный выбор производится выкручиванием переменного резистора отвёрткой, но, понятное дело, точная до секунды длительность зависит от погоды на Марсе.
Модуль имеет стандартное реле SRD-12VDC-SL-C.
При подаче 12В на вход таймера он переключает реле, держит его заданный интервал времени и выключает по истечении.
При пропадании 12В на входе, реле также возвращается в исходное состояние.
Я соединил общий вывод реле с фазой 220В, нормально разомкнутый - с блоком ATX, нормально-замкнутый - с дежурной подсветкой, светодиодной лампой на 2 Вт.
Для продления таймера изнутри ванной комнаты я добавил вовнутрь ванной нормально замкнутый выключатель, размыкающий цепь питания таймера, а после отпускания кнопка снова замыкалась и интервал начинал идти заново.
Однако блок питания брал большие пусковые токи, в результате чего реле вышло из строя "подгорели контакты", и часто стало "залипать" в замкнутом состоянии. Это типичная проблема с питанием светодиодов.
Было принято решение изменить схему: держать блок питания ATX в постоянно подключённом к сети 220В режиме, питать ардуину от 5В дежурной линии блока питания ATX (сиреневый и земля), а при необходимости включения света замыкать зелёную жилу на землю, имитируя включение компьютера. Сколько там при этом пускового тока съест блок питания из сети 220В, мою схему не волнует, на сигнальных контактах заметных скачков быть не должно.
Чтобы сохранить дежурное освещение, мне пришлось использовать два независимых реле: одно замыкало 5В в зелёной жиле на землю для включения блока питания, а второе коммутировало 220В для включения дежурной лампочки 2 Вт.
Также я принял решение сохранить кнопку продления таймера внутри ванной комнаты.
Понятно, что можно было оставить таймер как есть, замыкать те же контакты ATX, а для дежурки повесить 5В или 12В реле на те линии блока питания, которые получают напряжение только после включения. Но хотелось поиграть с ардуиной.
Итак, я припаял на ардуино нано 4 пина цифровых входов-выходов, 2 пина питания, а программирование осуществлял подключением ещё двух пинов RX/TX "в воздухе" и зажимал аппаратный Reset рукой.
Начал писать программу.
Первые грабли с которыми я столкнулся, относились, конечно, к подтяжке кнопок к земле.
Во всех руководствах пишут, что если вы коммутируете кнопку, то вам нужен подтяжечный резистор.
Провод, соединённый с линией цифрового входа, при подключении к 5V или GND, даёт на этом входе уровни 1 или 0 соответственно, а при отсутствии подключения - случайный мусор, помехи, череду из ноликов и единичек, полагаться на это нельзя.
Для надёжной коммутации кнопок необходимо делать схему, которая "подтягивает" линию цифрового входа постоянно через резистор 10кОм к GND, так, чтобы "гасить" случайные единички, а выключатель вешать одной ногой на VCC, а другой вот уже на этот цифровой вход с подтянутой землёй.
Получается, когда выключатель замкнут, он поддерживает соединение сопротивлением около 0 между VCC и нашим пином, а также соединение сопротивлением 10К между VCC и GND. Нетрудно посчитать, какой ток там будет течь и какая мощность излучаться. Пренебрегаем.
В Arduino встроена софтверная "подтяжка" входных ног, но у меня она не заработала. Возможно, потому что это не оригинальные платы.
При программировании я начал писать какую-то фигню, а потом, когда запутался, задумался и родил вариацию на тему теории автоматов.
Выписываем все возможные состояния системы и правила перехода между ними:
0. Главный выключатель выключен, свет выключен.
1. Главный выключатель включен, таймер не вышел, свет горит.
2. Главный выключатель включен, таймер вышел, свет выключен.
И правила перехода между ними:
I. Состояние 0, главный выключатель включен. Перейти в состояние 1, сохранить отметку времени в момент включения.
II. Состояние 1, главный выключатель выключен. Перейти в состояние 0. Пользователь выключил свет не дожидаясь таймера.
III. Состояние 1, главный выключатель включен. Проверить, сколько времени прошло сейчас с момента включения (нужен корректный алгоритм для обработки превышения millis, см. дальше). Если прошло больше чем задержка выключения, перейти в состояние 2.
IV. Состояние 2, таймер вышел, свет выключился. Если выключили и главный выключатель, но перейти в состояние 0.
V. Состояние 1, нажата кнопка продления. Остаёмся в состоянии 1, сохранить новый момент включения. Время не вышло, но нас попросили продлить.
VI. Состояние 2, нажата кнопка продления. Перейти в состояние 1, заново сохранить момент включения. Время вышло, но нас попросили продлить.
Там были небольшие глюки, но отладка на Arduino очень удобна, особенно с возможностью писать произвольные данные в USB-порт, а со стороны компьютера смотреть за ними.
Рекомендую сделать комментируемую #define DEBUG в начале программы и писать код для коммуникации наружу в зависимости от неё:
#ifdef DEBUG
Serial.begin(9600);
#endif
Налетел на любимейшие грабли всех программистов:
if (variable = 5) { do something}
Это проблема языка Си. Приведённый код не будет сравнивать содержимое переменной variable с пятёркой. Он выполнит присвоение справа налево; потом, если там был не ноль то сочтёт условие выполненным и гарантированно выполнит то что в скобках.
А мне пришлось дебажить через USB-порт и пытаться понять, почему же это у меня всегда выполняется экзотическое редкое условие и слетает значение переменной состояния. Не делайте так.
Правильно писать if (variable == 5), а лучше и безопаснее if (5 == variable). Такой порядок, даже если вы забудете один знак равенства, даст вам ошибку, которую вы сразу же исправите.
Чтобы не тащить на стол реле, можно просто воткнуть в нужные ноги Arduino Uno спаянные светодиоды с резисторами 1кОм (необходимы чтобы не сжечь светодиод).
Следующие грабли проявились, когда я обнаружил, что соединять надо всё и со всем. Помимо естественных двух ножек на выключатель и двух ножек реле, обнаружилось, как я писал выше, что выключатель нужно коммутировать тремя ногами (подтяжечный резистор), реле аж четырьмя: VCC, GND, IN1, IN2.
Дальше - что блинские китайские реле почему-то включаются при уровне GND на IN1, а не при VCC. Ну, это легко переворачивается в программе.
На тот момент проект представлял собой макаронного монстра на столе: плата ардуины, плата реле, висюльки выключателей, адовы тройники питания из говна и палок. Хотелось привести в удобный красивый вид.
Подумал, нарисовал схему коммутации. Выделил на ней отдельные сущности: ардуина с шестью ногами и реле, с четырьмя.
Перерисовал попланарнее, всего с одним пересечением.
Напаял на макетную плату пины для Dupont wire: 2 пина VCC и GND, 6 пинов ардуина, 4 пина реле, 4 пина для подключения кнопок.
Соединения и резисторы между всеми этими сущностями выполнил на оборотной стороне платы.
Собрал, подключил, вроде завелось!
ATX-разъём взял из удлинителя. С материнской платы фиг отпаяешь, новые разъёмы ATX с ебея стоят негуманных денег, а удлинитель чуть больше доллара. Тем более была пара в загашнике.
Можно было отрезать хобот от живого блока питания, но я решил этого не делать: так и мой блок питания легко будет занять другим делом при необходимости, и в ванной, если тот сгорит, поменять проще.
Повесил в продакшен.
Попутно сжёг ардуину нану и USB-вход Uno питанием на столе от 12В.
Заявлена поддержка до 20 вольт на входе Arduino Uno? Гыгы, у вас из микросхемы вышел белый дым! Блин.
Ну да ладно, сама атмега в большой жива, сгорел только USB-TTL CH340, осталась возможность программировать Uno через первый-второй пины RX/TX.
Заказал мешок новых.
Следующие грабли.
День провисело, начало мигать. Почему?
Вытащил, подёргал за соединения между пинами и Dupont female - вроде перестало. Блин.
Ещё полдня провисело, опять начало щёлкать и мигать.
Снял, вернул на стол, подключил на месте - мигает.
Обе релюшки мигают, как будто у них помехи на входе. Прямо вот думал что и выходы нужно подтягивать к земле или питанию.
Ардуина на выходе даёт недостаточный ток для реле?
Да, там 40 мА на выход, ну так именно поэтому же реле дополнительно питается VCC и GND, и по входу из ардуины должна получать только маломощный сигнал.
Полез разбираться с тестером на контакты реле.
При питании схемы от 5В на реле всего 2-3-4 вольта. То же и на всех остальных элементах платы. Блин.
Неудивительно что оно глючит!
В общем, соединения Dupont хороши для макетирования на столе, а в реальной боевой обстановке, если вам нужна работа пока вы на модуль не смотрите, использовать это нельзя вообще никуда.
Пробовал подгибать, пропаивать пины. Не помогает, плюнул. Электроника - наука о плохих контактах.
Выкинул макетную плату, напаял на все выводы хорошие провода, соединил с помощью WAGO. Заработало.
Работает и сейчас.
Жду новую ардуину-нану, буду уменьшать до предела, пока не знаю как именно. Наверное, запаяю насмерть.
Была ещё программная проблема, связанная с переполнением millis. Встроенный в ардуину таймер - переменная типа unsigned long. Она хранит миллисекунды, прошедшие с момента включения. Каждые 49 с фигом дней она переполняется и становится снова 0.
Если при включении света millis была близка к переполнению (например, OVERFLOW-1000, за секунду) и правило I выглядело как "момент отключения = состояние таймера + задержка", то выполнится вышеописанный код: момент отключения = состояние таймера + задержка.
Задержка у нас по умолчанию 30 минут, 30*60*1000.
момент отключения = OVERFLOW-1000 + 1800000 = OVERFLOW + 1799000, что даст 1799000.
Момент отключения провернётся через переполнение и станет почти нулевым.
В следующем цикле (0.006 секунды примерно) модуль заметит, что нынешний таймер гораздо (на 49 суток) больше момента отключения (например, OVERFLOW-994), после чего немедленно отключит свет. При попытке перевзвести таймер кнопкой продления или выключить-включить, всё равно, ситуация повторится.
Получается, около 30 минут (дефолтная задержка) из 49 дней моя система полностью неработоспособна.
Нетрудно проверить это на живой плате или в эмуляторе (ниже), добавив следующие строки, "переводящие часы" в код:
В секции объявления переменных
extern volatile unsigned long timer0_millis;
В секции начальной настройки void setup() {
noInterrupts ();
timer0_millis = -10000;
interrupts ();
Из-за того что переменная unsigned long, попытка записать в неё отрицательное значение приводит к появлению в ней положительного значения OVERFLOW-10000.
В своём коде я сделал исполнение этих строк если в заголовке определён макрос #define TIMEROVERFLOW. Так, чисто для удобства включения/выключения всей этой дряни для тестов.
Да, проверено, работает. На полчаса из полутора месяцев подсветка превращается в тыкву.
Надо что-то придумать к этому. Поставить костыли, проверять, на сколько именно нынешний таймер больше момента отключения? Типа, если там больше суток, значит переполнилось или глюкнуло, сбрасываем?
Перезагружать плату каждые 20 суток?
Надо почитать понять сначала. Полез читать. Неудивительно, я, конечно, не первый кто с этим столкнулся.
В инструкциях учат не сбрасывать, не обрабатывать переполнение, а писать код, иммунный к переполнению millis, но я долго не мог понять как это делать.
В итоге так и зарелизил такую рабочую версию, потом по ходу действия пофиксим. Полтора месяца аптайма - это до фига. Кстати, уже подходят.
В той версии я при включении или продлении света сразу считал и записывал время отключения, экономил таким образом арифметическую операцию на каждом такте, не вычисляя сколько прошло времени, а сравнивая текущий таймер с дедлайном.
Не надо так. Правильно делать следующим образом:
Записывать только момент включения света и дальше, на каждом такте первого состояния "ждём пока таймер выйдет", вычитать его из нынешнего таймера, и сравнивать получившийся результат, количество прошедшего времени, с длительностью дедлайна.
Моделируем проблемную ситуацию:
Таймер равен OVERFLOW-1000. Задержка 1 800 000 (60*30*1000). Переключили главный выключатель.
В переменную МОМЕНТ записали OVERFLOW-1000, перешли в первое состояние.
Дальше на каждом такте считаем ПРОШЛО = ТАЙМЕР - МОМЕНТ.
До переполнения там будет ПРОШЛО = OVERFLOW-500 - (OVERFLOW-1000) = OVERFLOW - OVERFLOW - 500 + 1000 = 500, например.
Через 500 мс после переполнения: ПРОШЛО = 500 - (OVERFLOW-1000) = 500 - OVERFLOW + 1000 = 500 - OVERFLOW = 500.
Вроде бы данный код должен работать без глюков.
Так и вышло.
На самом деле, в процессе разборок я дописал ещё огромную фичу в код. Кнопка продления у меня не была занята ничем при выключенном свете, почему бы не напихать туда чего-нибудь полезного?
Итак, теперь, если при выключенной главной кнопке нажать (состояние 3), подержать 10 секунд (состояние 4) и отпустить (состояние 5) кнопку продления, то моя ардуина входит в режим обучения и мигает дежурным светом для индикации этого.
В этом режиме она записывает текущую метку времени и смотрит, не нажали ли снова на кнопку продления.
Когда нажали, она вычитает одно время из другого. Получится, например, 20 секунд. Она считает что это новая задержка, в масштабе "минута за секунду", теперь ждём 20 минут. Полученное значение становится новым интервалом таймера и сохраняется в EEPROM (у любой ардуины есть как минимум 512 байт энерго-независимой памяти) и подгружается из него при рестарте.
Это добавило состояния и правила перехода:
3. Начали вход в режим обучения, считаем десять секунд для этого.
4. Десять секунд прошло, готовы входить в режим обучения, но кнопка 2 ещё нажата, ждём пока отпустят.
5. Кнопка 2 отпущена, считаем секунды в режиме обучения.
VII. Состояние 0, первая кнопка отпущена, вторая нажата. Похоже, пользователь хочет обучить систему.
Записываем состояние таймера, переходим в состояние 3.
VIII. Состояние 3, но вторую кнопку отпустили. Отбой обучения, возвращаемся в состояние 0.
IX. Состояние 3, прошли десять секунд, переходим в обучение. Мигнуть дежурной лампой, перейти в состояние 4.
X. Если состояние 4 и отпущены обе кнопки. Сохраняем таймер, переходим в состояние 5.
XI. Если состояние 5 и нажата кнопка 2, то делаем все подсчёты, сохраняем новый таймер в EEPROM, мигаем дежуркой и возвращаемся в состояние 0.
XII. Если в третьем, четвёртом или пятом (обучающих) состояниях нажали первую кнопку, значит это было не обучение а ошибка. Ничего не перезаписываем, возвращаемся в состояние 0, на следующем такте состояние кнопки обработает правило I.
Уже при последующем допиливании нашёл прекрасный эмулятор ардуины, в котором можно набросать произвольную схему и убедиться, что она будет работать.
Очень рекомендую
https://www.tinkercad.com/. Бесплатно, наглядно, просто.
Там же можно нарисовать подобие схемы, понять как её разводить по печатной плате или коммутировать.
Мелочи: там нет поддержки сохранения в EEPROM, она всегда читает из него нули. И в модулях конструктора нет реле. Ну да и фиг бы с ним.
Раскладка для tinkercad и исходник скетча для Arduino я положил в гитхаб:
https://github.com/migdal-or/toilet-light.
Лампа на схеме заменяет реле, требующее VCC и GND. Светодиоды с сопротивлениями - соответственно входы реле, зелёный - вход для ATX, синий - вход для дежурного освещения. Да, не удивляйтесь что при включении горит зелёный светодиод. Блинские китайские реле почему-то включаются при уровне GND на IN1, а не при VCC.
Итак, ещё раз, что делать если у вас что-то глючит с ардуиной:
1. Постарайтесь понять, программная проблема или аппаратная.
2. Если программная - используйте большую Arduino Uno (её полезно купить заранее), выводы на LED через резисторы, диагностические сообщения в зависимости от #define DEBUG через Serial.print.
3. Не делайте if (variable == 1), всегда делайте if (1 == variable).
4. Если проблема аппаратная - упрощайте схему до простых модулей, выясняйте как что правильно себя ведёт, промеряйте напряжения на компонентах. Нестабильная работа может быть вызвана плохими соединениями с просадкой напряжений. На столе соберите всё на WAGO, когда оттестируете в работе - паяйте.
5. Сложные схемы сначала моделируйте в эмуляторе, а потом уже собирайте. Вполне возможно, что вы что-нибудь неправильно поняли и работать не будет.