Reading the DCF77 Signal

Date:

2017-10-01

Intro

Adding a DCF77 radio receiver to a clock is a fairly obvious feature, in Europe at least. So here we go: the details of how the information is transferred over the course of a minute is given e.g. on wikipedia https://en.wikipedia.org/wiki/DCF77#Time_code_details Please note that DCF77 is not the only time signal available. For a list consult https://en.wikipedia.org/wiki/Radio_clock

The DCF77 signal is modulated by amplitude shift keying. Full power of the carrier signal indicates one state, reduced power (ca. 15%) indicates another state. Power is reduced at the beginning of each second. A low state lasting 0.1 s indicates a bit value of 0. A low state of 0.2 s indicates a bit value of 1. In second 59 no power reduction occurs. It serves as a sync pattern to indicate the start of the next minute at the beginning of the next falling edge.

The signal is generally ok to receive in large parts of Europe, however, the signal might fade at any moment. Moreover the signal might be disturbed in any way at any time, e.g. by lightning or interference.

The DCF77 receiver chosen provides a TTL level signal, which is high in idle, and goes low during the low power state of the carrier signal.

Design Decisions

  • The signal of the receiver is sampled during each tick of the MasterClock, 128 times per second. In my experience it is not a good idea to trigger interrupts at each edge of the receiver signal and try to measure the length of a pulse using a timer/counter

  • (dcf.tick) is doing the sampling and some book keeping. It is called via dcf.tick (a deferred word) which in turn is called in job.tick of the MasterClock

  • The leading edge is detected if the last 16 readings lead to $000F

  • pin readings of high are simply added up in counter dcfPulsedcfPause remained unused for too long, so I removed it

  • the end of a second is at dcfTick == 127. This is not neccessarily in sync with the ticks of the MasterClock

  • At the end of a second these counters are evaluated. If dcfPulse is within 9 to 12, a valid bit 0 has been seen. If it is within 20 to 25, a valid bit 1 has been seen. If its value is below 2, a sync59 has been seen, i.e. the last second before the start of the next minute. Every other value is regarded to be an error!

  • the extracted bit values are evaluated at the end of each second (they are not collected and evaluated together at the end of the minute)

  • a jump table with the current second as index is used to call functions, which evaluate the last bit value

  • all parity bits are evaluated. If any of them are not correct, the telegram is discarded

  • the concept of daylight saving time is implemented

  • the concept of leap seconds is ignored at this stage

  • if desired, the valid telegram is converted to Unix time, corrected to UTC and fed to the MasterClock at the end of the minute

This is quite a bit of material, but there is no shorter way, I’m afraid.

Structure

  • sampling the radio signal (128/s) — produces the Pulse Counter

  • evaluating Pulse (1/s) — produces 1 Bit value

  • processing one bit (1/s) — produces a data telegram and some flags (f.dcf.err …)

  • processing one telegram (1/60s) — produces a timestamp valid for the start of the next second. This can be copied over to MasterClock, if desired.

I shall attempt to explain the code starting from the fast part, namely sampling the signal.

Code 0: Pin and LED

The DCF77-receiver signal is connected to pin D[7]. There also is a LED definition, to show the signal or other state.

PORTB 2 portpin: led.0
: led_dcf  [: led.0 ;] execute ;

\ dcf77 RX
PORTD 7 portpin: _dcf

Reading the pin takes place in (dcf.tick) (see below) in a block similar to this

_dcf pin_low? if
    \ account low signal level (active)
else
    \ account high signal level
then

Code 1: dcf.tick — sampling the radio signal

dcf.tick is a deferred word due to code organisation. At runtime it always points to (dcf.tick) which is defined later in the source code.

(dcf.tick) is called through job.tick of the MasterClock, so this routine runs ticks/sec (128) times per second. Its job is to sample the signal over the course of a second so that at the end of this second the received bit value is available.

So we declare a variable to count the (active) low samples. Then we define a function to clear the counter, and another one to increment it. These are for better readability only!

variable dcfPulse                       \ sample count, reduced tx power
: dcfPulse.clr  ( -- )  0 dcfPulse  ! ;
: dcfPulse++    ( -- )  1 dcfPulse +! ;

In order to synchronize the dcf second to the radio signal, we add some detection of the begining of the second (leading edge of signal). Every sample is shifted into a 16-bit variable (dcfEdge).

variable dcfTick                        \ separate tick for dcf
: dcfTick++/mod ( -- )  dcfTick @ 1+ ticks/sec mod dcfTick ! ;
variable dcfEdge                        \ edge detector
#4                               constant dcfEdge:leading.bits
1 dcfEdge:leading.bits lshift 1- constant dcfEdge:leading.mask
: dcfEdge<<1    ( -- )  dcfEdge @ 1 lshift dcfEdge ! ;
: dcfEdge+1     ( -- )  1 dcfEdge +! ;
: dcfEdge?      ( -- )  dcfEdge @ dcfEdge:leading.mask = ;

: dcfTick.set   ( -- )  dcfEdge:leading.bits dcfTick ! ;

If the resulting pattern is 0x000f, then I interpret this as a leading edge, and dcfTick is set to 4. These magic numbers have been moved into constants. I want to see the 4 low bits set, the corresponding mask is constructed.

Setting dcfTick to 4 is a brutal thing, and it will be wrong occasionally, e.g. in an electrically noisy environment. It might then be needed to add some “window” in the time bookkeeping, where this edge can be expected. It would be valid only during this window.

So we have all puzzle pieces now to write down the needed word.

: (dcf.tick)
    dcfClock  1 tick +!         \ keep dcfClock ticking
    dcfTick++/mod               \ .
    dcfEdge<<1                  \ . edge detection

    _dcf pin_low? if
        \ active!
        dcfPulse++
        dcfEdge+1
        -1 dcf.led
    else
        0 dcf.led
    then

    dcfEdge? if                 \ keep dcfClock in sync with radio signal
        dcfTick.set             \ . this might be bad occasionally
    then

    \ dcfTick: 0 .. 127
    \ 127 indicates the last tick in this second
    dcfTick @ ticks/sec 1- = if
        dcf.sec                 \ handle "one second over"
    then
;

At the end of the current second we need to decide, what bitvalue was received, and what to do with it. This work is hidden behind the call to dcf.sec.

Code 2: dcf.bit — producing one bit value

This function is called at the end of the current second to determine the transmitted bit value. dcf.bit will look at dcfPulse (on top of the stack) and return an appropriate result. The comparison values in the nested if-else blocks depend on ticks/sec. They have been determined by staring at the debug output more than anything else.

-2 constant dcf.bit:sync59
-1 constant dcf.bit:error
 0 constant dcf.bit:0
 1 constant dcf.bit:1
: dcf.bit ( pulse -- bit|error )
  dup  #2 < if                dcf.bit:sync59 else
  dup  #9 < if f.dcf.err fset dcf.bit:error  else
  dup #13 < if                dcf.bit:0      else
  dup #20 < if f.dcf.err fset dcf.bit:error  else
  dup #26 < if                dcf.bit:1      else
               f.dcf.err fset dcf.bit:error
  then then then then then
  swap drop
;

I think this function is pretty strict. Of all values 0 .. 127, only a small portion is valid: 0 .. 1 is regarded as the sync in second 59, 9 .. 12 is regarded as 0, 20 .. 25 is regarded as 1, anything else triggers an error and thus an invalid telegram. Nonetheless I have had only occasional difficulties to receive a valid telegram.

Code 3: dcf.sec — processing one bit value

dcf.sec is a deferred word, again due to code organisation. At runtime it always points to (dcf.sec) which is defined later in the code. This function is called at the end of the current second. The variable dcfPos holds the position in the telegram, i.e. the current second in fact.

So in this code block we look at the value of dcfPulse, consult dcf.bit to evaluate the received bit, and then call the appropriate entry in the call table to process the bit just received.

dcf.dbg.sec hides the details of debugging output, and we ignore that for now.

#include case.frt
variable dcfPos

: dcfPos++ ( -- ) dcfPos @ 1+ #60 mod dcfPos ! ;

: (dcf.sec)
  dcfClock timeup
  0 tick !

  \ evaluate pulse/pause
  dcfPulse @ dcf.pulse>bit    dup dcfBit !
  ( bit ) case

    dcf.bit:0      of  0 dcfPos @ pos.cmd  dcf.dbg.sec  endof

    dcf.bit:1      of  1 dcfPos @ pos.cmd  dcf.dbg.sec  endof

    dcf.bit:error  of  f.dcf.err fset      dcf.dbg.sec  endof

    dcf.bit:sync59 of
      dcf.min
      dcf.tmp.counter.reset
      dcfErrCnt.set
    endof

  endcase
  dcfPulse.clr           \ reset pulse/pause
  dcfPos++
;

Code 4: calling into the jump table

In (dcf.sec) the real work is hidden in the calls into the jump table:

( bit value 0 or 1 )  dcfPos @  pos.cmd

pos.cmd inspects the top of stack item and uses it as the index into the jump table.

: pos.cmd ( index -- )
  dup 0 #60 within if
    ( index ) pos_cmd_map +  @i execute
  else
    drop
  then
;

The function thus called will consume one more item of the stack, namely the bit value.

So the processing is hidden in the functions listed in table pos_cmd_map. All of these functions have the same structure

:noname  ( 0|1 -- )
    if   ( bit:1  ) ...
    else ( bit:0  ) ...
    then ( always ) ...
;

So for every position in the telegram something needs to be done. As an example we will look at the handling of Bits 21 to 28, i.e. the minute value and its parity bit.

\ #21 .. #28: Minute
\ minute ones
:noname if ( bit:1 )   #1 dcfTMin   +!  dcfPar++ then ;     #21 >rt
:noname if ( bit:1 )   #2 dcfTMin   +!  dcfPar++ then ;     #22 >rt
:noname if ( bit:1 )   #4 dcfTMin   +!  dcfPar++ then ;     #23 >rt
:noname if ( bit:1 )   #8 dcfTMin   +!  dcfPar++ then ;     #24 >rt
\ minute tens
:noname if ( bit:1 )  #10 dcfTMin   +!  dcfPar++ then ;     #25 >rt
:noname if ( bit:1 )  #20 dcfTMin   +!  dcfPar++ then ;     #26 >rt
:noname if ( bit:1 )  #40 dcfTMin   +!  dcfPar++ then ;     #27 >rt
\ minute parity bit
:noname
  if   ( bit:1 )                         dcfPar++ then
  ( always )
  dcfPar? if f.dcf.parerr.min fclr  dcfErrCnt-- then
  0 dcfPar !
;                                                           #28 >rt

In Bits 21 to 27, if their value is 1, we add the corresponding decimal value to dcfTMin. Please note that this takes care of the conversion from BCD to decimal as well. If the received bit value is 0, there is nothing to do, so there is no else clause. If the received bit value is 1 we increment the parity variable dcfPar as well.

\ count 1 bits, really; result must be even
: dcfPar++ ( -- )      1 dcfPar +! ;
: dcfPar?  ( -- t/f )  dcfPar @ $01 and 0= ;

At the end, the least significant bit of dcfPar is consulted. If it is 0, then all is well and we decrement dcfErrCnt. This variable counts the checks to be done while receiving a telegram and must be 0 after the last bit of the telegram was processed.

Code 5: dcf.min — processing a telegram

(dcf.min) again is called via deferred word dcf.min due to code organisation. This function is called at the end of the current minute. We check that we are indeed at the end of second #59. If so, we decrement dcfErrCnt one more time. Then we check that its value has indeed dropped to zero — this indicates that all checks to the data telegram were successful. The result is saved in f.dcf.err for use elsewhere.

If all is well, and if f.dcf.commit is set, then we proceed to convert the telegram and update the clocks dcfClock and MasterClock, respectively. There is a potential problem here, if updating these clocks takes a noticeable time, they end up a little late.

The remainder of (dcf.min) is debug output and bookkeeping. There is another potential problem with the debug output as well, it adds possibly unexpected delays and the next call to (dcf.tick) might be late.

: (dcf.min) ( -- )
  dcfPos @ #59 =  if dcfErrCnt--    then
  dcfErrCnt @ 0=  if f.dcf.err fclr then
  \ this block runs at the *end* of second "59" i.e.
  \ at the start of second "00", thus copy counters
  f.dcf.err fclr? if
    f.dcf.commit fset? if
      dcfTemp>dcfClock
      dcfClock>MasterClock
      space [char] C emit \ FIXME: dbg conditional?
      f.dcf.insync fset \ dcfClock is in sync!
      f.dcf.commit fclr
    then
  then
  dcf.dbg.sec
  dcf.dbg.min
  f.dcf.commit fclr? if
    \ clear debug flags --- this is only to demonstrate resync/commit
    f.dcf.dbg fclr
  then
;

Updating the clocks from the DCF77 telegram is done in two steps. The first function call (dcfTemp>dcfClock) copies the values collected in the dcfT* counters into the clock counters of dcfClock. This clock runs independently from the master clock, tick and sec are just reset to zero. The clock is kept ticking from (dcf.sec) and it runs in the timezone indicated by DCF77.

: dcfTemp>dcfClock
  dcfClock
  0                       tick  !
  0                       sec   !
  dcfTMin   @             min   !
  dcfTHour  @             hour  !
  dcfTDay   @  1-         day   !
\ dcfTWday  @             ?
  dcfTMonth @  1-         month !
  dcfTYear  @  Century +  year  !
;

Updating MasterClock during the call to dcfClock>MasterClock is more work. As we have seen before, this is another deferred function pointing to (dcfClock>MasterClock). We now have to take care about the different time zones. dcfClock and MasterClock differ by one or two hours.

The new values are read from dcfClock, converted to epoch seconds (ut>s.short), corrected by one or two hours, converted back to clock counter values (s>dt.short) and written into the counters of MasterClock. Along the way Esec is updated, too. Unfortunately, we once again have the potential problem, that these steps might introduce more delay than acceptable.

\ copy time from dcfClock to MasterClock
: (dcfClock>MasterClock)
  dcfClock
  sec  @     min   @     hour @
  day  @ 1+  month @ 1+  year @
  MasterClock
                    \ -- sec min hour day month year
  \ convert "local time" to epoch seconds
  ut>s.short        \ -- T/sec
  \ adjust local time zone
  f.dcf.CEDT fset? if
    #3600. d-
  then
  #3600. d-
  2dup Esec 2! \ copy to Epoch seconds!

  \ convert back to "UTC date time" format
  s>dt.short        \ -- sec min hour day month year

  MasterClock
  year !  1- month !  1- day  !
  hour !  min      !  sec     !

  \ fixme: might cause wreaking havoc?
  dcfClock tick @
  dup MasterClock tick !
  ct.ticks.follow !
;

I have to admit that writing documentation helps to see less than ideal structure or implementation in my projects. So while I was aware about the unwanted delays as pointed out above, this clock is still useful for my daily life. I just would not expect more accuracy than a couple of seconds, which may or may not be good enough for the problem at hand.

Code 6: Some Debugging

During development of the dcf part I needed a lot of debugging output. Really a lot. But debugging output does have undesired consequences at times. It can delay the processing of a tick — visible clearly on the traces of the logic analyzer.

dcf.dbg.second.over emits one character per second to indicate the state of the received bit. . and + indicate values of 0 and 1, / indicates an error, and S indicates the detection of the sync event in second 59.

: dcf.dbg.second.over ( -- )
  f.dcf.dbg.rx fset? if
    dcfBit @ case
      dcf.bit:0      of [char] . emit endof
      dcf.bit:1      of [char] + emit endof
      dcf.bit:error  of [char] / emit endof
      dcf.bit:sync59 of [char] S emit endof
    endcase
  then
;

dcf.dbg.minute.over used to be quite large, but as indicated above, it started to create problems — which I still have not understood in detail.

\ short version
: dcf.dbg.minute.over ( -- )
  space
  f.dcf.err fclr? if
    [char] O emit
  else
    [char] F emit
  then
  cr
;

Code 7: resyncing hourly

I decided to request a new telegram to be committed to the running clock once per hour. It will handle the appropriate flags and enable debug output.

: dcf.resync ( -- )
  f.dcf.insync fclr \ resync dcf time
  f.dcf.commit fset \ sync dcf -> MasterClock wanted
  f.dcf.dbg fset    \ dbg.min on
  f.dcf.dbg.rx fset \ dbg.sec on
;

: job.min
  ...
  MasterClock min @  #58 = if dcf.resync then ;
;

The result can be seen on the console:

~6F> .uptime.dhms
859562s 9d 22:46:02 ok
~6F> .d
859567 s  2017-12-11_19:55:56 UTC 24  24   1513022156  \
          2017-12-11_20:55:56 UTC 1513022156 1513022156 0
 ok
~6F> .+....++...+.++...+.++..++.+......+++...+.+...+..++++.+...+ CS O X
.d
860802 s  2017-12-11_20:16:32 UTC 92  93   1513023392  \
          2017-12-11_21:16:32 UTC 1513023392 1513023392 0
 ok
~6F>

Resyncing should be sufficient once per day, perhaps some time during the night. Debug output is not needed any more, once the clock is in production state. Note that the above output was edited manuallay ant that the seconde “UTC” label above is wrong and should rather be “MEZ”.

Putting it all together

Switching the whole wonderstuff on (and off) is all there is left to do:

: +dcf
  _dcf pin_input
  ['] (dcf.tick) to dcf.tick           \ register DCF77 functions
  ['] (dcf.sec)  to dcf.sec

  0 dcfPos !                           \ assume we are at position 0
  f.dcf.err fset                       \ error unless proven otherwise
  f.dcf.commit fset                    \ request to set dcfClock
  f.dcf.insync fclr                    \ not in sync yet!
  dcfErrCnt.set                        \ more errors unless proven otherwise
;

: -dcf
  ['] noop to dcf.tick                 \ deregister DCF77 functions
  ['] noop to dcf.sec
;

This piece of code ist long. And I’m not even convinced it is nice and good and clean code. It is one way to solve the problem, and I’m sure, a few bugs or dragons are luring …

Leftovers

While this clock works, there is an uneasy feeling about it. There are still things that could or should be improved. This is the list of things I am aware of, there might be more, of course.

  • dcf.bit — construct comparison values from ticks/sec itself, since ticks/sec is defined at compile time, the comparison values might as well be calculated.

  • dcf.tick — edge detection might be wrong under certain conditions; perhaps defining a window in time, where the edge is acceptable, could help. However, it is unclear at this point, whether this is a problem that occurs in practice.

  • dcf.min — copying time counters from dcfClock to MasterClock takes some time, because timezones must be accounted for. So this intruduces a noticeable delay. The must be more clever ways to solve this. One thing could be to move some of the work to be done from the end of the current second to the middle of the second. There is plenty of idle time in this whole game.

  • dcf.min — debug output over serial possibly introduces more delays thus delaying dcf.tick in possibly unacceptable ways. One could write the output to a larger ringbuffer and create a third task to output these charcters at more convenient times. However, adding debugging output makes the problem disappear or worse is a very common phenomenon in programming. So I’m sure there are better solutions available.

The Code

  1\ 2017-07-23  dcf_01.fs
  2\
  3\ Written in 2017 by Erich Wälde <erich.waelde@forth-ev.de>
  4\
  5\ To the extent possible under law, the author(s) have dedicated
  6\ all copyright and related and neighboring rights to this software
  7\ to the public domain worldwide. This software is distributed
  8\ without any warranty.
  9\
 10\ You should have received a copy of the CC0 Public Domain
 11\ Dedication along with this software. If not, see
 12\ <http://creativecommons.org/publicdomain/zero/1.0/>.
 13\
 14\ functions to sample dcf signal
 15\     extract bit values,
 16\     extract values from bits,
 17\     verify parity
 18\     provide debug output
 19\
 20\ needs
 21\     #2000 constant Century
 22\     PORTD 7 portpin: _dcf
 23\     PORTB 2 portpin: led.0
 24\     : led_dcf  [: led.0 ;] execute ;
 25\
 26\ words
 27\     +dcf -dcf
 28\     f.dcf.insync
 29\     f.dcf.commit
 30\     +DD +DRX
 31
 32variable dcfFlags
 33dcfFlags   0 flag: f.dcf.err            \ any error, telegram invalid
 34\  Flags   1 flag: f.dcf.???            \
 35dcfFlags  #2 flag: f.dcf.commit         \ commit dcf -> masterclock wanted
 36dcfFlags  #3 flag: f.dcf.dbg            \ debug output active
 37dcfFlags  #4 flag: f.dcf.nosignal       \
 38dcfFlags  #5 flag: f.dcf.insync         \ tell the world, dcf time is ok
 39dcfFlags  #6 flag: f.dcf.CET            \ timezone bit
 40dcfFlags  #7 flag: f.dcf.CEDT           \ daylight saving time bit
 41dcfFlags  #8 flag: f.dcf.parerr.min     \ parity error minute
 42dcfFlags  #9 flag: f.dcf.parerr.hour    \ parity error hour
 43dcfFlags #10 flag: f.dcf.parerr.date    \ parity error date
 44dcfFlags #11 flag: f.dcf.parerr         \ any parity error
 45dcfFlags #12 flag: f.dcf.dbg.rx         \ debug output per received bit active
 46\  Flags #13 flag: f.dcf.???
 47\  Flags #14 flag: f.dcf.???
 48\  Flags #15 flag: f.dcf.???
 49
 50variable dcfPulse                       \ sample count, reduced tx power
 51: dcfPulse.clr  ( -- )  0 dcfPulse  ! ;
 52: dcfPulse++    ( -- )  1 dcfPulse +! ;
 53variable dcfBit                         \ value of last received bit
 54variable dcfTick                        \ separate tick for dcf
 55: dcfTick++/mod ( -- )  dcfTick @ 1+ ticks/sec mod dcfTick ! ;
 56variable dcfEdge                        \ edge detector
 57#4                               constant dcfEdge:leading.bits
 581 dcfEdge:leading.bits lshift 1- constant dcfEdge:leading.mask
 59: dcfEdge<<1    ( -- )  dcfEdge @ 1 lshift dcfEdge ! ;
 60: dcfEdge+1     ( -- )  1 dcfEdge +! ;
 61: dcfEdge?      ( -- )  dcfEdge @ dcfEdge:leading.mask = ;
 62
 63: dcfTick.set   ( -- )  dcfEdge:leading.bits dcfTick ! ;
 64
 65variable dcfPos                         \ position in telegram, == dcf second
 66: dcfPos++      ( -- )  dcfPos @ 1+ #60 mod dcfPos ! ;
 67variable dcfPar                         \ collect parity
 68: dcfPar++      ( -- )  1 dcfPar +! ;
 69: dcfPar?       ( -- t/f )  dcfPar @ $01 and 0= ; \ even parity
 70variable dcfErrCnt                      \ counter (down) of passed checks
 71: dcfErrCnt--   ( -- )  -1 dcfErrCnt +! ;
 72: dcfErrCnt.set ( -- )  #7 dcfErrCnt  ! ;
 73
 74\ these collect the bits in the DCF bit stream
 75variable dcfTMin
 76variable dcfTHour
 77variable dcfTDay
 78variable dcfTWday
 79variable dcfTMonth
 80variable dcfTYear
 81
 82: +DD  f.dcf.dbg fset ;
 83: -DD  f.dcf.dbg fclr ;
 84: .DD
 85  ." dcf.error:  " f.dcf.err     fset? if 1 else 0 then . cr
 86  ." dcf.parity: " f.dcf.parerr  fset? if 1 else 0 then . cr
 87  ." dcf.commit: " f.dcf.commit  fset? if 1 else 0 then . cr
 88  ." dcf.debug:  " f.dcf.dbg     fset? if 1 else 0 then . cr
 89  ." dcf.dbg.rx: " f.dcf.dbg.rx  fset? if 1 else 0 then . cr
 90;
 91: +DRX  f.dcf.dbg.rx fset ;
 92: -DRX  f.dcf.dbg.rx fclr ;
 93
 94
 95\ create temporary RAM table
 96variable dcf.tmp.table #60 cells allot \  only temporary really!
 97\ fill RAM table with ' drop  \ noop -> drop: remove argument!
 98' drop dcf.tmp.table #60 ramtable.init
 99\ dcf.tmp.table #60 ramtable.dump \ DEBUG
100: >rt ( xt idx -- )  dcf.tmp.table >ramtable ;
101
102\ code snippet XTs to ram table
103\ structure of functions
104\ :noname  ( 0|1 -- )
105\     if   ( bit:1  ) ...
106\     else ( bit:0  ) ...
107\     then ( always ) ...
108\ ;                                                      #idx >rt
109\ :noname
110\     drop ( always ) ...
111\ ;                                                      #idx >rt
112
113\ #0   always 0!
114:noname
115  0= if ( bit:0 ) dcfErrCnt--
116  then
117;                                                           #0 >rt
118\ #1 .. #14  civil warning bits, MeteoTime data bits
119\ #15  call bit
120\ #16  1: day light savings time switch at the end of this hour
121\ #17  0 CET, 1 CEST
122:noname
123  if   ( bit:1 ) f.dcf.CEDT fset
124  else ( bit:0 ) f.dcf.CEDT fclr
125  then
126;                                                           #17 >rt
127\ #18  1 CET, 0 CEST
128:noname
129  if   ( bit:1 ) f.dcf.CET fset   f.dcf.CEDT fclr? if dcfErrCnt-- then
130  else ( bit:0 ) f.dcf.CET fclr   f.dcf.CEDT fset? if dcfErrCnt-- then
131  then
132;                                                           #18 >rt
133\ #19  1: leap second announcement
134\ #20  always 1: start of encoded time
135:noname
136    if   ( bit:1 ) dcfErrCnt--
137    then
138    f.dcf.parerr      fset
139    f.dcf.parerr.min  fset
140    f.dcf.parerr.hour fset
141    f.dcf.parerr.date fset
142    0 dcfPar !
143;                                                           #20 >rt
144\ #21 .. #28: minute
145\ minute ones
146:noname if ( bit:1 )   #1 dcfTMin   +!  dcfPar++ then ;     #21 >rt
147:noname if ( bit:1 )   #2 dcfTMin   +!  dcfPar++ then ;     #22 >rt
148:noname if ( bit:1 )   #4 dcfTMin   +!  dcfPar++ then ;     #23 >rt
149:noname if ( bit:1 )   #8 dcfTMin   +!  dcfPar++ then ;     #24 >rt
150\ minute tens
151:noname if ( bit:1 )  #10 dcfTMin   +!  dcfPar++ then ;     #25 >rt
152:noname if ( bit:1 )  #20 dcfTMin   +!  dcfPar++ then ;     #26 >rt
153:noname if ( bit:1 )  #40 dcfTMin   +!  dcfPar++ then ;     #27 >rt
154\ minute parity bit
155:noname
156  if   ( bit:1 )                       dcfPar++ then
157  ( always )
158  dcfPar? if f.dcf.parerr.min fclr  dcfErrCnt-- then
159  0 dcfPar !
160;                                                           #28 >rt
161
162\ #29 .. #35: hour
163\ hour ones
164:noname if ( bit:1 )   #1 dcfTHour  +!  dcfPar++ then ;     #29 >rt
165:noname if ( bit:1 )   #2 dcfTHour  +!  dcfPar++ then ;     #30 >rt
166:noname if ( bit:1 )   #4 dcfTHour  +!  dcfPar++ then ;     #31 >rt
167:noname if ( bit:1 )   #8 dcfTHour  +!  dcfPar++ then ;     #32 >rt
168\ hour tens
169:noname if ( bit:1 )  #10 dcfTHour  +!  dcfPar++ then ;     #33 >rt
170:noname if ( bit:1 )  #20 dcfTHour  +!  dcfPar++ then ;     #34 >rt
171\ hour parity bit
172:noname
173  if   ( bit:1 )                        dcfPar++  then
174  ( always )
175  dcfPar? if f.dcf.parerr.hour fclr  dcfErrCnt-- then
176  0 dcfPar !
177;                                                           #35 >rt
178\ #36 .. #41: day
179\ day ones
180:noname if ( bit:1 )   #1 dcfTDay   +!  dcfPar++ then ;     #36 >rt
181:noname if ( bit:1 )   #2 dcfTDay   +!  dcfPar++ then ;     #37 >rt
182:noname if ( bit:1 )   #4 dcfTDay   +!  dcfPar++ then ;     #38 >rt
183:noname if ( bit:1 )   #8 dcfTDay   +!  dcfPar++ then ;     #39 >rt
184\ day tens
185:noname if ( bit:1 )  #10 dcfTDay   +!  dcfPar++ then ;     #40 >rt
186:noname if ( bit:1 )  #20 dcfTDay   +!  dcfPar++ then ;     #41 >rt
187\ #42 .. #44: Wochentag
188\ day of week
189:noname if ( bit:1 )   #1 dcfTWday  +!  dcfPar++ then ;     #42 >rt
190:noname if ( bit:1 )   #2 dcfTWday  +!  dcfPar++ then ;     #43 >rt
191:noname if ( bit:1 )   #4 dcfTWday  +!  dcfPar++ then ;     #44 >rt
192\ #45 .. #49: Monat
193\ month ones
194:noname if ( bit:1 )   #1 dcfTMonth +!  dcfPar++ then ;     #45 >rt
195:noname if ( bit:1 )   #2 dcfTMonth +!  dcfPar++ then ;     #46 >rt
196:noname if ( bit:1 )   #4 dcfTMonth +!  dcfPar++ then ;     #47 >rt
197:noname if ( bit:1 )   #8 dcfTMonth +!  dcfPar++ then ;     #48 >rt
198\ month tens
199:noname if ( bit:1 )  #10 dcfTMonth +!  dcfPar++ then ;     #49 >rt
200\ #50 .. #57: Jahr % 100
201\ year ones
202:noname if ( bit:1 )   #1 dcfTYear  +!  dcfPar++ then ;     #50 >rt
203:noname if ( bit:1 )   #2 dcfTYear  +!  dcfPar++ then ;     #51 >rt
204:noname if ( bit:1 )   #4 dcfTYear  +!  dcfPar++ then ;     #52 >rt
205:noname if ( bit:1 )   #8 dcfTYear  +!  dcfPar++ then ;     #53 >rt
206\ year tens
207:noname if ( bit:1 )  #10 dcfTYear  +!  dcfPar++ then ;     #54 >rt
208:noname if ( bit:1 )  #20 dcfTYear  +!  dcfPar++ then ;     #55 >rt
209:noname if ( bit:1 )  #40 dcfTYear  +!  dcfPar++ then ;     #56 >rt
210:noname if ( bit:1 )  #80 dcfTYear  +!  dcfPar++ then ;     #57 >rt
211\ date parity bit
212:noname
213  if   ( bit:1 )                        dcfPar++ then
214  ( always )
215  dcfPar? if f.dcf.parerr.date fclr  dcfErrCnt-- then
216  f.dcf.parerr.min  fclr?
217  f.dcf.parerr.hour fclr? and
218  f.dcf.parerr.date fclr? and if f.dcf.parerr fclr then
219;                                                           #58 >rt
220
221
222\ dcf.tmp.table #60 ramtable.dump \ DEBUG
223\ copy RAM table to FLASH
224dcf.tmp.table #60 >flashtable pos_cmd_map
225\ release RAM
226dcf.tmp.table to here \ fixme: possibly bad???
227
228: pos.cmd ( index -- )
229  dup 0 #60 within if
230    ( position ) pos_cmd_map +  @i execute
231  else
232    drop
233  then
234;
235
236-2 constant dcf.bit:sync59
237-1 constant dcf.bit:error
238 0 constant dcf.bit:0
239 1 constant dcf.bit:1
240: dcf.pulse>bit ( pulse -- bit|error )
241  dup  #2 < if                dcf.bit:sync59 else
242  dup  #9 < if f.dcf.err fset dcf.bit:error  else
243  dup #13 < if                dcf.bit:0      else
244  dup #20 < if f.dcf.err fset dcf.bit:error  else
245  dup #26 < if                dcf.bit:1      else
246               f.dcf.err fset dcf.bit:error
247  then then then then then
248  swap drop
249;
250
251: dcf.led ( t/f -- )
252  if led_dcf on else led_dcf off then
253;
254: dcf.dbg.minute.over ( -- )
255  space
256  f.dcf.err fclr? if
257    [char] O emit
258  else
259    [char] F emit
260  then
261  cr
262;
263: dcf.dbg.second.over ( -- )
264  f.dcf.dbg.rx fset? if
265    dcfBit @ case
266      dcf.bit:0      of [char] . emit endof
267      dcf.bit:1      of [char] + emit endof
268      dcf.bit:error  of [char] / emit endof
269      dcf.bit:sync59 of [char] S emit endof
270    endcase
271  then
272;
273
274
275
276\ --- dcf.tick: 128/s, sample signal  ----
277Rdefer dcf.tick
278Rdefer dcf.sec
279Rdefer dcf.min
280
281: (dcf.tick)
282    dcfClock  1 tick +!         \ keep dcfClock ticking
283    dcfTick++/mod               \ .
284    dcfEdge<<1                  \ . edge detection
285
286    _dcf pin_low? if
287        \ active!
288        dcfPulse++
289        dcfEdge+1
290        -1 dcf.led
291    else
292        0 dcf.led
293    then
294
295    dcfEdge? if                 \ keep dcfClock in sync with radio signal
296        dcfTick.set             \ . this might be bad occasionally
297    then
298
299    \ dcfTick: 0 .. 127
300    \ 127 indicates the last tick in this second
301    dcfTick @ ticks/sec 1- = if
302        dcf.sec                 \ handle "one second over"
303    then
304
305    \ it is unclear whether deriving the dcf-second is
306    \ ok from dcfTick, or whether ist must be
307    \ dcfClock tick
308    \ (dcf.tick) does both increments
309    \ dcfTick is clear when calling dcf.sec
310    \ dcf.sec clears dcfClock tick
311;
312
313\ --- dcf.sec: 1/s, book keeping ---------
314: dcf.tmp.counter.reset
315  #59 dcfPos  !
316  0 dcfTMin   !
317  0 dcfTHour  !
318  0 dcfTDay   !
319  0 dcfTWday  !
320  0 dcfTMonth !
321  0 dcfTYear  !
322  0 dcfPar    !
323;
324: dcfTemp>dcfClock
325  dcfClock
326  0                       tick  !
327  0                       sec   !
328  dcfTMin   @             min   !
329  dcfTHour  @             hour  !
330  dcfTDay   @  1-         day   !
331\ dcfTWday  @             ?
332  dcfTMonth @  1-         month !
333  dcfTYear  @  Century +  year  !
334;
335
336: dcf.dbg.sec ( -- ) f.dcf.dbg fset? if dcf.dbg.second.over then ; \ debug
337: dcf.dbg.min ( -- ) f.dcf.dbg fset? if dcf.dbg.minute.over then ; \ debug
338
339: (dcf.sec)
340  dcfClock timeup
341  0 tick !
342
343  \ evaluate pulse
344  dcfPulse @ dcf.pulse>bit  dup dcfBit !
345  ( bit ) case
346
347    dcf.bit:0      of  0 dcfPos @ pos.cmd  dcf.dbg.sec  endof
348
349    dcf.bit:1      of  1 dcfPos @ pos.cmd  dcf.dbg.sec  endof
350
351    dcf.bit:error  of  f.dcf.err fset      dcf.dbg.sec  endof
352
353    dcf.bit:sync59 of
354      dcf.min
355      dcf.tmp.counter.reset
356      dcfErrCnt.set
357    endof
358
359  endcase
360  dcfPulse.clr           \ reset pulse
361  dcfPos++
362;
363
364: (dcf.min) ( -- )
365  dcfPos @ #59 =  if dcfErrCnt-- then
366  dcfErrCnt @ 0=  if f.dcf.err fclr then
367  \ this block runs at the *end* of second "59" i.e.
368  \ at the start of second "00", thus copy counters
369  f.dcf.err fclr? if
370    f.dcf.commit fset? if
371      dcfTemp>dcfClock
372      dcfClock>MasterClock
373      space [char] C emit
374      f.dcf.insync fset \ dcfClock is in sync!
375      f.dcf.commit fclr
376    then
377  then
378  dcf.dbg.sec
379  dcf.dbg.min
380  f.dcf.commit fclr? if
381    \ clear debug flags --- this is only to demonstrate resync/commit
382    f.dcf.dbg fclr
383  then
384;
385
386\ --- init: enable DCF clock -------------
387: +dcf
388  _dcf pin_input
389  ['] (dcf.tick) to dcf.tick
390  ['] (dcf.sec)  to dcf.sec
391  ['] (dcf.min)  to dcf.min
392
393  0 dcfPos !        \ assume we are at 0
394  f.dcf.err fset    \ error unless proven otherwise
395  f.dcf.commit fset \ request to set dcfClock
396  f.dcf.insync fclr \ not in sync yet!
397  dcfErrCnt.set
398;
399: -dcf
400  ['] noop to dcf.tick
401  ['] noop to dcf.sec
402;