Van valami a levegőben

Atomórák, rádiójelek és az időszinkronizálás

A korábbi óra építős projekt kapcsán arra jutottam, hogy egyszerűbben is meg lehetne ezt oldani. Úgyhogy vettem egy asztali órát.

Na jó, ez azért nem teljesen így történt, csak valahogy úgy hozta az élet, hogy egyszerre több idő-szerű dolog is foglalkoztat. Ez a kis projekt például arról szól, hogy vajon az Internet előtt az emberek hogyan juthattak hozzá a pontos időhöz?

Természetesen kinéztek az ablakon és leolvasták a pontos időt a templomtoronyról. Hála a modern technológiának, ezt mi is viszonylag egyszerűen meg tudjuk oldani. Csak ráirányítunk egy kamerát a templomtoronyra és mesterséges intelligencia segítségével leolvassuk a mutatók pozíciója alapján az időt.

Persze nem ennyire egyszerű a dolog. Valahogy meg kell állapítanunk azt is, hogy délelőtt vagy délután van és valószínűleg azt sem ártana megtudni, hogy mi a mai dátum. Arról nem is beszélve, ha valakinek a közelében épp nincsen egy templomtorony se.

Szemfüles olvasóknak feltűnhetett, hogy ehhez ugyan nem kellett volna egy asztali óra. Igaz, ami igaz, nem ebbe az irányba indultam el, nem szerettem volna ennyire visszamenni az időben. Valójában azon gondolkodtam el, hogy hogyan működhetnek a rádióvezérlésű órák.

Rádió időszinkron

Az egész egy adótoronnyal kezdődik. Mifelénk a DCF77 nevű, Németországban található torony által leadott jel lesz az, amit fogni tudunk, de van több is, ami a világ többi részét fedi le.

A DCF77 esetén a jelet egy atomóra generálja és a sugárzása 60 másodpercig tart. Másodpercenként egy bit érkezik. A jelsorozatot egy hosszabb szünet zárja le. Viszont elég nagy a zaj, úgyhogy előfordulhat, hogy a hosszabb szünet megérkezésekor nincs elég adatunk, vagy ami valószínűbb, hogy előbb lesz 59 bitnyi adatunk, minthogy a szünet jönne, így a vételi viszonyok függvényében elég sokáig eltarthat az idő szinkronizáció.

Már csak egy vevő készülékre van szükségünk. Ez általában egy ferritrudas antennával és némi elektronika segítségével történik meg. Lehet rendelni vevőt Kínából is, de nem volt kedvem egy hónapot várni, aztán még a postával is vacakolni, úgyhogy a leggyorsabb megoldásnak az tűnt, ha egy olcsó rádióvezérelt órát rendelek és "megvizsgálom" közelebbről.

Az áldozat

A korábban emlegetett óra belsejébe belenézve láthatjuk, hogy lent van egy különálló nyomtatott áramkör, alatta pedig egy ferritrudas antenna. Ezeket némi forrasztással el is távolítottam.

Az óra sikeresen túlélte a műtétet, minden ugyanúgy működik rajta, csak a (rádiójel) hallását vesztette el.

EMAX 6007 V1
GE16-1055R5
NEW GE13-887

A rádió vevőn lévő feliratok nem segítettek abban, hogy valami leírást találjak, de más hasonló lapkák alapján kikövetkeztettem, hogy a GND a földbe megy, a VCC-n 3,3 voltot szeretne kapni, a PON a teljes modult tudja ki-be kapcsolni (de nem kell bekötni sehova) és kizárásos alapon az NTCO-n jön az adat.

A vezetékek kaptak egy kis toldást, hogy legyen a végükön jumper csatlakozó a próbapaneles felhasználáshoz, aztán rákötöttem az egészet egy Pico-ra.

Szép is lett volna, ha elsőre sikerül. Néhány óra itt ráment arra, hogy kiderítsem miért nem jön semmi adat az antenna felől. Próbáltam bármi leírást keresni a modulhoz, kapcsolgatni a PON-t, különböző GPIO pin-eken rákötni a Pico-ra, gyanakodtam a kódra is, de végül az lett a megoldás, hogy átkötöttem a modult egy dedikált áramforrásra és a Pico csak az adat jelet kapta meg. Valószínűleg a Pico nem tudott elég áramot adni az eszköznek.

Bitek a zajban

Jön valamiféle adat, valamit kezdeni is kellene vele. Először csak a Pico-n található LED-et kezdtem el villogtatni, hogy legyen visszajelzés a történésekről.

#include "pico/stdlib.h"

#define DCF_PIN 16
#define LED_PIN 25

void on_change(uint gpio, uint32_t event_mask) {
  gpio_put(LED_PIN, event_mask & GPIO_IRQ_EDGE_RISE);
}

int main() {
  gpio_init(DCF_PIN);
  gpio_init(LED_PIN);

  gpio_set_dir(DCF_PIN, GPIO_IN);
  gpio_set_dir(LED_PIN, GPIO_OUT);

  gpio_set_irq_enabled_with_callback(
    DCF_PIN,
    GPIO_IRQ_EDGE_FALL | GPIO_IRQ_EDGE_RISE,
    true,
    &on_change
  );

  while (true) {
    sleep_ms(1000);
  }
}

A következő lépés, hogy elkezdjük mérni, hogy milyen sokáig van magas és alacsony állapotban a jel. A dokumentáció szerint 0 érkezik, ha 100 ezredmásodpercig magas a jel, 1 érkezik, ha 200 ezredmásodpercig magas a jel. Mivel másodpercenként egy bit jön, ezért két magas állapot között kell lennie 800-900 ezredemásodpercnyi alacsony állapotnak. Az utolsó másodpercben nem jön adat, úgyhogy ott van egy 1800-1900 ezredmásodperces alacsony állapot.

Először is definiálunk néhány konstans értéket a zajszűréshez, annak megállapításához, hogy 0 vagy 1 jött és az adat végének detektálásához.

#define MINIMAL_HIGH_PULSE_WIDTH 50
#define MINIMAL_LOW_PULSE_WIDTH 700
#define PULSE_WIDTH_THRESHOLD 150
#define END_OF_DATA_PULSE_WIDTH 1500

Aztán néhány változó is kelleni fog, amiben az előző állapotváltozások időpontját és az eddig megérkezett adatot tároljuk.

uint32_t rise_time = 0;
uint32_t fall_time = 0;

uint64_t buffer = 0;
uint32_t buffer_position = 0;

Ezek után már csak az on_change belsejét kell megírni. Először is lekérjük, hogy a Pico elindítása óta hány ezredmásodperc telt el.

uint32_t now = to_ms_since_boot(get_absolute_time());

Aztán csinálunk egy kis zaj szűrést, különben szinte lehetetlen lenne megkapni az adatot.

if (now - fall_time < MINIMAL_LOW_PULSE_WIDTH) {
  return;
}

if (now - rise_time < MINIMAL_HIGH_PULSE_WIDTH) {
  return;
}

Ha alacsonyról magasra váltott a jel, akkor megnézzük, hogy elég hosszú volt-e az alacsony jel ahhoz, hogy az adat végét jelentse. Ha ezen a ponton kaptunk 59 bitnyi adatot, akkor minden oké, ha nem, akkor kezdjük előről az egészet.

if (event_mask & GPIO_IRQ_EDGE_RISE) {
  rise_time = now;

  if (rise_time - fall_time > END_OF_DATA_PULSE_WIDTH) {
    if (buffer_position == 59) {
      printf(" - data received: %lld\n", buffer);
    }
    else {
      printf(" - reset: not enough data\n");
    }
    buffer = buffer_position = 0;
  }
}

Ha magasról alacsonyra váltott a jel, akkor a jel hossza alapján eldöntjük, hogy 0-t vagy 1-et kaptunk és eltesszük a kapott értékünket a buffer-be. Itt előfordulhat, hogy a kelleténél több adatunk van (a zaj miatt), ha ez a helyzet, akkor előről kezdjük az egészet.

else if (event_mask & GPIO_IRQ_EDGE_FALL) {
  fall_time = now;

  uint64_t next_bit = fall_time - rise_time > PULSE_WIDTH_THRESHOLD ? 1 : 0;

  printf("%lld", next_bit);

  buffer |= next_bit << buffer_position;
  ++buffer_position;

  if (buffer_position > 59) {
    printf(" - reset: too much data\n");
    buffer = buffer_position = 0;
  }
}

Ha minden jól ment, a végén lesz egy adatsorozatunk, ami remélhetőleg az aktuális pontos időt tartalmazza.

Szépen lassan csordogál az adat...

Menjünk biztosra

Többször volt említve a zaj, ami elég nagy probléma tud lenni. Abban a szobában, ahol az asztali gépem és a szerverek vannak nem is sikerült használható adatot kinyerni. Egy laptoppal költöztem át egy másik szobába, hogy ki tudjam próbálni a kódot. Napközben ott is sok volt a zaj és kellett egy fél óra is akár, hogy megkapjam a pontos időt, de este szinte minden percben sikeresen átért az adat.

Szóval van egy adag bitünk, de nem tudhatjuk, hogy attól, hogy mi azt gondoltuk, hogy 1-es értéket kaptunk, a másik oldal tényleg 1-est küldött-e. Ennek ellenőrzésére van az adatban három darab paritás bit, ami 0, ha az előtte lévő adatban páros számú 1-es van és 1, ha páratlan. Először is nézzük meg, hogy hogyan számolunk paritást egy tetszőleges int-re:

int parity(int num) {
  num ^= num >> 16;
  num ^= num >> 8;
  num ^= num >> 4;
  num ^= num >> 2;
  num ^= num >> 1;
  return num & 1;
}

A részletekbe nem mennék bele, a Stack Overflow oldalon, ahonnan loptam a kódot remek magyarázat van hozzá. Ezen kívül tudnunk kell, hogy melyek a paritás bitek és milyen adatokra számolódtak ki. Ezt a kapcsolódó Wikipédia oldalon meg tudjuk nézni. Például a perc esetén:

int min_data = (int) ((buffer >> 21) & 0b1111111);
int min_parity = (int) ((buffer >> 28) & 1);

if (parity(min_data) != min_parity) {
  printf("invalid parity for minute\n");
}

A buffer-ünket eltoljuk jobbra 21 bittel (gyakorlatilag kidobjuk az első 21 bitet), mert a 22. bittől kezdődik a perchez tartozó adat és vesszük az első 7 bitet (& 0b1111111), mert addig tart a perc.

A paritáshoz az első 28 bitet dobjuk ki és a maradék adatból 1 bitet tartunk csak meg. Az általunk kiszámolt paritásnak egyeznie kell az így kapott adattal.

Az órát és a dátumot hasonló módon ellenőrizzük, csak a jobbra eltolások száma és az utána megtartott adatok mennyisége változik.

int hour_data = (int) ((buffer >> 29) & 0b111111);
int hour_parity = (int) ((buffer >> 35) & 1);

if (parity(hour_data) != hour_parity) {
  printf("invalid parity for hour\n");
}

int date_data = (int) ((buffer >> 36) & 0b1111111111111111111111);
int date_parity = (int) ((buffer >> 58) & 1);

if (parity(date_data) != date_parity) {
  printf("invalid parity for date\n");
}

Itt az idő

Ha átment a buffer az ellenőrzéseken, akkor már csak ki kell nyernünk belőle az adatokat és be kell állítanunk a Pico-n a pontos időt. Először nézzük meg itt is a percet.

int min = (int) ((buffer >> 21) & 0b1111111);
min = (min >> 4) * 10 + (min & 0b1111);

Az adat kinyerése ugyanaz, mint a paritás esetén, de mivel az adat binárisan kódolt decimálisként van ábrázolva, ezért van vele még egy kis extra dolgunk (az első négy bit az első számjegy, a második négy bit (ami csak három) a második számjegy).

A maradék adatot hasonlóan kaphatjuk meg, a hét napja (dow) esetén a vasárnap 7-esként jön és a Pico azt 07-ként szeretné megkapni, valamint az év esetén hozzá kell adnunk 2000`-et az értékhez, mert az évszám utolsó két számjegyét kapjuk csak meg.

int hour = (int) ((buffer >> 29) & 0b111111);
hour = (hour >> 4) * 10 + (hour & 0b1111);

int dom = (int) ((buffer >> 36) & 0b111111);
dom = (dom >> 4) * 10 + (dom & 0b1111);

int dow = (int) ((buffer >> 42) & 0b111);
if (dow == 7) {
  dow = 0;
}

int month = (int) ((buffer >> 45) & 0b11111);
month = (month >> 4) * 10 + (month & 0b1111);

int year = (int) ((buffer >> 50) & 0b11111111);
year = 2000 + (year >> 4) * 10 + (year & 0b1111);

Már csak meg kell mondani a Pico RTC moduljának, hogy mennyi a pontos idő.

rtc_init();

datetime_t t = {
  .year = (int16_t) year,
  .month = (int8_t) month,
  .day = (int8_t) dom,
  .hour = (int8_t) hour,
  .min = (int8_t) min,
  .sec = 0,
  .dotw = (int8_t) dow,
};

rtc_set_datetime(&t);

És ezzel kész is vagyunk, Internet nélkül sikerült megkapnunk a pontos időt.

A másik irány

Maradt egy szegény, szerencsétlen óránk, aki most nem tudja magát szinkronizálni, mert elvettük tőle a rádió modult. Aztán kedves kollégám, potato bedobta az ötletet, hogy mi lenne, ha adnánk neki egy kamu jelet, úgyhogy ismét azon kaptam magam, hogy csavarozom szét az órát és a régi modul helyére forrasztottam pár jumper kábelt. Először csak a GND és az NTCO helyére, de később bekötöttem a PON-t is.

Összekötöttem a Pico-val és elkezdtem neki jelet küldeni, de nem annyira tetszett az órának.

Először arra gyanakodtam, hogy a PON hiánya okozza a problémát, hogy akkor kap jelet az óra, amikor nem számít rá, úgyhogy bekötöttem azt is, de nem lett jobb a villogás. Aztán a forrasztásra gyanakodtam, hogy véletlenül rövidre zárhattam valamit, de néhány perc nagyítóval vizsgálgatás alapján minden jónak tűnt.

Végül az lett gyanús, hogy a Pico 3,3 voltot ad ki magából, az óra pedig csak 3 voltról megy, hátha túl sok neki a 3,3 volt. Előtúrtam pár ellenállást egy dobozból, de nem sikerült olyat találni, ami egy az egyben megoldja a problémát. Egy ellenállás bekötése után javult a helyzet, kettő után megjavulni látszott, úgyhogy biztonság kedvéért hármat kötöttem be végül.

Már csak egy kis kód kell hozzá. Egy korábban felvett valós adatot próbáltam meg visszajátszani, amit percenként ismételtem, de az óra sehogy sem akarta az igazságot. Belefutottam néhány bug-ba, hogy rossz adatot küldtem ki, de ezek javítása után sem akart még működni. Állítgattam egy kicsit az időzítésen, hátha túl pontos neki az, ahogy küldöm, de semmi. Végül az lett a megoldás, hogy az óra biztosra akar menni és egy adatsorozat kevés neki. Egymás után két sikeres adatsorozatra van szükség ahhoz, hogy beállítsa magát.

#include <stdio.h>

#include "pico/stdlib.h"

#define DCF_SIGNAL_PIN 12
#define DCF_ENABLED_PIN 13
#define LED_PIN 25

int main() {
  stdio_init_all();

  gpio_init(DCF_SIGNAL_PIN);
  gpio_init(DCF_ENABLED_PIN);
  gpio_init(LED_PIN);

  gpio_set_dir(DCF_SIGNAL_PIN, GPIO_OUT);
  gpio_set_dir(DCF_ENABLED_PIN, GPIO_IN);
  gpio_set_dir(LED_PIN, GPIO_OUT);

  uint64_t buffers[] = {
    //-----PYYYYYYYYMMMMMWWWDDDDDDPHHHHHHPmmmmmmm1AZZARxxxxxxxxxxxxxx0
    0b0000000010010000001111100001001011100000000101000010100001000100,
    0b0000000010010000001111100001001011110000001101000010100001000100,
    0b0000000010010000001111100001001011110000010101000010100001000100,
    0b0000000010010000001111100001001011100000011101000010100001000100,
    0b0000000010010000001111100001001011110000100101000010100001000100,
    0b0000000010010000001111100001001011100000101101000010100001000100,
  };
  int buffer_idx = 0;

  while (true) {
    if (gpio_get(DCF_ENABLED_PIN)) {
      printf("dcf module is not enabled\n");
      sleep_ms(5000);
      continue;
    }

    uint64_t b = buffers[buffer_idx];
    ++buffer_idx;

    int length;
    for (int i = 0; i < 59; ++i) {
      length = b & 1 ? 200: 100;
      printf(b & 1 ? "1" : "0");

      gpio_put(LED_PIN, true);
      gpio_put(DCF_SIGNAL_PIN, true);
      sleep_ms(length);

      gpio_put(LED_PIN, false);
      gpio_put(DCF_SIGNAL_PIN, false);
      sleep_ms(1000 - length);

      b >>= 1;
    }
    printf("\n");

    sleep_ms(1000);
  }
}

Nem teljesítettem túl, az már egyszer biztos. Csak be vannak égetve az adatsorok, de így egy csomó konvertálgatástól megkíméltem magam. Lényeg, hogy az óra a bekapcsolás után elkezd szinkronizálni és néhány perc múlva beállítja a kapott "pontos" időt. A Pico-n lévő LED pedig eközben a jel ütemére villog.

Ezzel azt hiszem a végére is értünk kis kalandunknak, kiaknáztunk majdnem minden szórakozási lehetőséget, amit egy olcsó rádió időszinkronos óra nyújthat. Van még benne egy hőmérséklet szenzor, egy Piezo hangjelző és egy LED háttérvilágítás is, a vállalkozó szelleműeknek.

This post is also available in english: Something's in the air

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre vagy irány a bejegyzéshez tartozó tweet.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha a régi jó dolgokat kedveled, de követheted a blogot Twitteren is.