DCF
The following is a description of experimenting with DCF. This is a time signal originating from Germany but covers most of Europe. It is a very simple signalling system but you would not think so looking at the data that is available. The signal is transmitted at 77.5KHz which is on the old long wave band. There is also a UK transmitter using 60KHz but as far as I know there is no ready made receiver made for this.
As an aside there are lots of ways of obtaining an accurate time standard, internet via GSM, DPS of course gives accurate time and it should also be possible to use the data that is transmitted with radio now.
To start with I used the BN641138 DCF receiver from Conrad. I don’t think that this is available from anywhere else.
The coding is very simple; one bit is transmitted every second, a 0 is 100ms long and a 1 is 200ms long
At the start of the minute a pulse is missed and so the 1000mS above will be nearer 2000mS.
As a simple scheme for testing we wait while the signal is high, then when it goes low we start a timer. When the signal goes high again at T: if it is > 100mS then it is a 1 else it is a 0. We don’t do anything then for another 600 or so mS. To detect the start of a minute, whilst waiting for the signal to go low at A, it it takes linger then a second that that is the start.
The main problem with this scheme is that it relies on a good signal, in practice (looking at the signal with a scope), there are lots of spurious transitions. I was able to get a good signal in the north of the UK but only by careful positioning of the receiver.
(** Program will work but needs updating to reflect the new functions available in rookie **)
http://www.byvac.com/mBlib/flb/Projects/dcf/dcf.bas
// dcf time code // 100 ms is 0 200ms is 1 #option only on // only load functions that don't already exists // include all of the registers although only some are needed #include "http://byvac.com/mBlib/flb/Library/PIC32MX3_Family/BRegisters/MX3_reg_timer.bas" // interrupts hold the flag addresses #include "http://byvac.com/mBlib/flb/Library/PIC32MX3_Family/BRegisters/MX3_reg_interrupt.bas" // MX3 constant TRISD 0xBF8860C0 constant TRISDCLR 0xBF8860C4 constant TRISDSET 0xBF8860C8 constant TRISDINV 0xBF8860CC constant PORTD 0xBF8860D0 constant TIMERON 1 << 15 // on bit // ***************************************************************************** // clocked with PCLK that is running at 40MHz, prescale 256 = 156.25kHz (6.4uS) // setting PR2 1/6.4uS = 156250 should see the counter reset every second so // using 1562500 is 10 seconds. // ***************************************************************************** function dcf_init() // timer_on | prescale_256 | 32_bit_on | Internal_clock @T2CON = 0x8078 // port rd1 as i/p @TRISDSET = 2 endf // just to look at timer values function xa() @PR2 = 0xffffffff dcf_init() while comkey?(2) = 0 print "\n",@TMR2 while @TMR2 < 1562500; wend @TMR2=0 wend endf dim tcs(61) function xc() dim j=0, limit = 50000, bit dim started = 0 dcf_init() while comkey?(2) = 0 @TMR2 = 0 while @TMR2 < 100000; wend while (@PORTD & 2) = 2; wend if @TMR2 > 400000 then if started = 1 then print "Finished" return endif print "\nSTART", j = 0 started = started + 1 endif print "\n",@TMR2, @TMR2 = 0 while (@PORTD & 2) = 0; wend print j, @TMR2 if @TMR2 > limit print format$(" %d",1) bit = 1 else print format$(" %d",0) bit = 0 endif tcs(j) = bit j = j + 1 if j > 60; j=0; endif wend endf function dcf_calc(parityField,start,end) dim j, value=0, parity = 0, count = 1 for j = start to end value = value + tcs(j) * count parity = parity + tcs(j) if count = 8 then count = 10 else count = count + count endif next if parityField <> 0 then parity = parity + tcs(parityField) if (parity & 1) <> 0 then print "\nminutes parity error" endif endif return value endf function xd() dim h, m dim dom=0, dow=0,month=0,year=0 m = dcf_calc(28,21,27) h = dcf_calc(35,29,34) dom = dcf_calc(0,36,41) dow = dcf_calc(0,42,44) month = dcf_calc(0,45,49) year = dcf_calc(58,50,57) print "\n",h,m print "\n",dom,month,year,dow endf function aa(start,end) dim j for j = start to end print "\n",j,tcs(j) next endf
The code is very simple and only serves to test the theory and to see if the signal is suitable for reception. To run it make sure that the port you are using is the same, or change it in dcf_init() then type xc(). The count from TRM2 is shown so that comparisons can be made. If it goes perfectly then the global array tcs() will be filled with the data, each index representing a bit value this can be decoded with xd().
Further
A more advanced scheme would be this one. An interrupt on the falling edge of the signal starts timers T4 and T2/3. Any timers can be used but the T2/T3 combination needs to be able to count for more than 1 second without overflowing. when the signal goes high again, T4 can be examined. If it is much less than 100mS or much greater than 200mS then the bit can be rejected, otherwise a 0 or 1 can be stored to the array. The interrupt needs to be switched off now for at least 600mS to avoid any spurious signals, T2/3 can be used to switch the interrupt back on say at 800mS. If when the next interrupt arrives T2/T3 is greater then 1200mS then this is the start of a new minute, or more accurately the next falling edge of the signal is second zero of the new minute.
Contributions
There are two contributions from Ross, both don’t use interrupts. The first is straightforward and simple so that it is easy to see what’s going on:
http://www.byvac.com/mBlib/flb/Projects/dcf/MSF_MX1.bas
// MSF Decoder // for MX1 and standard 20 x 4 LCD by RSH // // MSF source is inverted buffered output from Symtrik module (on eBay); // module output is inverted compared to NPL data sheet on MSF format. // Decoder assumes input in NPL-specified format - see TAF001v06 July 2007. // // Idea is to find the minute start, then ignore the next 16 seconds // as these are DUT data and do not concern us. The time code, (which // is BCD), starts at second #17, uses bit "A", has bit "B" always 1, // and is 35 seconds long. // Bit "A" lasts 100 ms, starting 100 ms after start of second marker, // so input is sampled 150 ms after input goes low: low is a "1". #option only on // only load functions that don't already exist // include all of the registers although only some are needed #include "E:/BV500/LibraryFiles/PIC32MX1_Family/BRegisters/MX1_reg_timer.bas" // Assumes LCD_1_setup (LCD drive routines) is in Flash memory // B registers for MX1: constant ANSELB 0xBF886100 constant TRISB 0xBF886110 constant TRIBCLR 0xBF886114 constant TRISBSET 0xBF886118 constant TRISBINV 0xBF88611C constant PORTB 0xBF886120 constant TIMERON 1 << 15 // on bit // ***************************************************************************** // clocked with PCLK that is running at 40MHz, prescale 256 = 156.25kHz (6.4uS) // setting PR2 1/6.4uS = 156250 should see the counter reset every second so // using 1562500 is 10 seconds. (So 400 ms = 62500; 150 ms = 23438) // ***************************************************************************** function msf_init() // timer_on | prescale_256 | 32_bit_on | Internal_clock @T2CON = 0x8078 // port RB1 as i/p poke(ANSELB+CLR, 1 << 1) // set for input @TRISBSET = 2 endf function wait_LO() // returns when input goes low (ie 1) while (@PORTB & 2) = 2; wend endf function wait_HI() // returns when input goes high (ie 0) while (@PORTB & 2)= 0; wend endf dim status$[20] function show_status() lcd_xy(0,3) lcd_puts(" ") lcd_xy(0,3) lcd_puts(status$) endf function find_start() // finds the minute start and waits 16 seconds msf_init() status$="waiting minute start" show_status() while comkey?(2) = 0 // make sure there is an exit! wait_LO() @TMR2 = 0 wait_HI() if @TMR2 > 65000 // 400 ms is 62500, minute start goes low for 500 ms status$="waiting 16s for code" show_status() @TMR2 = 0 while @TMR2 < 2460000; wend // wait 16 seconds status$="getting time code" show_status() break endif wend endf dim treg(36) // this array holds the bit values function get_time() // reads the 35 time bits dim pointer find_start() for pointer=0 to 34 wait_LO() @TMR2=0 while @TMR2<23500; wend //wait 150 ms if (@PORTB & 2) = 2 treg(pointer) = 0 else treg(pointer) = 1 endif wait_HI() next endf dim y, mo, mth$, dom, dow, dow$, h, m, function calc_time() // also calculates month name and day of week y = 80*treg(0)+ 40*treg(1)+ 20*treg(2)+ 10*treg(3)+ 8*treg(4)+ 4*treg(5)+ 2*treg(6)+ treg(7) mo = 10*treg(8) + 8*treg(9)+ 4*treg(10)+ 2*treg(11)+ treg(12) dom = 20*treg(13)+ 10*treg(14)+ 8*treg(15)+ 4*treg(16)+2*treg(17)+ treg(18) dow = 4*treg(19)+ 2*treg(20)+ treg(21) h = 20*treg(22) + 10*treg(23)+ 8*treg(24)+ 4*treg(25)+ 2*treg(26)+ treg(27) m = 40*treg(28)+ 20*treg(29)+ 10*treg(30)+ 8*treg(31)+ 4*treg(32)+ 2*treg(33)+ treg(34) select(mo) case(1) mth$="January" break case(2) mth$="February" break case(3) mth$="March" break case(4) mth$="April" break case(5) mth$="May" break case(6) mth$="June" break case(7) mth$="July" break case(8) mth$="August" break case(9) mth$="September" break case(10) mth$="October" break case(11) mth$="November" break case(12) mth$="December endselect select(dow) case(0) dow$="Sunday" break case(1) dow$="Monday" break case(2) dow$="Tuesday" break case(3) dow$="Wednesday" break case(4) dow$="Thursday" break case(5) dow$="Friday" break case(6) dow$="Saturday" endselect endf function lcd_title() lcd_xy(4,0) lcd_puts("Time by MSF") endf function write_time() dim date$[20], time$[20] status$=" " show_status() // clear the status date$= " "+int2str(dom)+" "+mth$+" 20"+y time$=dow$+" "+int2str(h)+":"+m lcd_xy(0,1) lcd_puts(" ") lcd_xy(2,1) lcd_puts(date$) lcd_xy(0,2) lcd_puts(" ") lcd_xy(2,2) lcd_puts(time$) endf function msf() // This is the main loop lcd_init() lcd_nocurs() lcd_title() while comkey?(2) = 0 // make sure there is an exit! find_start() get_time() calc_time() write_time() wait(8000) // allow time to pass wend endf
The second is more complete. It now counts seconds (almost correctly) and has parity checking, plus adding leading zeros where necessary. The main new feature is that it also deals with the problem of all such decoders, namely that by the time the information is received, the minute to which it applies is almost over. The solution is to increment the minute count, dealing with carries, and present this immediately at the start of the next minute. The same problem occurs with a DCF decoder, of course – no amount of error checking will fix bad data.
http://www.byvac.com/mBlib/flb/Projects/dcf/MSF_MX1_3.bas
// MSF Decoder for MX1 ByPic controller and standard 20 x 4 LCD display by RSH // ByPic, libraries and LCD drive code by Jim Spence // V.3 17 December 2012 // V.1 cleans up code, adds parity checking and a partial seconds count. // V.2 improves documentation, increments the time immediately after the start of the minute. // V.3 extends the seconds count to the first 16 seconds, eliminates the display of garbage // during the first minute, and fixes display errors after midnight. // MSF source is the output from Symtrik module (eBay supplier), // buffered and inverted, as module output is inverted compared to NPL data sheet on MSF format. // Decoder assumes input in NPL-specified format - see TAF001v06 July 2007. // Idea is to find the minute start, then ignore the next 16 seconds // as these are DUT data and do not concern simple time displays. // The minute start is marked by a 500 ms low period. // The time code (which is BCD) starts at second #17, uses bit "A", // always has bit "B" set to 1, and is 35 seconds long. // Seconds are not transmitted; these are counted incrementally. // A major disadvantage with these systems is that it takes nearly a minute to // receive the information for that minute, thus the display is only correct // for a few seconds (this is really bad if the parity bits are also used). // This code gets round the problem by taking the data and waiting for the next // minute start, then incrementing the minute (and dealing with any carries) // then displaying the time immediately, with a running count for the seconds. // // Bit "A" lasts 100 ms, starting 100 ms after start of second marker, so for // time code the level is sampled 150 ms after input goes low: low denotes a "1". // // The decoder is totally dependent on a good signal. Daytime signal is quite // variable, so a parity check is a good idea - however, parity is also very // erratic when the signal is poor! It is amazing how often the data are garbage, // yet the parity check is OK, and further error checking is impractical. // The parity bits are 54B to 57B, so this gets tricky - mainly keeping // track of where they are, and sampling after 250 ms. Parity bits // combined with the data provide an odd number of bits set to 1. #option only on // only load functions that don't already exist // include all of the registers although only some are needed #include "E:/BV500/LibraryFiles/PIC32MX1_Family/BRegisters/MX1_reg_timer.bas" // Assumes LCD_1_setup (LCD drive routines) is in Flash memory // B registers for MX1: constant ANSELB 0xBF886100 constant TRISB 0xBF886110 constant TRIBCLR 0xBF886114 constant TRISBSET 0xBF886118 constant TRISBINV 0xBF88611C constant PORTB 0xBF886120 constant TIMERON 1 << 15 // on bit // ***************************************************************************** // clocked with PCLK that is running at 40MHz, prescale 256 = 156.25kHz (6.4uS) // setting PR2 1/6.4uS = 156250 should see the counter reset every second so // using 1562500 is 10 seconds. (So 400 ms = 62500; 150 ms = 23438; 250 ms = 39052) // ***************************************************************************** dim y, mo, mth$, dom, dow, dow$, h, mi, ey, em, ed, et dim status$[20] , inh dim treg(45) // this array holds the time and parity bits function msf_init() // timer_on | prescale_256 | 32_bit_on | Internal_clock @T2CON = 0x8078 // port RB1 as i/p poke(ANSELB+CLR, 1 << 1) // set for input @TRISBSET = 2 endf function wait_LO() // returns when input goes low (ie 1) while (@PORTB & 2) = 2; wend endf function wait_HI() // returns when input goes high (ie 0) while (@PORTB & 2)= 0; wend endf function show_status() // puts status on bottom line lcd_xy(0,3) lcd_puts(" ") lcd_xy(0,3) lcd_puts(status$) endf function get_time() // reads the 35 time bits and the 4 parity bits dim pointer, sec=17 for pointer=0 to 40 wait_LO() @TMR2=0 lcd_xy(15,2) // display (fake) seconds count lcd_puts(":"+ int2str(sec)) sec=sec + 1 if pointer < 35 // time code bits "A" while @TMR2<23500; wend //wait 150 ms if (@PORTB & 2) = 2 treg(pointer) = 0 else treg(pointer) = 1 endif else // parity code bits "B" while @TMR2<39060; wend //wait 250 ms if (@PORTB & 2) = 2 treg(pointer) = 0 else treg(pointer) = 1 endif endif wait_HI() next endf function calc_time() // also calculates month name and day of week dim j,py=0,pm=0,pd=0,pt=0 y = 80*treg(0)+ 40*treg(1)+ 20*treg(2)+ 10*treg(3)+ 8*treg(4)+ 4*treg(5)+ 2*treg(6)+ treg(7) mo = 10*treg(8) + 8*treg(9)+ 4*treg(10)+ 2*treg(11)+ treg(12) dom = 20*treg(13)+ 10*treg(14)+ 8*treg(15)+ 4*treg(16)+2*treg(17)+ treg(18) dow = 4*treg(19)+ 2*treg(20)+ treg(21) h = 20*treg(22) + 10*treg(23)+ 8*treg(24)+ 4*treg(25)+ 2*treg(26)+ treg(27) mi = 40*treg(28)+ 20*treg(29)+ 10*treg(30)+ 8*treg(31)+ 4*treg(32)+ 2*treg(33)+ treg(34) select(mo) case(1); mth$="January" case(2); mth$="February" case(3); mth$="March" case(4); mth$="April" case(5); mth$="May" case(6); mth$="June" case(7); mth$="July" case(8); mth$="August" case(9); mth$="September" case(10); mth$="October" case(11); mth$="November" case(12); mth$="December" endselect select(dow) case(0); dow$="Sunday" case(1); dow$="Monday" case(2); dow$="Tuesday" case(3); dow$="Wednesday" case(4); dow$="Thursday" case(5); dow$="Friday" case(6); dow$="Saturday" endselect for j=0 to 7 // year parity check py=py+treg(j) next ey = ((treg(37) + py) & 1) for j=8 to 18 // month parity check pm=pm+treg(j) next em = ((treg(38) + pm) & 1) for j=19 to 21 // dow parity check pd=pd+treg(j) next ed = ((treg(39) + pd) & 1) for j=22 to 35 //time parity check pt=pt+treg(j) next et = ((treg(40) + pt) & 1) endf function lcd_title() lcd_xy(2,0) lcd_puts("Time by MSF v3") endf function write_time() dim date$[20], time$[20], h$, mi$ date$= int2str(dom)+" "+mth$+" 20"+int2str(y) if (ey + em) <> 2 date$=date$+" ?" // show parity error as a query endif if mi > 9 // fix leading zero for minutes mi$ = int2str(mi) else mi$ = "0" + int2str(mi) endif if h > 9 h$ = int2str(h) else h$ = "0" + int2str(h) endif time$ = h$ + ":" + mi$ if (ed + et) <> 2 time$=time$+" ?" // show parity error as a query endif lcd_xy(0,1) lcd_puts(" ") // clear the line lcd_xy(1,1) lcd_puts(date$) lcd_xy(0,2) lcd_puts(" ") // clear the line lcd_xy(0,2) lcd_puts(dow$) lcd_xy(10,2) lcd_puts(time$) endf function time_increment() // increments minutes immediately mi = mi + 1 if mi = 60 // fix minute overflow mi = 0 h = h+1 if h = 24 //fix hour overflow h = 0 // leave day of week to change 1 minute later! endif endif write_time() endf function find_start() // finds the minute start and waits 16 seconds dim k, v$ msf_init() status$="waiting minute start" show_status() while comkey?(2) = 0 // make sure there is an exit! wait_LO() @TMR2 = 0 wait_HI() if @TMR2 > 65000 // 400 ms is 62500, minute start goes low for 500 ms if inh = 1 time_increment() // increment the time and display it endif status$="waiting 16s for code" show_status() k = 0 // wait 16 seconds whilst displaying seconds count while k < 16 wait_LO() @TMR2 = 0 while @TMR2 < 56000; wend wait about 350 ms to avoid "B" bit transitions wait_HI() if k < 9 v$ = ":0" + int2str(k+1) // fix leading zero else v$ = ":" + int2str(k+1) endif lcd_xy(15,2) // display the seconds count lcd_puts(v$) k = k + 1 wend status$="getting time code" show_status() return endif wend endf function msf() // This is the main loop lcd_init() lcd_nocurs() lcd_title() inh = 0 // inhibit time display the first time round while comkey?(2) = 0 // make sure there is an exit! find_start() // first call has inh = 0 so no garbage display inh = 1 // now allow time display on minute start get_time() calc_time() // time is written within find_start wend endf