惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
Jina AI
Jina AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
罗磊的独立博客
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
博客园 - 叶小钗
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题
博客园 - 【当耐特】
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
量子位
宝玉的分享
宝玉的分享
V
Visual Studio Blog
博客园_首页
IT之家
IT之家
V
V2EX
腾讯CDC
aimingoo的专栏
aimingoo的专栏
博客园 - 司徒正美
Microsoft Security Blog
Microsoft Security Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Blog — PlanetScale
Blog — PlanetScale
I
InfoQ
有赞技术团队
有赞技术团队
J
Java Code Geeks
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
H
Help Net Security

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Ловим время в формате DCF77
JBFW · 2026-04-22 · via Все публикации подряд на Хабре

Время на прочтение9 мин

Охват и читатели2.2K

В комментариях к "серверу точного времени" (https://habr.com/ru/articles/1023414) предлагали вдобавок к NTP и GPS подключить еще и DCF77, как еще один источник времени.
И я таки сделал это, хоть и в виде отдельной железки, а поскольку техника тут аналоговая - были свои нюансы.

В качестве справки:
DCF77 - это радиостанция, передающая точное время от атомных часов, собственно, это ее основное назначение.
Расположена в Европе, в Германии, неподалеку от Франкфурта. Вещает на длинных волнах на всю Европу, захватывая в том числе часть exUSSR. Передает сигнал, содержащий информацию о времени и дате, UTC+1/UTC+2 в зависимости от "летнего времени".
Также передает местную погоду и может быть использована как средство оповещения, но нас это мало касается.
Рабочая частота 77.5 кГц - поэтому и "DCF77".

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

Проблема простая: расстояние.
Несмотря на большую мощность передатчика и хорошее распространение длинных волн на большие расстояния - 2000 км это 2000 км.

Типовой DCF77-модуль имеет компактную магнитную антенну, которую наверное удобно размещать в типовых настольных часах:

Вообще, для понимания, для приема электромагнитных волн существует два типа антенн: электрические и магнитные, соответственно для электрической или магнитной составляющей поля.
Электрические - кусок провода, металлический штырь, рисунок на печатной плате - их размеры должны соответствовать длине волны (1/4, 1/2, 1), длина волны зависит от частоты и скорости света (примерно в метрах = 300000000 / Гц), поэтому для высокочастотных сигналов типа сотовой связи или WiFi они очень удобны.
Но для частот типа 77500 Гц длина волны 3.870км (поэтому они и длинные волны), и более-менее рабочая антенна должна иметь длину около километра (1/4 от 3.870).

Для таких частот удобнее использовать магнитные антенны: по сути катушку провода, намотанную на стержне (и иногда без него).
Там есть свои нюансы, по поводу того что катушка должна быть правильно ориентирована относительно силовых линий магнитного поля волны - но это уже детали.
Вот как раз, как в случае этого модуля: ферритовый стержень с обмотками.

Но физику не обманешь: размер имеет значение, чем больше стержень - тем выраженнее эффект.
Маленькая магнитная антенна, наверное, неплохо работает в Европе, или рядом.
В моем случае ее оказалось недостаточно для устойчивого приема сигнала: то мусор, то вообще ничего.

Тут надо сказать о том, что, собственно говоря, мы ловим?
DCF77 передает просто некоторый сигнал, который раз в секунду прерывается на определенное время. Пауза 0.1 сек - логический "0", пауза 0.2 секунды - логическая "1". Нет паузы - конец цикла. Потом передача начинается заново.
Длина одного цикла - 60 секунд, длина сообщения 58 (или 59, смотря как считать) бит.

Таблица декодирования (из Вики):

Модуль RC8000, который как раз должен это ловить, принимает сигнал, только инвертирует, вместо пауз - импульсы: по хорошему они должны быть 0.1 сек или 0.2 сек.
Он не декодирует сигнал в код, он просто принимает и фильтрует аналоговый сигнал.
И когда не может принять качественный сигнал - либо "молчит", либо начинает сыпать случайными имульсами в случайное время, потому что ловит помехи.

Тут еще одно важное отступление: на дальнее распространение радиоволн сильно влияет состояние ионосферы планеты, которое под влиянием излучения ближайшей звезды может существенно меняться.
Если коротко - ночью дальняя связь лучше, и то, что не ловится днем - может ловиться ночью. Если повезет, конечно.
В данном случае - не помогла и ночь, модуль либо молчал, либо сыпал мусором.

Оставался вариант - попробовать увеличить размер магнитной антенны.
Для этого нужно взять ферритовый стержень побольше, намотать на него катушку и подключить вместо штатной антенны.

Найти стержень побольше оказалось не так просто: современная техника высокочастотная, магнитных антенн либо уже нет, либо они маленькие, а вот такое нак надо - было в старинных советских радиоприемниках ДВ/СВ.
Пришлось найти такой, и извлечь феррит из него (ну или заказывать из Китая, где есть всё - но это долго).

Катушка на антенне - это не просто так, это часть старого доброго колебательного контура: резонансная частота контура зависит от индуктивности катушки и емкости конденсатора, а от резонанской частоты зависит, что именно будет ловить антенна.
Таким образом, надо было намотать катушку такой же индуктивности, как штатная.

К счастью, сейчас есть простые приборчики, которые могут измерять свойства различных радиоэлементов, в том числе и индуктивность: так выяснилось, что индуктивность штатной катушки на антенне - 1.34мГн.
Тут даже неважно, насколько точно она измерена в мГн - важно сделать такую же.

Для этого нужно просто намотать на стержень правильное количество витков провода, очень хорошо подходит обмоточный ПЭЛШО (провод электрический лакированный в шелковой оплетке) - ради него пришлось посетить магазинчик радиоприбабахов.

Чтобы узнать нужное количество витков - можно сделать, например, так:
Намотать 20 витков - измерить индуктивность: получится сколько-то там.
Общая индуктивнось катушки зависит от свойств стержня (которые мы не знаем), но важнее - зависит от квадрата их количества, то есть если для 20 она X, то для 2*20 будет X*2^2, для 3*20 будет X*3^2, и так далее.
В общем, получилось, что нужно чуть больше 100 витков.

Но есть проблема: чтобы попасть точно в заданную индуктивность нужно намотать точно рассчитанное количество витков, включая нецелые (причем не ошибиться в расчетах).
Сделать это не так просто, к тому же нет возможности подстроить под нужную частоту конденсатор колебательного контура, а значит нельзя компенсировать неизбежные погрешности.

Но есть древний лайхак, времен массового использования ферритовых антенн: можно взять витков чуть побольше, скажем, 120, разделить одну катушку на две, намотать каждую на отдельной бумажной втулке, соединить последовательно, а потом раздвигать их по стержню: чем больше общая длина - тем ниже будет индуктивность.
Передвигая их по стержню - подобрать оптимальную индуктивность и наилучшее качество приема.

сравните размеры антенн

сравните размеры антенн

И вот, собственно, пробуем:
У модуля RC8000 четыре вывода: VCC, GND, OUT и EN.
VCC - 3.3в, OUT - наши импульсы, EN - enable, который надо подключить к GND (почему так? а вот так, логично же: enable на 0).
Вместо штатной антенны - франкенштейн самолепный.
OUT - пока нужны просто импульсы, поэтому подключаем просто светодиодик.

Лайфхак: все видели светодиодные ленты, в т.ч. со светодиодами 2525 - мелкими квадратными. Они не слишком долговечные, некоторые светодиоды чернеют и перегорают, ленты выбрасывают и меняют - так вот, подобные светодиоды идеальные индикаторы! Они очень чувствительные, вспышки хорошо заметные, и хорошо сочетаются с 3-вольтовой логикой. Именно такой светодиод и будет индикатором импульсов

Чтобы исключить помехи при настройке - никаких работающих ESP рядом, никаких импульсных блоков питания, две батарейки по 1.5В.
Включаем, постепенно раздвигаем катушки - и вот пошел сигнал.
Раз в секунду - чуть дольше, чуть короче, а вот пауза - и снова пошли вспышки раз в секунду.
Готово, сигнал DCF77 принимается, даже на парковке днем.
Фиксируем катушки - и вот теперь можно попробовать подключить это к ESP.

И новая проблема: ESP интересна наличием WiFi, а батарейки и WiFi - вещи плохо совместимые.
Конечно, можно подключить всё через блок питания - но современные импульсные блоки очень шумят в ДВ-диапазоне (обычно всем наплевать, сейчас он почти не используется, но не в этом случае).
К счастью, нашелся старый трансформаторный блок питания - из тех, которые шли когда-то к телефонам. Он оказался на 12 вольт, но вот тут уже их можно понизить до 3.3 модулем DCDC - у него рабочая частота выше, чем у радиосигнала DCF77, эта антенна ее не ловит.

Модуль RC8000 вместе с аннтенной подключен отдельно, проводом - подальше от ESP.
Пришлось подключить к нему конденсатор на VCC и GND, побольше, без него он работать отказывался.

Сигнальный выход модуля - ко входу ESP, на котором настроена обработка прерываний.
Смысл в том, что импульсы на этом входе будут вызывать прерывания: при этом будут отмечаться времена возникновения и спада импульсов, что позволит вычислять длительнность имульсов и пауз.

В идеале DCF77 имеет строгие правила: импульсы начинают идти ровно с началом очередной минуты (атомные часы, вот это всё), и идут каждую секунду кроме 59-й.
Логический 0 представлен импульсами длительностью 100 мс, логическая единица - 200 мс, после каждого - пауза до начала следующей секунды, 900 и 800 мс соответственно.
На деле - есть шумы, которые даже при хорошем приёме немного портят время импульсов, мешая их распознавать.

В процессе отладки потребовалось анализировать поток времён импульсов-пауз, из-за ограниченного размера памяти и необходимости быстрой отработки прерывания пришлось делить значения на степени двойки (стандартная операция деления - долго, но деление на 2-4-8 делается сдвигом на N бит вправо, так быстрее).
Оказалось, это удобно - сразу избавляемся от слишком мелких различий, и тогда анализ импульсов сводится к нескольким правилам:
- если пауза была больше некоторого значения A1 - новый импльс начинает минуту.
- если импульс был меньше некоторого значения A2 - это был 0
- если был больше A2, но меньше другого значения A3 - это была 1

Если поделить так миллисекунды на 2^6 ( >> 6) и добавлять к значениям символ 'A' - лог времен начинает напоминать ДНК-код (]BNBNDOBNDOBN), можно просто сравнивать по символам: B = 0, D = 1, >Z - начало минуты, можно читать глазами из лога.
А всё что не укладывается в эту схему - считать ошибками приема.

^BOBOBNDMBNDMDMBOBOBNDMDMBOBODMBNBNDMBNBNDMBNBOBNBNBNBNDMDMBNDMBNBODMBNBOB...

Это очень помогло при отладке, когда почему-то сигнал не принимается (видно в логе - потому что начинает идти шум типа ACBBCDJ - нарушены интервалы).

Биты набираются в 64-битный аккумулятор (в обратном порядке, но какая разница, так просто удобнее), после очередной паузы A1 аккумулятор переходит в текущие данные и начинается сбор новых битов.
И если за время предыдущего сбора явных ошибок не было - по данным строится текущее время, с учетом момента начала новой минуты (передается всегда время следующей минуты, с первым импульсом после паузы она и начинается).

Для ведения полученого времени использована та же библиотека JbTime, что в NTP-сервере, с микросекундами.
И такая же библиотека раздачи NTP JbNTP - прежде всего, чтобы можно было получать время и сравнивать его с другими источниками.

....
#define INTERRUPT_PIN 13

#define READ_PIN(pin) ((GPIP(pin) ? 1 : 0))

volatile byte int_pulse;                // счетчик импульсов
volatile uint32_t mark_time;            // отметка времени

volatile uint32_t start_second;         // отметка старта новой минуты
volatile bool set_second;               // флаг готовности установки минуты

volatile bool dcf_ok;                   // валидность текущая

volatile uint64_t dcf_data;             // данные для обработки
volatile uint64_t dcf_tmp;              // аккумулятор данных

// для отладки вспомогательное
volatile byte xlog[180];
volatile byte log_cnt;

// ---------------------------------------------------
void ICACHE_RAM_ATTR run_interrupt(){

  uint32_t tmp = micros();                        // отметили микросекунды начала

  uint32_t diff = millis() - mark_time;           // длительность предыдущей фазы
  mark_time = millis();                           // новая метка времени
  byte sym  = (byte)(diff >> 6) + 'A';            // уменьшаем до байта

  bool signal = READ_PIN(INTERRUPT_PIN);          // что там у нас?

  if(signal) {                                    // импульс

    if(sym >= 'Z'){                              // начало новой минуты

      start_second = tmp;

      dcf_data = dcf_tmp;                         // скидываем старый буфер
      dcf_tmp = 0;                                // очищаем буфер

      set_second = false;
      if(dcf_ok && int_pulse == 58)
        set_second = true;                        // прошлая секунда считана

      dcf_ok = true;                              // считаем ОК
      int_pulse = 0;                              // битовый счетчик
      log_cnt = 0;

    }
    else{
      int_pulse ++;
      // можно пробовать проверять на соответствие пар
      // корректные паузы - M,N,O, при этом правильнее BN, BO и DN, DM
      // но можно и не проверять
    }
  }else{

    if(sym == 'B'){
      // это 0
    }
    else if(sym == 'D' ){
      // это 1
      dcf_tmp |= (uint64_t)(0x1ULL << int_pulse );
    }
    else {
      // это мусор
      dcf_ok = false;
    }
  }

  // для отладки
  xlog[ log_cnt ] = sym;
  log_cnt ++;
  if(log_cnt > 170) log_cnt = 0;

}

// ---------------------------------------------------
void PulseSetup(){
  int_pulse = 0;
  mark_time = 0;
  dcf_data = 0;
  dcf_tmp = 0;
  set_second = false;
  dcf_ok = false;

  // для отладки
  log_cnt = 0;
  memset((void*)xlog,0,sizeof(xlog));

  pinMode(INTERRUPT_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN),run_interrupt,CHANGE);

}

byte dcf_weights[] = {1,2,4,8,10,20,40,80};
#define CEST_OFFSET 3600*2
#define CET_OFFSET  3600

#include <RTClib.h>

// ---------------------------------------
void PulseLoop(){

  if(set_second){

    // пример
    // 0100011100011000010011100110000001011000010100010101100100100000
    // 0000110111110100010010100000110001001000010100010001100100100000
    if(dcf_data & 1ULL) return;                 // must be 0
    if(! (dcf_data & (1ULL << 20)) ) return;    // must be 1


    bool sum = 0;
    // minute
    int minute = 0;
    for(int i = 21; i < 28; i++){
      if(dcf_data & (1ULL << i )){
        minute += dcf_weights[i - 21];
        sum = !sum;
      }
    }
    if((bool)(dcf_data & (1ULL << 28 )) != sum) return;
    // hour
    sum = 0;
    int hour = 0;
    for(int i = 29; i < 35; i++){
      if(dcf_data & (1ULL << i )){
        hour += dcf_weights[i - 29];
        sum = !sum;
      }
    }
    if((bool)(dcf_data & (1ULL << 35 )) != sum) return;
    // date
    sum = 0;
    int mday = 0;
    for(int i = 36; i < 42; i++){
      if(dcf_data & (1ULL << i )){
        mday += dcf_weights[i - 36];
        sum = !sum;
      }
    }
    int wday = 0;
    for(int i = 42; i < 45; i++){
      if(dcf_data & (1ULL << i )){
        wday += dcf_weights[i - 42];
        sum = !sum;
      }
    }
    int month = 0;
    for(int i = 45; i < 50; i++){
      if(dcf_data & (1ULL << i )){
        month += dcf_weights[i - 45];
        sum = !sum;
      }
    }
    int year = 2000;
    for(int i = 50; i < 58; i++){
      if(dcf_data & (1ULL << i )){
        year += dcf_weights[i - 50];
        sum = !sum;
      }
    }
    if((bool)(dcf_data & (1ULL << 58 )) != sum) return;

    DateTime now = DateTime(year, month, mday, hour, minute, 0);
    unsigned long dtm = now.unixtime();

    bool cest = dcf_data & (1ULL << 17);
    bool cet = dcf_data & (1ULL << 18);
    if (cest && !cet){
      dtm -= CEST_OFFSET;
    }
    else if (!cest && cet){
      dtm -= CET_OFFSET;
    }
    else return;

    uint32_t usec = micros() - start_second;

    systime.settime(dtm, usec);
    systime.fresh = true;

    if(systime.fresh){
      if(RTCSetTime(&systime)){
        systime.fresh = false;
      }
    }

    set_second = false;
  }
}
// ---------------------------------------
void setup(){
  ...

  PulseSetup();

  NTPSetup();

  ...
}

void loop(){
  ...

  PulseLoop();

  NTPLoop();

  ...
}

По такой же схеме как там - тут тоже подключен модуль RTC.
Вся разница в том, что единственным источником времени будет DCF77.

И вот - пробую результат:

/sbin/ntpdate -d 192.168.1.49
ntpdig: querying 192.168.1.49 (192.168.1.49)
org t1: ed92223c.60d05000 rec t2: ed92223c.9f7af640
xmt t3: ed92223c.9f7af640 dst t4: ed92223c.aecb0000
org t1: 1776788412.378179 rec t2: 1776788412.622970
xmt t3: 1776788412.622970 dst t4: 1776788412.682785
rec-org t21: 0.244792  xmt-dst t34: -0.059815
2026-04-21 19:20:12.622970 (+0300) +0.092488 +/- 0.152309 192.168.1.49 s1 no-leap

Неплохо (0.092488 - отклонение от ранее установленного), учитывая что время тут берется буквально из воздуха.
Можно запускать девайс в работу...

Но есть и минусы: например, обычная дрель-шуроповерт рядом сводит прибор с ума, индикатор моргает как ненормальный.
В общем, это нечто такое, что должно работать долго, неспешно, в деревне, постепенно синхронизируясь как бы само по себе.