[Из песочницы] Watchdog на базе Arduino Nano
Watchdog — это устройство, предназначенное для обнаружения и устранения проблем оборудования. Обычно для этого используется таймер, периодический перезапуск которого предотвращает отправку сигнала на перезагрузку.
Целевой сервер на Gentoo используется мной в основном для экспериментов, однако на нём работает ряд сервисов, которые, по возможности, должны быть доступны без перебоев. К сожалению, последствия некоторых экспериментов приводят к kernel panic, 100% загрузке CPU и другим неприятностям в самый не подходящий момент. Так что идея добавить watchdog давно требовала внимания и наконец материализовалась в данное устройство.
После пристального осмотра того, что было в наличии и оценки доступного времени, оптимальным вариантом стал watchdog собранный на базе Arduino Nano. Примерно также появился и список требований:
- Запуск и останов демона, для работы с таймером, штатным средством ОС (OpenRC).
- Собственный watchdog на устройстве, в ATmega он есть, нужно использовать.
- Лог событий на устройстве для фиксации перезагрузки и срабатывания таймера.
- Синхронизация времени устройства с хостом для записи в лог корректного времени.
- Получение и отображение статуса устройства и записей его лога.
- Очистка лога и сброс устройства в исходное состояние.
Таким образом, «микроскоп» был найден, «гвоздь» обозначен… можно забивать.
Аппаратная часть
Основой устройства стал китайский клон Arduino Nano, выполненный на базе чипа CH340. Свежие Linux ядра (проверял начиная с 3.16) имеют подходящий драйвер, так что устройство легко обнаруживается как USB последовательный порт.
Нежелательная перезагрузка Arduino
При каждом подключение терминала, Arduino перезагружается. Причина в отправке терминалом сигнала DTR (Data Terminal Ready), который вызывает перезагрузку устройства. Таким образом Arduino IDE переводит устройсво в режим для загрузки скетчей.
Существует несколько вариантов решения проблемы, но рабочим оказался только один — необходимо установить электролит 10µF (C1 на схеме ниже) между контактами RST и GND. К сожалению, это также блокирует загрузку скетчей на устройство.
Как итог — схема получилась следующий:
Нарисовано с помощью KiCad
- R1 — резистор для ограничения тока, рассчитывается согласно спецификации на оптопару PC817: (5V — 1.2V / 0.02A) = 190Ω, ближайшей стандартный номинал 180Ω.
- U2 — оптопара для гальванической развязки Arduino и PC. Можно обойтись и транзистором, так как земля общая (через USB разъем), но лучше не нужно.
- JP1 — джампер, в рабочем положении должен быть замкнут. Для загрузки скетча на устройство его необходимо разомкнуть.
- С1 — конденсатор, блокирует перезагрузку устройства в ответ на сигнал DTR.
- MB_RST, MB_GND — RESET активен при низком уровне сигнала, соответственно нужно замкнуть RST на землю (GND). В оптопаре используется транзистор, следовательно важно соблюсти полярность.
- BTN_RST, BTN_GND — кнопка на корпусе, обычно это механический переключатель, следовательно, полярность не важна, но бывают исключения.
Boot-loop (циклическая перезагрузка) при работе с WDT
Микроконтроллеры ATmega имеют встроенный механизм перезагрузки по таймеру WDT (WatchDog Timer). Однако все попытки использовать данную функцию приводили к boot-loop, выйти из которого можно было только отключив питание.
Не долгие поиски выявили, что загрузчики большинства клонов Arduino не поддерживают WDT. К счастью, данная проблема была решена в альтернативном загрузчике Optiboot.
Для того, чтобы прошить загрузчик, необходим программатор умеющий работать по протоколу SPI, также желательно, чтобы Arduino IDE знала это устройство «в лицо». В данном случае идеально подойдёт ещё одна Arduino.
Если взять Arduino UNO, в качестве программатора, и последнию на данный момент версию Arduino IDE v1.6.5, то алгоритм будет следующий:
- Добавить содержимое файла boards-1.6.txt из пакета optiboot в конец файла hardware/arduino/avr/boards.txt в директории с Arduino IDE.
- В Arduino Uno, загрузить скетч из File → Examples → ArduinoISP.
- Соединить программатор с целевой Arduino Nano следующим образом:
Arduino Uno (программатор) Arduino Nano (ICSP разъём) 5V → Vcc GND → GND D11 → MOSI D12 → MISO D13 → SCK D10 → Reset Pin1 (MISO) ← D12 Pin2 (Vcc) ← 5V Pin3 (SCK) ← D13 Pin4 (MOSI) ← D11 Pin5 (Reset) ← D10 Pin6 (GND) ← GND На фото это выглядит так - В Arduino IDE в меню Tools установить настройки как на скриншоте:
- Выбрать пункт меню Tools → Burn Bootloader и убедиться, что процесс завершился без ошибок.
После этой процедуры, загружать скетчи в Arduino Nano нужно будет выбирая те-же настройки — Board: Optiboot on 32 pin cpus, Processor: ATmega328p, CPU Speed: 16MHz.
Пайка
Далее необходимо всё спаять, так чтобы выглядело одним куском.
Здесь USB штекер понадобился из-за того, что у меня mini-ITX мат.плата только с одним разъем на пару USB2.0, которые нужны на передней панели, а к контактной площадке USB3.0 нечем было подключиться. По возможности такие устройства нужно подключать прямо к мат.плате, чтобы провода наружу не торчали.
Пайка, как правило, проблем не вызывает, но в данном случае используется макетная плата, и тут есть своя специфика.
Выглядеть должно примерно так:
Результат:
Здесь может показаться, что некоторые контакты плохо пропаяны, но это не так, проблема в припое, он содержал 40% флюса. Учитывая расход припоя на макетных платах — это очень много. В итоге флюсом тут заляпано всё, что только можно. На самом деле, это хороший пример как не нужно оставлять изделие после пайки. Флюс необходимо смыть, иначе могут быть проблемы с коррозией соединений. Допишу и пойду отмывать… Вот так лучше:
Программная часть
Объективно говоря, код этого проекта особого интереса не представляет. Вводные далеко не экстремальные, а архитектура описывается одной фразой: отправить команду — подождать ответ. Для порядка опишу здесь основной функционал и кратко остановлюсь на самых интересных моментах, с моей точки зрения.
Весь код опубликован на GitHub, так-что если вы знакомы с Bash и С/C++ (в контексте Arduino скетчей), чтение на этом месте можно закончить. При наличии интереса, с готовым результатом можно ознакомиться здесь.
Подключение watchdog
При подключении watchdog создается файл устройства, содержащий порядковый номер. Если в системе есть другие ttyUSB устройства (в моём случае — модем), то возникает проблема с нумерацией. Чтобы однозначно идентифицировать устройство, необходимо создать симлинк с уникальным именем. Для этого предназначен udev, который наверняка уже есть в системе.
Для начала нужно визуально найти подключённый watchdog, например, подсмотрев в системный лог файл. Затем, заменив /dev/ttyUSB0 на нужное устройство, написать в терминале:
udevadm info -a -p "$(udevadm info -q path -n /dev/ttyUSB0)"
KERNEL==«ttyUSB0»
SUBSYSTEM==«tty»
…
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4/1-1.4:1.0/ttyUSB0':
KERNELS==«ttyUSB0»
SUBSYSTEMS==«usb-serial»
DRIVERS==«ch341-uart»
…
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4/1-1.4:1.0':
…
looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4':
SUBSYSTEMS==«usb»
DRIVERS==«usb»
ATTRS{idVendor}==«1a86»
ATTRS{idProduct}==«7523»
ATTRS{product}==«USB2.0-Serial»
…
В данном случае, правило будет иметь следующий вид:
ACTION=="add", KERNEL=="ttyUSB[0-9]*", SUBSYSTEM=="tty" SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="ttyrst-watchdog"
Разместить его нужно в отдельном файле в директории /etc/udev/rules.d, например 51-ttyrst-watchdog.rules и скомандовать udev перезагрузить правила:
udevadm control --reload-rules
С этого момента, при подключении watchdog будет создаваться ссылка /dev/ttyrst-watchdog на нужное устройство, которая и будет использоваться далее.
Bash скрипт (ttyrst-watchdog.sh)
Общение с watchdog производится на скорости 9600 бод. Arduino без проблем работает с терминалами на больших скоростях, но команды для работы с текстом (cat, echo и т.п.), получают и отправляют только мусор. Не исключено, что это особенность только моего экземпляра Arduino Nano.
Для основного цикла перезапуска таймера и для функций командной строки, используется один скрипт. Причина в том, что оба компонента используют общий ресурс — файл устройства, и к нему необходимо обеспечить синхронный доступ.
Синхронизация по сути состоит цикла ожидания:
while fuser ${DEVICE} >/dev/null 2>&1; do true; done
и захвата устройства на необходимое время:
cat <${DEVICE}
Очевидно, такая схема подвержена состоянию гонки (race condition). Бороться с этим можно по взрослому (например, организовать очередь сообщений), но в данном случае, достаточно грамотно расставить таймауты, чтобы гарантированно получать результат за приемлемое время. По сути весь скрипт и есть работа с таймаутами.
Демонизация (запуск в фоновом режиме) производится средствами пакета OpenRC. Предполагается, что данный скрипт находится в файле /usr/local/bin/ttyrst-watchdog.sh, а OpenRC скрипт в /etc/init.d/ttyrst-watchdog.
При остановке демона требуется корректная дезактивации watchdog. Для этого в скрипте устанавливается обработчик сигналов, требующих завершение работы:
trap deactivate SIGINT SIGTERM
И тут всплывает проблема — OpenRC не может остановить демон, точнее может, но не часто.
Дело в том, что команда kill, отправляет сигнал скрипту, а программа sleep, которая используется для приостановки работы скрипта, выполняется в другом процессе и сигнал не получает. В результате функция deactivate запускается только после завершения работы sleep, а это слишком долго.
Решение заключается в том, чтобы запустить sleep в фоне и ждать завершения процесса в скрипте:
sleep ${SLEEP_TIME} & wait $! # переменная $! содержит ID последнего запущенного процесса
Основные константы:
WATCHDOG_ACTIVE — YES или NO, соответственно, отправлять сигнал на перезагрузку при срабатывании таймера или нет.
WATCHDOG_TIMER — время в секундах на которое устанавливается таймер.
SLEEP_TIME — время в секундах через которое необходимо перезапускать таймер. Должно быть много меньше, чем WATCHDOG_TIMER, но не сильно маленькое, что бы не создавать чрезмерную нагрузку на систему и устройство. При текущих таймаутах разумный минимум — примерно 5 секунд.
DEFAULT_LOG_LINES — число последних записей лога устройства, возвращаемых командой log по умолчанию.
Команды скрипта:
start — запуск основного цикла перезапуска таймера. В функцию is_alive можно добавить код дополнительных проверок, например проверить возможность подключения по ssh.
status — вывод статуса устройства.
reset — обнуление EEPROM (данных лога) и перезагрузка устройства для приведения watchdog в исходное состояние.
log <число записей> — вывод заданного числа последних записей лога.
Arduino скетч (ttyrst-watchdog.ino)
Для успешной компиляции скетча потребуется сторонняя библиотека Time, необходимая для синхронизации времени.
Скетч состоит из двух файлов. Это связанно с тем, что Arduino IDE не воспринимает структуры (struct) объявленные в основном файле, их необходимо выносить во внешней файл заголовков. Также для объявления структуры не обязательно ключевое слово typedef, вероятно даже вредно… проверив стандартные варианты, подобрать подходящий синтаксис у меня не получилось. В остальном это более или менее стандартный C++.
Функции wdt_enable и wdt_reset работают со встроенным в микроконтроллер watchdog. После инициализации WDT главное не забывать сбрасывать его в основном цикле и внутри циклов всех длительных операций.
Записи лога пишутся в энергонезависимую память EEPROM, доступный её размер можно указать в logrecord.h, в данном случае это число 1024. Лог выполнен в виде кольца, разделителем служит структура с нулевыми значениями. Максимальное число записей для 1 KiB EEPROM — 203.
Запись о загрузке устройства попадает в лог только после синхронизации времени. Синхронизация производится одновременно с перезапуском таймера и перед выполнением любой команды во время инициализации устройства. По другому сопоставить корректное время данному событию не получится, да и информация о перезагрузках устройства, в отрыве от работающего демона, не сильно интересна.
На этом всё, спасибо за внимание!
Исходные файлы проекта расположены на GitHub