YaRC

Yet another Radio Clock


One more radio clock

For no particular reason except my own entertainment, I've started to build yet another radio clock. I don't keep up with counting them, so I don't know how many of them I've built before.

A few that I remember of:

  • A kit named "DOC 85" - basically just assembled it
  • An early homemade radio clock based on a TCA whatever AM receiver chip and an 8051 MCU for decoding and display control. Coding was done in pure 8051 assembler.
  • Used an IR transmitter to push the demodulated DCF77 pulses into the IRDA port of a laptop, running ntpd to decode it.
  • Wrote and embedded an DCF decoder into a measurement instrument, using an external ready made DCF77 receiver module. The MCU used for decoding was a NEC V25 (8086 compatible), coded in Turbo Pascal
  • Wrote and embedded another DCF / high precision PPS decoder into another measurement instrument, this time using an ADSP-2181 DSP. Coding was ADSP-2181 assembler and/or embedded C
  • Ported the above decoder to yet another instrument, using a Hitachi SH2 processor. Coded in pure embedded C
  • An attempt to make a noise / disturbance tolerant radio clock using an Infineon XE167 MCU.
  • Implemented a dual channel DCF / PPS receiver to an STM32F7 nucleo board. This one provides time and date to my home CAN network.

  • Note, these are all DC77 (German / European time standard) radio clocks. Most of the above is lost or not available for public disclosure. Another common "feature" of these radio clocks is: All of them use the DCF77 AM (amplitude modulation) time code only. This is pretty common for most commercially available radio clocks.

    So let's go for something completely different

    The DCF77 doesn't transmit the time code amplitude modulated (AM) alone, but also on PM (phase modulation). See the DCF77 Wikipedia article for a detailed description. The PM receiving method provides one with superiour time accuracy in comparison with the usual AM receivers, though it's not as accurate as an GPS timing receiver.

    So the goal for is set: Build a Phase Modulation DCF77 radio clock with some robustness in time code decoding. Additionally, this radio clock will provide a 10MHz reference frequency and a PPS (pulse per second) output.

    Amplitude modulation time code

    It's well-known and widely in use: DCF77 transmits the time code using the AM (amplitude modulation) method. Each second (except for :59) the amplitude of the transmitted signal is reduced for either 100ms or 200ms, representing a binary "0" or "1". Due to the low data rate (one Bit per second, one complete timecode telegram per minute) and simple modulation scheme, this is easy to receive, and one can build real low power receivers, resulting in these ubiquitous battery operated radio clocks.

    One of the drawbacks of these simple radio clocks using the AM timecode: Although they are quite sensitive, allowing for reception in a wide range (up to 2000km from Frankfurt), they require a rather clean signal. Placing one of these in an electromagnetic distorted environment (like an electronics lab) or inside a larger concrete building (using steel reinforcement) may render the clock useless.

    The timing accuracy achievable using the AM time code typically is in the 20ms ballpark. For simple radio clocks, anything better than 200ms is called a "very accurate atomic clock".

    Phase modulation time code

    Not so widely-known and less often used with actual radio clocks is the PM time code. DCF77 uses a phase-modulation scheme to transmit the same time code in a different way, concurrently with the AM time code. To achieve better accuracy and noise tolerance, one time code bit is encoded as a sequence of 512 "random" bits. The sequence isn't random at all, but well known and predictable though it looks random. So this is why it's called "Pseudo Random Binary Sequence" (PRBS) or "Pseudozufallsfolge" (PZF). The term PZF is quite often used in the literature regarding DCF77.

    Reception of the PM / PZF time code is done by cross-correlating the known sequence against the phase demodulated receiver signal.

    From other results, one expects a timing accuracy better than 100us, depending on location and signal quality down to 10us receiving the PM / PZF time code.

    DCF77 frequency standard

    The DCF77 carrier frequency is highly stable and precise, so phase locking a local oscillator to the received carrier signal should result in a pretty good frequency standard. One can expect real "atomic clock" standard for long term stability, since DCF77 is disciplined to national and international timing standards, and short term stability from 10e-8 to 10e-10, depending on the receiver and signal quality.

    So let's go for it

    The antenna

    What's the purpose of that ferrite rod sitting in the junk box? As so often with this kind of stuff, it just happens to be there. Now can I do something useful with it?
    ferrite rod

    Yes, I can. Make an DCF77 antenna. Use isolated wire and apply an arbitrary amount of turns to the rod - the more the better. Now add some capacitance to create a resonant tank and tune the whole thing to the DCF77 carrier frequency (77.5kHz). Add a few turns in a separate winding to tap the received signal:
    ferrite rod antenna
    Using an oscilloscope with a highly sensitive input, one can see now the received DCF77 carrier.
    DCF77 carrier received by the antenna
    By rotating the antenna rod, one can evaluate the directivity of this construction. If one uses such kind of antenna to take a bearing of the transmitter, one rotates the antenna to minimum signal level. The transmitter is now aligned to the longitudinal direction of the antenna.

    Some gain required

    As one can see, the output level is rather low. An amplifier is required. Nothing spectacular in this case, just a chain of three OP-Amps, each one having a gain of 15 (23dB), resulting in a total gain of 3375 (70dB). Depending on the size of the antenna / reception conditions / distance to the transmitter, this might require some adjustment later on. There's a last stage in the amplifier, creating a 180 phase shifted (inverted) signal to feed the mixer with a differential signal:
    amplifier

    Mix it down to DC

    Now feed a synchronous rectifier with that signal. In fact, use two synchronous rectifiers, one in-phase and one quadrature. As these mixers are implemented using analog switches that are reversing the polarity of the signal, I'd rather call them rectifiers than mixers. Although your typical diode ring mixer works the same way and is called a mixer.
    amplifier
    The mixer requires two LO signals: Both of them at the 77.5kHz carrier frequency, but having a 90 phase shift. I'm using a CPLD to create these signals from the 10MHz master clock.

    Low pass filter

    The rectifiers output must be converted to a single ended signal and one wants to apply a bit of low pass filtering. A final OP-Amp stage takes care of this:
    low pass filter

    That's all she wrote. At least for the analog signal processing part.

    See the complete analog schematic page here.

    Clock Generation

    The receiver requires an accurate 77.5kHz I/Q clock frequency to demodulate the signal. A commonly used reference frequency for all kind of purposes is 10MHz. So I decided to create all the required frequencies from a single 10MHz source. Your favourite calculator tells there's no even divider ratio (not even an odd one) from 10MHz to 77.5kHz. To create even more difficulties, usually one uses four times the carrier frequency (say 310kHz) as an input to a divide-by-four circuit to create the in-phase and quadrature clocks. 10M divided by 310k results in 32.25806452. By chance, this equals 1000/31 or 32 8/31.

    Dividing 10MHz by 32 results in 312.5kHz. 310kHz / 312.5kHz gives an (124/125)/32 ratio (which suprisingly equals 1000/31) required to get 310kHz from 10MHz. So one can use an divide-by-32 circuit and skip some pulses (one out of 125) to make up that division ratio. It's all implemented in a few lines of VHDL code and runs within a small CPLD.

    See the clock generator schematic page here.

    Digital Signal Processing

    No, not a fancy Digital Signal Processor here, but a Cortex-M4 based MCU. For lazyness, I've used a STM Nucleo-32 Module. Due to the memory (especially large RAM buffers) requirements of the demodulator and decoder, this turned out to an STM32L432 based module.

    See the MCU schematic page here.
    And the whole shebang: DCF77 radio clock prototype schematic.

    The Prototype

    The above resulted in this prototype, built using some modules on a perfboard:
    DCF77 PM receiver prototype

    Gain some track

    To get an idea of how the whole thing is intended to work, I've created a block diagram:
    block diagram of AM/PM DCF77 receiver
    Click the image for a higher resolution version (works for the other images too).

    Another block diagram, showing the details of the CPLD clock generation logic:
    clock generation
    While drawing the latter diagram, I noticed the divider chain for generating a slighly offset carrier frequency beeing unnecessarily complex. A simple 125/126 clock pulse skip would have done the job, not that cascaded 125/126 and 124/125 logic that I struggle to understand now by myself. That's one of the downsides of the modern stuff, one can easily pin down some VHDL code that works and fulfils the job, but is too complex from a hardware view.

    Anyway, it works, and its purpose is as said to generate a slightly offset 77.5kHz LO frequency. This is required for the ADC auto offset calibration routine. The demodulator outputs a small DC offset that must be subtracted from the signal to make the phase detection work at low signal levels. As the demodulator is intended to mix down the signal to DC, there's no easy way to discriminate the signal from the offset. Slightly changing the LO frequency does the trick then. By doing so, the demodulators output a low frequency AC signal that gets averaged over some periods. By averaging, the offset (still the DC part of the signal) is measured seperately from the signal (now the LF AC signal) and is used to compensate for the offset error while the LO is running at its nominal frequency. Some means to not mess up the LO phase isn't shown in the block diagram. The automatic offset compensation is run once a minute.

    The Simulator

    Debugging and testing the DCF77 time code decoder firmware required a means to generate arbitrary time codes. Especially the code transitions at DST / regular time switchover and leap second insertion were kind of interesting. Say - it took a significant amount of effort to get these work seamlessly.

    To simulate arbitrary time codes, I've put together a simple DCF77 transmitter. Its output signal is directly coupled to the ferrite rod antenna by applying a third winding to it:
    DCF77 AM/PM transmitter simulator

    I've used just another STM32 nucleo board to make up the simulator. As the on-board 8MHz crystal isn't adjusted at all, it's deviation is too large for the receiver PLL to lock. So I've used the 10MHz output from the receiver to clock the simulator. Two of the Timers are used to create 90 phase shifted PWM signals representing the I and Q of the AM/PM modulated carrier. A simple resonant tank takes care of filtering the ugly PWM to a (more or less) nice sine wave. That signal is tapped from the transformer, attenuated and coupled into the receiver:
    DCF77 AM/PM transmitter simulator

    The time code encoder uses the very same C code as the reference time code encoder of the receiver, so once the simulator is verified to produce the correct codes, the receiver is believed to use the correct time code to correlate against the received bits.

    Progress

    I've added some gizmos to the orignal schematic and created a layout. Ordered some PCBs:
    DCF77 PM receiver PCB top view   DCF77 PM receiver PCB bottom view
    KiCAD project is provided here for your convenience.
    Created from that, the schematic as a PDF.

    The PCBs were produced and delivered. So I did assemble them and continued development.
    DCF77 PM receiver PCB   DCF77 PM receiver PCB   DCF77 PM receiver PCB

    As you can see, I'm still using the modules, mostly because they're easy to use and I'm too lazy to replicate their contents into my own schematic and layout.

    Found an old enclosure in the junk box and used it for the radio clock:
    DCF77 PM receiver   DCF77 PM receiver

    Mounting the antenna on top of that aluminium enclosure resulted in a slight change of its inductance, so I had to re-tune the antenna.

    I've added a GPS receiver module and some line transmitters, transmitting the GPS PPS and DCF77 encoded PPS from my receiver to somewhere else, using the red ethernet cable. You can see these components on the left hand side. The yellow wire routes the GPS PPS to a capture input, using this to compare the received DCF77 time to the GPS time.

    Firmware

    As one might figure from the block diagrams, most of the signal processing is done within the uC firmware.

    For your reference:
    rcvr.c - the main receiver program code
    rcvr.h - the main receiver header
    dcf-code.h - some code common to the receiver and the simulator

    The general structure resembles a DMA TC (transfer complete) interrupt routine and a big busy loop. The DMA TC runs at the ADC sampling frequency (3875Hz) and requires higher priority than the other interrupts in the system. The main loop is a function call, that is to be called from the main program in a regular manner. Depending on flags set by the DMA interrupt, different tasks are run from here at normal priority.

    DMA interrupt

    
    void DMA1_Channel1_IRQHandler(void)
    ...
      RESET_P1;
      if (bufp==pll_ph) SET_P1;
      if (s2pf>0) {SET_P2; s2pf=0; };
      if (s2pf<0) {RESET_P2; s2pf=0; };
    
      if (skip_c>0) {
    	TIM1->ARR=78;
    	skip_c--; skip_i++;
      } else  if (skip_c<0) {
    	TIM1->ARR=80;
    	skip_c++; skip_i--;
      } else {
        TIM1->ARR=79;
      };
    
    At the very start of that routine the PPS pulse (P1) and the DCF77 encoded pulse (P2) is output. This needs to be done here to achieve low jitter and lag. bufp cycles from 0 to 3874 all the time, and several other variables (like pll_ph) are used to set the sampling points for various actions. The next few lines adjust the sampling clock phase by skipping or adding a single count to the TIM1 sampling clock divider. These steps are requested by the PM cross-correlation routine.

    
      sum_i += (ad_buf[0]-32768);
      sum_q += (ad_buf[1]-32768);
      ad_i = (ad_buf[0]-ofs_i_z);
      ad_q = (ad_buf[1]-ofs_q_z);
    ...
      {
      signed long re, im;
      re=ad_i*r_i+ad_q*r_q;
      im=ad_q*r_i-ad_i*r_q;
    ...
      buf_i[bufp] = ad_i;
      buf_q[bufp] = ad_q;
      };
    
    Next, the ADC samples are read from the DMA buffer. The samples are summed up to get the DC component, required for the carrier PLL. Then a coordinate transformation to get the I and Q data stream takes place. These samples undergo some downsampling (by simply calculating an sliding window average) and are stored in the receive buffers.

    
      dc_i=0; i=155;
      cp=dcf_am+154;
      ip=&buf_i[bufp];
      while (--i) {
        dc_i += *ip * *cp;
        ip-=25; cp--;
        if (ip < buf_i) ip+=SMP_1SEC;
      };
    ...
      ph_q=0; i=512/4;
      cp=dcf_pm+511; qp=&buf_q[bufp];
      while (--i) {
        ph_q += *qp * *cp;
        qp-=6; cp--;
        if (qp < buf_q) qp+=SMP_1SEC;
        ph_q += *qp * *cp;
        qp-=6; cp--;
        if (qp < buf_q) qp+=SMP_1SEC;
        ph_q += *qp * *cp;
        qp-=6; cp--;
        if (qp < buf_q) qp+=SMP_1SEC;
        ph_q += *qp * *cp;
        qp-=6; cp--;
        if (qp < buf_q) qp+=SMP_1SEC;
      };
    ...
      if (abs(ph_q)>ph_max) {
        ph_max=abs(ph_q);
        ph_ampl1=ph_q;
        ph_pos1=bufp;
      };
    
      if (abs(dc_i)>dc_max) {
        dc_max=dc_i;
        dc_ampl1=dc_i;
        dc_pos1=bufp;
      };
    
    These are the cross-correlations for AM and PM reception. The PM correlation loop is partially unrolled to increase the speed. Peak value detection indicates the phase within one second, the result is stored for later processing. There's more code following, in particular the correlation results of one sample before PM maximum and one sample afterwards are also stored.

    
      if (bufp==pll_ph) {
        rxtm++;
        rs_i = sum_i; sum_i = 0;
        rs_q = sum_q; sum_q = 0;
        pll_f=2; 
    ...    
      };
      if (bufp==pll_p) {
        rs_i = sum_i; sum_i = 0;
        rs_q = sum_q; sum_q = 0;
        pll_f=1;
        pll_p+=(SMP_1SEC/5);
        if (pll_p>=SMP_1SEC) pll_p-=SMP_1SEC;
        l_bufp=bufp;
        switch (ofs_run) {
        case 2: RESET_FSEL; // 77504.961Hz
                ofs_run++; ofs_c=1;
                break;
        case 3: RESET_FSEL; // 77504.961Hz
                ofs_run++; ofs_c=1;
                break;
        case 4: RESET_FSEL; // 77504.961Hz
                ofs_run++; ofs_c=1;
                break;
        case 5: SET_FSEL; // 77500Hz
                ofs_run++; ofs_c=0;
                break;
        };
      };
    
    This is synchronisation stuff. pll_ph represents the start of a second and is signaled to the main loop by setting pll_f to 2. The next part (synchronized by pll_p) gets executed every 200ms. As a result, the variables rs_i, rs_q contain the DC component required by the carried PLL. Otherwise, the LO frequency switching required for the automatic offset adjustment is done here.

    Main loop

    
    void do_rcvr(void)
    {
      pll();
      sec_am();
      sec_pm();
    ...
      if (done_am==1) {
        done_am=0;
        sync_min(amv, amts_p, 0, &am_fm);
        adv_fm_sec(&am_fm);
        find_min(amv, amts_p, 0, &am_fm);
        find_hrs(amv, amts_p, 0, &am_fm);
        find_year(amv, amts_p, 0, &am_fm);
        find_mday(amv, amts_p, 0, &am_fm);
        find_mon(amv, amts_p, 0, &am_fm);
        find_flags(amv, amts_p, 0, &am_fm);
        verify_code(amv, amts_p, 0, &am_fm);
      };
    
      if (done_pm==1) {
        done_pm=0;
        sync_min(pmv, pmts_p, 1, &pm_fm);
        adv_fm_sec(&pm_fm);
        find_min(pmv, pmts_p, 1, &pm_fm);
        find_hrs(pmv, pmts_p, 1, &pm_fm);
        find_year(pmv, pmts_p, 1, &pm_fm);
        find_mday(pmv, pmts_p, 1, &pm_fm);
        find_mon(pmv, pmts_p, 1, &pm_fm);
        find_flags(pmv, pmts_p, 1, &pm_fm);
        verify_code(pmv, pmts_p, 1, &pm_fm);
      }
    ...
      sync_phase();
      tx_code();
    
      if (sec_f) {
        if (l_am==RX_BIT_S) flush_sync(&am_fm, 0);
        do_sec(&am_fm, enc_code_am, vc_am, 0);
        if (l_pm==RX_BIT_S) flush_sync(&pm_fm, 1);
        do_sec(&pm_fm, enc_code_pm, vc_pm, 1);
    ...   
        rc_sec++;
        sec_f=0;
      };
    }
    
    A bunch of functions gets called from here. In particular:

    PLL

    
    void pll(void)
    {
      float s_i, s_q, f;
      int i,j;
    
      if (pll_f) {
    ...
        if (pll_f==2) ampl_p=0;
    
        if (ofs_sm == 1) {
          ofs_run=1;
          if (pll_ph>=SMP_1SEC) ofs_run=2;
        };
        if ((ofs_sm>200) && (pm_fm.m_sec==ofs_sec)) {
          ofs_sm=0;
          ofs_sec++;
          if (ofs_sec>7) ofs_sec=0;
        };
        ofs_sm++; if (ofs_sm>=402) ofs_sm=0;
    
        s_i = rs_i * (1.0f / SMP_200MS);
        s_q = rs_q * (1.0f / SMP_200MS);
        valid_rx=1;
    
        if (ofs_run==2) {
          ofs_s_i=0.0f; ofs_s_q=0.0f;
        };
        if ((ofs_run==4) || (ofs_run==5) || (ofs_run==6)) {
          ofs_s_i+=s_i;
          ofs_s_q+=s_q;
          valid_rx=0;
        };
        if (ofs_run==6) {
          ofs_i=(1.0f/12.0f) * (ofs_i*9.0f + ofs_s_i);
          ofs_q=(1.0f/12.0f) * (ofs_q*9.0f + ofs_s_q);
          ofs_run=0;
          ofs_i_z = 32768 + (int)ofs_i;
          ofs_q_z = 32768 + (int)ofs_q;
          if (LOG_OFS) printf("ofs: %5d %5d ", ofs_i_z, ofs_q_z);
        };
        s_q -= ofs_q;
        s_i -= ofs_i;
    
        if (valid_rx) {
          ampl[ampl_p]=sqrtf((s_i*s_i)+(s_q*s_q));
          iq_i[ampl_p] = s_i;
          iq_q[ampl_p] = s_q;
        };
        s_i = iq_i[0] + iq_i[1] + iq_i[2] + iq_i[3] + iq_i[4];
        s_q = iq_q[0] + iq_q[1] + iq_q[2] + iq_q[3] + iq_q[4];
    ...
        phase = (atan2f(s_q, s_i)) * (180.0f/PI);
    ...
        angv = (phase - phase2); // phase2 is phase( -1 sec )
    ...
        f=phase * (PI/180.0f);
        r_i = cosf(f)*32767.0f;
        r_q = sinf(f)*32767.0f;
    ...
        tim_ccri += -5.0f   * angv;
        tim_ccri += -0.2f * phase * anga;
        tim_ccrp  = -50.0f * phase * anga;
    ...
        if (fabsf(phase)<5.0f) {
          anga=anga*0.9985f;
          if (anga<1.0f) anga=1.0f;
        };
    ...
        // add some dither to tim_ccr
        tim_ccr = tim_ccri + tim_ccrd + tim_ccrp;
    ...    
        TIM2->CCR2=tim_ccr/1000;
    ...
        if (ampl_p==0) {
          s_i = (ampl[0]+ampl[1]+ampl[2]+ampl[3]+ampl[4]) / 200.0f;
          rssi = 20.0f*log10f(s_i) - 90.0f;
          sec_f=1; 
    ...      
        };
        ampl_p++; if (ampl_p>=5) ampl_p=0;
        pll_f=0;
      };
    }
    
    This routine is run 5 times per second and calculates the carrier phase from the received I/Q DC components and runs a control loop, steering the TCXO through a PWM DAC using TIM2. Once in a minute, the LO frequency is requested to switch to a slightly offset value, using the averaged samplings results to cancel the demodulator / ADC offset error.

    Seconds decoding and phase alignment

    
    void sec_pm(void)
    {
      int i,j,k;
      signed short n_pos;
      if (secf_pm) {
        secf_pm=0;
        n_pos=ph_pos;
    
        pmv[pmts_p] = RX_BIT_X;
        k = histo(&pm_histo, n_pos);
    ...    
          pm_phase(k);
    ...
          pmv[pmts_p] = ph_ampl2<0?-10:10;
    ...
        if (abs(ph_ampl_p) > abs(ph_ampl_n)) skip_cc++;
        if (abs(ph_ampl_n) > abs(ph_ampl_p)) skip_cc--;
    ...    
        if (skip_cc>skip_t) {
          skip_c=1; skip_cc=0;
    ...
        };
        if (skip_cc<-skip_t) {
          skip_c=-1; skip_cc=0;
    ...      
        };
    ...    
      };
    }
    
    This routine takes the phase of PM / AM correlation maximum (here ph_pos) into a histogram based decision routine (k = histo(&pm_histo, n_pos);). The histogram returns the most often received phase, this is used to set the seconds phase.

    The polarity of the received PM correlation is used to make the decision if a "0" or "1" bit was received, and this information is stored into the one-hour-sized buffer each second.

    The cross-correlation results from the adjacent samples to the maximum are used to shift the ADC sampling phase to the ideal position, adjusting the receiver PPS as near as possible to the transmitted second boundary.

    Similar processing is done for the received AM bits within void sec_pm(void).

    Synchronizing the minutes boundary

    
    void sync_min(signed char *rb, unsigned short bp, char pm, fm_t *fm)
    {
    ...
      fbuf_t(&t, FBUF_SYN(pm), 60, enc_buf);
      p1=rb+(bp+45); if (p1>=(rb+DECODE_HIST)) p1-=DECODE_HIST;
      p1--; if (p1f_tm=(i-45);
    
      rp=fm->s_idx;
      fm->s_res[rp] = k;
      fm->s_idx++; if (fm->s_idx>=60) fm->s_idx=0;
    
      kll=kl=k=(-20*DECODE_HIST); r=rl=rll=-1; s=15;
      for (i=0; i<60; i++) {
        kk=fm->s_res[rp];
        if (kk > k) {
          kll=kl=k; k=kk;
          rll=rl; rl=r; r=s;
        } else if (kk > kl) {
          kll=kl; kl=kk;
          rll=rl; rl=s;
        } else if (kk > kll) {
          kll=kk;
          rll=s;
        };
        rp-=1; if (rp<0) rp=59;
        s+=1; if (s>=60) s=0;
      };
    
      rp+=1; if (rp>=60) rp=0;
      fm->s_idx=rp;
      fm->s[0]=r; fm->s[1]=rl; fm->s[2]=rll;
    }
    
    This lovely piece of code (I struggle to understand it myself at the moment I'm writing this) calculates the correlation of the time code pattern indication the minute start every second and stores the results (the three most probable seconds to start a minute within a 60 seconds interval) to fm->s[].
    As AM and PM time code uses different methods to indicate the minute start, different patterns get used to correlate against.

    Recovering the time code

    The functions:
    
        find_min(pmv, pmts_p, 1, &pm_fm);
        find_hrs(pmv, pmts_p, 1, &pm_fm);
        find_year(pmv, pmts_p, 1, &pm_fm);
        find_mday(pmv, pmts_p, 1, &pm_fm);
        find_mon(pmv, pmts_p, 1, &pm_fm);
        find_flags(pmv, pmts_p, 1, &pm_fm);
    
    get called for each received bit of the time code. These functions correlate the history of received time code bits to a predicted time code, using a similar scheme as above. In particular, each of the functions tries to find the most probable part of the time code related to its name. For example, find_min() excercises 60 times through the received bits buffer, once for each minute, revealing the most alike minute matching the received bits. As soon as a certain confidence level is reached, the minute is considered decoded. Rinse and repeat for all the other functions.

    Finally,

    verify_code()
    
    verifies the complete guessed time code against the receive buffer. If enough confidence is reached, the time code is received.

    Output the result

    As soon as a correct time code was received,
    
      tx_code();
    
    starts to output DCF77 encoded pulses. These can be used to synchronize other clocks and systems.

    Otherwise, there's a bunch of debug output through the VCP (virtual COM port), part of the build-in ST-Link debugger to watch the operation of the receiver. Also part of the complete system is reporting the time and receiver status over the CAN bus, using my proprietary homebus protocol.

    Some experiences and notes

  • Received signal level:
  • I've deliberately left out an automatic gain control stage, assuming the ADC would provide enough dynamic range to operate the receiver. This is true to the extent I'm able to test. In some cases, especially with the shown large tuned antenna, the received signal level gets too strong and saturates the demodulator. To prevent this, I've added a simple limiter to the last amplifier stage (one might spot the bodged components at the fully assembled and encased board).
  • Robustness of the decoder:
  • One goal was to provide a robust decoder. Testing the radio clock at a few places with high EMI and / or low signal level showed this worked out quite well. One can place the receiver (including the antenna) quite near all kind of electronic equipment known to emit low frequency disturbances, and the receiver is still able to synchronize. It just takes longer, e.g. 15 minutes when placed close to a nasty SMPS wall wart. It also works nice and reliable at my workplace, in a basement surrounded by concrete walls and all kind of equipment.
  • Tuned antenna:
  • Quite useful at places with low signal level and / or high EMI. Otherwise, the receiver operates fine with the larger antenna winding connected to its input and no resonant tank. This configuration is useful to achieve high timing precision at places with undistorted DCF77 reception.
  • Loss of signal:
  • This isn't properly handled in the firmware. Especially the PLL starts to run wild and ruins all precision and stability. This topic is left to be done in further versions of the firmware.
  • Stability and precision:
  • I haven't got proper means and instrumentation to determine this now. Some short term observation (using a GPSDO reference) showed a frequency stability of better than 1e-9 is achieved in the daytime. The stability of the PPS output was observed within 20uS. Things get worse at night. Not surprising at all, short term stability gets better with stronger and cleaner received signal, using a non-resonant antenna.
  • Direct conversion to DC:
  • This kind of receiver might suffer from various issues, like coupling the amplified signal back into the antenna causing oscillations and DC offset. For the latter one, I've added an automatic offset control loop. Otherwise I didn't experience problems, it just works.

    Some diagrams

     
    Deviation of PPS output to GPS, observation period: one day / five minutes
    The offset (ca. 70us) doesn't necessarily show the distance of the receiver to Mainflingen, since the overall receiver lag isn't evaluated and taken into calculation yet. Indeed, a time lag of about 250us is expected here.

     
    TCXO control signal, observation period: one day / five minutes
    One can observe noise at night and slowish long term deviation caused by the sun heating up the room the receiver is placed.

     
    Receiver carrier phase, observation period: one day / five minutes

    For comparison, TCXO and phase of a second unit, placed somewhere else receiving a weak and distorted signal:
     
     

    References and Credits

    While building the clock, of course I did some research on the Internet and found a bit of documentation of how an DCF77 PM receiver should work, what accuracy one can expect and some ideas on how to make a robust time code decoder:
    Some interesting documents found on the almigthy Internet
    Another DCF77 decoder library
    A paper about a robust DCF77 AM/PM receiver
    A paper by the PTB regarding the PM receiver
    Some thoughts and measurements about the frequency stability of DCF77 (German)
    DCF77 Logs
    Another simple PM DCF77 receiver

    These inspired me to create this receiver. Although my design isn't a copy of any of these, you'll find part of their concepts and ideas in my design.

    So a big "Thanks" to the authors of this stuff.

    Downloads

    Finally, the downloads section:

    KiCAD project files
    PDF schematic
    Source code

    Back ... und ein Zaehlpixel hab ich auch :-)