jongar47
Published © GPL3+

Software Radio Clock for Windows

Decode and display the 60KHz time signal in MS windows.

IntermediateShowcase (no instructions)792
Software Radio Clock for Windows

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
Any Arduino board. Just make sure the interrupt pin matches the board
×1
Canaduino Atomic clock receiver module
https://www.universal-solder.ca https://www.universal-solder.ca
×1
Box 130x68x44 mm
https://www.bitsbox.co.uk/
×1
stripboard 9x25 holes 25x64 mm
https://www.bitsbox.co.uk/
×3
H11 L1M optoisolator
https://www.bitsbox.co.uk/
×1
resistor 1k ohm 0.5watt
https://www.bitsbox.co.uk/
×3
single pole single throw miniture on/off switch
https://www.bitsbox.co.uk/
×1
Red Led 5mm
https://www.bitsbox.co.uk/
×1
Led fixing ring to fit 5mm led
https://www.bitsbox.co.uk/
×1
Dual in Line Socket 6 pin
https://www.bitsbox.co.uk/
×1
3 pin stereo socket 3.5mm
https://www.bitsbox.co.uk/
×1
3pin streo plug 3.5mm
×1
3 wire headphone lead 5metres
https://www.bitsbox.co.uk/
×1
PP3 battery
×1
PP3 connector with two tail
×1
Reel of kynar wire 30awg
×1

Hand tools and fabrication machines

2mm drill tip (for isolating the necessary copper strips on the strip boards)
soldering iron miniture tip
https://www.bitsbox.co.uk/
solder flux
https://www.bitsbox.co.uk/
miniture electrical pliers
https://www.bitsbox.co.uk/
miniture electrical cutters
https://www.bitsbox.co.uk/

Story

Read more

Schematics

Arduino_Lazarus_60KHz_Clock

A self extracting WinZip file.
All the relevant files needed to understand and make this project.
A pdf reader such as acrobat is required to read them.
Lazarus requires installing on the PC.

Code

Arduino-Lazarus_MSF Clock

C/C++
Arduino interrupt sketch
//**************************************************************************************************
// Serial MSF Test Clock (C) Copyright 2014 Phil Morris              

// a "Proof of Concept" sketch for the Arduino Uno Rev 3 and an MSF Clock module      
// No apologies given for the messy code as this is a sketch simply to show       
// some ideas around decoding MSF time signals. The code takes account of Leep        
// seconds and performs full parity checking. A simple output is provided to the      
// Serial port at 9600 baud. BST/GMT is decoded along with UTC & DUT difference.      
// even partial data reception can produce a valid output as long as seconds 17 thru 59     
// are received correctly.                    
//                                              
// For diagnostic purposes, the minimum and maximum pulse lengths are also displayed in ms  
//                          
// You may use, modify and/or distribute this sketch as long as you leave this text intact. 

//****************************************************************************************************
   // MSF 60KHz Interrupt driven decode program to feed Lazarus/Delphi(Pascal) high level decode program.
   // John Garrett 2020.
   // Modified from a part of sketch copyright of Phil Morris.

   // the msf transmitter is switched off for brief intervals (on-off keying) near the beginning of each second
   // to encode the current time and date. When carrier is low data is active. Every second the 
   // carrier goes low to signify data. Apart from the first second every second consists
   // of a 100ms pulse. This is followed by no data when the carrier is high for 900ms or
   // another 100ms pulse giving 200ms all together signifying an A data. Or an extra 100ms
   // pulse making 300ms in all, giving an A and B data. For B data only the second pulse
   // consists of the statuary 100ms low followed by 100ms high followed by another 100ms low
   // beginning of every minute the first second consists of 500ms low followed by 500ms high.
   // each second pulse apart from the minute marker should be up to 300ms low and up to 900ms high.
   // the data during the current minute is for the following minute of time.

const  int CARRIER_OFF = LOW;  // this constant is set for non-inverted MSF receiver output ie Carrier_Off equals a low. 
                               // invert it if your output = HIGH for carrier off.

const int MSFPIN = 3;            // the Arduino pin for the MSF device (2 or 3).

volatile long pulseStart = 0;        // milliseconds when start of pulse occurred.                      
volatile long pulseEnd = 0;          // milliseconds when pulse ended.
volatile long lastPulseStart = 0;    // the previous pulse start value.  
volatile int pulseLength = 0;        // length of pulse/100 as an integer.
volatile bool bitBonly = false;      // set if a 'B' only pulse detected.
volatile bool printTime = false;
volatile bool sync = false;           //true if valid start pulse.
volatile int counter = 0;               // seconds counter.
volatile int LED_PIN = 13;          // pin LED is attached (optional).
volatile int minMaxPulse = 0;       // actual length in millis.

void setup() {
  Serial.begin(9600);               // start the Serial port
  attachInterrupt(MSFPIN - 2, MsfPulse, CHANGE);  // set the interrupt
  pinMode(LED_PIN,OUTPUT);              // configue the LED pin 
}

void loop()
{
}

void MsfPulse()  // interrupt routine which is called every time the MSF receiver output changes state
{
// This routine is called every time the selected Interrupt pin changes state. If it is the start of
// a pulse, the millis count is stored in "pulseStart". If it is the end of a pulse the millis count
// is stored in "pulseEnd". "pulseLength" is the result in millis/100. The data is processed to produce an
// integer 1 - 5 representing 100 - 500 ms pulses (no "4" is decoded).If pulselength = 5 then the sync flag becomes true
// counter is then = '0'. Once counter reaches == '59' the sync flag becomes false. 
// If the sequence was 100ms off + 100ms on + 100ms off, this is a 'B' only bit.So if this start pulse is less than 300ms 
// after the last start pulse it must be a double 100ms pulse second resulting a bitBonly flag being true.

  
  bitBonly = false;            // clear the bitOnly flag
  bool pinState = digitalRead(MSFPIN);  // get the state of the interrupt pin
  
  // is this a pulse start?

 if (pinState == CARRIER_OFF) // pulse or sub-pulse has started, carrier going low
  {    
    pulseStart = millis();    // pulseStart = current millis everytime the MSFPIN goes low (carrier going low)   
    digitalWrite(LED_PIN,HIGH); // turn on LED
    counter = counter +1;       
    if (printTime == true && counter == 60)       
      {
        Serial.print('6');       // signify to Lazarus to display time scenario
          if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
          Serial.print(counter);                      
          Serial.print(".");           
      }  
    return;            // until there's a another interrupt change
  }    
  

 // is it a pulse end?

 if(pinState != CARRIER_OFF)               // pulse end, MSFPIN goes high (carrier going high)
  {        
    pulseEnd = millis();                   // set the pulse end ms,the MSFPIN goes high   
    minMaxPulse = pulseEnd - pulseStart;   // actual length in millis    
    pulseLength = abs((minMaxPulse) / 100);       // get the pulse length in ms/100        
    pulseStart = millis();                // set the pulseStart to current millis
    if (minMaxPulse < 90) return;             // the pulse is too short ("noise"), return    
    if (counter == 59 && sync) pulseLength = 4;      // 59th second    
    
// if the sequence was 100ms off + 100ms on + 100ms off, this is a 'B' only bit
// so, if this start pulse is less than 300ms after the last start pulse it must be
// a double 100ms pulse second

    if(pulseStart - lastPulseStart < 300) bitBonly = true;  // this is a 'B' bit
    lastPulseStart = pulseStart;        // keep the last pulse start ms count
    digitalWrite(LED_PIN,LOW);          // turn off the LED    
  }

 switch(pulseLength) // start processing the valid pulse
  {
    case 5: // check for start pulse i.e. 500ms  (11111)
      {
        if(minMaxPulse > 490 && minMaxPulse < 550)
        {
          sync = true;
          printTime = false;
          counter = 0;
          Serial.print('5');       // signify to Lazarus a sync scenario
          if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
          Serial.print(counter);                      
          Serial.print(".");
        }       
        break;
      }

    case 4:  // 59th second tell Lazarus to compute time (100)
      {        
        sync = false;
        Serial.print('4');       // signify to Lazarus to compute time
        if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
        Serial.print(counter);                      
        Serial.print(".");
        printTime = true;                
        break;
      }

    case 3:  // check for 300ms pulse, this is an 'A' + 'B' bit case   (111)  
     {
        if(minMaxPulse > 290 && minMaxPulse < 350 && sync)                      
         {           
           Serial.print('3');       // signify to Lazarus a 'A + B' data scenario
           if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
           Serial.print(counter);                      
           Serial.print(".");
         }
      break;    
     }

    case 2:  // check for 200ms pulse, this is an 'A' bit case   (110)
      {
        if(minMaxPulse > 190 && minMaxPulse < 250 && sync)                
         {           
           Serial.print('2');       // signify to Lazarus a 'A' data scenario
           if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
           Serial.print(counter);                      
           Serial.print(".");
         }
        break;              
      }

    case 1:  // check for 100ms pulse   (101 or  100)
      {
        if(minMaxPulse > 90 && minMaxPulse < 150 && sync)        
          {                        
            if(bitBonly)
              {
               counter = counter-1;   // to prevent a double count 'B' data only
               Serial.print('1');       // signify to Lazarus a 'B' data scenario
               if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
               Serial.print(counter);                      
               Serial.print(".");
              }
            else
              {               
               Serial.print('0');       // signify to Lazarus a 'No' data scenario
               if (counter <= 9) Serial.print('0');   //too satisfy Lazarus program
               Serial.print(counter);                      
               Serial.print('.');
              }
          }   
        break;
      }
  
  }
 
}

Lazarus Pascal source file

Pascal
Pascal source file
unit mfs60;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls,
  StrUtils,Controls.SegmentDisplay, Controls.Blinker, CPort;


type

  { TForm1 }

  TForm1 = class(TForm)
    BlinkerBlue: TBlinker;
    BlinkerGreen: TBlinker;
    BlinkerPurple: TBlinker;
    BlinkerRed: TBlinker;
    BlinkerWhite: TBlinker;
    BlinkerYellow: TBlinker;
    ComPort4: TComPort;
    ImageList1: TImageList;
    LabelBST: TLabel;
    LabelBstImm: TLabel;
    LabelPEBits: TLabel;
    LabelSync: TLabel;
    LabelParity: TLabel;
    LabelComPort: TLabel;
    LabelDay: TLabel;
    LabelDayName: TLabel;
    LabelDow: TLabel;
    LabelDut: TLabel;
    LabelDutms: TLabel;
    LabelMonth: TLabel;
    LabelSeconds: TLabel;
    LabelTime: TLabel;
    LabelYear: TLabel;
    RadioButtonDutMinus: TRadioButton;
    RadioButtonDutPlus: TRadioButton;
    SegmentDisplayDay: TSegmentDisplay;
    SegmentDisplayDow: TSegmentDisplay;
    SegmentDisplayDut: TSegmentDisplay;
    SegmentDisplayMonth: TSegmentDisplay;
    SegmentDisplaySeconds: TSegmentDisplay;
    SegmentDisplayTime: TSegmentDisplay;
    SegmentDisplayYear: TSegmentDisplay;

    procedure Setup(Sender: TObject);
    procedure Close(Sender: TObject; var CloseAction: TCloseAction);
    procedure DataInput(Sender: TObject; Count: Integer);

  private
         procedure DataSort;
         function Dut1(start,finish:integer):string;
         function Year:string;
         function Month:string;
         function Day: string;
         function DOW: string;
         function Time:string;
         function BcdToStr(bit,count:integer):string;
         function ParityCheck(Sbit,count,Pbit:integer):boolean;
  public

  end;

var
  Form1: TForm1;
  bufferA, bufferB: array[0..59] of integer;
  Strbuff: string;
  Sync: Boolean;
  Bst: Integer;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Setup(Sender: TObject); //Form create event
begin
     SegmentDisplayDay.Text:= '';
     SegmentDisplayMonth.Text:= '';
     SegmentDisplayYear.Text:= '';
     SegmentDisplayDow.Text:= '';
     SegmentDisplayTime.Text:= '';
     SegmentDisplaySeconds.Text:= '';
     SegmentDisplayDut.Text:= '';
     LabelDayName.Caption:='';
     BlinkerYellow.IsOn:= false;
     BlinkerPurple.IsOn:= false;
     BlinkerGreen.IsOn:= false;
     BlinkerRed.Ison:= false;
     BlinkerBlue.Ison:= false;
     LabelPEBits.Visible:= false;
     RadioButtonDutPlus.Checked:= false;
     RadioButtonDutMinus.Checked:= false;
     Strbuff:='';
     Sync:= false;
     Bst:= 0;

     ComPort4.LoadSettings(stIniFile,'C:\ComPortSettings.ini');

     if not ComPort4.Connected then
  begin
    try
      ComPort4.Open;
    except
      ShowMessage('Unable to open COM port.');
    end;
  end;
  if ComPort4.Connected then
      BlinkerBlue.IsOn:= true
    else
    BlinkerBlue.IsOn:= false;

end;


procedure TForm1.Close(Sender: TObject; var CloseAction: TCloseAction);
begin
     ComPort4.Close;
     CloseAction:= caFree;
end;

procedure TForm1.DataInput(Sender: TObject; Count: Integer);
var
  Str, s: String;
begin
  // Receives  msf data from Arduino.
  Str:= '';
  ComPort4.ReadStr(Str, Count);
  Strbuff:= Strbuff + Str;  //Save in Global Buffer
  s:= RightStr(Str,1);
  if s = '.' then  DataSort;
end;

procedure TForm1.DataSort;
var
  secpointer,counter,error: string;
  i,j,n: integer;

begin
  secpointer:= LeftStr(Strbuff,1);
 counter:= MidStr(Strbuff,2,2);
 if counter = '60' then counter := '00';
 j:= StrToInt(secpointer);
 n:= StrToInt(counter);
 SegmentDisplaySeconds.Text:= counter;

 if odd(n) then BlinkerWhite.IsOn:= true //switch secs indicator on/off
 else
 BlinkerWhite.IsOn:= false;


 Case j of
   6: begin                                 //show time at 00
        SegmentDisplayDut.Text:= Dut1(1,8);
        SegmentDisplayDut.Text:= Dut1(9,16);
        SegmentDisplayYear.Text:= Year;
        SegmentDisplayMonth.Text:= Month;
        SegmentDisplayDay.Text:= Day;
        SegmentDisplayDow.Text:= LeftStr(DOW,1);
        LabelDayName.Caption:= MidStr(DOW,2,(Length(DOW)-1));
        SegmentDisplayTime.Text:= Time;

        if Bst = 0 then                    //GMT Time
        begin
          BlinkerYellow.IsOn:= false;
          BlinkerPurple.IsOn:= false;
        end;

        if Bst = 1 then                   //BST imminent
        begin
          BlinkerYellow.IsOn:= false;
          BlinkerPurple.IsOn:= true;
        end;

        if Bst = 2 then
        begin                  //BST Time
          BlinkerYellow.IsOn:= true;
          BlinkerPurple.IsOn:= false;
        end;

        for i:= 0 to 59 do  // Clear time Buffers
        begin
          bufferA[i]:= 0;
          bufferB[i]:= 0;
        end;

      end;

   5: begin
        Sync:= true;
        BlinkerGreen.IsOn:= true;
        bufferA[n]:= 5;
        bufferB[n]:= 5;
      end;

  4: begin                  // 59 seconds
        if Sync then
        begin
          Sync:= false;
          BlinkerGreen.IsOn:= false;

          Bst:= 0;                           // Reset to GMT flag
          if bufferB[53] = 1 then Bst:= 1;   //BST imminent flag
          if bufferB[58] = 1 then Bst:= 2;  // BST flag

           error:= '';

          if not ParityCheck(17,8,54) then error:= '17 to 24, ';
          if not ParityCheck(25,11,55)then error:= error + '25 to 35, ';
          if not ParityCheck(36,3,56) then error:= error + '36 to 38, ';
          if not ParityCheck(39,13,57)then error:= error + '39 to 51';

          If Length(error) = 0 then
            begin
              BlinkerRed.IsON:= false;
              LabelPEBits.Visible:= false;
              LabelPEBits.Caption:= '';
            end
            else
            begin
              BlinkerRed.IsON:= true;
              LabelPEBits.Visible:= true;
              LabelPEBits.Caption:= 'Bits '+ error;
            end;


          bufferA[n]:= 4;
          bufferB[n]:= 4;

        end;

     end;

  3: begin              //A & B pulse
        if Sync then
        begin
          bufferA[n]:= 1;
          bufferB[n]:= 1;
        end;
      end;
  2: begin              //A pulse only
        if Sync then
        begin
          bufferA[n]:= 1;
          bufferB[n]:= 0;
        end;
      end;
  1: begin             //B pulse only
        if Sync then
        begin
          bufferA[n]:= 0;
          bufferB[n]:= 1;
        end;
      end;
  0: begin             //No A or B pulse
        if Sync then
        begin
          bufferA[n]:= 0;
          bufferB[n]:= 0;
        end;
      end;

  end;
 If RightStr(Strbuff,1)= '.' then  Strbuff:= '';
end;


function TForm1.Dut1(start,finish:integer):string;
var
  j,n:integer;
begin
  RadioButtonDutPlus.Checked:= false;
  RadioButtonDutMinus.Checked:= false;

  j:= 0;
  for n := start to finish do
  begin
    if bufferB[n] = 1 then j := j+1;
  end;

  if (finish = 8) and (j > 0) then RadioButtonDutPlus.Checked:= True;
  if (finish = 16) and (j > 0) then RadioButtonDutMinus.Checked:= True;

  j:= j*100;
  Result:= IntToStr(j);
end;

function TForm1.Year:string;
var
  s,t: string;
begin
  s:= BcdToStr(20,4);
  t:= BcdToStr(24,4);
  Result:= s + t;
end;

function TForm1.Month:string;
var
  s,t: string;
begin
  s:= BcdToStr(25,1);
  t:= BcdToStr(29,4);
  Result:= s + t;
end;

function TForm1.Day:string;
var
  s,t: string;
begin
  s:= BcdToStr(31,2);
  t:= BcdToStr(35,4);
  Result:= s + t;
end;

function TForm1.DOW:string;
var
  i: integer;
  s: string;
begin
  s:= BcdToStr(38,3);
  i:= StrToInt(s);
  Case i of
    0: Result:= s + 'Sunday';
    1: Result:= s + 'Monday';
    2: Result:= s + 'Tuesday';
    3: Result:= s + 'Wednesday';
    4: Result:= s + 'Thursday';
    5: Result:= s + 'Friday';
    6: Result:= s + 'Saturday';
  end;
end;

function TForm1.Time:string;
var
  s,t,u,v: string;
begin
  s:= BcdToStr(40,2);
  t:= BcdToStr(44,4);
  u:= BcdToStr(47,3);
  v:= BcdToStr(51,4);
  Result := s + t + u + v;
end;

function TForm1.BcdToStr(bit,count:integer):string;
var
  total: integer;
begin
  total:= 0;
  while count > 0 do
  begin
    if bufferA[bit] = 1 then total:= 1;
    count:= count-1;
    if count = 0 then break;
    if bufferA[bit-1] = 1 then total:= total + 2;
    count:= count-1;
    if count = 0 then break;
    if bufferA[bit-2] = 1 then total:= total + 4;
    count:= count-1;
    if count = 0 then break;
    if bufferA[bit-3] = 1 then total:= total + 8;
    count:= count-1;
  end;
  Result:= IntToStr(total);
end;

function TForm1.ParityCheck(Sbit,count,Pbit:integer):boolean;
var
  total,n: integer;
begin
  total:= 0;
  for n:= 0 to count -1 do
  if bufferA[Sbit + n] = 1 then total := total + 1;

  total:= total + bufferB[Pbit];

  if odd(total) then Result:= true
  else
  Result:= false;
end;

end.

Credits

jongar47
0 projects • 0 followers
Contact

Comments

Please log in or sign up to comment.