В этом уроке поговорим о способах повышения надежности программ для Ардуино, узнаем, как использовать сторожевой таймер в системе Ардуино.
Предыдущий урок Список уроков Следующий урок
Как и любое электронное устройство на микроконтроллерах, платы Ардуино могут зависать. Причины могут быть разные:
- неисправность контроллера;
- ненормальная система питания контроллера, броски питающего напряжения, помехи через цепи питания;
- наводки, электромагнитные помехи на плату и компоненты контроллера;
- импульсные помехи через общие цепи внешних компонентов системы (компьютер, другие контроллеры, длинные линии связи и т.п.);
- паразитные связи на плате, особенно в тяжелых условиях эксплуатации (высокая влажность, температура, вибрации);
- неправильные (скорее непредусмотренные программистом) действия пользователя или реакция внешних компонентов;
- ошибки в программе.
Конечно, это ненормальное явление. Допускать этого нельзя, но что-то не предусмотрели, что-то случилось независящее от нас. К тому же мы программируем на языке высокого уровня, а это означает, что есть программная среда, которую мы не контролируем. Компилятор создает какие-то стеки, области собственных данных, и мы не знаем, насколько надежно они реализованы.
Поэтому необходимо контролировать ход выполнения программы и принимать меры, если она работает ненормально.
Сторожевой таймер (watchdog) в системе Ардуино.
Одним из способов повышения надежности является использование сторожевого таймера (watchdog) для контроля выполнения программы. Он представляет собой аппаратный таймер, который должен периодически сбрасываться программой. Если сброс сторожевого таймера не произойдет в течение заданного времени, то он вызовет перезагрузку всей системы, т.е. выработает сигнал сброс микроконтроллера. Таким образом, если программа зависнет и перестанет сбрасывать сторожевой таймер, то микроконтроллер будет перезагружен, как будто нажали кнопку сброс.
В системах Ардуино есть сторожевой таймер, который является внутренним узлом микроконтроллера ATmega.
Библиотека для работы со сторожевым таймером Ардуино.
Для управления сторожевым таймером необходимо подключить к проекту библиотеку avr/wdt.h.
Эту библиотеку не надо искать в интернете, скачивать. Ее не надо устанавливать. Это стандартная библиотека, она находится в каталоге Arduino. У меня в D:Arduinohardwaretoolsavravrincludeavrwdt.h
Просто добавьте в проект строку:
Библиотека имеет три функции.
void wdt_enable(timeout)
Функция разрешает работу сторожевого таймера, задает время тайм-аута. Аргумент timeout (время тайм-аута) может принимать следующие значения.
WDTO_15MS // 15 мс WDTO_30MS // 30 мс WDTO_60MS // 60 мс WDTO_120MS // 120 мс WDTO_250MS // 250 мс WDTO_500MS // 500 мс WDTO_1S // 1 сек WDTO_2S // 2 сек WDTO_4S // 4 сек WDTO_8S // 8 сек
Пример:
wdt_enable(WDTO_120MS); // разрешение работы сторожевого таймера с тайм-аутом 120 мс
void wdt_reset(void)
Сброс сторожевого таймера. Для нормальной работы необходимо вызывать эту функцию не реже периода сторожевого таймера. При задержке превышающей тайм-аут произойдет аппаратный сброс контроллера.
wdt_reset(); //сброс сторожевого таймера
void wdt_disable(void)
Отключение сторожевого таймера.
wdt_disable(); //запрет работы сторожевого таймера
Применение сторожевого таймера в системе Ардуино.
Сторожевой таймер позволяет контролировать выполнение отдельных циклов программы. Функцию его сброса не надо ставить, где попало. Я обычно контролирую цикл прерывания по таймеру, от которого отсчитываются остальные циклы программы. В предыдущих уроках я поставил бы сброс watchdog в цикле обработки прерывания по таймеру 2 мс.
Давайте проверим работу сторожевого таймера на реальной программе. В программе:
- Организован цикл прерывания по таймеру 2мс.
- В основном асинхронном цикле реализовано управление светодиодом платы (мигает с периодом 1 сек).
- В основном цикле проверяются данные с последовательного порта, и при появлении любого данного запрещается работа прерывания по таймеру. Этим имитируется сбой установок таймера.
// проверка работы сторожевого таймера#include
#define LED_PIN 13 // светодиод подключен к выводу 13int ledCount; // счетчик времени мигания светодиода
void setup() { pinMode(LED_PIN, OUTPUT); // определяем вывод светодиода как выход Serial.begin(9600); // инициализируем последовательный порт MsTimer2::set(2, timerInterupt); // задаем период прерывания от таймера 2 мс MsTimer2::start(); // разрешаем прерывание от таймеру // wdt_enable(WDTO_15MS); // разрешение работу сторожевого таймера с тайм-аутом 15 мс }
void loop() { // мигание светодиода if ( ledCount > 250 ) { ledCount= 0; digitalWrite(LED_PIN, ! digitalRead(LED_PIN)); // инверсия состояния светодиода }
// проверка данных в буфере последовательного порта (имитация сбоя) if ( Serial.available() != 0 ) MsTimer2::stop(); // запрет прерывания от таймера }
// обработчик прерывания void timerInterupt() { ledCount++; // счетчик светодиода // wdt_reset(); // сброс сторожевого таймера }
Загрузим программу в плату. Светодиод мигает раз в секунду. Откроем монитор порта и пошлем какой-нибудь символ. Светодиод перестанет мигать. Мы имитировали сбой установок таймера и программа зависла. Если нажать кнопку сброс на плате, то программа снова начнет работать, светодиод замигает.
Теперь освободим от комментариев две строчки разрешения и сброса сторожевого таймера.
wdt_enable(WDTO_15MS); // разрешение работы сторожевого таймера с тайм-аутом 15 мс wdt_reset(); // сброс сторожевого таймера
Загрузим программу в плату, запустим. Теперь при имитации сбоя сторожевой таймер сбрасывает микроконтроллер, и программа продолжает работать.
Способы повышения надежности работы программы.
Сторожевой таймер не панацея от неправильной работы программы. Представьте, что в предыдущем примере установки таймера прерывания собьются так, что он будет работать с периодом не 2 мс, а 5 мс. И как тут поможет сторожевой таймер? Если собьются параметры последовательного порта? Программа не будет выполнять своих функций.
Только комплекс мер позволит написать надежную программу. Я разрабатываю программы для PIC-контроллеров фирмы Microchip. Программы работают в устройствах, зависание или неправильная работа контроллеров в которых приведет к фатальным последствиям. Это мощные специализированные источники питания, станции катодной защиты, системы контроля технологических процессов, GSM телеметрия и т.п. Надежность программ это очень обширная, сложная тема. Я коротко расскажу об основных принципах создания надежных программ.
Что может случиться с программой? После чего она перестает правильно работать?
- Могут испортиться данные (переменные) в оперативной памяти.
- Возможно ошибочное изменение состояния регистров микроконтроллера.
- Программа может бесконечно ожидать какого-то события, которое не наступает, а разработчик полагал, что оно будет обязательно.
Контроль данных, переменных, регистров микроконтроллера.
Переменные, которые используются для промежуточных результатов вычислений мы контролировать не в состоянии. Откуда мы знаем, что в них должно быть в данный момент. А переменные, в которых хранятся параметры, режимы, технологические установки и т.п. вполне можно контролировать. А можно циклически переустанавливать.
В моих программах на PIC-контроллерах всегда есть программный блок с названием ”циклические установки”. В нем с определенным периодом переустанавливаются все переменные и регистры микроконтроллера. Конечно те, которые не меняются в цикле.
Например, в программе происходит циклическое прерывание по таймеру с определенным временем периода. В циклических установках переустанавливаются все режимы таймера и контроллера прерываний. При таком подходе время периода прерывания по таймеру не может измениться, сбиться, испортиться. Хотя, надо признать, что при разработке программ на языке высокого уровня все переменные и регистры переустанавливать непросто.
Большие блоки данных я защищаю контрольными суммами и периодически проверяю. О контроле целостности данных написано в уроке 14.
В случае ошибочных данных лучше перезагрузить всю программу, сформировав программный сброс. Неизвестно, что в ней еще испортилось.
Но лучше использовать аппаратный сброс, чтобы переустановить все регистры микроконтроллера. Сделать это можно, используя сторожевой таймер:
wdt_enable(WDTO_15MS);while (1) { }
Контроль хода выполнения программы.
Одной из распространенной ошибкой, приводящей к зависанию программы, является ожидание события в бесконечном цикле.
Допустим, Вы принимаете данные с компьютера по последовательному порту. Ждете 10 байтов, а пришло 9. И программа бесконечно ждет 10го байта. Я видел много программ, которые зависали при нарушении приема данных по последовательному интерфейсу.
В подобных случаях необходимо контролировать время выполнения операции. Если байт не пришел в течение 1 сек, то он не придет никогда. Надо считать время ожидания события и при отсутствии его принимать меры. У каждого ожидаемого события должен быть свой тайм-аут – время ожидания. А использовать для этого сторожевой таймер или программные счетчики – решать программисту.
В следующем уроке разработаем на базе платы Ардуино полностью рабочее устройство – охранную сигнализацию. Знаний для этого теперь достаточно.
Предыдущий урок Список уроков Следующий урок
Поддержать проект
- Tutorial
Речь пойдет о том, как держать Arduino всегда в работоспособном состоянии. Механизм watchdog встроен в контроллеры Atmega, но, к сожалению, не всякий загрузчик (bootloader) Arduino правильно обрабатывает эту функцию. Попробуем разобраться с этой проблемой. Итак, что такое watchdog? Простыми словами — это встроенный таймер на определенное время (до 8 сек в зависимости от чипа), который можно запустить программно. Как только таймер «дотикает» до нуля, контроллер подает правильный сигнал сброса (RESET) и всё устройство уходит в hard перезагрузку. Самое главное, что этот таймер можно сбрасывать в начальное состояние также программным способом.
- Правильный сигнал сброса — достаточный по длительности для того, чтобы контроллер начал перегружаться. Иногда есть соблазн подключить к RST входу какой-либо цифровой выход Arduino и устанавливать его в 0 когда надо перегрузиться. Это плохой подход к решению проблемы, т.к. такого сигнала может быть недостаточно по времени, хотя и не исключено, что в некоторых случаях это тоже будет работать..
- hard перезагрузка это самая настоящая перезагрузка, которая происходит при нажатии на кнопку RESET. Дело в том, что есть еще понятие soft перезагрузки — это программный переход на 0-вой адрес. В принципе, это тоже полезная вещь, но с помощью нее невозможно перегрузить зависший контроллер Ethernet или взглюкнувший LCD.
Короче говоря, встроенный watchdog это как раз то, что нужно и без дополнительных схем, пайки и соединений.
Функции Watchdog
Чтобы использовать функции Watchdog нужно подключить к проекту стандартную библиотеку:
#include
Теперь нам доступны следующие три функции: 1. Запуск таймера watchdog:
wdt_enable(WDTO_8S); /* Возможные значения для константы WDTO_15MS WDTO_30MS WDTO_60MS WDTO_120MS WDTO_250MS WDTO_500MS WDTO_1S WDTO_2S WDTO_4S WDTO_8S */
Таймер будет считать ровно столько, сколько указано в константе. По истечении этого времени произойдет перезагрузка. 2. Сброс таймера watchdog:
wdt_reset();
Думаю, понятно для чего нужна эта функция — пока вы вызываете ее, контроллер не сбросится. Как только система зависнет и эта функция вызываться перестанет, то по истечении заданного периода произойдет перезагрузка. 3. Отключение watchdog:
wdt_disable();
Отключение таймера watchdog. Собственно, на этом можно было бы и закончить наше повествование о watchdog… но дело в том, что все это работает только в Arduino Uno, а на Arduino Mega, Mini и Nano все это работает ровно наоборот, т.е. не работает совсем 🙂
Почему watchdog не работает на большинстве современных плат Arduino
Дело в том, что после перезагрузки, которая была вызвана watchdog, контроллеры последних выпусков оставляют включенным watchdog на минимальный период, т.е. 15ms. Это нужно для того, чтобы программа как-то узнавала, что предыдущая перезагрузка была по watchdog. Поэтому первоочередная задача загрузчика (или вашей программы, если она запускается первой) — сохранить информацию о том, что перезагрузка была «неожиданной» и сразу же выключить watchdog. Если этого не сделать, то система уйдет в bootloop, т.е. будет вечно перегружаться. Как известно, в Arduino есть специальный загрузчик, который выполняется в первую очередь после перезагрузки системы. И, к огромному сожалению, стандартный загрузчик не сбрасывает watchdog! Таким образом, система заходит в жестокий bootloop (состояние «crazy led», при котором светодиод на 13-м пине мигает как сумасшедший).Выглядит это все примерно так:
Пути решения проблемы
Если посмотреть на исходники стандартного загрузчика (они есть в поставке платформы), то код отключения watchdog есть (!), но этот код вынесен под условную компиляцию и, по всей видимости, стандартный загрузчик скомпилирован без поддержки watchdog. По крайней мере в пакете платформы версии 1.5.2 (последней на момент написание статьи) дело обстоит именно так. Для решения проблемы я даже прочитал man-ы самой платформы (:) и вроде бы там описана эта проблема и даже приведен код, который должен сделать всех счастливыми:
uint8_t mcusr_mirror __attribute__ ((section (".noinit"))); void get_mcusr(void) __attribute__((naked)) __attribute__((section(".init3"))); void get_mcusr(void){ mcusr_mirror = MCUSR; MCUSR = 0; wdt_disable(); }
Здесь описывается функция get_mcusr(), которая должна вызываться сразу после сброса. Это достигается макросом «__attribute__((section(«.init3″)))». Я пробовал прописывать эту функцию во все секции, которые только возможно — да, она действительно запускается до функции setup() из скетча, но, к сожалению, гораздо позже 15ms (минимальная константа watchdog) после сброса… Короче говоря, как я ни рыл интернет в поисках легкого решения проблемы, так ничего найдено не было. Я нашел только один способ заставить watchdog работать — перепрошить загрузчик… чем мы сейчас и займемся.
Проверка работоспособности watchdog
Прежде чем что-то прошивать, нужно проверить — вдруг ваша Arduino поддерживает watchdog. Для этого я написал небольшой скетч для теста. Просто залейте его, откройте монитор порта и смотрите, что будет происходить.Тестирование на watchdog
#include void setup() { wdt_disable(); // бесполезная строка до которой не доходит выполнение при bootloop Serial.begin(9600); Serial.println("Setup.."); Serial.println("Wait 5 sec.."); delay(5000); // Задержка, чтобы было время перепрошить устройство в случае bootloop wdt_enable (WDTO_8S); // Для тестов не рекомендуется устанавливать значение менее 8 сек. Serial.println("Watchdog enabled."); } int timer = 0; void loop(){ // Каждую секунду мигаем светодиодом и значение счетчика пишем в Serial if(!(millis()%1000)){ timer++; Serial.println(timer); digitalWrite(13, digitalRead(13)==1?0:1); delay(1); } // wdt_reset(); }
После перезагрузки (или подключения монитора к порту) встроенный светодиод мигнет, сигнализируя о том, что запустился загрузчик. Далее в секции setup происходит включение watchdog с таймером на 8 сек. После этого светодиод отсчитает нам это время и должна произойти перезагрузка. Далее начинается самое интересное — если перезагрузка произошла и все повторяется в такой же последовательности, то вы имеете на руках Arduino, в которой загрузчик правильно обрабатывает watchdog. Если же после перезагрузки светодиод на 13-м пине начинает бесконечно мигать, то значит загрузчик не поддерживает watchdog. Здесь даже кнопка сброса не поможет. Для последующей прошивки нужно плату отключать от питания и после включения успеть прошить до первой перезагрузки. Я протестировал 4 вида плат и только загрузчик в Arduino Uno сработал так как надо:Результаты на мониторе
Watchdog не поддерживается загрузчиком: | Watchdog поддерживается загрузчиком: |
Как легче всего прошить новый загрузчик?
Прошивать загрузчик в Arduino можно с помощью отдельных программаторов, а можно собрать свой программатор с помощью той же самой Arduino. Т.е. любую плату Arduino можно превратить в программатор, залив туда специальный скетч. Я не буду в этой статье описывать все премудрости создания программатора на основе Arduino, т.к. эта тема довольно подробно описана в интернете. В качестве программатора я использовал Arduino Uno. Как известно, прошивка производится через отдельный разъем ICSP, который есть почти на всех платах. В случае прошивки Arduino Pro Mini, у которого нет IСSP, подключение производится непосредственно к выводам.Подключение для прошивки bootloader
Где взять загрузчик, который поддерживает watchdog?
Эта глава напоминает танцы с бубном и скорее всего можно сделать все как-то проще, но, увы, у меня по-другому не получилось. Рекомендуется использовать загрузчики из пакета optiboot. В принципе, эти загрузчики идут в инсталляции самой платформы Arduino, но лучше скачать и установить последнюю версию optiboot отсюда. Установка заключается в двух шагах (возможно, это можно сделать как-то по-другому):
- Папка bootloadersoptiboot перезаписывается в C:Program Files (x86)Arduinohardwarearduinoavrbootloadersoptiboot
- Файл boards.txt дописывается к файлу C:Program Files (x86)Arduinohardwarearduinoavrboards.txt
Естественно, папка установки платформы Arduino у вас может быть другой. Далее перегружается среда разработки и в меню Сервис/Плата можно наблюдать новые платы с пометкой [optiboot]. К сожалению, при выборе этих плат происходят какие-то непонятные ошибки компиляции и появляются всякие другие странности… поэтому делаем еще проще. Открываем в любом текстовом редакторе файл C:Program Files (x86)Arduinohardwarearduinoavrboards.txt и меняем следующие строчки: Для Arduino Nano: menu.cpu.nano.atmega328.bootloader.file=optiboot/optiboot_atmega328.hex Для Arduino Mini: menu.cpu.mini.atmega328.bootloader.file=optiboot/optiboot_atmega328.hex Следующая проблема в том, что загрузчика optiboot для платы Arduino Mega не существует в природе, т.к. в Mega больше памяти и используется другой протокол. Поэтому мы используем стандартный, но модифицированный загрузчик, который качаем отсюда. Файл переименовываем в stk500boot_v2_mega2560_2.hex и записываем в папку C:Program Files (x86)Arduinohardwarearduinoavrbootloadersstk500v2. Далее меняем в уже знакомом файле boards.txt следующую строчку: mega2560.bootloader.file=stk500v2/stk500boot_v2_mega2560_2.hex Не пугайтесь, что файл модифицированной прошивки для Mega в 2 раза меньше стандартного — так вроде бы должно быть.
Процесс прошивки
После всех изменений можно прошивать загрузчики, выбирая в меню плат обычные платы (не [optiboot]!). В этом случае прошиваться будут именно те файлы hex, которые мы указали в файле board.txt. Процесс прошивки может не стартовать и выдаваться ошибка:
avrdude: stk500_getsync(): not in sync: resp=0x00
Для решения этой проблемы откройте скетч программатора и в секции setup выберите другую скорость последовательного порта. Во время заливки в Arduino Mega может появляться ошибка, которую следует игнорировать:
avrdude: verification error, first mismatch at byte 0x3e000 0x0d != 0xff avrdude: verification error; content mismatch
Заключительные манипуляции
Загрузчики optiboot имеют еще одну особенность — они увеличивают скорость загрузки скетчей, поэтому при использовании плат с optiboot нужно внести соответствующие изменения в boards.txt: Для Arduino Nano: menu.cpu.nano.atmega328.upload.speed=115200 Для Arduino Mini: menu.cpu.mini.atmega328.upload.speed=115200 Предыдущую скорость порта лучше тоже запомнить, т.к. ее нужно будет использовать на платах со стандартными загрузчиками. Если такие изменения не сделать, то процессе заливки скетчей будет выдаваться ошибка, типа такой:
avrdude: stk500_getsync(): not in sync: resp=0x00
Ссылки, литература
Пакет optibootПрошивка bootloaderКак прошить bootloader в Arduino Pro Mini С обратной стороны ничего интересного: только дорожки, соединяющие три переходных отверстия с пинами модуля. Да не очень качественно отмытый флюс: Модуль построен на базе далласовского чипа DS1232. Для тех, кто привык читать официальную литературу, вот даташит. Чип обеспечивает сразу три функции: мониторинг питания, сторожевой таймер и формирование правильного импульса Reset (нужных фронтов, амплитуды и длительности), даже при дребезге контактов ручной кнопки перезагрузки.Коротко и сжато о назначении выводов чипа Продавец услужливо опубликовал схему модуля и его разводку:
Как видим из схемы, линии чипа ST, RST и /RST выведены, как одноимённые, и на пины модуля. Производитель предоставил нам самое лояльное и комфортное включение чипа: максимально широкий коридор питающих напряжений (от 4,5 до 5V) и максимально возможный таймаут сторожевого таймера (1,2 секунды). Теперь становится понятной схема подключения модуля к нашей условной ардуине: — линии Vcc и Gnd на модуле подключаются к двум одноимённым линиям на arduino; — линия /RST на модуле подключается к пину RST на ардуине — линия ST подключается к любому свободному пину, желательно без аппаратного ШИМ, — допустим, у меня это пин 2. Набросаем простенький скетч для проверки модуля. Я (исключительно для наглядности) при инициализации программно мигаю встроенным светодиодом. Этого можно было не делать, светодиод мигнёт и без нас. Но так — нагляднее и понятнее новичкам. При значении задержки 1150 миллисекунд (в моём конкретном случае) или меньших, наша ардуина спокойно крутится в рабочем цикле, ей ничто не мешает. Светодиод, мигнув вначале, не светится — перезагрузок нет. Как только мы увеличим эту задержку хотя бы до 1200 миллисекунд (или внесём значительную задержку в процедуру Setup), мы увидим, что светодиод начнёт циклически вспыхивать: время для собаки становится критическим и она перезагружает ардуину. В реальных же условиях рабочего скетча, достаточно внести строчкув самый конец основного цикла, чтобы сторожевой модуль, подключённый к пину 2, чувствовал себя спокойно. При инициализации ардуины все пины передёргиваются, поэтому собаке абсолютно всё равно, на каком логическом уровне зависло устройство: таймер был запущен и, значит, перезагружающий импульс неминуемо придёт.Выводы. 1. Нужен ли сторожевой таймер в принципе? Если в проектируемом вами устройстве зависания могут привести к негативным последствиям, то да. С ним система будет, очевидно, более устойчивой, чем без него. 2. Является ли внешний сторожевой таймер панацеей? Конечно, нет. Он более эффективен, чем внутренний, имеющийся во многих микроконтроллерах, но и он не панацея. Для достижения максимальной эффективности его работы, важно его подключить в правильное место вашей системы. Лучше не подключать его к линиям, на которых сигналы формируются аппаратно (ШИМ, различные аппаратные порты со стробами), а также к сигналам внешнего или внутреннего тактового генератора. Ну и абсолютно бессмысленно подключать его к линиям, работающим на вход — так мы будем контролировать не свой микроконтроллер, а что-то другое. 3. Есть ли минусы у этого сторожа? Главным минусом внешнего сторожевого таймера, в общем случае, является потребность в выделении для его работы одного пина на вашем устройстве. Хотя на практике, этого очень часто удаётся избежать: если ваш микроконтроллер что-то постоянно пишет или рисует на дисплее, что-то постоянно посылает в порты, формирует какие-то управляющие импульсы для внешних устройств — подключайте сторожевую собаку к этим линиям, — ей абсолютно безразличны частота и скважность импульсов, ей лишь бы была постоянная движуха. Главным минусом конкретно этой реализации таймера я назову, пожалуй, довольно короткий контрольный таймаут. Бывают случаи, когда хотелось бы иметь запас хотя бы в 3-5 секунд. 4. Надо ли покупать именно этот watchdog? Ну, каждый решит для себя. Сторожевую собаку, совершенно точно, можно получить дешевле $3. Кому-то проще купить один лишь только обозреваемый выше чип и с помощью ЛУТ сделать такой модуль самому. Кому-то проще взять легендарный 555 и сделать сторожевой таймер на рассыпухе: плюсы — дёшево и доступно, минусы — больше возни и времязадающие электролитические конденсаторы, а, значит, с годами гарантированно поплывут все параметры. Кому-то проще сделать одновибратор с таймером вообще на полупроводниках. Тут каждый решает сам. Здесь же за вполне посильные деньги мы получаем полностью готовое и рабочее устройство с нормированными параметрами. Так что резонность покупки каждый оценит самостоятельно. Моей же задачей было рассказать о таких устройствах, о принципах их работы и, возможно, дать кому-то творческий импульс для самостоятельного построения новых интересных самоделок. Никто ничего не предоставлял и не спонсировал, всё куплено на свои.UPD: Пока писал обзор, продавец неожиданно поднял цену. Упс. Будем считать это временным приступом жадности. На этот товар у него периодически бывает скидка. Ну и ничто не мешает искать этот товар у какого-то другого продавца. Используемые источники:- http://mypractic.ru/urok-16-povyshenie-nadezhnosti-programm-dlya-arduino-storozhevoj-tajmer.html
- https://habr.com/post/189744/
- https://mysku.ru/blog/aliexpress/52212.html