Skip to content

wecand0/timeServer

Repository files navigation

timeServer

C++20 CMake libgpiod NTP Stratum 1 Platform License

Stratum 1 NTP-сервер на Raspberry Pi с двумя независимыми источниками времени:

Источник Точность Роль
ublox NEO-M8T + PPS (GPIO18) ±10 нс основной
DCF77 RC8000 (GPIO17) ±5 мс независимый fallback / детектор GPS-спуфинга

Chrony объединяет оба источника через SHM/PPS. Сервер продолжает раздавать время в LAN в режиме holdover при потере обоих сигналов.


Совместимость с новыми версиями Raspberry Pi OS

Важно. Подавляющее большинство инструкций и готовых решений по 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.

ntpd vs chrony

Все старые руководства по 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 и не рекомендуется.

Как проверить версию libgpiod

pkg-config --modversion libgpiod
# Нужно >= 2.0. На Bookworm: 2.x. На Bullseye: 1.6 — нужно собирать из исходников.

Правильный GPIO-чип

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 NEO-M8T

Файл ublox m8t config/config.txt — бинарный конфиг для u-center (UBX-формат). Загружается однократно через Tools → Load config в u-center, сохраняется в flash модуля.

Прошивка: TIM (Timing), а не стандартная навигационная

FWVER = TIM 1.10   MOD = NEO-M8T-0

NEO-M8T поставляется с двумя вариантами прошивки. Конфиг рассчитан на версию TIM, которая оптимизирована под задачи точного времени: улучшенная модель тактового генератора, поддержка RAIM (Receiver Autonomous Integrity Monitoring), Raw measurements. Стандартная навигационная прошивка менее точна в измерениях временны́х меток.

Ключевые параметры и почему они важны для NTP

Режим приёмника — 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) — минимальный рекомендованный набор + время UTC
  • GxZDA (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-config

vcpkg-зависимости (spdlog, cpp-httplib, CLI11, zlib) подтягиваются автоматически.

CMake

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.


Использование

dcf77_decoder

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 подряд корректных фреймов.

dcf77_web

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.1

API

Метод Путь Описание
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)

Деплой (Ansible)

# Настроить инвентарь
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 Порт веб-монитора

Systemd

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.tool

Диагностика

dcf77_decoder не может открыть GPIO

gpiodetect              # найти правильный чип (на Bookworm обычно gpiochip4)

PPS не появляется в chronyc sources

ls /dev/pps*            # должен быть /dev/pps0
dmesg | grep pps

gpsd не пишет в 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

About

setup a Raspberry Pi as a Stratum One time server (GPS with PPS) + DCF77

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors