Целевой сервер на 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)"
looking at device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4/1-1.4:1.0/ttyUSB0/tty/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"
Разместить его нужно в отдельном файле в директории
udevadm control --reload-rules
С этого момента, при подключении 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. Предполагается, что данный скрипт находится в файле
При остановке демона требуется корректная дезактивации 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
Комментарии (11)
Alexeyslav
23.09.2015 17:28но команды для работы с текстом (cat, echo и т.п.), получают и отправляют только мусор.
И правильно, потому что настроены на фиксированную скорость, которую им надо указать.custos
23.09.2015 18:18Напишите тут, пожалуйста, подробнее… если речь о stty то с этим я долго экспериментировал и эффекта не было, понятно, что менял соответственно Serial.begin(...) в скетче;
Alexeyslav
23.09.2015 19:40Скетч отвечает только за настройку порта микроконтроллера. А в схеме есть еще мост USB-UART, они должны быть настроены на одинаковую скорость чтобы не было проблем. Кстати насколько ардуина скрывает реализацию — если бы вы анализировали содержимое регистра состояния UART контроллера, видели бы кучу возникающих ошибок framing error возникающих при попытке приёма данных, что однозначно свидетельствует о несоответствии скорости передатчика и приёмника.
Вам надо в скетче установить скорость, и со стороны системы тоже указать те же параметры соединения, по умолчанию стоит конфигурация 9600-8N-1(9600бод, 8 бит, без контроля чётности, 1 стоп-бит) для соответствующего COM-порта под которым видится ардуина в системе.
Если программа умеет сама ставить скорость порта, то для команд в консоли работающих с портом настройки находятся на уровне системы или даже консоли.custos
23.09.2015 20:18Спасибо, в целом согласен, имеет смысл копнуть глубже. У меня инициализация порта производится командой
эксперименты начались с 115200 в итоге пришлось скатиться до 9600, не приятно конечно, но оказалось вполне приемлемо. Ради интереса надо будет сравнить чем эта инициализация отличается от minicom.stty -F "${DEVICE}" cs8 9600 raw ignbrk noflsh -onlcr -iexten -echo -echoe -echok -echoctl -echoke -crtscts -hupcl
lenz1986
23.09.2015 21:16Я в своем проекте стыковки ардуины и openwrt использую вот такую команду, и работает без сбоев с 2013 года
stty -F $ARDUINO_PORT ispeed $ARDUINO_PORT_SPEED ospeed $ARDUINO_PORT_SPEED cs8 ignbrk -brkint -imaxbel -opost -onlcr -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke noflsh -ixon -crtscts
Честно что означает каждый параметр я не знаю, где то когда то скопировал, но работает уже не в одном проекте и очень успешно.custos
23.09.2015 22:06Сравнил… эти команды эквивалентны за исключением icrnl (переводит \r -> \n), нужно будет тоже включить, спасибо за идею, и hupcl — я выключил пытаясь побороть проблему с reset по DTR, не помогло.
lenz1986
23.09.2015 21:11Проблема с резетом на более старших платах решалась аппаратным отключением линии DTR, перерезав дорожку на плате, а с электролитом интересное решение, но есть один нюанс.
Может мои предположения окажутся глупостью, а если обращения к плате станут слишком частыми, превысят какой то определенный порог, именно подключение, отключение терминала, когда проскакивает DTR, не успеет ли случаем зарядится электролит до состояния срабатывания резета?custos
23.09.2015 21:23не успеет ли случаем зарядится электролит до состояния срабатывания резета?
Там наоборот конденсатор всегда заряжен, для срабатывания reset нужен низкий уровень, его DTR выставляет на короткое время и конденсатор не успевает разрядиться, зато заряжается он потом очень быстро. Иначе говоря, частота для такого сценария должна быть не реальной.lenz1986
23.09.2015 21:30все понял. Я капитально сглупил ссори, сказывается отсутствие практики с чистыми авр-ками, и переход на ардуину… где все продумано за нас.
IronHead
Сколько ж вы словили граблей только из за того, что взяли ардуино.
Взяли бы чистую тиньку45 — проблемы были бы только с прошивкой.
На основе этой схемы http://microsin.net/programming/AVR/avr-usb-tiny45.html можно с легкостью сделать все, что требовалось от вашего устройства.
custos
Спасибо за ссылку, очень интересный вариант! А то, что тяжеловата железка для данной задачи, я осознавал в полной мере, просто так сложилось, что микроконтроллера в подходящем корпусе не было, а данную Arduino нужно было утилизировать, т.к. наигрался.