One Component Radio Clock Time Transmitter
Intro: One Component Radio Clock Time Transmitter
Believe it or not, you can generate a time signal to set your WWVB controlled radio clocks with just an attiny45, wire antenna, and a battery. Upload the code to an attiny45, put a 20" or so wire on pin 6 and power with 5v. You'll need to put your radio clock close to pick up the signal best.
STEP 1: The WWVB Time Signal and 60Khz Carrier
The WWVB time signal is a 60khz carrier modulated by reducing the carrier in a particular sequence to encode the time. Wikipedia has a good article which I used to design this project: https://en.wikipedia.org/wiki/WWVB
The attiny45/85 has a fast timer that can be set to generate a square wave at 60khz like this:
/* Initalize Fast PWM on OCR1A*/
DDRB |= _BV(PB1); // Set PWM pin as output
PLLCSR |= _BV(PLLE); // Start PLL
_delay_us(100); // Wait till PLL stablizes p. 9
PLLCSR |= _BV(PCKE); // Set Clock source to PLL
OCR1C = 132; // Set OCR1C to top p. 91 (60kkHz)
OCR1A = 66; // Set beginning OCR1A value (50% duty cycle)
TCCR1 |= _BV(CS12); /* Set clock prescaler to 8 */
TCCR1 |= _BV(PWM1A) /* Enable PWM based on OCR1A */ \
| _BV(COM1A0) /* Set PWM compare mode p. 89 */ \
;
The attiny45/85 has a fast timer that can be set to generate a square wave at 60khz like this:
/* Initalize Fast PWM on OCR1A*/
DDRB |= _BV(PB1); // Set PWM pin as output
PLLCSR |= _BV(PLLE); // Start PLL
_delay_us(100); // Wait till PLL stablizes p. 9
PLLCSR |= _BV(PCKE); // Set Clock source to PLL
OCR1C = 132; // Set OCR1C to top p. 91 (60kkHz)
OCR1A = 66; // Set beginning OCR1A value (50% duty cycle)
TCCR1 |= _BV(CS12); /* Set clock prescaler to 8 */
TCCR1 |= _BV(PWM1A) /* Enable PWM based on OCR1A */ \
| _BV(COM1A0) /* Set PWM compare mode p. 89 */ \
;
STEP 2: Modulating the 60khz Carrier
To modulated the carrier, just reduce the duty cycle of the wave by changing OCR1A to less than 66. I set the other timer on the attiny45 to fire an interrupt 61 times a second like this:
/* Initalize CTC interupt on timer0 at 61hz */
TCCR0A |= _BV(WGM01); //pg. 82 Mode 2 CTC OCR0A TOP
OCR0A |= 127; // 8mhz / ((127+1) * 1024 prescale) = 61hz
TCCR0B |= _BV(CS00) | _BV(CS02); // set prescaler to 1024
TIMSK |= _BV(OCIE0A);// enable compare match interrupt
sei(); // Enable interupts
This gives 61 times slices in each second to reduce the power of the carrier for modulation.
/* Initalize CTC interupt on timer0 at 61hz */
TCCR0A |= _BV(WGM01); //pg. 82 Mode 2 CTC OCR0A TOP
OCR0A |= 127; // 8mhz / ((127+1) * 1024 prescale) = 61hz
TCCR0B |= _BV(CS00) | _BV(CS02); // set prescaler to 1024
TIMSK |= _BV(OCIE0A);// enable compare match interrupt
sei(); // Enable interupts
This gives 61 times slices in each second to reduce the power of the carrier for modulation.
STEP 3: Encoding the Modulation With a Time Signal
Next I wrote a routine which would keep track of the 61 time slices of each second and change to duty cycle of the carrier to encode the time signal. Each case represents a second of the minute long time signal. Each parameter can be set. I have a few defines that can be used to easily change the hour and minutes. You can add other defines too.
ISR(TIMER0_COMPA_vect){
switch (slot) {
case 0 : { signal = 2;break;}
case 1 : { signal = ((minute_tens >> 2) & 1);break;} // min 40
case 2 : { signal = ((minute_tens >> 1) & 1);break;} // min 20
case 3 : { signal = ((minute_tens >> 0) & 1);break;} // min 10
case 5 : { signal = ((minute_ones >> 4) & 1);break;} // min 8
case 6 : { signal = ((minute_ones >> 2) & 1);break;} // min 4
case 7 : { signal = ((minute_ones >> 1) & 1);break;} // min 2
case 8 : { signal = (minute_ones & 1);break;} // min 1
case 9 : { signal = 2;break;}
case 12 : { signal = ((hour_tens >> 1) & 1);break;} // hour 20
case 13 : { signal = ((hour_tens >> 0) & 1);break;} // hour 10
case 15 : { signal = ((hour_ones >> 4) & 1);break;} // hour 8
case 16 : { signal = ((hour_ones >> 2) & 1);break;} // hour 4
case 17 : { signal = ((hour_ones >> 1) & 1);break;} // hour 2
case 18 : { signal = (hour_ones & 1);break;} // hour 1
case 19: { signal = 2;break;}
case 26: { signal = 1;break;} //
case 27: { signal = 1;break;} // Day of year 60
case 29: { signal = 2;break;} //
case 31: { signal = 1;break;} //
case 32: { signal = 1;break;} // Day of year 6
case 37: { signal = 1;break;} //
case 39: { signal = 2;break;}
case 42: { signal = 1;break;} //
case 43: { signal = 1;break;} // DUT1 = 0.3
case 49: { signal = 2;break;}
case 50: { signal = 1;break;} // Year = 08
case 55: { signal = 1;break;} // Leap year = True
case 59: { signal = 2;break;}
default: { signal = 0;break;}
}
switch (signal) {
case 0: {
// 0 (0.2s reduced power)
if (timer < 12) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
case 1: {
// 1 (0.5s reduced power)
if (timer < 30) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
case 2: {
// Marker (0.8s reduced power)
if (timer < 48) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
}
timer++; // Advance timer
if (timer == 61) { // Check to see if at end of second
timer = 0; // If so reset timer
slot++; // Advance data slot in minute data packet
if (slot == 60) {
slot = 0; // Reset slot to 0 if at 60 seconds
minute_ones++; // Advance minute count
}
}
}
ISR(TIMER0_COMPA_vect){
switch (slot) {
case 0 : { signal = 2;break;}
case 1 : { signal = ((minute_tens >> 2) & 1);break;} // min 40
case 2 : { signal = ((minute_tens >> 1) & 1);break;} // min 20
case 3 : { signal = ((minute_tens >> 0) & 1);break;} // min 10
case 5 : { signal = ((minute_ones >> 4) & 1);break;} // min 8
case 6 : { signal = ((minute_ones >> 2) & 1);break;} // min 4
case 7 : { signal = ((minute_ones >> 1) & 1);break;} // min 2
case 8 : { signal = (minute_ones & 1);break;} // min 1
case 9 : { signal = 2;break;}
case 12 : { signal = ((hour_tens >> 1) & 1);break;} // hour 20
case 13 : { signal = ((hour_tens >> 0) & 1);break;} // hour 10
case 15 : { signal = ((hour_ones >> 4) & 1);break;} // hour 8
case 16 : { signal = ((hour_ones >> 2) & 1);break;} // hour 4
case 17 : { signal = ((hour_ones >> 1) & 1);break;} // hour 2
case 18 : { signal = (hour_ones & 1);break;} // hour 1
case 19: { signal = 2;break;}
case 26: { signal = 1;break;} //
case 27: { signal = 1;break;} // Day of year 60
case 29: { signal = 2;break;} //
case 31: { signal = 1;break;} //
case 32: { signal = 1;break;} // Day of year 6
case 37: { signal = 1;break;} //
case 39: { signal = 2;break;}
case 42: { signal = 1;break;} //
case 43: { signal = 1;break;} // DUT1 = 0.3
case 49: { signal = 2;break;}
case 50: { signal = 1;break;} // Year = 08
case 55: { signal = 1;break;} // Leap year = True
case 59: { signal = 2;break;}
default: { signal = 0;break;}
}
switch (signal) {
case 0: {
// 0 (0.2s reduced power)
if (timer < 12) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
case 1: {
// 1 (0.5s reduced power)
if (timer < 30) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
case 2: {
// Marker (0.8s reduced power)
if (timer < 48) {OCR1A = 6;}
else {OCR1A = 66;}
} break;
}
timer++; // Advance timer
if (timer == 61) { // Check to see if at end of second
timer = 0; // If so reset timer
slot++; // Advance data slot in minute data packet
if (slot == 60) {
slot = 0; // Reset slot to 0 if at 60 seconds
minute_ones++; // Advance minute count
}
}
}
STEP 4: The Hex Files and C Code
The hex files for direct uploading of your microcontroller can be downloaded below. Also included is the C source file.
Have fun!
Have fun!
24 Comments
johnchev2004 6 years ago
Using attiny85 i get the fallowing error "expected unqualified-id before 'volatile' " for this line of code "TIMSK |= _BV(OCIE0A);// enable compare match interrupt ".
imnlfn 8 years ago
I'm well on my way to implementing a device similar to this using a Particle Photon that can retrieve the current time via NTP. This article was a big help in that effort. It's pretty much my primary reference, along with the Wikipedia article on WWVB.
I would like to point out, though, that the code block for the switch statement is incorrect. Everywhere it says ">> 4", it should say ">> 3". Otherwise, those slots will always evaluate to zero.
Thanks!
rpourzia 10 years ago
Great Article. What is the reason for slicing the second into 61? I would have thought that you want to slice by 10 and change the duty cycle for 8 slices for marker, 2 slices for 0, and 5 slices for 1.
Thanks.
sbmull 10 years ago
That is a good question. Look in the code comments and you will see this calculation: 8mhz / ((127+1) * 1024 prescale). That yields 61.05. So by setting the counter to 127, you get an integer number of slices at an adequate resolution. There are other solutions.
Rezer 8 years ago
I'm sure it's not an issue, but that .05 bothers me. That means that every minute your modulated signal differs from an actual minute by (.05*60)/61 seconds, or roughly 50ms per minute. Meaning, if you started out in sync with WWVB, after 20 minutes you'd have transmitted 1199 bits when you should be at 1200, and WWVB would be 1 bit ahead. I'm sure the markers bring everything back in sync with anything receiving, but ideally the frames would add up to exactly one minute. This can be accomplished with a prescale of 512 and counter of 125, giving exactly 125 interrupts per second. Realistically the internal resonator isn't accurate enough to matter, but hey...whole numbers are prettier :p
rpourzia 10 years ago
Thanks. I love the simplicity and the elegance of this. As you know, depending on your location and how the radio is facing, you don't always get the optimal WWVB signal. This can be annoying right after the DST change where one or more of radios stay behind. This is a great way of nudging them to the right time until they catch up.
BaldvinH 9 years ago
Tnx for this excellent piece of code and description. I wonder, is this something one could port to or re-implement using Arduino, like the pro-mini or similar, for example? Do you know of any such implementations?
sbmull 9 years ago
DawnLagdao 9 years ago
I want to try this project for my watch. What are the needed parts? Software?
tz1 10 years ago
Go farther, start with a GPS with 1PPS output, and you can be your own WWV, even where the WWV signal can't reach. Second, you could use something like a neopixel to flash the timecode (and a higher resolution tick), aligned to the UTC second so you could also timestamp video with high accuracy. (Or send tones like the 5/10/20Mhz time services do).
sbmull 10 years ago
BillL2 9 years ago
I was thinking about synchronizing clocks on a sailing boat using this idea. I have several brass clocks with cheap mechanisms I could replace with cheap radio controlled mechanisms but they would not get synchronized out to sea. Also maybe it would be nice to set them to local time. The boat has a computer connected to GPS so there is a time signal. Soon after thinking about this I thought maybe some one has done it and then found your article. Well done! I would be interested if anyone has extended the project to take time from GPS.
linuxuser2 10 years ago
Sorry for this elementary question, but how are you making the 60kHz signal with Timer1?
I am assuming that using the internal oscillator, you start with 8 MHz and you use a prescale of 8. So the timer clock is at 1 MHz, right?
So each count is 1 microsecond. So with 66 as OCR1A, and clearing the counter with 132, don't you toggle PB1 at 66 us?
Sorry for the noob question, and thanks in advance.
sbmull 10 years ago
No need to apologize! In this case the Datasheet is your friend. Check out page 91 of the datasheet for the attiny85. There are listed there settings for timer1 for 20khz to 500khz. For 60khz, it is PCK/8 and OCR1C=132. The formula is Fpwm = Fclk/(OCR1A+1) (page 90).
Note that the PLL is being used, so the base clock is at 64mhz. We are using a 8 prescaler so the clock for the timer is 8mhz.
8mhz/(132+1) => 60,150hz.
I hope that helps and thanks for your interest in my project!
hberg32 10 years ago
This is really cool. My first thought was "ok, now how do we get it to pull the current time from a timeserver so we don't have to hardcode the time" and brought the code over to the Arduino IDE to see if I could translate the code for an Uno (atmega328). After googling the myriad errors I understand now what PLL is and see that the atmega828 doesn't support it at the hardware level. Does this mean one would have to use an algorithm in place of the hardware PLL or is this chip just unsuitable for the task? If so, do you think the attiny45 or 85 have enough pins to interface with an ethernet shield to get the time (pardon my ignorance, I'm not familiar with the attiny).
sbmull 10 years ago
Thanks for your comment! I see no reason why the code would not work on an atmega. The critical part is getting the timer to pulse at 60Khz. You will need to tinker with the timer settings on the atmega to find a configuration that works.
Also, if the ethernet shield code uses interrupts, you'll need to make sure they do not interfere with the time code modulation interrupt.
If the ethernet shield uses spi, and I think it does, you only need three datalines, and that many are still available on the attiny. So I would say, it is possible... if you can get the code to work in the dataspace.
Sounds like a fun challenge!
hberg32 10 years ago
So it doesn't necessarily need PLL as the clock source?
sbmull 10 years ago
Yes, the PPL is not critical. It only provides an elegant and simple way to generate a 60kHz squarewave. Also, you can tweek a couple of numbers and make it a 1, 2 or 4 Mhz transmitter too ;). At 1Mhz you can listen to the signal on an AM or shortwave radio.
rdk1207 10 years ago
Whats the usb ISP you're using to program the attiny??
sbmull 10 years ago
It is a bus pirate. I like it because I can use a terminal interface to turn the power on and off. I also use a homemade USBtiny which is faster for programming. Mine is an older version that has a case. The newer ones are plain pcb's with headers.