A few that I remember of:
So the goal for is set: Build a Phase Modulation DCF77 radio clock with some robustness in time code decoding. Additionally, this radio clock will provide a 10MHz reference frequency and a PPS (pulse per second) output.
One of the drawbacks of these simple radio clocks using the AM timecode: Although they are quite sensitive, allowing for reception in a wide range (up to 2000km from Frankfurt), they require a rather clean signal. Placing one of these in an electromagnetic distorted environment (like an electronics lab) or inside a larger concrete building (using steel reinforcement) may render the clock useless.
The timing accuracy achievable using the AM time code typically is in the 20ms ballpark. For simple radio clocks, anything better than 200ms is called a "very accurate atomic clock".
Reception of the PM / PZF time code is done by cross-correlating the known sequence against the phase demodulated receiver signal.
From other results, one expects a timing accuracy better than 100us, depending on location and signal quality down to 10us receiving the PM / PZF time code.
Yes, I can. Make an DCF77 antenna. Use isolated wire and apply an arbitrary amount of turns to the
rod - the more the better. Now add some capacitance to create a resonant tank and tune the whole
thing to the DCF77 carrier frequency (77.5kHz). Add a few turns in a separate winding to tap the received signal:
Using an oscilloscope with a highly sensitive input, one can see now the received DCF77 carrier.
By rotating the antenna rod, one can evaluate the directivity of this construction. If one uses
such kind of antenna to take a bearing of the transmitter, one rotates the antenna to minimum
signal level. The transmitter is now aligned to the longitudinal direction of the antenna.
That's all she wrote. At least for the analog signal processing part.
See the complete analog schematic page here.
Dividing 10MHz by 32 results in 312.5kHz. 310kHz / 312.5kHz gives an (124/125)/32 ratio (which suprisingly equals 1000/31) required to get 310kHz from 10MHz. So one can use an divide-by-32 circuit and skip some pulses (one out of 125) to make up that division ratio. It's all implemented in a few lines of VHDL code and runs within a small CPLD.
See the clock generator schematic page here.
See the MCU schematic page here.
And the whole shebang: DCF77 radio clock prototype schematic.
Another block diagram, showing the details of the CPLD clock generation logic:
While drawing the latter diagram, I noticed the divider chain for generating a slighly offset carrier frequency
beeing unnecessarily complex. A simple 125/126 clock pulse skip would have done the job, not that
cascaded 125/126 and 124/125 logic that I struggle to understand now by myself. That's one of the downsides
of the modern stuff, one can easily pin down some VHDL code that works and fulfils the job, but is too
complex from a hardware view.
Anyway, it works, and its purpose is as said to generate a slightly offset 77.5kHz LO frequency. This is required for the ADC auto offset calibration routine. The demodulator outputs a small DC offset that must be subtracted from the signal to make the phase detection work at low signal levels. As the demodulator is intended to mix down the signal to DC, there's no easy way to discriminate the signal from the offset. Slightly changing the LO frequency does the trick then. By doing so, the demodulators output a low frequency AC signal that gets averaged over some periods. By averaging, the offset (still the DC part of the signal) is measured seperately from the signal (now the LF AC signal) and is used to compensate for the offset error while the LO is running at its nominal frequency. Some means to not mess up the LO phase isn't shown in the block diagram. The automatic offset compensation is run once a minute.
To simulate arbitrary time codes, I've put together a simple DCF77 transmitter. Its output signal
is directly coupled to the ferrite rod antenna by applying a third winding to it:
I've used just another STM32 nucleo board to make up the simulator. As the on-board
8MHz crystal isn't adjusted at all, it's deviation is too large for the receiver PLL
to lock. So I've used the 10MHz output from the receiver to clock the simulator.
Two of the Timers are used to create 90° phase shifted PWM signals representing
the I and Q of the AM/PM modulated carrier. A simple resonant tank takes care of
filtering the ugly PWM to a (more or less) nice sine wave. That signal is tapped from
the transformer, attenuated and coupled into the receiver:
The time code encoder uses the very same C code as the reference time code encoder of the receiver, so once the simulator is verified to produce the correct codes, the receiver is believed to use the correct time code to correlate against the received bits.
The PCBs were produced and delivered. So I did assemble them and continued development.
As you can see, I'm still using the modules, mostly because they're easy to use and I'm too lazy to replicate their contents into my own schematic and layout.
Found an old enclosure in the junk box and used it for the radio clock:
Mounting the antenna on top of that aluminium enclosure resulted in a slight change of its inductance, so I had to re-tune the antenna.
I've added a GPS receiver module and some line transmitters, transmitting the GPS PPS and DCF77 encoded PPS from my receiver to somewhere else, using the red ethernet cable. You can see these components on the left hand side. The yellow wire routes the GPS PPS to a capture input, using this to compare the received DCF77 time to the GPS time.
For your reference:
rcvr.c - the main receiver program code
rcvr.h - the main receiver header
dcf-code.h - some code common to the receiver and the simulator
The general structure resembles a DMA TC (transfer complete) interrupt routine and a big busy loop. The DMA TC runs at the ADC sampling frequency (3875Hz) and requires higher priority than the other interrupts in the system. The main loop is a function call, that is to be called from the main program in a regular manner. Depending on flags set by the DMA interrupt, different tasks are run from here at normal priority.
void DMA1_Channel1_IRQHandler(void)
...
RESET_P1;
if (bufp==pll_ph) SET_P1;
if (s2pf>0) {SET_P2; s2pf=0; };
if (s2pf<0) {RESET_P2; s2pf=0; };
if (skip_c>0) {
TIM1->ARR=78;
skip_c--; skip_i++;
} else if (skip_c<0) {
TIM1->ARR=80;
skip_c++; skip_i--;
} else {
TIM1->ARR=79;
};
At the very start of that routine the PPS pulse (P1) and the DCF77 encoded pulse (P2) is output. This needs to be done here to achieve low jitter and lag. bufp
cycles from 0 to 3874 all the time, and several other variables (like pll_ph
) are used to set the sampling points for various actions. The next few
lines adjust the sampling clock phase by skipping or adding a single count to the TIM1 sampling clock divider. These steps are requested by the PM cross-correlation routine.
sum_i += (ad_buf[0]-32768);
sum_q += (ad_buf[1]-32768);
ad_i = (ad_buf[0]-ofs_i_z);
ad_q = (ad_buf[1]-ofs_q_z);
...
{
signed long re, im;
re=ad_i*r_i+ad_q*r_q;
im=ad_q*r_i-ad_i*r_q;
...
buf_i[bufp] = ad_i;
buf_q[bufp] = ad_q;
};
Next, the ADC samples are read from the DMA buffer. The samples are summed up to get the DC component, required for the carrier PLL.
Then a coordinate transformation to get the I and Q data stream takes place. These samples undergo some downsampling (by simply calculating an sliding window average) and are stored
in the receive buffers.
dc_i=0; i=155;
cp=dcf_am+154;
ip=&buf_i[bufp];
while (--i) {
dc_i += *ip * *cp;
ip-=25; cp--;
if (ip < buf_i) ip+=SMP_1SEC;
};
...
ph_q=0; i=512/4;
cp=dcf_pm+511; qp=&buf_q[bufp];
while (--i) {
ph_q += *qp * *cp;
qp-=6; cp--;
if (qp < buf_q) qp+=SMP_1SEC;
ph_q += *qp * *cp;
qp-=6; cp--;
if (qp < buf_q) qp+=SMP_1SEC;
ph_q += *qp * *cp;
qp-=6; cp--;
if (qp < buf_q) qp+=SMP_1SEC;
ph_q += *qp * *cp;
qp-=6; cp--;
if (qp < buf_q) qp+=SMP_1SEC;
};
...
if (abs(ph_q)>ph_max) {
ph_max=abs(ph_q);
ph_ampl1=ph_q;
ph_pos1=bufp;
};
if (abs(dc_i)>dc_max) {
dc_max=dc_i;
dc_ampl1=dc_i;
dc_pos1=bufp;
};
These are the cross-correlations for AM and PM reception. The PM correlation loop is partially unrolled to increase the speed. Peak value detection indicates the
phase within one second, the result is stored for later processing. There's more code following, in particular the correlation results of one sample before PM maximum and
one sample afterwards are also stored.
if (bufp==pll_ph) {
rxtm++;
rs_i = sum_i; sum_i = 0;
rs_q = sum_q; sum_q = 0;
pll_f=2;
...
};
if (bufp==pll_p) {
rs_i = sum_i; sum_i = 0;
rs_q = sum_q; sum_q = 0;
pll_f=1;
pll_p+=(SMP_1SEC/5);
if (pll_p>=SMP_1SEC) pll_p-=SMP_1SEC;
l_bufp=bufp;
switch (ofs_run) {
case 2: RESET_FSEL; // 77504.961Hz
ofs_run++; ofs_c=1;
break;
case 3: RESET_FSEL; // 77504.961Hz
ofs_run++; ofs_c=1;
break;
case 4: RESET_FSEL; // 77504.961Hz
ofs_run++; ofs_c=1;
break;
case 5: SET_FSEL; // 77500Hz
ofs_run++; ofs_c=0;
break;
};
};
This is synchronisation stuff. pll_ph
represents the start of a second and is signaled to the main loop by setting pll_f
to 2. The next
part (synchronized by pll_p
) gets executed every 200ms. As a result, the variables rs_i, rs_q
contain the DC component required by the carried PLL.
Otherwise, the LO frequency switching required for the automatic offset adjustment is done here.
void do_rcvr(void)
{
pll();
sec_am();
sec_pm();
...
if (done_am==1) {
done_am=0;
sync_min(amv, amts_p, 0, &am_fm);
adv_fm_sec(&am_fm);
find_min(amv, amts_p, 0, &am_fm);
find_hrs(amv, amts_p, 0, &am_fm);
find_year(amv, amts_p, 0, &am_fm);
find_mday(amv, amts_p, 0, &am_fm);
find_mon(amv, amts_p, 0, &am_fm);
find_flags(amv, amts_p, 0, &am_fm);
verify_code(amv, amts_p, 0, &am_fm);
};
if (done_pm==1) {
done_pm=0;
sync_min(pmv, pmts_p, 1, &pm_fm);
adv_fm_sec(&pm_fm);
find_min(pmv, pmts_p, 1, &pm_fm);
find_hrs(pmv, pmts_p, 1, &pm_fm);
find_year(pmv, pmts_p, 1, &pm_fm);
find_mday(pmv, pmts_p, 1, &pm_fm);
find_mon(pmv, pmts_p, 1, &pm_fm);
find_flags(pmv, pmts_p, 1, &pm_fm);
verify_code(pmv, pmts_p, 1, &pm_fm);
}
...
sync_phase();
tx_code();
if (sec_f) {
if (l_am==RX_BIT_S) flush_sync(&am_fm, 0);
do_sec(&am_fm, enc_code_am, vc_am, 0);
if (l_pm==RX_BIT_S) flush_sync(&pm_fm, 1);
do_sec(&pm_fm, enc_code_pm, vc_pm, 1);
...
rc_sec++;
sec_f=0;
};
}
A bunch of functions gets called from here. In particular:
void pll(void)
{
float s_i, s_q, f;
int i,j;
if (pll_f) {
...
if (pll_f==2) ampl_p=0;
if (ofs_sm == 1) {
ofs_run=1;
if (pll_ph>=SMP_1SEC) ofs_run=2;
};
if ((ofs_sm>200) && (pm_fm.m_sec==ofs_sec)) {
ofs_sm=0;
ofs_sec++;
if (ofs_sec>7) ofs_sec=0;
};
ofs_sm++; if (ofs_sm>=402) ofs_sm=0;
s_i = rs_i * (1.0f / SMP_200MS);
s_q = rs_q * (1.0f / SMP_200MS);
valid_rx=1;
if (ofs_run==2) {
ofs_s_i=0.0f; ofs_s_q=0.0f;
};
if ((ofs_run==4) || (ofs_run==5) || (ofs_run==6)) {
ofs_s_i+=s_i;
ofs_s_q+=s_q;
valid_rx=0;
};
if (ofs_run==6) {
ofs_i=(1.0f/12.0f) * (ofs_i*9.0f + ofs_s_i);
ofs_q=(1.0f/12.0f) * (ofs_q*9.0f + ofs_s_q);
ofs_run=0;
ofs_i_z = 32768 + (int)ofs_i;
ofs_q_z = 32768 + (int)ofs_q;
if (LOG_OFS) printf("ofs: %5d %5d ", ofs_i_z, ofs_q_z);
};
s_q -= ofs_q;
s_i -= ofs_i;
if (valid_rx) {
ampl[ampl_p]=sqrtf((s_i*s_i)+(s_q*s_q));
iq_i[ampl_p] = s_i;
iq_q[ampl_p] = s_q;
};
s_i = iq_i[0] + iq_i[1] + iq_i[2] + iq_i[3] + iq_i[4];
s_q = iq_q[0] + iq_q[1] + iq_q[2] + iq_q[3] + iq_q[4];
...
phase = (atan2f(s_q, s_i)) * (180.0f/PI);
...
angv = (phase - phase2); // phase2 is phase( -1 sec )
...
f=phase * (PI/180.0f);
r_i = cosf(f)*32767.0f;
r_q = sinf(f)*32767.0f;
...
tim_ccri += -5.0f * angv;
tim_ccri += -0.2f * phase * anga;
tim_ccrp = -50.0f * phase * anga;
...
if (fabsf(phase)<5.0f) {
anga=anga*0.9985f;
if (anga<1.0f) anga=1.0f;
};
...
// add some dither to tim_ccr
tim_ccr = tim_ccri + tim_ccrd + tim_ccrp;
...
TIM2->CCR2=tim_ccr/1000;
...
if (ampl_p==0) {
s_i = (ampl[0]+ampl[1]+ampl[2]+ampl[3]+ampl[4]) / 200.0f;
rssi = 20.0f*log10f(s_i) - 90.0f;
sec_f=1;
...
};
ampl_p++; if (ampl_p>=5) ampl_p=0;
pll_f=0;
};
}
This routine is run 5 times per second and calculates the carrier phase from the received I/Q DC components and runs a control loop, steering the TCXO through a PWM DAC using TIM2. Once in a minute,
the LO frequency is requested to switch to a slightly offset value, using the averaged samplings results to cancel the demodulator / ADC offset error.
void sec_pm(void)
{
int i,j,k;
signed short n_pos;
if (secf_pm) {
secf_pm=0;
n_pos=ph_pos;
pmv[pmts_p] = RX_BIT_X;
k = histo(&pm_histo, n_pos);
...
pm_phase(k);
...
pmv[pmts_p] = ph_ampl2<0?-10:10;
...
if (abs(ph_ampl_p) > abs(ph_ampl_n)) skip_cc++;
if (abs(ph_ampl_n) > abs(ph_ampl_p)) skip_cc--;
...
if (skip_cc>skip_t) {
skip_c=1; skip_cc=0;
...
};
if (skip_cc<-skip_t) {
skip_c=-1; skip_cc=0;
...
};
...
};
}
This routine takes the phase of PM / AM correlation maximum (here ph_pos
) into a histogram based decision routine (k = histo(&pm_histo, n_pos);
).
The histogram returns the most often received phase, this is used to set the seconds phase.
The polarity of the received PM correlation is used to make the decision if a "0" or "1" bit was received, and this information is stored into the one-hour-sized buffer each second.
The cross-correlation results from the adjacent samples to the maximum are used to shift the ADC sampling phase to the ideal position, adjusting the receiver PPS as near as possible to the transmitted second boundary.
Similar processing is done for the received AM bits within void sec_pm(void)
.
void sync_min(signed char *rb, unsigned short bp, char pm, fm_t *fm)
{
...
fbuf_t(&t, FBUF_SYN(pm), 60, enc_buf);
p1=rb+(bp+45); if (p1>=(rb+DECODE_HIST)) p1-=DECODE_HIST;
p1--; if (p1f_tm=(i-45);
rp=fm->s_idx;
fm->s_res[rp] = k;
fm->s_idx++; if (fm->s_idx>=60) fm->s_idx=0;
kll=kl=k=(-20*DECODE_HIST); r=rl=rll=-1; s=15;
for (i=0; i<60; i++) {
kk=fm->s_res[rp];
if (kk > k) {
kll=kl=k; k=kk;
rll=rl; rl=r; r=s;
} else if (kk > kl) {
kll=kl; kl=kk;
rll=rl; rl=s;
} else if (kk > kll) {
kll=kk;
rll=s;
};
rp-=1; if (rp<0) rp=59;
s+=1; if (s>=60) s=0;
};
rp+=1; if (rp>=60) rp=0;
fm->s_idx=rp;
fm->s[0]=r; fm->s[1]=rl; fm->s[2]=rll;
}
This lovely piece of code (I struggle to understand it myself at the moment I'm writing this) calculates the correlation of the time code pattern indication the minute
start every second and stores the results (the three most probable seconds to start a minute within a 60 seconds interval) to fm->s[]
.
find_min(pmv, pmts_p, 1, &pm_fm);
find_hrs(pmv, pmts_p, 1, &pm_fm);
find_year(pmv, pmts_p, 1, &pm_fm);
find_mday(pmv, pmts_p, 1, &pm_fm);
find_mon(pmv, pmts_p, 1, &pm_fm);
find_flags(pmv, pmts_p, 1, &pm_fm);
get called for each received bit of the time code. These functions correlate the history of received time code bits to a predicted time code, using a similar scheme as above.
In particular, each of the functions tries to find the most probable part of the time code related to its name. For example, find_min()
excercises 60 times through
the received bits buffer, once for each minute, revealing the most alike minute matching the received bits. As soon as a certain confidence level is reached, the minute is
considered decoded. Rinse and repeat for all the other functions.
Finally,
verify_code()
verifies the complete guessed time code against the receive buffer. If enough confidence is reached, the time code is received.
tx_code();
starts to output DCF77 encoded pulses. These can be used to synchronize other clocks and systems.
Otherwise, there's a bunch of debug output through the VCP (virtual COM port), part of the build-in ST-Link debugger to watch the operation of the receiver. Also part of the complete system is reporting the time and receiver status over the CAN bus, using my proprietary homebus protocol.
TCXO control signal, observation period: one day / five minutes
One can observe noise at night and slowish long term deviation caused by the sun heating up the room the receiver is placed.
Receiver carrier phase, observation period: one day / five minutes
For comparison, TCXO and phase of a second unit, placed somewhere else receiving a weak and distorted signal:
These inspired me to create this receiver. Although my design isn't a copy of any of these, you'll find part of their concepts and ideas in my design.
So a big "Thanks" to the authors of this stuff.