Frequency counter, simplified and improved


In this article, we assume your Atmega328 is operated at 16 MHz. As the Nyquist Theorem says, the maximum signal frequency that can be detected will be half of the sampling frequency, in this case 8 MHz.
The ATmega328 hat three timers; Timer-0 is busy with housekeeping. In order to measure frequencies above 400 kHz you have to disable Timer-0.
If your controller is running faster or slower, all timing statements have to be recalculated accordingly.
If you need to measure higher frequencies you always can use a frequency divider (a hardware counter ic) that is able to operate at the desired frequency.

To design a frequency counter, it will take

The counter

Using the ATmega328, you got the 16-bit Timer-1 with its external counter input T1 (D5).
As in many cases, a 16-bit resolution (just 4½ decimal digits) won't be sufficient, it is a good idea to install an ISR(TIMER1_OVF_vect) routine to increment a software counter to extend the resolution.
An ISR could look like this (tofl1 being the 8-bit counter):

ISR(TIMER1_OVF_vect) {
  tovf1++;
}

Usually, you are advised to make your ISRs as short and fast as possible. Well, this can be done easily:

register byte tovf1 asm("r5"); // keep tovfl always in r5 to avoid PUSHs
register byte sreg asm("r4");  // temporary storage for SREG

ISR(TIMER1_OVF_vect, ISR_NAKED) {
  asm("in %1,__SREG__" "\n\t"   // save flags,   1 cycle
      "inc %0"  "\n\t"          // tovf1++       1 cycle
      "out __SREG__,%1" "\n\t"  // restore flags 1 cycle
      "reti"                    //               4 cycles
      : "=r" (tovf1), "=r" (sreg) : : ); 
}

This version locates two variables in fixed MCU registers which is not supported by IDE versions later than 1.6.6.

But coding in assembler in this case doesn't make much sense at all as the "C" version takes about 1.7 microseconds while the "ASM" version takes 7 cycles equal to 0.4375 microseconds. As the next call of the ISR will ocuur at least 8.192 milliseconds (=65536/8000000) later there is really no hurry.

With the additional 8-bit software counter you get a total resolution of 24 bits. As the precision of most available crystals is less than 24 bits there is no use in getting more than an 8-bit counter.

The time base

You have at least these options As even the micros() show an inaccuracy of 4 microseconds, you better go for a better time base.

The control logic

The task of the control logic is to prepare the counters (reset) and start both at the same moment. Once the time base reached the top the pulse counter must be stopped immediately.
If you use an external frequency divider triggered by the time base and take the least significant bit to open/close the gate. For instance, you can connect this bit to both, the external interrupt 0 and 1 and let them set or reset the interrupt flag of the pulse counter.

The display

The result of the pulse counter which will be stopped by now must be read, divided by the gate time interval and display in a readable way.
For a start you can always use the Serial Terminal.

Solution Nr. #1

/*   Frequency Counter Arduino Sketch
 *   original by: Jim Lindblom, SparkFun Electronics
 *   D5 = Frequency input 
 *   Modified 8.4.2016 
 *   With the 8000000 Hz reference you get the following results: 
 *   mean: 7999996,59
 *   min:  7999935
 *   max:  8000016
 *   standard deviation: 13,25
 *   Possible errors:
 *   - Pulse occurs while ISR is performed
 *   - Gate: resetting counters and restarting not at the same time
 *   - Quartz not precise
 */

const byte freqPin =  5;
const byte pwmPin  =  6; // reference:     977 Hz
const byte outpin  = 11; // reference: 8000000 Hz

void setup() {
  Serial.begin(9600);
  Serial.println(__FILE__);
  pinMode(freqPin, INPUT);        // This is the frequency input
  // generate 977 Hz at pwmPin 
  pinMode(pwmPin,OUTPUT);
  analogWrite(pwmPin,128);
  // generate 8 MHz signal at outpin:
  TCCR2B = B00000001;
  TCCR2A = B01000010;
  OCR2A  = 0;
  pinMode(outpin, OUTPUT);
  //---------------------
  // Timer 1 will be setup as a counter. Maximum frequency is F_CPU/2
  // (recommended to be < F_CPU/2.5). F_CPU is 16 MHz
  TCCR1A = 0;
  // External clock source on D5, trigger on rising edge:
  TCCR1B = 7; // (1 << CS12) | (1 << CS11) | (1 << CS10);
  // Enable overflow interrupt ISR(TIMER1_OVF_vect) when overflowed:
  TIMSK1 = 1; // (1 << TOIE1);  
}

register byte tovf1 asm("r5"); // keep tovfl always in r5 to avoid PUSHs
// 1 byte is enough: 255 * 65536 = 16711680 > max. frequency

void loop() {
  // Reset all counter variables and start over
  long t = micros() + 1000000; // + add something for correction
  TCNT1 = 0;
  tovf1 = 0;
  // Delay 1 second. While we're delaying Counter-1 is still triggered by 
  // input on D5 and also keeping track of how many times it's overflowed
  while (micros() < t);
  const unsigned long M = 0x10000; 
  // Add the overflow value to frequency
  unsigned long frequency = TCNT1 + M * tovf1; // max = 16.711.680  
  Serial.println(frequency);
}

// Timer-1 is our counter. 16-bit counter overflows after 65536 
// counts tovfl will keep track of how many times we overflow.
// assembler enables you to remove unwanted PUSHs/POPs
register byte sreg asm("r4"); // SREG

ISR(TIMER1_OVF_vect, ISR_NAKED) {
  asm("in %1,__SREG__" "\n\t"            // save flags
      "inc %0"  "\n\t"                   // tovf1++;
      "out __SREG__,%1" "\n\t"           // restore flags
      "reti"
      : "=r" (tovf1), "=r" (sreg) : : ); 
}



Solution No. #2

to be continued



contact: nji(at)gmx.de