In the post “Time synchronisation in local nets” I described the uses of DCF77 receivers to get the standard time from PTB’s atomic clocks. The choice of AM receiver modules was commented on as well as their connection to a µController like a Raspberry Pi.

Here we discuss the decoding of the DCF77 AM signal by C software on a Raspberry Pi.

The nature of the AM signal

At the begin of all seconds of a minute but the last one the amplitude is reduced to 15% for either 100 or 200 ms. For the rest of the one second or two second period the full (100%) amplitude is transmitted.

The AM receivers’ modules digital output is usuallya good DCF77 signal
  •   HI (1, true)  for the 15% modulation time   and
  •   LO (0, false) for the rest of the 1 or 2 s period.
But it can be the other way round – what the software should handle by, e.g. an option - - invert.

For the modulation time
  •   200ms means true   and
  •   100ms means false.

Hence, the information we must acquire and decode is
  a)   an 58 bit telegram in every minute   and
  b)   the start time of the modulation period.

a): The telegram starts with 14 bits of secret commercial information. The remaining 44 relevant bits contain all time and date information. The code is simple and well published.

b): The time should be captured as exactly as possible, best as µs time stamp with 20..50µs accuracy. With the time stamp of the modulation flank and their DCF77 time we get the correction respectively setting of our system time – for the use of DCF77 as our system’s redundant or only time source.

So in the end, it’s all about getting the time (time stamps) of the modulation flanks.

Sampling the AM signal

We can think of three approaches
  A)   read the receiver signal in the 1 ms cycle,
  B)   have an interrupt handler for both signal flanks or
  C)   exploit the abilities of pigpiod.

Having a runtime or framework providing PLC like cycles, as described in Raspberry for remote services (see publications) and used in most of our controller projects, approach A is feasible. On the other hand, when wanting DCF77 as substitute or redundancy for NTP the sync would have to be at least one order of magnitude better than the fastest cycle’s timing: a reason to exclude A).

Generating sequence of time stamped events to be handled later in in an other thread or process is per se a good approach. And an interrupt handler (B) could do this. Raspberry processors have an interrupt system allowing flank interrupts for any GPIO. But the handling is complicated and the application requires sudo privileges.

A comprehensible C solution with GPIO interrupts is hardly found. Some libraries or frameworks employ a fast sampling thread + interthread/interprocess communication – and call it “interrupt”.
Well the approach is OK. It’s what what A) does too slow (what could be mended by an extra asynchronous fast cycle) and what C) (see below) supports perfectly. But selling it as interrupt is label fraud.

Well I’m an aficionado of Joan N.N.’s pigpiod library. See the chapter in question and the literature list in above mentioned publication. So, not surprisingly, I exploit special abilities of a library used anyway.

Capturing modulation flanks with pigpiod

As said, pigpiod is our preferred approach to Raspberry IO and the only one we use for real control applications. It defines a server or daemon which does all initialisations and has control over all functions of the GPIOs used. This server has to be started with sudo to run forever in background. On all Raspberry Pis where we installed it we start it at boot by a (sudo) crontab entry:

@reboot  /usr/local/bin/pigpiod  -s 10

The -s 10 means pigpiod running ia 10µs cycle instead of a default 5µs one. For all our applications sofar a 20µs delay, time stamp and signal generation accuracy was sufficient. (The fastest would be 1µs.)

Programs doing (process) IO just communicate with the daemon by
  •   socket (as in the GPIO with Java project),
  •   pipe (never used here) or by
  •   a set of C functions.

pigpiod allows to set a callback function for the flank(s) of an input pin:

set_mode(thePi, dcfGpio, PI_INPUT);             // make dcfGpio input
if (dcfPUD <=PI_PUD_UP) // Raspberry Pi's pull up is sufficient for open
   set_pull_up_down(thePi, dcfGpio, dcfPUD);  // collector output stages
dcf77callbackID = callback(thePi,            // register dcf77receiveRec
   dcfGpio, EITHER_EDGE, &dcf77receiveRec); // as callback function

Semantically this is not so far from interrupts. But we avoid all interrupt complications (and dangers) and get 32 bit system time stamp for every signal flank in 1µs resolution and about 15µs accuracy as extra.

/*  The actual respectively last modulation period data received. */
dcf77recPerData_t dfc77actRecPer;
   
/*  DCF77 receive recorder.
 *
 *  This is a pigpiod callback function for .... (in other file)
 */
void dcf77receiveRec(int pi, unsigned gpio, unsigned level, uint32_t tick){
  uint32_t const dif = tick - dfc77actRecPer.tic;
  dcf77lastLevel = dcfInvert ? !level : level; // handle AM receiver polarity
  if (dcf77lastLevel) { // in 15% modulation, i.e end of last period
    dfc77actRecPer.per = dif; // calculate duration
    dfc77ringBrecPer[dfc77ringBrecWInd] = dfc77actRecPer; // put in FiFo
    ++dfc77ringBrecWInd;  // signal FiFo write respectively period end
    dfc77actRecPer.tic = tick;  // start of new period
  } else {  // full (100%) signal 
    memcpy(dfc77actRecPer.sysClk, lastSysClk, 12); // log time for current
    memcpy(lastSysClk, stmp23 + 11, 12);  // log time text for next period
    dfc77actRecPer.cbTic = get_current_tick(pi);  // call back tick
    dfc77actRecPer.tim = dif;
    dfc77actRecPer.sysClk[12] = dfc77actRecPer.sysClk[13] = '\0';
  }
} // dcf77receiveRec(int, 2*unsigned, uint32_t)

For every modulation period the callback function shown fills a record of dfc77actRecPer of type dcf77recPerData_t and fills it in a FiFo (ring buffer).
The structure is (excerpt with comments shortened):

/** Data for one received DCF77 AM period. */
typedef struct {
/** The piogpiod time tick in µs. */
  uint32_t tic;
/** The system time as text hh:mm:ss.mmm at period start for logging convenience.
 *
 *  It is the time when the call back function for start of 15% AM is called.
 */
  char sysClk[14]; // text provided by rasProject_01 framework 
/** The system tick at second's start in µs.
 *
 *  It is the time of entering the call back function for 15% AM.
 *  By the pigpiod event to callback entry delay cbTic should be later than tic. By
 *  their difference an apparent system-DCF77 time difference has to be shortened. 
 */
  uint32_t cbTic;
/** The 15% modulation's duration in µs. */
  uint32_t tim; // 100ms: FALSE, 200ms: TRUE
/** The period's duration in µs. */
  uint32_t per; // 1s: second's end, 2s: minute's end
} dcf77recPerData_t;

With
  •   start time .tic, ideally every real atomic second,
  •   period .per, ideally either 1s or 2s, and
  •   modulation time .tim, ideally either 100ms or 200ms,
recorded over a minute we got all needed to decode and use DCF77 time.

Remarks on discriminating and decoding

With the chain of records – one for each modulation period, spoiled or correct – the first step is to discriminate the values .tim and .per.
We define a stucture for a (uint32_t) value range:

/** Values for discrimination of duration. */
typedef struct {
/** Index.
 *  The lowest value in a chain (array or enum) shall get the index 0. */
  unsigned i;
/** Value.
 *  This is the upper bound value of the range. <br />
 *  The highest value in a chain (array or enum) is implied as the maximum
 *  possible / measurable value. */
  uint32_t v;
/** Name.
 *  This is a short (5 character max.) name of the discrimination range. */
  char n[6];
/** Charakter.
 *  This is a recognisable character for the discrimination range, unique
 *  for the complete chain. It may be used for (narrow) logs instead of .n */
  char c;
} durDiscrPointData_t;

For modulation period (.per) and modulation time (.tim) we define five ranges each two of them good and three bad (below, in beetween and above):

/*  Discrimination values for the 15% modulation time. */
durDiscrPointData_t timDiscH[5] = {
    {0,  60000, "spike", 's'},  // 0 .. 59.9 ms modulation spike
    {1, 128999, "false", 'F'},  
    {2, 130000, "undef", 'u'},
    {3, 228000, "true",  'T'},
    {4, MXUI32, "error", 'e'}      };
/*  Discrimination values for the modulation period. */
durDiscrPointData_t perDiscH[5] = {
    {0,  960000, "below", 'b'},
    {1, 1040000, "secTk", 'S'}, // seconds tick
    {2, 1960000, "undef", 'u'},
    {3, 2040000, "minTk", 'M'}, // end minute tick (last two seconds)
    {4,  MXUI32, "error", 'e'}    };

Depending on the receivers, their quality and perhaps filters extra sets with other values are provided. In any case, obviously, the array length is 5. So one optimal discriminating function getting the array and a value returning the entry:

/*  Discriminating a value.
 *  @param table discrimination table of length 5. With other lengths the
 *               function will fail. Must not be null.
 *  @param value the number to be discriminated
 *  @return  the lowest table entry with value < table[i].v
 */
durDiscrPointData_t * disc5(durDiscrPointData_t table[], uint32_t const value){
  if (value < table[1].v) {
    if (value < table[0].v) return & table[0];
    return & table[1];
  }
  if (value < table[3].v) {
    if (value < table[2].v) return & table[2];
    return & table[3];
  }
  return & table[4]; // Note: table[4].v (MXUI32 above) is never used
} // disc5(durDiscrPointData_t const *, uint32_t const)

When discrimination both the modulation time (.tim) and the period (.per) we get four good outcomes:
  •   F.S  false . second tick
  •   T.S  true  . second tick
  •   F.M  false . end minute tick (2 seconds)
  •   T.M  false . end minute tick (2 seconds)
All other combinations are bad. Without bad ones we have a chain of true and false only, we can put indices respectively second numbers 0 .. 58 on and we can decode. And ideally each time stamp .tic of a modulation period would mark a “real” atomic second.

But, alas, we get outages and spikes of all sorts leading to over 100 instead of 59 modulation periods per minute. With good receiver modules and antenna positioning you get very few disturbance. But yet, they occur.

Filtering spikes with pigpiod

On common sort of (AM) disturbances are short – often sub millisecond – spikes either as outage of the full modulation signal or as bursts appearing as full signal instead of 15% amplitude. Both disturbances can get in the range of 40 ms and more.

Spikes could be wiped away with a simple de-bouncing algorithm ignoring all signal changes below a minimal duration. And, fortunately, pigpiod offers this feature for any input captured by call back function.

   if (dcfGlitch > 30000) dcfGlitch = 0;
   set_glitch_filter(thePi, dcfGpio, dcfGlitch);

With glitch filtering the signal change call back, of course, would come later, but the time stamp is still correct.

The spike or glitch filter value can be in the range of 0 (no filter) to 30000 µs. After many experiments (over weeks) we keep it below 10ms.
    dcf77onPi --glitch 9999 >> logs/dcf77test32cAnG.log &

It emerged that higher values might filter out many seconds periods in sequence. Hence, one might detect “no reception at all” respectively “receiver off” wrongly.

An extra filter for modulation disturbances

The extra gain of a 30ms (maximum) glitch/spike filter compared with 10 ms is marginal with good receivers. And anyway, the shortening of a period below e.g. 400ms by a spike could not be handled by pigpiod’s glitch filter. These cases do occur and in most cases they lead to two periods where one should be. Without extra measures all counting and decoding hence on ist lost for the rest of the minute.

A relatively simple extra filter therefore is holding back the first occurrence of ?.b and add this period correctly to next. By this most of those cases could be repaired. The log (Fr, 29.01.2021 14:16) shows a minute otherwise spoiled in second 41:

###        .tic       .tim    sec dis    .per  system time  -cbck decode
DCF77 1.839.060.809   184080  36: T.S 1000492  14:17:35.138 -.185
DCF77 1.840.061.301    84361  37: F.S  998913  14:17:36.238 -. 85
DCF77 1.841.060.214    80060  38: F.S  998333  14:17:37.138 -. 80
DCF77 1.842.058.547   186510  39: T.S 1004452  14:17:38.133 -.187
DCF77 1.843.062.999    79390  40: F.S 1002533  14:17:39.238 -. 80
DCF77 1.844.065.532    28300  41: s#b   53930  14:17:40.135 ignor= // spiky |
DCF77 1.844.119.462   174560  41: T=S  994862  14:17:41.087 -. 29 29.mm. // v
DCF77 1.845.060.394   180051  42: T.S 1004343  14:17:41.232 -.180
DCF77 1.846.064.737    79360  43: F.S  996692  14:17:42.233 -. 79
DCF77 1.847.061.429   181361  44: T.S 1003933  14:17:43.136 -.181 Day:Fr
DCF77 1.848.065.362   175890  45: T.S  994952  14:17:44.235 -.176
DCF77 1.849.060.314    82120  46: F.S 1003043  14:17:45.234 -. 82
DCF77 1.850.063.357    74190  47: F.S  997652  14:17:46.134 -. 75
DCF77 1.851.061.009    82901  48: F.S 1002913  14:17:47.131 -. 83
DCF77 1.852.063.922    78590  49: F.S  997822  14:17:48.136 -. 79 dd.01.

This moderately “intelligent” filter rescues most of those cases (which are quite seldom with good receiver modules). There are some samples where this algorithm failed while adding the spiky period to the one before would have saved that case. Well, implementing that is feasible but according to our data not worth the effort – and the reduced readability of the program.

Final remarks

On a Pi we implemented receiving and decoding DCF77 with inexpensive AM receiver modules. With the reception tick and the “call back tick + call back system time” pair we have all data to correct or set the system time. Hence, we have a (redundant) time source to replace or substitute NTP. It can also synchronise distributed systems in a closed network and or provide a NTP server there.

The full code can be found in the SVN repository weinert-automation.de/svn/.

References

Weinert, Albrecht,  “Time synchronisation in local nets”,  2021 post

Weinert, Albrecht,  “Raspberry for remote services”,   2018 – 2020 publication

N.N, Joan,  “pigpio Daemon”  2020 documentation

PTB  “DCF77”  2004 German survey