Watchdog — это устройство, предназначенное для обнаружения и устранения проблем оборудования. Обычно для этого используется таймер, периодический перезапуск которого предотвращает отправку сигнала на перезагрузку.



Целевой сервер на Gentoo используется мной в основном для экспериментов, однако на нём работает ряд сервисов, которые, по возможности, должны быть доступны без перебоев. К сожалению, последствия некоторых экспериментов приводят к kernel panic, 100% загрузке CPU и другим неприятностям в самый не подходящий момент. Так что идея добавить watchdog давно требовала внимания и наконец материализовалась в данное устройство.

После пристального осмотра того, что было в наличии и оценки доступного времени, оптимальным вариантом стал watchdog собранный на базе Arduino Nano. Примерно также появился и список требований:

  1. Запуск и останов демона, для работы с таймером, штатным средством ОС (OpenRC).
  2. Собственный watchdog на устройстве, в ATmega он есть, нужно использовать.
  3. Лог событий на устройстве для фиксации перезагрузки и срабатывания таймера.
  4. Синхронизация времени устройства с хостом для записи в лог корректного времени.
  5. Получение и отображение статуса устройства и записей его лога.
  6. Очистка лога и сброс устройства в исходное состояние.

Таким образом, «микроскоп» был найден, «гвоздь» обозначен… можно забивать.

Аппаратная часть


Основой устройства стал китайский клон 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, то алгоритм будет следующий:

  1. Добавить содержимое файла boards-1.6.txt из пакета optiboot в конец файла hardware/arduino/avr/boards.txt в директории с Arduino IDE.
  2. В Arduino Uno, загрузить скетч из File > Examples > ArduinoISP.
  3. Соединить программатор с целевой 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


    На фото это выглядит так

  4. В Arduino IDE в меню Tools установить настройки как на скриншоте:
  5. Выбрать пункт меню 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"


Разместить его нужно в отдельном файле в директории /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

Комментарии (11)


  1. IronHead
    23.09.2015 15:53

    Сколько ж вы словили граблей только из за того, что взяли ардуино.
    Взяли бы чистую тиньку45 — проблемы были бы только с прошивкой.
    На основе этой схемы http://microsin.net/programming/AVR/avr-usb-tiny45.html можно с легкостью сделать все, что требовалось от вашего устройства.


    1. custos
      23.09.2015 16:14

      Спасибо за ссылку, очень интересный вариант! А то, что тяжеловата железка для данной задачи, я осознавал в полной мере, просто так сложилось, что микроконтроллера в подходящем корпусе не было, а данную Arduino нужно было утилизировать, т.к. наигрался.


  1. Alexeyslav
    23.09.2015 17:28

    но команды для работы с текстом (cat, echo и т.п.), получают и отправляют только мусор.

    И правильно, потому что настроены на фиксированную скорость, которую им надо указать.


    1. custos
      23.09.2015 18:18

      Напишите тут, пожалуйста, подробнее… если речь о stty то с этим я долго экспериментировал и эффекта не было, понятно, что менял соответственно Serial.begin(...) в скетче;


      1. Alexeyslav
        23.09.2015 19:40

        Скетч отвечает только за настройку порта микроконтроллера. А в схеме есть еще мост USB-UART, они должны быть настроены на одинаковую скорость чтобы не было проблем. Кстати насколько ардуина скрывает реализацию — если бы вы анализировали содержимое регистра состояния UART контроллера, видели бы кучу возникающих ошибок framing error возникающих при попытке приёма данных, что однозначно свидетельствует о несоответствии скорости передатчика и приёмника.

        Вам надо в скетче установить скорость, и со стороны системы тоже указать те же параметры соединения, по умолчанию стоит конфигурация 9600-8N-1(9600бод, 8 бит, без контроля чётности, 1 стоп-бит) для соответствующего COM-порта под которым видится ардуина в системе.
        Если программа умеет сама ставить скорость порта, то для команд в консоли работающих с портом настройки находятся на уровне системы или даже консоли.


        1. custos
          23.09.2015 20:18

          Спасибо, в целом согласен, имеет смысл копнуть глубже. У меня инициализация порта производится командой

          stty -F "${DEVICE}" cs8 9600 raw ignbrk noflsh -onlcr -iexten -echo -echoe -echok -echoctl -echoke -crtscts -hupcl
          эксперименты начались с 115200 в итоге пришлось скатиться до 9600, не приятно конечно, но оказалось вполне приемлемо. Ради интереса надо будет сравнить чем эта инициализация отличается от minicom.


          1. 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
            Честно что означает каждый параметр я не знаю, где то когда то скопировал, но работает уже не в одном проекте и очень успешно.


            1. custos
              23.09.2015 22:06

              Сравнил… эти команды эквивалентны за исключением icrnl (переводит \r -> \n), нужно будет тоже включить, спасибо за идею, и hupcl — я выключил пытаясь побороть проблему с reset по DTR, не помогло.


  1. lenz1986
    23.09.2015 21:11

    Проблема с резетом на более старших платах решалась аппаратным отключением линии DTR, перерезав дорожку на плате, а с электролитом интересное решение, но есть один нюанс.
    Может мои предположения окажутся глупостью, а если обращения к плате станут слишком частыми, превысят какой то определенный порог, именно подключение, отключение терминала, когда проскакивает DTR, не успеет ли случаем зарядится электролит до состояния срабатывания резета?


    1. custos
      23.09.2015 21:23

      не успеет ли случаем зарядится электролит до состояния срабатывания резета?

      Там наоборот конденсатор всегда заряжен, для срабатывания reset нужен низкий уровень, его DTR выставляет на короткое время и конденсатор не успевает разрядиться, зато заряжается он потом очень быстро. Иначе говоря, частота для такого сценария должна быть не реальной.


      1. lenz1986
        23.09.2015 21:30

        все понял. Я капитально сглупил ссори, сказывается отсутствие практики с чистыми авр-ками, и переход на ардуину… где все продумано за нас.