В комментариях к «серверу точного времени» (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.49ntpdig: querying 192.168.1.49 (192.168.1.49)org t1: ed92223c.60d05000 rec t2: ed92223c.9f7af640xmt t3: ed92223c.9f7af640 dst t4: ed92223c.aecb0000org t1: 1776788412.378179 rec t2: 1776788412.622970xmt t3: 1776788412.622970 dst t4: 1776788412.682785rec-org t21: 0.244792 xmt-dst t34: -0.0598152026-04-21 19:20:12.622970 (+0300) +0.092488 +/- 0.152309 192.168.1.49 s1 no-leap
Неплохо (0.092488 — отклонение от ранее установленного), учитывая что время тут берется буквально из воздуха.
Можно запускать девайс в работу…
Но есть и минусы: например, обычная дрель-шуроповерт рядом сводит прибор с ума, индикатор моргает как ненормальный.
В общем, это нечто такое, что должно работать долго, неспешно, в деревне, постепенно синхронизируясь как бы само по себе.
ссылка на оригинал статьи https://habr.com/ru/articles/1026588/