9
\$\begingroup\$

I need to measure the frequency of square-wave that can vary between 0 and 1MHz, and has a resolution of 0.25Hz.

I havent decided on which controller yet but It will most likely be one of the 20pin Attiny's.

Normally how I would measure lower frequency signals would be by using two timers one configured in timer capture mode to interrupt on say the rising edges of the external signal and another timer set up to interrupt every second therefore the former timers counter register value after 1 second would be equal to the frequency of the signal.

However this method obviously wont work for capturing signals ranging between 0 and 1MHz with a resolution 0.25Hz for this I would need a 22Bit counter (AFAIK 8bit micros only have 8/16bit counters).

One Idea I had was to divide the signal before applying it to the micro but this would be impratical as the signal would have to be divided by 61 therefore the frequency could only be updated every 61 seconds where I would like it to be every few seconds.

Is there another method that would allow the frequency to be updated say every 4 seconds?


Update:

The simplest solution is to use an use an external interrupt or a timer capture to interrupt on the rising edge of the signal and have the isr increment a variable of type long int. Read the variable every 4 seconds (to allow for frequencies down to 0.25Hz to measured).


Update 2:

As pointed out by JustJeff an 8bit MCU will not be able to keep up with a 1MHz signal so that rules out interrupting on every rising edge and incrementing a long int...

I have chosen the method suggested by timororr. Once I get around to implementing it Ill post back and share the results. Thanks to all for your suggestions.


Progress Report:

Iv'e started to test some of the ideas presented here. Firstly I tried vicatcu's code. There was a obvious problem of TCNT1 not been cleared after the frequency been calculated -not a big deal...

Then I noticed when debugging the code that about every 2 to 7 times the frequency was calculated timer 1's (the timer configured to count external events) overflow count would be short by two. I put this down to latency of Timer 0 ISR and decided to move the if statement block form the ISR to the main (see snippet below) and just set a flag in the ISR. Some debugging showed that the first measurement would be ok but with every subsequent reading Timer 1's overflow count would be over by 2. which I can't explain -I would have expected it to be under not over...

int main()
{
    while(1)
    {
        if(global_task_timer_ms > 0 && (T0_overflow == 1))
        {
            global_task_timer_ms--;
            T0_overflow = 0;
        }

        .....
    }
}

Next I decided I would try to implement timrorrs suggestion. To generate the neccesary interval (of approx 15ms between each timer_isr interrupt) I would have to cascade the two 8-bit timers as the only 16-bit timer on the Atmega16 is being utilized to capture the rising edges of the external signal.

I thought this solution would work and be much more efficient as most of the overhead is shifted to the timers and only one short isr left for the cpu to handle. However it wasn't as accurate as I had hoped, measurements shifted back and forth by approx 70Hz which I wouldn't mind at high frequencies but its definitely not acceptable at lower frequencies. I didnt spend two much time analysing the problem but Im guessing the timer cascading arrangement is not so accurate as I have implemented a similar arrangement to timrorrs suggestion on a far slower 8051 controller that had 2 16-bit timers and the results were quite accurate.

I have now gone back to vicatcu's suggestion, but I have moved the frequency calculation into the Timer 0 isr (see snippet below), this code has produced consistent and reasonably accurate measurements. With a little calibaration accuracy should be approximatly +/-10Hz.

ISR(TIMER0_OVF_vect)
{            

    TCNT0 = TIMER0_PRELOAD;         //Reload timer for 1KHz overflow rate

    if(task_timer_ms > 0)
    {
        task_timer_ms--;
    }
    else
    {     
        frequency_hz = 1.0 * TCNT1;
        TCNT1 = 0;
        frequency_hz += global_num_overflows * 65536.0;
        global_num_overflows  = 0;
        frequency_hz /= (TASK_PERIOD_MS / 1000.0);
        task_timer_ms = TASK_PERIOD_MS;
    }                                                 
}       

If anybody has any other suggestions Im open to them although but I rather not have to use ranges... Im also no longer to intent on getting 0.25% resolution, there doesnt seem much point with the level of accuracy that I have at the moment.

\$\endgroup\$
5
  • \$\begingroup\$ There is a relatively easy way to do this using a capture interrupt on a PIC and Timer 1 running at a very high speed. If you are still interested in other methods let me know and I can outline it in an answer. \$\endgroup\$
    – Kortuk
    Commented May 31, 2010 at 19:51
  • \$\begingroup\$ I haven't started work on this yet so yes Im still interested. \$\endgroup\$
    – volting
    Commented Jun 1, 2010 at 10:07
  • \$\begingroup\$ For some reason it did not ever let me know you had commented on my comment. \$\endgroup\$
    – Kortuk
    Commented Jun 4, 2010 at 19:21
  • \$\begingroup\$ @Kortuk: The software only notifies you if I leave a comment to one of your answers or questions. It might also notify you of this comment, because I put @Kortuk in front of it. But that is a StackOverflow software change, and I don't know if it got trickled into the StackExchange codebase or not. \$\endgroup\$ Commented Jun 4, 2010 at 19:40
  • \$\begingroup\$ no, it did not let me know you had responded, even with the @kortuk. No worries. It looks like an answer has been found. \$\endgroup\$
    – Kortuk
    Commented Sep 13, 2010 at 3:14

7 Answers 7

4
\$\begingroup\$

If possible I'd suggest selecting a microcontroller that supports a counter operation using the timer inputs; rather than manually incrementing a counter inside an ISR (which at high frequencies quickly ends up saturating the microcontroller activity) you allow the hardware to handle the counting. At this point your code simply becomes a matter of waiting for your periodic interrupt then calculating the frequency.

To extend the range and make the frequency counter more generalised (removing the need for multiple ranges at the expense of a little more work for the MCU) you could use the following technique.

Select a periodic interrupt rate that allows for measurement accuracy at the highest input frequency; this should take into account your counter size (you need to select the timer period such that the timer counter will not overflow at the maximum input frequency). For this example I'll assume that the input counter value can be read from the variable "timer_input_ctr".

Include a variable for counting periodic interrupts (should be initialised to 0 at startup); for this example I'll refer to this variable as "isr_count". The interrupt period is contained in the constant "isr_period".

Your periodic interrupt should be implemented as (C pseudo-code):

void timer_isr()
{
  isr_count++;
  if (timer_input_ctr > 0)
  {
    frequency = timer_input_ctr / (isr_count * isr_period).
    timer_input_ctr = 0;
    isr_count = 0;
  }
}

Obviously this rough example relies on some floating point math that may not be compatible for low-end microcontrollers, there are techniques to overcome this but they are outside of the scope of this answer.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Excellent timororr, that will do I exactly what I want without cost of extra IC's which is always good, I think I was too quick to dismiss the possibility of solving the problem in software. Thanks \$\endgroup\$
    – volting
    Commented May 31, 2010 at 7:24
  • \$\begingroup\$ @timrorr, i'm interested in your thoughts on my answer below if you feel like reading it \$\endgroup\$
    – vicatcu
    Commented Jun 1, 2010 at 18:40
7
\$\begingroup\$

You might want to consider having two (or more) ranges. The issues with capturing very low frequencies are somewhat different from the issues with the higher ones. As you've already noted, at the high end of your range you have counter overflow problems.

But consider at the low end of your range, your accuracy will suffer from not having enough counts in the register. Not sure if you really want to discriminate between 0.25Hz and 0.5Hz, but if you do, then you will actually have to count for four seconds to do that.

Also, specifying a flat 0.25Hz resolution, strictly interpreted, means you'd be able to discern 500,000.00Hz from 500,000.25Hz, which is a rather high degree of precision.

For those reasons, designing for distinct ranges could alleviate the counter size problem. Pulling numbers at random for the sake of example, for the low end, say 0 to 100Hz, count for 10 second intervals, and you get 0.1Hz resolution, and you counter only needs to go up to 1000, not even 10 bits. Then from 100Hz to 10kHz, count for 1 second intervals; you only get 1Hz resolution, but your counter only needs to run up to 10,000 still smaller than 16 bits. The upper range of 10kHz to 1MHz could count for just 0.01 sec, and the max count would still only be 10,000 and though your resolution would be 100Hz, this would be reasonable precision.

\$\endgroup\$
1
  • \$\begingroup\$ Yes I mentioned that in the update to my question (earlier)that I would have to count to 4 seconds for... and I yes I would like to be able to differentiate between say 500,000.00Hz and 500,000.25Hz. I had thought of using different ranges, I could easily tie this in with the rest of the hardware as the signal has 6 selectable ranges so I could probably design a simple 6 to 3 encoder to indicate which range... but I'm not sure if would be necessary if I use a hardware counter coupled with 4 second update time, this should take care of the problems at either end of the spectrum \$\endgroup\$
    – volting
    Commented May 30, 2010 at 20:40
5
\$\begingroup\$

You can mix a hardware and a software counter by counting the overflows of the hardware counter in an ISR.

Counting every edge of the signal in an ISR will be too slow for a 1 MHz signal. I think you could do up to about 50kHz that way.

\$\endgroup\$
3
  • \$\begingroup\$ Yes your probably right -it will be too slow for 1MHz but Id imagine a 20MIPS RISC processor could do better than 50KHz. Anyway I was also considering clocking an 8bit binary counter with the signal and connecting the carry out of the counter to the external interrupt pin of the MCU, then reading frequency of the signal as the sum of the carry bit interrupts plus the o/p count value of the counter every n seconds, Im guessing that is what you were getting at when you said a combination of hardware and software counters. \$\endgroup\$
    – volting
    Commented May 30, 2010 at 19:32
  • \$\begingroup\$ I think the OP was referring to the built-in hardware counter. They all have overflow interrupts that can be used for improving the counting range. \$\endgroup\$
    – jpc
    Commented May 31, 2010 at 20:59
  • \$\begingroup\$ @starblue, is the code I wrote below what you had in mind with your answer? \$\endgroup\$
    – vicatcu
    Commented Jun 2, 2010 at 20:35
2
\$\begingroup\$

Instead of doing a 1 second counter, make it a 0.1 second counter and multiply the count by 10?

If it's just a matter of storing the counter number, can't you use additional code to keep track of when the counter is about to overflow and write to another memory location to keep the tally?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ I think I must of had brain freeze.. the simplest solution I think is just to increment an variable of type long int every time a rising edge is detected. Read that value once every second and then reset it to zero. \$\endgroup\$
    – volting
    Commented May 30, 2010 at 16:12
  • 2
    \$\begingroup\$ Actually I will need to read the value every 4 seconds to measure down to 0.25Hz \$\endgroup\$
    – volting
    Commented May 30, 2010 at 16:25
2
\$\begingroup\$

Can't you just use a 16-bit timer's input capture and overflow interrupts (plus a variable) to do the measurement? Here's how I would do it with the ATTiny24A with AVR-GCC (untested and potentially buggy of course):

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define TIMER1_BITS           16    // 16 bit timer
#define TIMER1_HZ             8.0e6 // 8MHz crystal
#define TIMER1_OVF_PERIOD_SEC (1.0 * (1 << TIMER1_BITS) / TIMER1_HZ)
#define TIMER1_SEC_PER_TICK   (1.0 / TIMER1_HZ)

//global variables for time keeping
double total_period_sec = 0.0;
uint16_t  num_overflows = 0;

void setup_timer1_capture(){
   // set the ICP (input caputure pin) to a floating input
   DDRA  &= ~_BV(7); // it's A7 on the ATTiny24A...
   PORTA &= ~_BV(7);

   TIMSK1 =   _BV(ICIE1)  // enable input pin capture interrupt
            | _BV(TOIE1); // enable overflow interrupt

   TCCR1B =   _BV(ICNC1)  // activate the input noise canceller
            | _BV(ICES1)  // capture on rising edge of ICP
            | _BV(CS10);  // run the timer at full speed

}

ISR(TIM1_CAPT_vect, ISR_NOBLOCK){ //pin capture interrupt
  uint16_t capture_value_ticks = ICR1; // grab the captured value
  // do some floating point math
  total_period_sec =   1.0 * num_overflows * TIMER1_OVF_PERIOD_SEC
                     + 1.0 * capture_value_ticks / TIMER1_SEC_PER_TICK; 

  num_overflows = 0; // clear helper variable to be ready for next time
}

ISR(TIM1_OVF_vect){   //timer overflow interrupt
    num_overflows++;
}

int main(int argc, char *argv[]){
   setup_timer1_capture();

   sei(); // enable interrupts!

   for(;;){ //forever
      // do whatever you want...
      // the most recently calculated period is available in the 
      // total_period_sec variable 
      // (obviously 1.0 / total_period_sec is the frequency in Hz)
   }

   return 0;
} 

... at any rate, it compiles :)


EDIT I looked at the lss file output from my code, and the generated code has too many instructions to not trip over itself at 1MHz with an 8MHz clock... even the simple increment by one line in the TIM1_OVF_vect generates 19 instructions! So to handle 1MHz events, you would definitely need to optimize, probably register allocate some stuff (probably num_overflows and capture_value_ticks), use inline assembler (steal the important stuff from the lss file), and move processing out of the interrupts and into the main loop wherever possible.

\$\endgroup\$
2
  • \$\begingroup\$ Measuring a frequency using the period works quite well with slow waveforms (you're relying on the internal timer being much quicker than the external signal) but quickly hits a limit as the input signal frequency increases. Basically, as you've found, the time spent inside the timer capture interrupt becomes dominant; there is no time left for any other parts of the code to run. While I'm not that familiar with the ATTiny a quick look at the datasheet shows that timer/counter1 does support external event counting so let the hardware handle the counting. \$\endgroup\$
    – timrorr
    Commented Jun 4, 2010 at 4:57
  • \$\begingroup\$ @timrorr, wow yes that is way smarter way of doing it :) I posted updated AVR-GCC code in a separate post. Care to take a look and see what you think? \$\endgroup\$
    – vicatcu
    Commented Jun 4, 2010 at 18:25
2
\$\begingroup\$

Posting this code as an alternative per @timrorr's suggestion to my previous post. This compiles for the ATTiny24A using c99 language standard, but I haven't actually tested it in any way beyond that.

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#define TIMER0_PRELOAD   0x83 // for 8MHz crystal and overflow @ 1kHz
#define TIMER0_PRESCALE 0x03  // divide by 64
#define TASK_PERIOD_MS 4000   // execute task every 4 seconds

//global variables for time keeping
volatile uint16_t  global_num_overflows = 0;
volatile uint16_t  global_task_timer_ms = TASK_PERIOD_MS;

void setup_timers(){
    // set the T1 pin (PA.4) to a floating input (external event)
    DDRA  &= ~_BV(4);
    PORTA &= ~_BV(4);

    // set Timer1 to count external events
    TIMSK1 = _BV(TOIE1);      // enable overflow interrupt
    TCCR1B =   _BV(CS10)      // clock on external positive edge of T1 pin
        | _BV(CS11)
        | _BV(CS12);

    // set Timer0 for task timing (overflow once per ms)
    TCCR0B = TIMER0_PRESCALE;
    TCNT0  = TIMER0_PRELOAD;  // setup appropriate timeout
    TIMSK0 = _BV(TOIE0);      // enable timer0 overflow interrupt
}


ISR(TIM1_OVF_vect){   //timer1 overflow interrupt
    global_num_overflows++;
}

ISR(TIM0_OVF_vect){            //timer0 overflow interrupt @ 1kHz
    TCNT0 = TIMER0_PRELOAD;   // preload timer for 1kHz overflow rate
    if(global_task_timer_ms > 0){
        global_task_timer_ms--;
    }
}

int main(int argc, char *argv[]){
    double frequency_hz = 0;
    uint16_t num_overflows = 0;
    uint16_t num_positive_edges  = 0;
    setup_timers();
    sei(); // enable interrupts!
    for(;;){ //forever
        if(global_task_timer_ms == 0){ // wait for task to be scheduled
            ATOMIC_BLOCK(ATOMIC_FORCEON){
                num_overflows        = global_num_overflows; // copy the volatile variable into a local variable
                global_num_overflows = 0;                    // clear it for next time
                num_positive_edges   = TCNT1;                // copy num positive edge events to local variable
            }

            // calculate the 'average' frequency during this task period
            frequency_hz  = 1.0 * num_positive_edges;  // num edges since last overflow
            frequency_hz += num_overflows * 65536.0;   // edges per overflow of 16 bit timer
            frequency_hz /= (TASK_PERIOD_MS / 1000.0); // over the task interval in seconds

            global_task_timer_ms = TASK_PERIOD_MS;     // reschedule task
        }

        // use frequency_hz for whatever other processing you want to do
    }
    return 0;
}

This is a nice little use of the hardware capabilities of the Timer1 and frees up a ton of processing cycles in comparison to my original post.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.