Сторожевой таймер и что он значит для кода
Суть в трёх абзацах
На плате, кроме основного процессора, сидит отдельный микроконтроллер-надзиратель. Внутри него — сторожевой таймер: часы обратного отсчёта. Пока приложение живо, оно периодически говорит «я жив», и отсчёт начинается заново. Если такого сигнала нет достаточно долго, надзиратель аппаратно перезагружает плату, дёргая сброс напрямую, минуя операционную систему.
Таймер взведён прошивкой самого микроконтроллера с момента подачи питания. Android его не включает и выключить не может — это проверено и доказано, см. ниже. Если не кормить, плата перезагружается примерно каждые 295 секунд, и в логах ядра при этом нет ничего: журнал обрывается на полуслове, ни паники, ни записи «я перезагружаюсь».
Это правильно, а не поломка. Постамат стоит без присмотра и выдаёт людям вещи. Если софт
завис, единственная альтернатива перезагрузке — прислать человека за 20 километров.
Простое объяснение всей истории — docs/WATCHDOG-PRIMER.md в репозитории прошивки.
Сброс аппаратный, система о нём не предупреждена. Сохранённый хвост предыдущей загрузки выглядит абсолютно нормальным. Единственный надёжный признак — периодичность: смотреть надо не на то, что было перед сбросом, а на аптайм в момент сброса. Два сброса подряд с почти одинаковым аптаймом (у нас 294 и 298 секунд) — это таймер, и гипотезы про событие можно даже не строить.
Почему нельзя просто выключить
Первая мысль у всех одна. Команда выключения существует, программа её посылает, микросхема отвечает «принято». Разобрано 2026-08-31: надзиратель команду игнорирует.
- Подобрать «правильный аргумент» невозможно — в ветке выключения аргумент вообще не читается, уходит всегда одна и та же неизменная посылка.
- «Успех» означает только доставку. Возвращается число переданных байтов — подтверждение, что микросхема приняла посылку по шине, а не что она что-то сделала.
- Прямой опыт. Кормление остановили, выключение послали, за аптаймом следили. Плата перезагрузилась через 288 секунд после последнего кормления — ровно штатный период.
Проверка кода ядра показала и объяснение: функции «включить таймер» и «выключить таймер» не вызывает никто. Android таймер не взводит, единственная его обязанность — кормить.
Выбор сузился до одного варианта: кормить.
Как кормить
int fd = open("/dev/mcu_7502", O_RDWR);
ioctl(fd, 0x40024D03, N); // N — окно в секундах
- N — окно 10…60 секунд. Меньше десяти микроконтроллер не принимает: 0 и 1 дают отказ, 10 и 60 проходят (проверено на живом). Смысл вызова — «я жив, дай мне ещё N секунд», а не «сбрось счётчик».
- 🔴 Аргумент передаётся значением, а не указателем. Передача указателя даёт отказ.
- ⚠️ Успешный код возврата не доказывает ничего — это подтверждение доставки по шине. Единственное доказательство, что таймер укрощён, — аптайм, уверенно переваливший за пять минут.
- 🔴 Путь
/sys/devices/platform/misc_power_en/mcu— не кормление: запись туда проходит успешно, а плата всё равно сбрасывается на 293-й секунде. Не поддаваться.
Kotlin ioctl не умеет — нужен маленький нативный вызов, по образцу cpp/tty.c.
Почему приложение обязано жить в /system/priv-app
Узел /dev/mcu_7502 имеет метку SELinux mcu_device. Эта метка разрешена домену
priv_app и запрещена untrusted_app. Приложение, поставленное обычным adb install,
попадёт в untrusted_app и кормить таймер не сможет — причём сегодня, в permissive, оно бы
«работало», а после залочки молча перестало.
Это и есть настоящее обоснование системной установки, а не украшательство.
Как устроено сейчас и куда переходим
Кормит отдельный системный сервис из образа:
service mcuwd /system/bin/mcuwd daemon 20 --guard net.thepragmaticdev.postamat --grace 240
Период кормления 20 секунд, окно 40 (вдвое больше периода — намеренный запас: пропущенный
цикл ещё не роняет плату). --grace 240 — фора на загрузку, первые четыре минуты кормим
безусловно, иначе плата сбрасывалась бы на каждом старте, не дождавшись киоска.
--guard — не украшение, а весь смысл. Демон, кормящий безусловно, превращает страховку
в фикцию: киоск висит, экран мёртвый, вещь никто не выдаёт — а плата исправно рапортует
надзирателю, что всё прекрасно. Со сторожем кормление привязано к жизни процесса киоска:
пропал процесс — перестаём кормить — плата перезагружается сама.
Почему --guard мало и чем его заменяют
Сторож --guard видит только, что процесс существует. Зависшее приложение процесс
не теряет: экран мёртвый, а сторож честно докладывает «процесс на месте». Страховка
превращается в фикцию, только на шаг дальше.
✅ Вопрос «откуда кормить» закрыт. У демона появился ключ --require-ping: он кормит
микроконтроллер, только пока приложение шлёт признаки жизни. А приложение шлёт их, только
если плата замков недавно ответила кадром со сошедшейся контрольной суммой — то есть
«жив» након ец означает «ответил», а не «существует».
Цепочка целиком:
плата замков отвечает кадром с верным XOR
│
▼
LockBus ставит отметку живого обмена
│
▼
HealthSupervisor решает, что киоск жив
│
▼
пинг сервису mcuwd (нативный модуль, сокет)
│
▼
mcuwd кормит микроконтроллер — и только тогда
Приложение при этом не открывает /dev/mcu_7502: узел монопольный, и пока его держит
системный сервис, открыть его нельзя. Схема с пингом даёт три вещи даром: нет борьбы за
дескриптор, нет окна без кормления при перезапуске приложения, и загрузочное окно
закрывает уже работающая фора сервиса.
✅ Проверено на живой плате — обе половины
Страховка проверяется только двусторонним опытом: мало показать, что при кормлении плата живёт, — надо показать, что без кормления она перезагружается. Иначе непонятно, работает таймер вообще или мы просто смотрим на исправную плату.
| Что делали | Что получилось |
|---|---|
демон с --require-ping, пинги идут | аптайм 460 → 824 секунды, ни одной перезагрузки |
| пинги остановили | плата перезагрузилась через 281 секунду и поднялась сама |
Вторая строка и есть доказательство: это не обещание в документе, а измеренное поведение. 824 секунды — почти три штатных периода таймера подряд; 281 секунда — ровно то самое окно «около пяти минут», отсчитанное от последнего пинга.
Пинги слал скрипт из adb с правами root, а не приложение, и система была в permissive.
❓ Достучится ли до сервиса приложение — то есть хватит ли ему прав на сокет из своего
домена, и что скажет на это enforcing, — вопрос открытый. Это гипотеза, закрытая
рассуждением («узел разрешён домену priv_app»), а не измерением. Проверять придётся
на живой плате, на настоящем APK, вместе со сбором отказов перед залочкой.
Сперва APK, умеющий пинговать, потом строка сервиса в образе.
- APK с пингом → залить → убедиться, что аптайм уверенно перевалил за 10 минут;
- только потом строка сервиса меняется на
--require-ping; - снова проверить аптайм.
Если залить образ с --require-ping раньше, чем APK, плата начнёт перезагружаться каждые
пять минут — и в логах ядра при этом не будет ничего, лог просто оборвётся на полуслове.
Этот симптом уже стоил проекту суток.
Имя сокета в нативном модуле киоска обязано совпадать с ключом --socket в строке сервиса.
Расхождение не даст ни ошибки сборки, ни падения: пинги будут исправно уходить в никуда,
а плата — перезагружаться каждые пять минут. Артефакт: NATIVE-GATE.md.
Чем это грозит на практике
adb rootубивает кормильца, запущенного руками. Эта команда перезапускает службу отладки, и кормилец, стартовавший изadb shell, умирает вместе с ней — даже подsetsid. Плата остаётся без кормления и примерно через пять минут уходит в сброс. Коварство в том, что причина и следствие разнесены на пять минут: команда, которая всё сломала, к моменту перезагрузки давно отработала, и виноватым выглядит что-то другое. Сервис из образа — ребёнокinit, а не отладочной службы, и задевать его это не должно, ❓ но на живом после 2026-09-01 не перепроверялось. Правило пока прежнее:adb rootбез нужды н е звать, а если позвали — проверитьadb shell getprop init.svc.mcuwd.- Таймер тикает и во время прошивки. Микроконтроллеру безразлично, что делает процессор:
он считает и когда плата стоит в загрузчике. Отсюда жёсткое окно на заливку — около
295 секунд с последнего кормления, а запись раздела
superзанимает ~140 секунд. Подробности — сборка и заливка. - К залочке. Сейчас кормление идёт из домена
init, и в логе отказов SELinux эти вызовы есть. Вenforcingони будут отклонены, кормление прекратится молча, и плата уйдёт в цикл «пять минут — сброс». Нужна своя политика.
Как проверить, что всё хорошо
Единственное доказательство — аптайм, уверенно переваливший за пять минут.
adb shell getprop init.svc.mcuwd # ожидаем running
adb shell uptime # ожидаем заметно больше пяти минут
Контрольные измерения: 829 секунд аптайма при безусловном кормлении и 460 → 824 секунды при кормлении по пингу — оба без единого сброса, при том что без кормления сбросы шли стабильно на 281–298-й секунде.
⚠️ Проверять надо обе стороны. «Плата не перезагружается» само по себе не значит, что страховка работает: ровно так же выглядит и плата, которую кормят безусловно, и плата, на которой сторож смотрит не туда. Убедиться, что при пропаже пингов она действительно уходит в сброс, так же обязательно, как убедиться, что при пингах она живёт.
Технический справочник со всеми номерами команд — docs/MCU-WATCHDOG.md в репозитории
прошивки.