AVR - Frequency measurement - Page 1
Forum section: electronics / general

author: xelion
author_rank: Newbie
timestamp: March 05, 2015, 02:26:36 pm
content: Hi guysFirst of all I would like to say thank you for providing this excellent community for sharing information and knowledge! Really appreciate it!I have been working with electronics for many years purely as a hobby, and started on embedded programming a few years back. With that in mind, please be patient with we if I don't quite understand things right off the bat.Anyway. I was hoping that someone could help me out with this little project of mine:I am trying to make a code for measuring the frequency of an incomming square wave signal with an ATmega88P MCU. I have already read the application note from Atmel (AVR205) on the subject, but got quite lost very quickly in there! Is it really that complicated??As I understand, there are different approaches on how to measure frequency; the route I have choosen revolves around the use of the input capture pin as input for the signal I want to measure, and setting up T/C1 as a counting module.I have thus far managed to set up T/C1 correctly, and I have programmed the fuses to run the MCU at 16MHz. No problem.I have even been able to set up interrupts, so I can measure the number of pin change events on ICP1. But I can't seem to get my head around how to count the input events during a certain time interval? I'm able to display overflows, TCNT1 and ICR1 values on my LCD, so that works a treat;)I would in essence like to be able to count the number of input events during, say, 1000ms or so, and then extrapolate the frequency from that.I know that this is possible but I have not been able to understand how to write the code for doing so. I read about DFM (Direct Frequency Measurement) and RFM (Reverse Frequency Measurement), and that I need to have a TOC (Timebase Oscillator Circuit), T/C1, to generate a steady timing interval for measurement, but that's all she wrote...!I hope this made sense, and await in anxeity your answers;)Best regards,X.el_ION

author: ajb
author_rank: Super Contributor
timestamp: March 05, 2015, 07:27:41 pm
content: Easiest would be to use the external timer clock pin (Tn) to increment one of the hardware counters on every rising or falling edge of your measured signal. Select the external clock source on your chosen timer/counter to start your sampling window, and use a second hardware timer to generate an interrupt that clears the clock select bits on the counter to disable it and close the sampling window.There will also be some error in your sampling window timing due to the time it takes to enable and disable the counter, which you could probably measure and compensate for. Or for improved accuracy you can gate the incoming signal with an external AND gate. The measured signal is feed to one input of the gate, one of the output compare channels is feed to the other, and your timer clock pin is connected to the output. Setup the output compare channel to give a pulse that corresponds to your desired sampling window, and the external clock pin will only see the measured signal during that window.The ICP pins are generally used to measure the period of a signal. So you'd have your counter running, and on each rising or falling edge, the input capture unit would capture the current value of the counter in the input capture register. Capture two transitions that way, and the difference between the two captured counter values gives you the number of timer counts and thus the time between the two edges, which is the period of the signal. Watch out for timer overflows, though! If you need to capture a wide range of periods you'll want to setup a timer overflow interrupt and increment an additional soft counter that gets prepended to the hardware counter value.If you really wanted to avoid using an external gate, you could combine these two methods to do gated edge counting. You'd use the external timer clock pin to count the transitions in the measured signal, and use an output compare channel on a second timer to generate a gate pulse, just as in the first method. But instead of routing that gate pulse to an external gate, you'd route that to the input capture pin on your first timer. On each edge of the gate timer, the input capture unit would capture the number of counts accumulated by the edge counter. You'll want to use an ICP interrupt to read those counts into a buffer, probably, for analysis in your main loop.Note that as your detectable frequency increases you'll need to be increasingly concerned about the signal integrity of the square wave you're measuring. Slow or noisy edges could course multiple input transitions that lead to spurious counts, so an external filter and/or Schmitt trigger may be called for.

author: xelion
author_rank: Newbie
timestamp: March 05, 2015, 08:59:58 pm
content: Thank you for the in-depth explanation, ajbI will have to read it through a couple of times to get the grasp of things.If I'm able to decipher the poopsheet correctly, the signal on T1/T0 respectively, must never go beyond half the F_CPU, meaning F_CPU/2.5 (to be safe) which equates to ~6MHz in the input signal to be measured. Since I'm not going to be measuring signals above 1MHz I think this method could be right for the job.But I don't understand where the values from the T/C1 counting register are stored? The nice thing about the input capture method is, that the TCNT1 value is instantaniously available through the input capture register (ICR1) after edge-detection.Maybe I need to read TCNT1 manually after each edge-detect?I'm also a little unclear as to how to set up the sampling window. Could you elaborate on this?Thanks!

author: ajb
author_rank: Super Contributor
timestamp: March 05, 2015, 09:38:53 pm
content: If you use the external counter inputs (T1), yes, you'd need to read TCNT at the end of each window and then manually clear it before starting the next window.If you do direct measurement of the frequency by counting transitions in the measured signal over a known time period, you can setup the window according to your required precision and update rate. A longer window gives you more precision, and minimizes the relative impact of any jitter in the start and end of the sampling window, but of course reduces your update rate. If you do reciprocal measurement by counting the transitions in your reference between transitions in your measured signal, then the period of the measured signal becomes your de facto sampling window.

author: dannyf
author_rank: Super Contributor
timestamp: March 06, 2015, 12:17:55 pm
content: QuoteThat's not how you use input capture: each time there is an input event (of unknown frequency), you save the timer counter's value. Between two interrupts, you get the "duration" of the input event, thus its frequency.The approach you are describing now is the counter approach: at start of the counting, you reset the counter; the input signal would then advance the counter. At the same time, you have another time base to count up time. Once the time base expires, you stop the counter and read its value.

author: xelion
author_rank: Newbie
timestamp: March 06, 2015, 09:38:12 pm
content: OK.I have now configured the µC to clock T/C0 on positive edge signals. T/C1 will provide the sampling window (1000ms)MWE; (FOSC@16MHz):(my attempt in making the functions self-explanatory)Code:void TIMER0_INITIALIZE(){TIMER0_RESET();TIMER0_WGM_MODE_NORMAL();TIMER0_CMO_MODE_CHANNEL_A_NORMAL();TIMER0_CMO_MODE_CHANNEL_B_NORMAL();TIMER0_OVERFLOW_INTERRUPT_ENABLE();TIMER0_COMPARE_MATCH_A_INTERRUPT_ENABLE();TIMER0_PRESCALE_CLK_ON_RISING_EDGE_PINT0();}void TIMER1_INITIALIZE(){TIMER1_RESET();TIMER1_WGM_MODE_CTC_OCR1A();TIMER1_CMO_MODE_CHANNEL_A_NORMAL();TIMER1_CMO_MODE_CHANNEL_B_NORMAL();TIMER1_COMPARE_MATCH_A_INTERRUPT_ENABLE();TIMER1_PRESCALE_1024();}Then I set the OCR1A value to:Code:OCR1A = 15625;So, every time T/C1 reaches the defined compare A value, the timer gets reset to TCNT1 = 0. This would generate a 1000ms sampling window, right? FOSC/1025 = T/C1CLK@ajb: Would it be more reasonable if I switched the timers around? So as to make T/C1 handle the external clock, giving it's 16-bit nature, or would it suffice to count the number of ovf's on T/C0 and sum up the pulses? Maybe I'm not attacking this the right way?

author: xelion
author_rank: Newbie
timestamp: March 07, 2015, 09:27:06 pm
content: Slowly progressing...I'm now able to read the TCNT0 register every 1000ms, but the value I get is not very consistent. I believe it has something to do with my TIMER1_COMPA interrupt? Maybe I have crammed too many instructions into the interrupt, and it hasn't got time to finish before the next pulse comes along?That would none the less explain the irregularities I see in the sampled TCNT0 values.I'm uotputting everything to my LCD so I can get an understanding as to what is going on inside the magic little chippieHere's my code so far:Code:#define F_CPU 16000000UL#include <avr/io.h>#include <util/delay.h>#include <avr/interrupt.h>#include <inttypes.h>#include <stdio.h>#include <stdlib.h>#include <avr/pgmspace.h>#include <compat/ina90.h>// FOR _NOP(); _CLI(); AND _SEI();#include "variables.h"#include "timer-lib.h"#include "lcd4bit-lib.h"#include "adc-lib.h"volatile unsigned int TIMER1_COMPARE_EVENTS;volatile uint16_t TIMER0_INPUT_CLOCK_EVENTS;ISR(TIMER1_COMPA_vect){TIMER1_COMPARE_EVENTS++;// UPDATE EVENT COUNTTIMER0_INPUT_CLOCK_EVENTS = 0;// CLEAR VARIABLETIMER0_INITIALIZE();// START T/C0: CLOCK ON +EDGETCNT1 = 0x0000;// CLEAR T/C1 REGISTER AND BEGIN NEW COUNTING CYCLETIMER0_PRESCALE_OFF_TIMER_STOPPED();// STOP T/C0TIMER0_INPUT_CLOCK_EVENTS = TCNT0;// STORE T/C0 REGISTER VALUE (# OF +EDGES)_NOP();// . . .TCNT0 = 0x00;// CLEAR T/C0 AND BE READY FOR NEXT CYCLE}volatile unsigned int TIMER0_OVERFLOW_COUNT;ISR(TIMER0_OVF_vect){TIMER0_OVERFLOW_COUNT++;// KEEP TRACK ON POSSIBLE OVFs!}int main(void){LCD_INITIALIZE();// INITIALIZE LCD: SEE <lcd-lib.h> FOR DETAILS_SEI();// ENABLE GLOBAL INTERRUPTSTIMER0_INITIALIZE();// INITIALIZE T/C0: SEE "timer-lib.h" FOR DETAILSTIMER1_INITIALIZE();// INITIALIZE T/C1: SEE "timer-lib.h" FOR DETAILSLCD_GOTO_XY(0,0);LCD_PRINT_STRING("TCNT1:");LCD_GOTO_XY(0,1);LCD_PRINT_STRING("C_EVNTs:");LCD_GOTO_XY(0,3);LCD_PRINT_STRING("CLKs:");// define sample window lenghtOCR1A = 15625;// 16MHz/1024 = 15625 Hz == 1000mswhile(1){LCD_GOTO_XY(10,0);LCD_SEND_INTEGER(TCNT1H);// DISPLAY THE HIGH REGISTER VALUELCD_PRINT_STRING(" ");LCD_SEND_INTEGER(TCNT1L);// DISPLAY THE LOW REGISTER VALUELCD_GOTO_XY(10,1);LCD_SEND_INTEGER(TIMER1_COMPARE_EVENTS);// DISPLAY NUMBER OF T1COMPA EVENTSLCD_GOTO_XY(10,3);LCD_SEND_INTEGER(TIMER0_INPUT_CLOCK_EVENTS);// DISPLAY THE CLOCK COUNT ON T0-PIN}DURING 1000ms SAMPLE WINDOW}

author: xelion
author_rank: Newbie
timestamp: March 07, 2015, 09:45:49 pm
content: Made a minor adjustment to the order in which T/C0 was stoppet and startet:Code:ISR(TIMER1_COMPA_vect){TIMER0_PRESCALE_OFF_TIMER_STOPPED();// STOP T/C0TIMER1_COMPARE_EVENTS++;// UPDATE EVENT COUNTTIMER0_INPUT_CLOCK_EVENTS = 0;// CLEAR VARIABLETIMER0_INITIALIZE();// START T/C0: CLOCK ON +EDGETCNT1 = 0x0000;// CLEAR T/C1 REGISTER AND BEGIN NEW COUNTING CYCLETIMER0_INPUT_CLOCK_EVENTS = TCNT0;// STORE T/C0 REGISTER VALUE (# OF +EDGES)_NOP();// . . .TCNT0 = 0x00;// CLEAR T/C0 AND BE READY FOR NEXT CYCLE}Now the readings are much more consistent! So now the first compare match event starts the sampling window and saves the count. The next compare event stops the timer, and runs the cycle again. This would mean, I guess, that the first reading is not valid. But that doesn't really mean anything in my application.Thanks for all your helpful insight guys!!!

author: ajb
author_rank: Super Contributor
timestamp: March 07, 2015, 11:10:53 pm
content: For best accuracy you probably want to disable both timers as the very first thing you do in the interrupt. Reset both to zero, and then reenable both right before you exit the ISR. This minimizes the latency between the end of the sampling window and when you actually capture the count. There will still be some latency due to the overhead of entering the ISR--the only way to avoid that is to use the input capture hardware or to use an external gate.

author: xelion
author_rank: Newbie
timestamp: March 08, 2015, 11:18:22 am
content: Quote from: ajb on March 07, 2015, 11:10:53 pmThat sounds reasonably enough;)This is how I have setup the ISR now:Code:volatile uint16_t TIMER1_COMPARE_EVENTS;volatile uint16_t TIMER0_INPUT_CLOCK_EVENTS;volatile int CALCULATED_FREQUENCY;ISR(TIMER1_COMPA_vect){TIMER0_RESET();// STOP T/C0TIMER1_COMPARE_EVENTS++;// UPDATE THIS EVENT'S COUNTERTIMER0_INPUT_CLOCK_EVENTS= 0;// CLEAR INPUT CLOCK COUNTTIMER0_INPUT_CLOCK_EVENTS= TCNT0;// STORE T/C0 REGISTER VALUE (# OF +EDGES)CALCULATED_FREQUENCY= (((int)TIMER0_OVERFLOW_EVENTS * 256) + (int)TIMER0_INPUT_CLOCK_EVENTS);TIMER0_OVERFLOW_EVENTS= 0;// CLEAR OVERFLOW COUNTTCNT0= 0;// CLEAR T/C0 AND BE READY FOR NEXT CYCLETCNT1= 0;// CLEAR T/C1 REGISTER AND BEGIN COUNTING CYCLETIMER0_INITIALIZE();// START T/C0: CLOCK ON +EDGE}Now I'm only having difficulties getting my LCD to display the calculated frequency correctly. I have tried using the SPRINTF and ITOA conversions, but I don't get the right numbers.I guess I'll have to play around with this a bit;)Thanks for helping me out, ajb!

author: xelion
author_rank: Newbie
timestamp: March 08, 2015, 11:49:36 am
content: FINALLY IT WORKS!I have now (with the help from you guys) made a working frequency counter!I have included the source files for my project, in case anybody would like to know how I ended up putting it together.I will post everything on my site (hostek.dk) once I get the time to do it (site is i danish, and still under heavy construction).Thanks once again!!

author: uChip
author_rank: Contributor
timestamp: March 10, 2015, 12:43:21 am
content: Congratulations! You can find another example of using the AVR as a frequency counterhere. That one and yours here work pretty well, but do have the issue of instruction timing and interrupt latency affecting the accuracy of the frequency measurement. Martin's implementation tries to compensate for the inaccuracy with a tunable delay but if you are willing to dedicate a couple of pins you can eliminate these inaccuracies altogether by wiring the counters so that the instruction timing does not get in the way.The trick is to generate the gate clock as an output on one pin and feed that into the input capture pin (ICP) on the other timer. Then instead of reading the timer directly you read the input capture register (ICR). The gate timer also generates an interrupt, but the timing is not critical since the gate clock is relatively infrequent and the ICR holds the value from the instant of the gate clock edge. On the first gate clock record the ICR as the startCount. On the ending gate clock record the ICR as the endCount. The frequency is then the endCount-startCount (taking into account overflows as necessary).The other source of inaccuracy is the gate clock itself which is only as accurate as the microcontroller clock. An Arduino implemented with a ceramic resonator has an accuracy of about 0.5% or 5000ppm for about $0.48. If you use a crystal instead you can improve this to 10ppm for not much more, $0.79. Temperature stability is also a factor as is aging. If you move up to a temperature compensated crystal oscillator (TCXO) you can increase the accuracy to 1.5ppm and stability to 2.5ppm at a cost of around $3.00. The next step after that would be to spend $3.50 and go to a voltage controlled temperature compensated crystal oscillator (VCTCXO) with a stability of 0.5ppm which can be calibrated to better than 0.5ppm. Note: you can get down to 0.1ppm off-the-shelf if you are willing to go to other frequency clocks, but I have restricted my search to oscillators at 16MHz to make the AVR counter prescaling and math easy. Beyond 0.1ppm I will leave to the volt nuts, but I'm sure it can be done.The cool thing is that from a pure counter standpoint, with an accurate clock the AVR counter is as accurate as any discrete implementation up to its max count frequency of just under 8MHz.Now that we have an accurate counter, can we also get some input on a good front end design? I am currently building up the attached but would love it if somebody has a better idea!Thanks,Chip