Stratum 1 NTP-сервер на Raspberry Pi с двумя независимыми источниками времени:
| Источник | Точность | Роль |
|---|---|---|
| ublox NEO-M8T + PPS (GPIO18) | ±10 нс | основной |
| DCF77 RC8000 (GPIO17) | ±5 мс | независимый fallback / детектор GPS-спуфинга |
Chrony объединяет оба источника через SHM/PPS. Сервер продолжает раздавать время в LAN в режиме holdover при потере обоих сигналов.
Важно. Подавляющее большинство инструкций и готовых решений по DCF77/GPIO на Raspberry Pi рассчитаны на старые версии ОС (Buster, Bullseye 32-bit) и не работают на современных системах.
Проект написан специально под актуальный стек:
| Параметр | Старые решения | Этот проект |
|---|---|---|
| ОС | Buster / Bullseye | Bookworm / Trixie |
| Разрядность | 32-bit | 64-bit |
| Ядро | 5.x | 6.12+ |
| Debian | 10 / 11 | 12 / 13 |
| libgpiod | 1.x (gpiod_*) |
2.x (gpiod_chip_open, gpiod_line_request_*) |
| GPIO API | /sys/class/gpio |
chardev (/dev/gpiochipN) |
| GPIO-чип | всегда gpiochip0 |
gpiochip4 на RPi 5 / Trixie |
libgpiod v2 полностью сломала ABI по сравнению с v1. Функции gpiod_chip_open_by_name, gpiod_line_get, gpiod_line_request_* удалены. Новый API построен на объектах gpiod_line_settings / gpiod_line_config / gpiod_line_request. Весь код в этом репозитории использует только v2 API.
Все старые руководства по DCF77 на Raspberry Pi используют ntpd (классический демон из пакета ntp). На современных Raspberry Pi OS ntpd вытеснен chrony, который установлен по умолчанию и несовместим с конфигами для ntpd.
Chrony — это не просто замена ntpd, а принципиально лучшее решение для embedded-систем:
| Параметр | ntpd | chrony |
|---|---|---|
| Сходимость после старта | минуты | секунды (makestep) |
| Работа при нестабильном сигнале | плохо | хорошо (алгоритм фильтрации) |
| Holdover (работа без источника) | нет | есть (local stratum N) |
| PPS через ядро | костыли (pps-tools + ntp-pps) |
нативно (refclock PPS) |
| SHM-интерфейс для внешних источников | SHM 0/1 |
SHM 0–3, тот же формат |
| Потребление CPU | выше | ниже |
| Точность на embedded | ~1–5 мс | <1 мкс с PPS |
Протокол NTP SHM (shared memory) — единственное, что совместимо между ntpd и chrony. Декодер пишет время в SHM-сегмент, chrony его читает — именно это используется в проекте (refclock SHM 2 refid DCF7).
ntpd на Bookworm/Trixie можно установить принудительно (
apt install ntp), но это конфликтует с системным chrony и не рекомендуется.
pkg-config --modversion libgpiod
# Нужно >= 2.0. На Bookworm: 2.x. На Bullseye: 1.6 — нужно собирать из исходников.gpiodetect
# RPi 4, Bookworm: pinctrl-bcm2711 [gpiochip0]
# RPi 5, Trixie: pinctrl-rp1 [gpiochip4] ← использовать --chip /dev/gpiochip4 ┌──────────────┐ ┌─────────────────┐
│ RC8000 │ │ dcf77_decoder │
│ (DCF77) ├────▶│ GPIO → NTP SHM ├─── NTP SHM #2 ────────────────┐
│ GPIO 17 │ └─────────────────┘ │
└──────────────┘ ┌─────────▼───────┐
│ chrony ├──▶ LAN
┌──────────────┐ ┌─────────────────┐ │ Stratum 1 │
│ NEO-M8T │ │ gpsd │ └─────────▲───────┘
│ (GPS + PPS) ├────▶│ UART 115200 ├─── NTP SHM #0 ────────────────┤
│ │ └─────────────────┘ │
│ GPIO 18 ├──────────────────────────── PPS (/dev/pps0) ──────────┘
└──────────────┘
┌──────────────┐ ┌───────────────────────────────────────┐
│ Браузер │◀────┤ dcf77_web │
└──────────────┘ │ chronyc + journald ── HTTP :8080 │
└───────────────────────────────────────┘
| Бинарник | Описание |
|---|---|
dcf77_decoder |
Читает GPIO через libgpiod v2, декодирует DCF77, пишет в NTP SHM |
dcf77_web |
HTTP-сервер, парсит chronyc, отдаёт JSON API и встроенный UI |
Железо
- Raspberry Pi (тестировалось на RPi 4, 64-bit OS)
- DCF77-приёмник RC8000 → GPIO17 (active HIGH; стандартные модули active LOW)
- ublox NEO-M8T (UART
/dev/ttyAMA0+ PPS GPIO18)
ПО на RPi
- Raspberry Pi OS Bookworm / Bullseye (64-bit)
libgpiod >= 2.0,chrony,gpsd- CMake ≥ 3.19, vcpkg
На сборочной машине
- Ansible (для автодеплоя)
Файл ublox m8t config/config.txt — бинарный конфиг для u-center (UBX-формат).
Загружается однократно через Tools → Load config в u-center, сохраняется в flash модуля.
FWVER = TIM 1.10 MOD = NEO-M8T-0
NEO-M8T поставляется с двумя вариантами прошивки. Конфиг рассчитан на версию TIM, которая оптимизирована под задачи точного времени: улучшенная модель тактового генератора, поддержка RAIM (Receiver Autonomous Integrity Monitoring), Raw measurements. Стандартная навигационная прошивка менее точна в измерениях временны́х меток.
Режим приёмника — Stationary (CFG-NAV5, dynModel=2)
Приёмник знает, что он неподвижен. Фильтр Калмана работает в режиме минимальной динамики: позиция почти не меняется, весь «бюджет» вычислений уходит на уточнение временно́й метки. Сравнение:
| Режим | Применение | Jitter PPS |
|---|---|---|
| Portable (0) | смартфон / авто | ~20–50 нс |
| Stationary (2) | фиксированный сервер | ~5–10 нс |
| Airborne (8) | самолёт | ~100 нс |
Многосистемный GNSS (CFG-GNSS)
Включены все четыре констелляции: GPS (8–16 кан.) + GLONASS (8–14 кан.) + Galileo (4–8 кан.) + BeiDou (8–16 кан.) — до 32 спутника одновременно. Больше спутников → лучшая геометрия → точнее позиция → точнее временна́я метка. Одна система GPS даёт 6–12 спутников, четыре — 20–30.
Компенсация задержки кабеля антенны (CFG-TP5, antCableDelay = 50 нс)
Сигнал в коаксиальном кабеле RG-58 распространяется со скоростью ~0.67c. Каждые 10 м кабеля добавляют ~50 нс задержки. Значение 50 нс соответствует примерно 10 м кабеля — прошито в модуль, фронт PPS выдаётся уже с поправкой. Без этой компенсации PPS будет смещён относительно UTC на время прохождения сигнала по кабелю.
Если длина вашего кабеля отличается:
задержка (нс) = длина (м) × 5. Изменить через u-center: CFG-TP5, полеAnt. cable delay.
PPS-сигнал (CFG-TP5[0])
Частота: 1 Гц
Длительность: 100 мс (высокий уровень)
Выравнивание: по секундной метке UTC (alignToTow)
Полярность: rising edge = начало секунды
Rising edge совпадает с границей UTC-секунды с точностью <10 нс. Именно этот фронт захватывает ядро Linux (pps0) и передаёт в chrony как refclock PPS.
Непрерывный режим (CFG-RXM, lpMode=0)
Power-saving режимы (Power Save Mode, Eco) периодически отключают RF-часть для экономии энергии. Это вносит джиттер в PPS от десятков до сотен микросекунд. Для NTP-сервера энергопотребление некритично — режим принудительно выставлен в Continuous.
Частота измерений 1 Гц (CFG-RATE, measRate=1000 мс)
Достаточно для NTP. Более высокая частота (10 Гц) не улучшает точность PPS — он всегда 1 Гц — но увеличивает трафик по UART и нагрузку на CPU.
NMEA-сообщения для gpsd
Включены только нужные:
GxRMC(F0 04) — минимальный рекомендованный набор + время UTCGxZDA(F0 08) — дата и время UTC
gpsd использует их как «метку секунды» для привязки PPS к конкретной секунде UTC (lock NMEA в chrony.conf). Без NMEA chrony не знает, к какой именно секунде UTC относится фронт PPS.
# Через u-center (Windows/Linux)
# Tools → Load config → выбрать ublox m8t config/config.txt
# Или через ubxtool (Linux, пакет gpsd-clients)
ubxtool -f /dev/ttyAMA0 -s 9600 -p SAVE # сначала сохранить текущий
# затем загрузить через u-centerПосле загрузки конфига перезагрузить модуль (отключить питание), убедиться что gpsd видит спутники:
gpsmon /dev/ttyAMA0 # должны появиться спутники через ~30 с (холодный старт)./scripts/install-deps.sh # libgpiod-dev, cmake, pkg-configvcpkg-зависимости (spdlog, cpp-httplib, CLI11, zlib) подтягиваются автоматически.
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DTIMESERVER_AUTHOR="Name <email>" # опционально
cmake --build build -j$(nproc)| Переменная | По умолчанию | Описание |
|---|---|---|
CMAKE_BUILD_TYPE |
Release |
Debug / Release / RelWithDebInfo |
TIMESERVER_AUTHOR |
(пусто) | Строка автора в выводе --version |
Версия берётся из git-тега (v1.2.3) автоматически; fallback — файл VERSION.
Usage: dcf77_decoder [OPTIONS]
Options:
--active-low / --active-high Полярность сигнала (RC8000 = active-high, default)
--debug, -d Подробный лог каждого импульса и бита
--log, -l <path> Файл ротируемого лога (5 МБ × 3)
--chip <path> GPIO-чип (default: /dev/gpiochip0)
--line <n> Линия GPIO (default: 17)
--version, -v Версия и дата сборки
--help, -h Справка
sudo ./build/dcf77_decoder # обычный запуск
sudo ./build/dcf77_decoder --debug # каждый импульс в лог
sudo ./build/dcf77_decoder --chip /dev/gpiochip4 # Bookworm
sudo ./build/dcf77_decoder --active-low # стандартный DCF77-модульПервая синхронизация — через ~2 минуты (нужен полный фрейм DCF77). Системное время устанавливается после 2 подряд корректных фреймов.
Usage: dcf77_web [OPTIONS]
Options:
--port, -p <n> Порт (default: 8080)
--bind, -b <addr> Адрес привязки (default: 0.0.0.0)
--version, -v Версия и дата сборки
--help, -h Справка
./build/dcf77_web # http://localhost:8080
./build/dcf77_web --port 80 --bind 127.0.0.1API
| Метод | Путь | Описание |
|---|---|---|
| GET | / |
Веб-дашборд |
| GET | /api/status |
Полный статус (кеш 2 с) |
| GET | /api/sources |
chronyc sources → JSON |
| GET | /api/tracking |
chronyc tracking → JSON |
| GET | /api/sourcestats |
chronyc sourcestats → JSON |
| GET | /api/decoder |
Статус dcf77.service из journald |
| POST | /api/prefer |
Preferred source (только localhost) |
# Настроить инвентарь
nano ansible/inventory/hosts.yml # IP и пользователь RPi
# SSH-ключ
ssh-copy-id pi@<rpi-ip>
# Полный деплой
cd ansible
ansible-playbook -i inventory/hosts.yml site.yml| Роль | Что делает |
|---|---|
common |
apt, системные пакеты |
chrony |
/etc/chrony/chrony.conf (PPS → DCF77 → holdover) |
gps |
config.txt (UART + PPS overlay), /etc/default/gpsd |
build |
rsync исходников на RPi, cmake + make |
dcf77 |
Установка бинарников, systemd-юниты |
Частичный деплой
ansible-playbook -i inventory/hosts.yml site.yml --tags build,dcf77
ansible-playbook -i inventory/hosts.yml site.yml --tags chrony
ansible-playbook -i inventory/hosts.yml site.yml --tags gpsКлючевые переменные (ansible/group_vars/rpi.yml):
| Переменная | Дефолт | Описание |
|---|---|---|
dcf77_gpio_chip |
/dev/gpiochip0 |
Bookworm: /dev/gpiochip4 |
dcf77_gpio_line |
17 |
GPIO-пин RC8000 |
dcf77_active_level |
high |
low для стандартных модулей |
pps_gpio_pin |
18 |
GPIO-пин PPS от NEO-M8T |
gps_uart_device |
/dev/ttyAMA0 |
UART (PL011, после disable-bt) |
gps_nmea_offset |
0.0 |
Калибровочный offset NMEA (в секундах) |
dcf77_web_port |
8080 |
Порт веб-монитора |
dcf77.service — декодер (запускается после chrony)
dcf77-web.service — веб-монитор
dcf77-healthcheck.timer — проверка каждые 20 мин, алерт при потере сигнала
sudo systemctl status dcf77 dcf77-web
journalctl -u dcf77 -f# Источники времени (* = selected, + = candidate)
chronyc sources -v
# Точность и состояние часов
chronyc tracking
# Последние успешные фреймы декодера
journalctl -u dcf77 -g "OK DCF77" -n 10
# Веб-дашборд
curl http://localhost:8080/api/status | python3 -m json.tooldcf77_decoder не может открыть GPIO
gpiodetect # найти правильный чип (на Bookworm обычно gpiochip4)PPS не появляется в chronyc sources
ls /dev/pps* # должен быть /dev/pps0
dmesg | grep ppsgpsd не пишет в chrony SOCK
id gpsd # должна быть группа _chrony
sudo usermod -aG _chrony gpsd
sudo systemctl restart chrony gpsdНет декодированных фреймов DCF77
# Проверить сигнал напрямую
sudo dcf77_decoder --debug
# Норма: "bit 0 / bit 1" каждую секунду, "Minute marker" раз в минуту
# Нет импульсов → проблема с антенной или полярностью (попробовать --active-low)MIT