Ловим время в формате DCF77

от автора

В комментариях к «серверу точного времени» (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/