Electronic – Arduino frequency measurement

arduino

I am using an ATMEGA328p to measure the frequency of a 50% duty cycle square wave in the range 2khz to 15khz. The square wave is being generated by a good quality Function Generator and I can see on the scope that the output is stable.

For the purpose of raising this question I have simplified my code to

#define SERIAL_OUTPUT

#define led 12

volatile boolean FREQint = false;
volatile unsigned int FREQcount = 0;
volatile unsigned int FREQovfcount = 0;
volatile unsigned int temp_TCNT2 = 0;
volatile unsigned int temp_FREQovfcount = 0;


int FREQprescaler = 1;

int FREQdivider[6] = {
  0, 1, 8, 64, 256, 1024
};

#define FREQ_input_pin 8 // PCINT0

//////////////////////////////////////////////////

ISR(TIMER2_OVF_vect) {
  FREQovfcount++;
  //digitalWrite(led, !digitalRead(led));
}
//////////////////////////////////////

ISR(PCINT0_vect)
{
  if ((PINB & 0x01) == 0) { // falling edge
    //digitalWrite(led, !digitalRead(led));

    // take quick snapshot
    temp_TCNT2 = TCNT2;
    TCNT2 = 0; // reset timer
    temp_FREQovfcount = FREQovfcount;
    FREQovfcount = 0; // clear overflow counter

    // if just missed an overflow
    if (TIFR2 & bit (TOV2)) {
      TIFR2 = bit (TOV2);  //clear pending interrupts
      if (temp_TCNT2 <= 40) {
        temp_FREQovfcount++;
        //digitalWrite(led, !digitalRead(led));
      }
    }
    FREQcount = (temp_TCNT2 + temp_FREQovfcount * 256);
    //digitalWrite(led, !digitalRead(led));
    FREQint = true;
  }
}


//////////////////////////////////////////////////

void setup()   {

#ifdef SERIAL_OUTPUT
  Serial.begin(115200);
  while (!Serial) {
    ;
  }
#endif

  pinMode(led, OUTPUT);

  cli();//stop interrupts while setting up the timers
  TIMSK2 = 0; // turn off Timer
  TCCR2A = 0x00; // normal mode
  TCNT2 = 0; // clear timer
  TCCR2B = FREQprescaler;
  pinMode(FREQ_input_pin, INPUT);
  // set interrupt on change
  PCICR |= 0b00000001;    // turn on port B
  PCMSK0 |= 0b00000001;   // PCINT8  pin D8
  TIMSK2 = bit(TOIE2);// enabled global and timer overflow interrupt;
  sei();//allow interrupts

}

//////////////////////////////////////////////////////////////////////////////

void loop()
{
#ifdef SERIAL_OUTPUT

  if (FREQint) {
    byte oldSREG = SREG;   // remember if interrupts are on or off
    cli();   // turn interrupts off
    unsigned int tempcount = FREQcount;
    SREG = oldSREG;    // turn interrupts back on

    Serial.print(" Freq ");
    Serial.print(16000000.0 / FREQdivider[FREQprescaler] / tempcount);
    Serial.print(" Hz");
    Serial.print(" Count ");
    Serial.print(tempcount);
    //Serial.print(" TCNT2 ");
    //Serial.print(temp_TCNT2);
    //Serial.print(" OVF ");
    //Serial.print(temp_FREQovfcount);
    Serial.println();
    FREQint = false;
  }
#endif
}

This code is similar to a number of examples on the net eg www.gammon.com.au/timers

My issue is that the FREQcount value varies much more than I expected. The extent of the variation is also sensitive to the frequency of the square wave being measured.

For example

Freq 9750.15 Hz Count 1641
 Freq 9621.17 Hz Count 1663
 Freq 9632.75 Hz Count 1661
 Freq 9632.75 Hz Count 1661
 Freq 9227.22 Hz Count 1734
 Freq 10302.64 Hz Count 1553
 Freq 9638.55 Hz Count 1660
 Freq 9732.36 Hz Count 1644
 Freq 9638.55 Hz Count 1660
 Freq 9632.75 Hz Count 1661
 Freq 9638.55 Hz Count 1660
 Freq 9638.55 Hz Count 1660
 Freq 11228.07 Hz Count 1425
 Freq 9744.21 Hz Count 1642
 Freq 9632.75 Hz Count 1661
 Freq 9638.55 Hz Count 1660
 Freq 9632.75 Hz Count 1661
 Freq 9227.22 Hz Count 1734
 Freq 9950.25 Hz Count 1608
 Freq 9638.55 Hz Count 1660
 Freq 9632.75 Hz Count 1661
 Freq 9638.55 Hz Count 1660

The actual frequency should be 9.543khz.

I can certainly understand why there will be some variation

BUT why are there count values of 1425 and 1734 which are well outside the average?

I could understand the count may be bigger if there were other interrupts but why the lower value?

Can anyone explain why this might be happening please?

Best Answer

You've clearly given this some thought, but some of the low-level details are tripping you up. I'm going to focus on one aspect of your problem, and maybe this will lead you into thinking in the right way to solve the rest of it.

After doing a little research, I learned that

  • Interrupts are not nested on the ATMEGA328 (at least, not by default)
  • Timer 2 is an 8-bit timer
  • Pin change interrupts have a higher priority than timer interrupts.

In your pin change ISR, you capture the state of the hardware and software counters:

// take quick snapshot
temp_TCNT2 = TCNT2;
TCNT2 = 0; // reset timer
temp_FREQovfcount = FREQovfcount;
FREQovfcount = 0; // clear overflow counter

One problem is that you're getting readings of around 1660, when calculation shows that a frequency of 9.543 kHz should give you readings of about 1676. You're consistently losing 16 clocks somewhere.

The first statement above reads the TCNT2 hardware register and stores it in an int variable. One problem is that TCNT2 is an 8-bit unsigned variable, while in the Arduino environment an unsigned int is a 16-bit variable. Furthermore, you've declared the variable as volatile, which means that the compiler must generate code to extend the value to 16 bits and then write it to memory as 2 bytes. Then you clear the register.

This code between the reading and the clearing of the register could easily account for the missing 16 clocks.

It would be better to write this ISR in assembly so that you know exactly what code is running in such a time-critical area. Or at least, examine the compiler output and tweak the source code to minimize the number of instructions it generates here. For example, just changing

volatile unsigned int temp_TCNT2 = 0;

to

register unsigned char temp_TCNT2;

and making it local to the ISR (it isn't used anywhere else) will probably get you close.

Better still, don't clear the timer at all — let it free-run and take the difference between the current and previous values. This way, you eliminate any possibility of "dead time", but this does make the job of resolving overflow events somewhat tricky.