Hardware components | ||||||
![]() |
| × | 1 | |||
| × | 1 | ||||
| × | 1 | ||||
| × | 3 | ||||
| × | 1 | ||||
| × | 3 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Hand tools and fabrication machines | ||||||
| ||||||
| ||||||
| ||||||
| ||||||
|
This is not a new concept as regards Arduino sketches. It usually involves several libraries an Arduino controller, in my case a Uno Genuino, LCD display and a radio receiver to pick up the low frequency 60KHz signal.
In this project I wanted to make a software version that runs in Microsoft Windows. So basically there are three parts.First the radio receiver then a controller decoder in this case an Arduino Uno. Lastly a Pascal coded program to read the Uno output. I have used Lazarus2.10 combined with free Pascal 3.20 win 64.
The project can be of interest to Arduino programmers as the controller uses an interrupt only program. The idea was for the MS computer to shoulder most of the load. Those interested in serial communications between an Arduino and a PC might find this useful.
//**************************************************************************************************
// 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;
}
}
}
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.
Comments
Please log in or sign up to comment.