Retro-programming: ZX Spectrum as a G-Shock clock

The question is: could we turn the Spectrum into a G-Shock clock, synchronized via radio with an atomic clock somewhere in Europe via radio? Let’s see…

On the paper, it sounds worse than it actually is. Lately I’ve been trying to figure out actual use cases for the ZX beyond writing just one more game. I have this friend who has a Casio G-Shock and was showing me the wonders of it getting its time magically synchronized via some exotic radio signal. As a telecom engineer I was immediately intrigued with the whole G-Shock thing. I won’t be giving a lecture on the topic, but you can read on where the radio signal comes from (Germany and UK in Europe) here:

https://dk7ih.de/decoding-time-signals- … ontroller/

The DFC77 signal

There are several of these radio signal being broadcast all over the world, but i will focus in one of them, the one which provides service in Spain. That is DFC77. This signal is modulated on a 77.5 KHz carrier and transmitted from Mainflingen, Germany. The picture below shows the signal coverage.

The radio signal is both modulated in amplitude and phase. This is how the AM modulated signal looks like:

For our case we’ll focus on the AM part. Once demodulated, that is, AM carrier removed and keeping positive half only, the signal looks like this:

There is a continuous transmission that sets the 100% signal level on the receiver, with the signal decaying to a 15% of the maximum at the beginning of every second. That marks the start of the second. Depending on the duration of the decay:

  • 0.1s decay stands for a 0.
  • 0.2s decay stands for a 1.
  • No decay: that indicates second 59 in the minute (see figure below). On the next second a new minute starts. This detection gives us the next level (minutes) anchor.

Obviously, there is an impact related to the distance from the receiver to the antenna. For instance, a receiver at 1,000km will have a 3ms delay, but this does not seem critical for regular date and time applications.

Technical solution

Hardware

Several stages need to built together. This is a general diagram showing how it looks:

Radio signal to electrical signal conversion

The radio board demodulates the AM radio signal and provides an electrical signal that captures the signal before modulation. There are some commercial boards that will receive the radio signal and convert it to electric signal. Those boards are providing different output levels (see table below).

A typical example of the board plus antenna and its characteristics:

https://www.universal-solder.ca/product … or-europe/

Electrical levels adaption

We need to adapt the signal provided by the selected board level to the I/O port of the Spectrum. This requires a minimal circuitry between the radio board and the Speccy.

Assuming the radio board is working as expected, sampling is something the Speccy should be able to do through the I/O port. There is no actual communication with the radio board. It’s purely a matter of data acquisition.

—- UNDER CONSTRUCTION —

Read ZX data port

If we decide to read the signal at the ZX data port D0, we just need to sample it as many times as needed by reading the port and masking the corresponding bit:

IN A, (254)

AND A, %00000001

Software

In this section, I´ll describe what the decoder needs to do in order to decode the 60 bits provided by the signal every 60 seconds.

Identifying every bit

In order to determine the duration of the signal decay, several strategies are possible, all based in sampling the signal provided by the board. Let’s take a simple approach: count how many contiguous samples after the first detected decay give a low value and decide if the amount corresponds to a 1, a 0 or an indeterminate result. For instance, if we sample the input signal 100 times per second:

  • (around) 10 low value samples (0.1 secs) would correspond to a 0.
  • (around) 20 low value samples (0.2 secs) would correspond to a 1.
  • no low values would correspond to second 59, where no information is transmitted.

If we manage to get those bits decoded from the radio signal, the protocol itself is pretty straightforward.

Decoding the signal

A 60 bit frame is produced every 60 seconds, at a rate of 1 bps, which is a very affordable transmission speed even for a Speccy. This is the frame content:

Frame synchronization: the signal provided by the radio board itself will provide the end of every minute, as there is no voltage decay in second 59 of every minute. Next second (next decay) will provide the first bit of the frame. With this, we are frame synchronized. The frame is delimited every 60 seconds, so there is no real risk of losing frame synchronization.

Decoding the fields:

  • Leap second warning bit
  • Imminent change of CET to CEST, or vice versa
  • Announcement bit, Central European Time (CET) / Central European Summer Time (CEST) bit
  • Abnormal transmitter operation identification bit.
  • Several parity bits.
  • Data integrity: Although there is no CRC or similar value provided, there are several ways to verify data integrity.

  • Bits 0 and 20 are always set to 1.
  • Minute, hour and date have their own parity bit.
  • Additionally, although the protocol does not require it we can verify if minutes, hour, day, day of the week, month and year exceed expected maximum values.

JavaScript code

The code is written in JavaScript for Z80, so that the code is easier to follow. If you want to try it yourself, you can find the compiler here:

https://github.com/reeagbo/Jassco/

include ("io.asc")
include ("math.asc")

// Time/Date decoder for DCF77 signal
// https://en.m.wikipedia.org/wiki/DCF77

// this array simulates the bit stream received
var input_bits= [
0, // 00: Start of minute. Always 0.
0,0,0,0,0,0,0,0,0, // 01-09: Civil warning
0,0,0,0,0,

0, // 15
0, // 16: Summer time announcement. It works with bits 17 and 18.
0, // 17: CEST active bit (UTC+1)
0, // 18: CET active bit (UTC+2)
0, // 19: Leap second announcement
1, // 20: Start of encoded time. Always 1.

// 21-27: minutes
1, // 21: +1 minutes
0, // 22: +2 minutes
0, // 23: +4 minutes
1, // 24: +8 minutes
1, // 25: +10 minutes
0, // 26: +20 minutes
1, // 27: +40 minutes
0, // 28: minutes even parity

// 29-34: hour
1, // 29: +1 hour
1, // 30: +2 hour
0, // 31: +4 hour
0, // 32: +8 hour
0, // 33: +10 hour
1, // 34: +20 hour
1, // 35: hour even parity

// 36-41: day of the month
1, // 36: +1 day
0, // 37: +2 day
0, // 38: +4 day
0, // 39: +8 day
1, // 40: +10 day
1, // 41: +20 day

// 42-44: day of the week
1, // 42: +1 day of the week
1, // 43: +2 day of the week
1, // 44: +4 day of the week

// 45-49: month number
0, // 45: +1 month
0, // 46: +2 month
1, // 47: +4 month
1, // 48: +8 month
0, // 49: +10 month

// 50-57: year within century
1, // 50: +1 year
0, // 51: +2 year
0, // 52: +4 year
1, // 53: +8 year
1, // 54: +10 year
0, // 55: +20 year
0, // 56: +40 year
1, // 57: +80 year

0, // 58: date even parity (36-57)
0, // 59: no AM modulation
0, // 60: very rare. Only used in case of leap second being inserted.
]

var dow_str =["","Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
var month_str = ["","January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
const current_century=2000 // current century, as year is 2-digit only

var input_bits_pointer= 0 // pointer for input bit sequence array
var frame_store = Array(60) // array to store 60 (or 61) input bits
var frame_store_pointer = 0 // pointer to store array
var minute_length =60 // minute length, needed for the leap second case (61 seconds)
var i=0
var send_bit=0
var valid_frame= true

var dec_hours=0 // decoded hour
var dec_minutes=0 // decoded minutes
var dec_day=0 // decoded day
var dec_dow=0 // decoded dey of the week
var dec_month=0 // decoded month
var dec_year=0 // decoded year
var parity_minutes=0 // even parity for minutes
var parity_hour=0 // even parity for hour
var parity_date=0 // even parity for date

// this function takes the full time frame and decodes it.
function decode_frame() {
console.log ("Frame decoding…")

// Hour decode
dec_minutes= frame_store[21]1+frame_store[22]2+frame_store[23]4+frame_store[24]8+
frame_store[25]10+frame_store[26]20+frame_store[27]40 dec_hours= frame_store[29]1+frame_store[30]2+frame_store[31]4+frame_store[32]8+ frame_store[33]10+frame_store[34]20 // Date decode dec_day= frame_store[36]1+frame_store[37]2+frame_store[38]4+frame_store[39]8+ frame_store[40]10+frame_store[41]20 dec_dow= frame_store[42]1+frame_store[43]2+frame_store[44]4
dec_month= frame_store[45]1+frame_store[46]2+frame_store[47]4+frame_store[48]8+
frame_store[49]10 dec_year= current_century+frame_store[50]1+frame_store[51]2+frame_store[52]4+
frame_store[53]8+frame_store[54]10+frame_store[55]20+frame_store[56]40+
frame_store[57]*80

// Bits 15-19
// Bit 15: antenna issue. No impact in decoding.
// Bit 16: Summer time announcement. It works together with bits 17 and 18.
if (frame_store[16] == 1) {
console.log ("Summer time change announced.")
// Bit 17: CEST active bit (UTC+1)
if (frame_store[17] == 1) console.log ("CEST in effect.")
// Bit 18: CET active bit (UTC+2)
if (frame_store[18] == 1) console.log ("CET in effect.")
}
// Bit 19: Leap second announcement. Should remain active for 24 hours if leap second is happening.
if (frame_store[19] == 1) console.log ("Leap second announced.")

console.log ("Checking frame validity…")
valid_frame = true
// fixed bit validity checks
if (frame_store[0] != 0) valid_frame = false
if (frame_store[20] != 1) valid_frame = false

// parity validity checks
// minutes parity
parity_minutes= frame_store[21]+frame_store[22]+frame_store[23]+frame_store[24]+frame_store[25]+frame_store[26]+frame_store[27]
parity_minutes= parity_minutes % 2
if (parity_minutes != frame_store[28]) {
console.log ("Invalid minutes parity.")
valid_frame=false
}
// hour parity
parity_hour= frame_store[29]+frame_store[30]+frame_store[31]+frame_store[31]+frame_store[33]+frame_store[34]
parity_hour= parity_hour % 2
//console.log ("Minutes parity:",parity_minutes)
if (parity_hour != frame_store[35]) {
console.log ("Invalid hour parity.")
valid_frame=false
}
// date parity
parity_date= frame_store[36]+frame_store[37]+frame_store[38]+frame_store[39]+frame_store[40]+frame_store[41]+
frame_store[42]+frame_store[43]+frame_store[44]+frame_store[45]+frame_store[46]+frame_store[47]+
frame_store[48]+frame_store[49]+frame_store[50]+frame_store[51]+frame_store[52]+frame_store[53]+
frame_store[54]+frame_store[55]+frame_store[56]+frame_store[57]
parity_date= parity_date % 2
if (parity_date != frame_store[58]) {
console.log ("Invalid date parity.")
valid_frame=false
}

if (valid_frame){
console.log ("Frame decoding successful.")
console.log ("--------------------------------")
console.log ("Time: ", dec_hours, ":", dec_minutes)
console.log ("Date: ", dow_str[dec_dow],", ", dec_day, " ", month_str[dec_month], " ", dec_year)
console.log ("--------------------------------")
}
else {
console.log ("Invalid frame. No time available.")
}
}

// this function receives a bit from the input sequence and stores it in the frame array
// Once frame is complete, decoding is triggered
function receive_bit(r_bit) {
frame_store[frame_store_pointer]= r_bit
frame_store_pointer++
if (frame_store_pointer == 60) {
console.log ("Full frame received.")
decode_frame ()
frame_store_pointer = 0
}
}

// Main program
console.log ("G-Shock Decoder (DCF77)")

// pass all bits in input sequence for processing
if (frame_store[19]==1) minute_length=61
for (input_bits_pointer=0; input_bits_pointer<minute_length; input_bits_pointer++) {
send_bit= input_bits[input_bits_pointer]
receive_bit(send_bit)
}
minute_length =60

Leave a comment

Design a site like this with WordPress.com
Get started