GPS precise frequency measurements with arduino (AtMega series). - Page 1
Forum section: electronics / general

author: MasterT
author_rank: Frequent Contributor
timestamp: April 05, 2018, 02:19:24 pm
content: Answering a question on another forum, I write an arduino sketch to measure frequency. Let's call it "GPS assisted" to get extra-precision.Thought, someone could find it useful as well. Written for AtMega2560, though it would be easy to adapt code to any AtMega uCPU, arduino or similar. One condition apply, both pin Tn and ICPn has to be brought out to header. Arduino UNO, for example has Timer1, Mega2560 - Timer5, etc.Code:/*Using Input Capture to extra-precise frequency measurementson Arduino Mega2560.GPS 1 PPS clock is supplied on pin 48 (ICP5).(Ublox NEO-7M unit was tested.)External signal is connected to pin 47 (T5).Prints the result over Serial Monitor.Commands:1: activate printout - "d".2: switch to internal clock - "i1".3: switch to external clock - "i0".Range 1 Hz <-> 6 MHz, (16 MHz if internal selected).Released into the public domain.Created by Anatoly Kuzmenko, April 2018k_anatoly@hotmail.com*/String in_String = "";boolean end_input = false;uint8_t adres_reg = 0;uint8_t debug_osm = 0;volatile int32_t frequency = 0;volatile uint8_t tmr_overf = 0;volatile uint8_t f_capture = 0; // flagvoid setup(){Serial.begin(115200);in_String.reserve(200);init_tmr5();}void loop(){int32_t tempr = 0;char * pEnd;if(debug_osm) {if(f_capture) {Serial.print(F("\n\tFreq: "));Serial.print(frequency, DEC);f_capture = 0;}}serialEvent();if( end_input) {char cmd = in_String;in_String = '+';if( cmd == 'd' ) {debug_osm = 1 - debug_osm;if(debug_osm) Serial.print(F("\nDebug aktiv."));else Serial.print(F("\nDebug de-aktiv."));}if( cmd == 'i' ) {tempr = strtol( in_String.c_str(), &pEnd, 10);Serial.print(F("\n\tInput: "));if(tempr) Serial.print(F("internal 16 MHz."));else Serial.print(F("external pin-D5."));input_clock(tempr);}in_String = "";end_input= false;}}void serialEvent() {while (Serial.available()) {char inChar = (char)Serial.read();in_String += inChar;if (inChar == '\n') {end_input= true;}}}Timer5 Tab:Code:void input_clock(uint8_t inp){if(inp) {TCCR5B &= ~(1<<CS52); // internalTCCR5B &= ~(1<<CS51);TCCR5B |= (1<<CS50);}else {TCCR5B |= (1<<CS52); // external clockTCCR5B |= (1<<CS51);TCCR5B |= (1<<CS50);}}void init_tmr5(void){TCCR5A = 0;TCCR5B = 0;TCCR5B |= (1<<CS50); // set prescaler to 16 MHzTCCR5B |= (1<<ICNC5); // input noise canceler onTCCR5B |= (1<<ICES5); // input capture edge selectTIMSK5 |= (1<<TOIE5); // Overflow Interrupt EnableTIMSK5 |= (1<<ICIE5);}ISR(TIMER5_OVF_vect) {tmr_overf++;}ISR(TIMER5_CAPT_vect) {static uint16_t last_v = 0;uint16_t curr_v = ICR5;if((TIFR5 & (1<<TOV5)) && (curr_v < 0x7F)) {tmr_overf++;TIFR5 |= (1<<TOV5);}frequency = curr_v + tmr_overf *65536UL;frequency -= last_v;last_v = curr_v;tmr_overf = 0;f_capture = 1;}

author: Kleinstein
author_rank: Super Contributor
timestamp: April 05, 2018, 07:41:16 pm
content: The code still has a slight bug inside, that might cause some rare glitches: There can be a kind of race condition when the ICP and timer overflow interrupt happen at about the same time. This causes some glitches that should be avoided. There is a way to check if the overflow interrupt should be run before the ICP one or not.

author: metrologist
author_rank: Super Contributor
timestamp: April 05, 2018, 09:27:04 pm
content: Thanks. I'll study the code and try to figure out how it works, and it may make use in one of my projects.

author: MasterT
author_rank: Frequent Contributor
timestamp: April 05, 2018, 10:34:10 pm
content: Quote from: Kleinstein on April 05, 2018, 07:41:16 pmThere is a Vector Interrupts tableQuoteCAPT has higher priority than OVF. Digital number associated with pulse edge arriving time is already "in-printed" when uCPU goes into ISR. And OVF arriving just after ICP can't change number of overflows, since it has to wait.

author: NivagSwerdna
author_rank: Super Contributor
timestamp: April 05, 2018, 10:48:12 pm
content: The timer ticks at XTAL rate which is only as good as the XTAL and subject to temperature effects etc.You may benefit from counting ticks between PPS pulses to calibrate the local oscillator?

author: MasterT
author_rank: Frequent Contributor
timestamp: April 06, 2018, 12:08:21 am
content: Quote from: NivagSwerdna on April 05, 2018, 10:48:12 pmThere are two mode, have you missed that? Primary objective is external frequency measurements, when timer is running by T5 input. Referencing internal 16 MHz is less practical, more like self verification that GPS is present and o'k.,

author: Kleinstein
author_rank: Super Contributor
timestamp: April 06, 2018, 10:09:27 am
content: Quote from: MasterT on April 05, 2018, 10:34:10 pmThere is an interrupt priority, however this only acts to decide that if two interrupts are waiting for execution which one is served first. However in the main program there are essentially always instructions that need more than one cycle. So it can still happen that OVF interrupt happens first but CAPT interrupt is serviced first, despite of coming 1 or 2 cycles later, but still during the same instruction. This is so tricky that even the 1. st version of the AVR simulator in AVRstudio got it wrong, the later version got it right.So it needs an extra test in the capture ISR to check if there is a pending OVF interrupt that should have come first. This is the case if the OVF interrupt flag is set and the ICP value is small (e.g. < 100). The limit value for small is not critical at all.

author: MasterT
author_rank: Frequent Contributor
timestamp: April 06, 2018, 02:20:47 pm
content: Yea, Kleinstein you are absolutely right. I run a code at much faster speed to accelerate interrupt collision rate, and here is what I get:QuoteInstead of PPS I applied 20 kHz, and counting internal 16 MHz clock, 800 is correct value for frequency. But accidentally there are wrong numbers, last digit in each line is total number of events. So, 167+129+115+153= 564 out of 9948004+12834639 = 22782643, probability about 564 / 22782643 * 100% = 0.002475569 %. Quite low, but not zero.I put couple line to fix an issue, and now it's working w/o single error. Have to update a code on initial post.Thanks, for advising.

author: Kleinstein
author_rank: Super Contributor
timestamp: April 06, 2018, 06:12:10 pm
content: Doing direct counting (via T5) and measuring the 1 PPS signal via ICP is not the way to get the best accuracy. Especially with low frequencies for the external signal the resolution is rather limited (e.g. 1 Hz). The better way usually would be to use reciprocal counting (e.g. use ICP to measure the time for 1 period or more periods) as well. However the direct capture method works only well for not so high frequencies. Depending on the code, the limit would be at around 50-500 kHz (maybe 1 MHz in ASM). So higher frequencies would still need direct counting or a prescaler (e.g. divide by 256).The next better stage could than be looking at not only signal transition, but more of them. However here the limited processing power of the AVR can become an issue (e.g. limit the frequency range), especially in C with only 32 Bit and 64 Bit resolution to choose from.The check of the Xtal clock does not need to be done that often. Even a simple Xtal is good for 0.1 ppm over quite some time. So one could even use the counter to either do a measurement or check the 1 PPS signal.

author: metrologist
author_rank: Super Contributor
timestamp: April 06, 2018, 07:34:01 pm
content: One idea I had was to count how many processor cycles between pps, and then do longer term averaging and slowly slew the tuning voltage of the clock oscillator, a 10 MHz OCXO clocking the micro (I would still program at base 16M and change osc after - absolute timing would not be important and could otherwise be inferred from the known clock).You would count 10M + or - x. X goes into some decay function to normalize that out.I try to convert this code to Uno. Do I just need to change timer5 to timer1? It compiles, but repeatedly runs setup as I put in a Serial.println("init"); in there and it loops that continuously (no other change to main).Code:void input_clock(uint8_t inp){if(inp) {TCCR1B &= ~(1<<CS12); // internalTCCR1B &= ~(1<<CS11);TCCR1B |= (1<<CS10);}else {TCCR1B |= (1<<CS12); // external clockTCCR1B |= (1<<CS11);TCCR1B |= (1<<CS10);}}void init_tmr5(void){TCCR1A = 0;TCCR1B = 0;TCCR1B |= (1<<CS10); // set prescaler to 16 MHzTCCR1B |= (1<<ICNC1); // input noise canceler onTCCR1B |= (1<<ICES1); // input capture edge selectTIMSK1 |= (1<<TOIE1); // Overflow Interrupt EnableTIMSK1 |= (1<<ICIE1);}ISR(TIMER5_OVF_vect) {tmr_overf++;}ISR(TIMER5_CAPT_vect) {static uint16_t last_v = 0;uint16_t curr_v = ICR1;if((TIFR1 & (1<<TOV1)) && (curr_v < 0x7F)) {tmr_overf++;TIFR1 |= (1<<TOV1);}frequency = curr_v + tmr_overf *65536UL;frequency -= last_v;last_v = curr_v;tmr_overf = 0;f_capture = 1;}

author: MasterT
author_rank: Frequent Contributor
timestamp: April 06, 2018, 08:33:27 pm
content: Quote from: metrologist on April 06, 2018, 07:34:01 pmThere is easier way to sync uCPU to GPS is to buy a GPS unit that could output 10 MHz directly , so you don't have to tune anything. Ublox NEO-7M supports that, timing adjustment range is from 1Hz to 10 MHz.You missed to change interrupts names:Code:ISR(TIMER1_OVF_vect) {ISR(TIMER1_CAPT_vect) {I tested it on UNO, all should works

author: NivagSwerdna
author_rank: Super Contributor
timestamp: April 06, 2018, 08:44:46 pm
content: Quote from: metrologist on April 06, 2018, 07:34:01 pmIndeed. You don't need to change the local clock just calculate the correction. Due to temperature changes the local oscillator will vary... could always put it in a temperature controlled box... or maybe use an OXCO... looks like we are heading towards a GPSDO

author: MasterT
author_rank: Frequent Contributor
timestamp: April 06, 2018, 08:49:00 pm
content: Quote from: Kleinstein on April 06, 2018, 06:12:10 pmSome arduino boards use a ceramic, for example Mega2560 where this code is running:QuoteIts' 16M - 15992503 / 16M = 0.000468562, 468 ppm off. And it's drifted, +- 10 Hz in a minute, and +-300 Hz on hours scale, more when warming up.

author: metrologist
author_rank: Super Contributor
timestamp: April 06, 2018, 09:23:48 pm
content: Quote from: NivagSwerdna on April 06, 2018, 08:44:46 pmI'm not sure how that's accomplished, since what MasterT mentioned, the LO is far too drifty. Also, the Uno cannot sample very high frequency, so there would need to be much division and loss of resolution as a result.That was the application. I already bought a couple of the Trimble GPSDO rebuilds, but need more to verify - I actually just want to build one.

author: iMo
author_rank: Super Contributor
timestamp: April 06, 2018, 10:32:13 pm
content: Mind the NEO-7M datasheet listsQuote

author: MasterT
author_rank: Frequent Contributor
timestamp: April 12, 2018, 01:42:52 am
content: I add a TFT display with touch panel, and here is the current project pictures:So far I have created trend & histogram modes. On trend view you may see a drift of internal arduino Mega ceramic resonator, almost 70 Hz in less than 2 minutes. Screen size is 5 minutes overall, 300 pixels/seconds.