# High Resolution Frequency Counter

15,623

63

67

## Introduction: High Resolution Frequency Counter

This instructable shows a reciprocal frequency counter capable of measuring frequencies fast and with reasonable precision. It is made with standard components and can be made in a weekend (it took me a bit longer :-) )

EDIT: The code is now available on GitLab:

https://gitlab.com/WilkoL/high-resolution-frequency-counter

## Step 1: Old School Frequency Counting

The old school way to measure the frequency of a signal is to use a logic AND-gate, feed the signal to be measured into one port and a signal with an exactly 1 second high time to the other port and count the output. This works quite well for signals of a few kHz well into the GHz. But what if you want to measure a low frequency signal with good resolution? Say you want to measure the frequency of mains (here 50 Hz). With the old school method you will see a constant 50 on your display if you are lucky, but more likely you will see the display switch from 49 to 50 or 50 to 51. The resolution is 1 Hz, and that's it. You will never see 50.002 Hz unless you are willing to increase the gate time to a 1000 seconds. That's more than 16 minutes, for a single measurement!

A better way to measure low frequency signals is to measure the period of it. Taking mains as an example again, has a period of 20 millisecond. Take the same logic AND-gate, feed it with, say 10 MHz (0.1 us pulses) and your signal on the other port and out come 200000 pulses, so the period time is 20000.0 uS and that translates back into 50Hz. When you measure just 199650 pulses the frequency is 50.087 Hz, that's a lot better, and it is in just one second measuring time.
Unfortunately this does not work well with higher frequencies. Take for example, we now want to measure 40 kHz. With the same 10 MHz input frequency as the reference we now measure just 250 pulses. When we count just 249 pulses the calculation gives 40161 Hz and with 251 the result is 39840 Hz. That's not an acceptable resolution. Of course increasing the reference frequency improves the results but there is a limit to what you can use in a micro controller.

## Step 2: The Reciprocal Way

A solution that works for both low and higher frequencies is a reciprocal frequency counter. I'l try to explain its principle.
You start off with a measuring time that is approximately 1 second, it does not have to be very precise but it is a reasonable time for a measurement. Feed this 1 Hz signal into a D-flipflop on the D-input. Nothing happens as yet on the output(s). Connect the signal that you want to measure to the CLOCK input of the D-flipflop.

As soon as the this signal goes from LOW to HIGH, the output of the D-flipflop transfers the state of the D-input to the output (Q). This RISING signal going is used to start counting the input signal as well as a reference clock signal.

So you are counting TWO signals at exactly the same time, the signal you want to measure and a reference clock. This reference clock has to have a precise value and be stable, a normal crystal oscillator is fine. The value isn't very important as long as it is a high frequency and its value is known well.

After some time, say a few milliseconds, you make the D-input of the D-flipflop low again. At the next CLOCK-input the output Q follows the state of the input, but nothing else happens because the micro controller is set to react to a RISING signal only. Then, after the measuring time is over (approx. 1 second) you make the D-input HIGH.

Again at the next CLOCK-input the Q output follows and this RISING signal triggers the micro controller, this time to end the counting of both counters.

The result is two numbers. The first number is the number of pulses counted from the reference. As we know the reference frequency, we also know the time it took to count those pulses.

The second the number is the number of pulses from the input signal we are measuring. As we started exactly on the RISING edges of this signal we are very confident about the number of pulses of this input signal.

Now it is just a calculation to determine the frequency of the input signal.

An example, lets say we have these signals and we want to measure f-input. The reference is 10 MHz, generated by a quartz crystal oscillator. f_input = 31.416 Hz f_reference = 10000000 Hz (10 MHz), the measuring time is approx. 1 second

In this time we counted 32 pulses. Now, one period of this signal takes 1 / 31.416 = 31830.9 uS. So 32 periods took us 1.0185892 seconds, which is just over 1 second.

In this 1.0186 second we also will have counted 10185892 pulses of the reference signal.

This gives us the following information: input_count = 32 reference_count = 10185892 f_reference = 10000000 Hz

The formula to calculate the resulting frequency is this: freq = ( input_count * f_reference ) / ref_count

In our example that is: f-input = (32 * 10000000) / 10185892 = 31.416 Hz

And this works well for low frequencies as well as high frequencies, only when the input signal comes close (or even higher than) to the reference frequency it is better to use the standard "gated"way of measuring. But then we could also simply add a frequency-divider to the input signal as this reciprocal method has the same resolution for any frequency (up to the reference again). So whether you measure 100 kHz directly of divided by an external 1000x divider, the resolution is the same.

## Step 3: Hardware and Its Schematic

I have made a few of this type of frequency counters. Long ago I made one with an ATMEGA328 (the same controller as there is in an Arduino), later with ARM micro controllers from ST. The latest was made with an STM32F407 clocked at 168 MHz. But now I wondered what if I do the same with a *much* smaller one. I chose an ATTINY2313, that has just 2kbyte of FLASH memory and 128 bytes of RAM. The display I have is a MAX7219 with 8 seven segment displays on it, these displays are available on Ebay for just 2 Euros. An ATTINY2313 can be bought for around 1.5 Euros the rest of the parts I used cost just cents a piece. Most expensive was probably the plastic project box.
Later I decided to make it run on a lithium-ion battery so I needed to add a (LDO) 3.3V voltage stabilizer a battery-charging-module and the battery itself. This increases the price somewhat, but I guess it can be build for less than 20 Euros.

## Step 4: The Code

The code was written in C with Atmel (Microchip) Studio 7 and programmed into the ATTINY2313 using an OLIMEX AVR_ISP (clone?). Open the (main.c) in the zip file below if you want to follow the description here.

INITIALIZATION

First the ATTINY2313 was set to use an external crystal as the internal RC-oscillator is useless for measuring anything. I use a 10 MHz crystal that I tune to the correct 10 000 000 Hz frequency with a small variable capacitor. The initialization takes care of setting ports to inputs and outputs, setting up the timers and enabling interrupts and initialization of the MAX7219. TIMER0 is setup to count an external clock, TIMER1 the internal clock and also to capture the value of the counter at the rising edge of ICP, coming from the D-flipflop.

I'll discus the main program last, so next are the interrupt routines.

TIMER0_OVF

As TIMER0 counts up to 255 (8 bits) and then rolls over to 0 we need an interrupt to count the number of overflows. That's all TIMER0_OVF does, just count the number of overflow. Later this number is combined with the value of the counter itself.

TIMER1_OVF

TIMER1 can count up to 65536 (16 bits), so the interrupt TIMER1_OVF also counts the number of overflows. But it does more. It also decrements from 152 to 0 which takes about 1 second and then sets an output pin, going to the D-input of the flipflop. And the last thing that is done in this interrupt routine is to decrement the timeout-counter, going from 765 to 0, which takes about 5 seconds.

TIMER1_CAPT

This is the TIMER1_CAPT interrupt that is triggered everytime the D-flipflop sends it a signal, at the rising edge of the input signal (as explained above). The capture logic takes care of saving the value of the TIMER1 counter at the moment of the capture, it is saved as well as the overflow counter. Unfortunately TIMER0 does not have an input capture function so here its current value and its current value of the overflow counter is read. A message-variable is set to one for he main program to tell it these is new data.

Next are two functions to control the MAX7219

SPI

While there is an Universal Serial Interface (USI) available in the chip I chose not to use it. The MAX7219 display needs to be controlled via SPI and that is possible with the USI. But bitbanging SPI is so simple that I didn't take the time to do it with the USI.

MAX7219

The protocol to setup the MAX7219 also is quite simple once you have read the manual of it. It needs a 16 bit value for every digit that consists of 8 bits for the digit number (1 to 8) followed by 8 bits for the number it needs to display.

MAIN-PROG

The last thing is to explain the main program. It runs in an infinite loop ( while(1) ) but only actually does do something when there is a message (1) from the interrupt routine or when the timeout counter has run down to zero (no input signal).

The first thing to do when the variable message is set to one, is reset the timeout counter, after all we know that there is a signal present. The D-flipflop is reset to make it ready for the next trigger that will come after the measuring time (wait-a-second).

The numbers registered in the capture interrupt are added to give the reference count and input-frequency count. (we have to make sure that the reference never can be zero as we will divide by it later on)

Next is a the calculation of the actual frequency. I surely do not want to use floating numbers on a microcontroller with just 2kbytes of flash and only 128 bytes of ram I use integers. But frequencies can be like 314.159 Hz, with several decimals. Therefore I multiply the input-frequency not only with the reference frequency but also with a multiplier, and then add a number to where the decimal point should go. These numbers will get very very large when you do that. E.g. with an input of 500 kHz, a reference of 10 MHz and a multiplier of 100, this gives 5 x 10^14, that's really huge! They will no langer fit in a 32 bit number so I use 64 bit numbers that will go all the way up to 1.8 x 10^19 (that works fine on an ATTINY2313)

And the last thing to do is to send the result to the MAX7219 display.

The code compiles into some 1600 bytes, so it fits into the 2048 bytes flash available in the ATTINY2313.

The fuse-registers should read like this:

EXTENDED 0xFF

HIGH 0xDF

LOW 0xBF

## Step 5: Accuracy and Precision

Accuracy and precision are two separate beasts. The precision here is seven digits, what the actual precision is depends on the hardware and the calibration. I calibrated the 10 MHz (5 MHz on the test point) with an other frequency counter that has a GPS disciplined oscillator.

And it works quite well, the lowest frequency I tried is 0.2 Hz, the highest 2 MHz. It is spot on. Above 2 MHz the controller starts to loose interrupts, not really surprising when you know that at 2 MHz input signal TIMER0 generates over 7800 interrupts per second. And the ATTINY2313 has to do other things too, the interrupts from the TIMER1, at another 150 interrupts per second and of course do the calculations, controlling the display and D-flipflop.
When you look at the actual device you'll see that I use just seven of the eight digits of the display. I do this for several reasons.

First is that the calculation of the input frequency is a division, it will almost always have a remainder, that you do not see as it is an integer division. Second is that the quartz crystal oscillator isn't temperature stabilized.

The capacitors that tune it to the correct 10 MHz are ceramic, very sensitive to temperature changes. Then there is the fact that TIMER0 does not have capture logic build in, and the interrupt functions all take some time to do their work. I think seven digits is good enough anyway.

## Recommendations

Testpoint 5MHz is just that, a test point. It doesn't connect to anything.

But with an oscilloscope you can check if the oscillator is running.

And if you have another frequency counter (from a nice friend or at school...) you can measure the exact frequency of the oscillator (divided by 2) and make corrections in the code.

Don't you think you should reset variables by assigning them zero every cycle or every few cycles.
(val_input_counter_high, val_input_counter_low
val_reference_counter_high, val_reference_counter_low)
Because when this values overflow you will get incorrect high result in "input_freq" or "reference_freq", but this of course will not be displayed because of this statement:
if (real_frequency < 99999999)
{
MAX7219_shownumber(real_frequency, dig_point);
}
What is upper limit of this meter, does it achieve F_CPU/2.5 or 4MHz ?
I tried to do my reciprocal frequency meter but without d-flipflop and i get not very precise measurement, and i do not know why. I want to understand where is my error and do the best.

Hi, T.
With unsigned variables it doesn't matter when it overflows, give the math a try, it works out fine.
The max frequency is around 2 MHz, any higher than that will cause troubles because the interrupts won't have enough time to complete.
The D-FF is essential in this reciprocal counter, it makes sure that the measurements start and stop at the precise edges of the input signal.

You can allow ICP interrupt when measure period start, save the counters and then disable interrupt. Then you can allow ICP interrupt when measure period ended , save counters and then calculate period and frequency. What do you think ? You shared your stm32 project, can i see ?

Doing that, enabling and disabling the interupt takes time, and that will mess up your measurements. I leave everything switched on and record the values of TIMER1_CAPTURE every approx. second. This way even the delays in the microcontroller are equal at the start and end of the measuring period, so they cancel out.
I say approx. second because it does not matter how long you measure because you measure both the input frequency AND the local frequency. With hose two you can calculate the real input frequency.

Hi. You are using long long 64bit variable for 'tussen_freq' and 'real_frequency', it has 18 446 744 073 709 551 615 maximum value. Dont you think it's too much ?
Can i use simple long 32bit variable, it's capable to hold maximum number of 4294967295 or frequency up to 4GHZ ?
Also is it possible to use float or double variable. I do not understand which type is enough for this calculation. Thank you.

In STEP 4 you can read this:

"E.g. with an input of 500 kHz, a reference of 10 MHz and a multiplier
of 100, this gives 5 x 10^14, that's really huge! They will no langer
fit in a 32 bit number so I use 64 bit numbers that will go all the way
up to 1.8 x 10^19"

Perhaps, if you are smart with mathematics you can avoid using 64bit variables, I haven't tried. This works. :-)
Yes, you can use float and double, but why would you do that? Integer arithmetic is exact and it uses as much bytes as 64bit integers.

(with the C compiler I use for the STM8 microcontrollers there is no support for 64bit integers, so there I have to use floats and doubles)

D-FF. I'm enabling ICP interrupt before measure period starts and after it ends.
I'm sending data through the UART.
Works fairly well but up to 240khz and gives error +75Hz. Then it starts to skip and ignore sending frequency to UART. It may be caused by incorrect work of ICP or because of my assembly on breadboard ?

Are you using plain C? Or Arduino-C++ ?

If you are using Arduino-C++ I would be surprised that you could get to 240 kHz at all!
If you are using C (GCC in Atmel Studio) my guess is that you are running out of time in the interrupt routine(s).

You say that it starts to ignore sending data to the uart, that is odd as in between measurements there is at leat one second of time available. That should be enough don't you agree? Are you sending data to the uart from within an interrupt routine perhaps?

What I usually do is use one or two gpio that I set at the beginning of some code (interrupt routine) and clear it at the end. With an oscilloscope I can then see how long that code takes to complete. Often that explains the problem.

Debugging ATMEGA code I do with an Atmel ICE debugger and even with that, it is hard to find timing issues. But with it, you can also set breakpoints at "strategic" places in the code and take a look at the values of variables, that can also explain a lot.

Hi. I'm using plain C with Atmel studio. I'm using UART interrupt "empty UDR" to put new symbol. Now i think that it may be incorrect work of ICP interrupt that cannot complete and influences on UART. interrupt. I do not have debugger.
Thanks for your advice with the oscilloscope. If you interested i can give you my code.

/*
* GccApplication1.c
* Created: 29.08.2020 12:57:54
* Author : Taras
*/
#define F_CPU 16000000UL
#include <avr/io.h>
#include "util/delay.h"
#include <avr/interrupt.h>
#include <stdio.h>
#include <string.h>

#define LED(PB5)
char bmax = 7; // Длина текстового буффера
char *buff = "1234567891234567";
unsigned char bidx;
volatile uint16_t timer0 = 0;
volatile uint16_t val_timer0_high;
volatile uint8_t val_timer0_low;
volatile uint16_t timer1 = 0;
volatile uint16_t val_timer1_high;
volatile uint16_t val_timer1_low;
unsigned char sreg;
uint32_t temp;
uint32_t input_freq;
uint32_t ref_freq;
uint32_t previous_input_freq = 0;
uint32_t previous_reference_freq = 0;
uint64_t real_frequency = 0;
double real_frequency1;
uint64_t tussen_freq;
float tussen_freq1;
volatile uint16_t timeout_counter = 1220;
volatile uint16_t measure_period = 0;
volatile uint8_t message = 0;
//USART functions
void init_uart(void);
void USART_sendString(char *string, char smax);
// TIMER functions
void timer0_init(void);
void timer1_init(void);
int main(void)
{
sei();
init_uart();
timer1_init();
timer0_init();

DDRB &=~(1<<DDB0);//ICP1 on PB0 set as input
PORTB &=~(1<<PORTB0); // PB0 set in HI-z mode

DDRB |= (1 << LED);// Built-in LED output
PORTB &= ~(1 << LED);// Built-in LED turn off
TIMSK1 |= (1<< ICIE1); //Timer/Counter1 input capture interrupt is enabled.

while (1)

{

if (timeout_counter == 0)//no input signal
{
real_frequency = 0;
_delay_ms(50);
}

if (message == 1) {

message = 0;
timeout_counter = 1220;

measure_period = 244;

temp = ((uint32_t) val_timer0_high << 8) | val_timer0_low;
input_freq = temp - previous_input_freq;
previous_input_freq = temp;
temp = ((uint32_t) val_timer1_high << 16) | val_timer1_low;
ref_freq = temp - previous_reference_freq;
previous_reference_freq = temp;

if (ref_freq == 0) ref_freq = 1;//never divide by 0

tussen_freq = (uint64_t)input_freq * F_CPU;
real_frequency = tussen_freq / (uint64_t)ref_freq;
real_frequency1 = (double)real_frequency;

sprintf(buff, "%16f",real_frequency1);
USART_sendString(buff,16);
_delay_ms(20);

}
}
}
ISR(TIMER1_CAPT_vect)
{

val_timer0_low = TCNT0;
val_timer0_high = timer0;

val_timer1_low = ICR1;
val_timer1_high = timer1;
message = 1;

}

ISR(TIMER1_OVF_vect)
{
++timer1;
if (measure_period > 0 ) { measure_period--;}
else { TIMSK1 |= (1<< ICIE1); } //Timer/Counter1 input capture interrupt is enabled.

if (measure_period <= (measure_period/2)) {PORTB ^= (1 << LED);} //invert LED reg ^= (1<<(bit))
if (timeout_counter > 0) {timeout_counter--;}

}

ISR(TIMER0_OVF_vect)
{
++timer0;
}
ISR(USART_UDRE_vect)
{
if(bidx >= bmax)
{
UDR0 = '\n';
bidx = 0; // buffer index = 0
buff -= bmax;// buffer (buff) pointer to first position
UCSR0B &=~(1<<UDRIE0); // disable buffer empty interrupt

}
else
{

UDR0 = *buff;// put one char to UDR register
++buff;// increase pointer of buffer
bidx++;//increase buffer index

}
}
void init_uart(void)
{
#define baudrate 9600L
#define bauddivider (F_CPU/(16*baudrate)-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)
//Init UART
UBRR0L = LO(bauddivider);
UBRR0H = HI(bauddivider);
UCSR0A = 0;

UCSR0B = 1<<RXEN0|1<<TXEN0|0<<RXCIE0|0<<TXCIE0;
//asynchronous, parity disabled, 8-bit
// UCSR0C = 0<<UMSEL00|0<<UMSEL01|0<<UPM01|0<<UPM00|1<<UCSZ00|1<<UCSZ01;
UCSR0C = 0<<UMSEL00|0<<UMSEL01|0<<UPM01|0<<UPM00|1<<UCSZ00|1<<UCSZ01;
}
void USART_sendString(char * string, char smax)
{
UCSR0B &=~(1<<UDRIE0); // disable buffer empty interrupt

bmax = smax; //lengh of string into bmax
buff = string; // string value assign to buff - buffer

UCSR0B|=(1<<UDRIE0); // enable buffer empty interrupt
}
void timer1_init()
{
//TIMSK1 |= (1<< ICIE1);//Timer/Counter1 input capture interrupt is enabled.
TIMSK1 |= (1<< TOIE1 );//Timer/Counter1 OVERFLOW interrupt enable
TCCR1B |= (0<< ICNC1);// Input Capture Noise Canceler off
TCCR1B |= (1<< ICES1);// Input capture rising edge
TCCR1B = (0<<CS12)|(0<<CS11)|(1<<CS10);// No prescaling
TCCR1A=(0<<COM1A1)|(0<<COM1A0)|(0<<WGM11)|(0<<WGM10); // PWM generation off

TCNT1 = 0;// Counter register = 0
}

void timer0_init()
{
TIMSK0 |= (1<<TOIE0); //TimerCounter0 overflow interrupt is enabled
TCCR0A = (0<<COM0A1)|(0<<COM0A0)|(0<<COM0B1)|(0<<COM0B0)|(0<<WGM00)|(0<<WGM01);// Normal mode operation
TCCR0B = (0<<WGM02); // Normal mode operation
TCCR0B = (1<<CS02)|(1<<CS01)|(1<<CS00);//External clock source on T0 pin. Clock on rising edge.

TCNT0 = 0; // Counter register = 0
}

Reading someone else's code is hard! :-)
So I build it, put in a 16 MHz crystal and connected it to a serial-usb converter.

I found at least one thing, you never disable the Input_Capture_Interrupt. I have put it here:

ISR(TIMER1_CAPT_vect)
{
TIMSK1 &= ~(1<< ICIE1); //Timer/Counter1 input capture interrupt is disabled.

val_timer0_low = TCNT0;
val_timer0_high = timer0;

val_timer1_low = ICR1;
val_timer1_high = timer1;
message = 1;
}

Another thing I had to change was the floats, I cannot get the floats to print. At all.
But this did work:

tussen_freq = (uint64_t)input_freq * F_CPU;
real_frequency = tussen_freq / (uint64_t)ref_freq;
real_frequency1 = (uint32_t) real_frequency;

sprintf(buff, "%ld",real_frequency1);
USART_sendString(buff,16);
_delay_ms(20);

The uart output still has some junk characters in it, but now I can at least read what frequency the atmega gets. As expected it is not as accurate as it would be with a D-flip-flop. With a D-FF you start counting at the exact edges of the input signal, now you just start at a random moment.

Why do you want to do this without a D-FF?

"Because I can" is a good answer :-)

I should try with D-FF. The answer is that i need to make PCB for this try. I want to build precise measurement instrument, and already bought prescaler for it. But i'm surprised why the upper limit is so low. If i'm using usual method of measurement i get the upper limit up to 5.5MHz.

I'm impressed with the ATMEGA328! :-)
I have it working without a D-flipflop and it works very well. Better than I expected.
This is what I did:
TIM0 is clocked by the external frequency
TIM1 is clocked by the internal 16MHz clock
TIM2 is also clocked by 16MHz, but precaled by 1024 (15625 Hz)

All timers produce IRQs on OVF (overflow)
TIM1 also produces IRQ on input capture

TIM2 interrupt is run (15625 / 256) 61 times a second, counts down a static variable from 62. At 0 it enables ICIE1 interrupt of TIM1

At the Input capture interrupt, it immediately makes a snapshot of the current value of TIM0, as there is no capture for this timer.

The rest is as it was for the frequency counter with the D-flipflop.

One thing is improved though, the raw-frequency, that is made from theTIM0 counter (8 bit) and the number of times it has overflowed (highcounter) is in fact 24 bit. So I shifted it 8 places left to make it 32bit. Now you can ignore any 32bit overflows. Of course you need to correct for this in the calculation of the real frequency.

The counter now works up to at least 5 MHz, although I wouldn't use it any higher than 1 MHz, because the problem with making a snapshot of the TIM0 counter.

If you want I can make a zip file of the whole project (including main.h uart.h and uart.c) and send it via email.

The code:

/*
* reciprocal_counter_without_d_flipflop.c
*
* Created: 21-9-2020 09:55:39
* Author : wilko
*/

#include "main.h"

volatile uint16_t highcounter = 0; //number of TIMER0 overflows
volatile uint8_t low_counter = 0; //snapshot of TCNT0 at moment of TIM1 input capture
volatile uint16_t highreference = 0; //number of TIMER1 overflows
volatile uint8_t message = 0; //signal that new data is available

void init_timer0(void);
void init_timer1(void);
void init_timer2(void);

int main(void)
{
char buffer;
uint32_t raw_reference = 0;
uint32_t raw_frequency = 0;
uint32_t previous_reference = 0;
uint32_t previous_frequency = 0;
uint32_t new_reference = 0;
uint32_t new_frequency = 0;

uint64_t between_frequency = 0;
uint32_t real_frequency = 0;

DDRB |= (1 << LED1); //output for debug LED1
DDRB |= (1 << LED2); //output for debug LED2

init_timer0();
init_timer1();
init_timer2();
sei(); //enable global interrupts

uart_init( UART_BAUD_SELECT(UART_BAUD_RATE,F_CPU) );
uart_puts("\n\rcounter\n\r\n\r");

while (1)
{
if (message)
{
PORTB |= (1 << LED2); //LED2 on
message = 0;

raw_reference = ((uint32_t) highreference << 16) | (uint32_t) ICR1;
raw_frequency = ((uint32_t) highcounter << 16) | ((uint32_t) low_counter << 8);

new_reference = raw_reference - previous_reference;
new_frequency = (raw_frequency - previous_frequency) >> 8;

between_frequency = (uint64_t) new_frequency * F_CPU;
real_frequency = (uint32_t) (between_frequency / new_reference);

previous_reference = raw_reference;
previous_frequency = raw_frequency;

sprintf(buffer, "%lu\t%lu\t%lu\n\r", new_reference, new_frequency, real_frequency);
uart_puts(buffer);
PORTB &= ~(1 << LED2); //LED2 off
}
}
}

ISR(TIMER0_OVF_vect)
{
highcounter++;
}

ISR(TIMER1_OVF_vect) //~244 Hz
{
highreference++;
}

ISR(TIMER2_OVF_vect) //61 times per second
{
static uint16_t secondtimer;

if (secondtimer) secondtimer--;
else
{
secondtimer = 62;
PORTB |= (1 << LED1); //switch ON debug LED1 when ICP interrupt enable
TIMSK1 |= (1 << ICIE1); //enable ICP1 interrupt
}
}

ISR (TIMER1_CAPT_vect)
{
low_counter = TCNT0; //make copy of TIM0 counter as quickly as possible
//a copy of TIM1 counter is in ICR1
TIMSK1 &= ~(1 << ICIE1); //disable ICP1 interrupt (THIS INTERRUPT)
PORTB &= ~(1 << LED1); //switch OFF debug LED1 when ICP interrupt enable

message = 1;
}

void init_timer0(void) //counter input frequency
{
DDRD &= ~(1 << TIM0_INPUT); //PD4 is T0 clock input
TCCR0A = 0x00; //no outputs, normal mode
TCCR0B |= (1 << CS02) | (1 << CS01) | (1 << CS00); //external clock, rising edge
TIMSK0 |= (1 << TOIE0); //interrupt on overflow
}

void init_timer1(void) //counter internal freq + input capture
{
DDRB &= ~(1 << TIM1_CAPT); //PB0 is ICP1 input
TCCR1A = 0x00; //no outputs, normal mode
TCCR1B |= (1 << ICES1); //input capture on rising edge
TCCR1B |= (1 << CS10); //prescaler = 1
TCCR1C = 0x00;
TIMSK1 |= (1 << TOIE1); //interrupt on overflow
TIMSK1 &= ~(1 << ICES1); //NO INTERRUPT ON ICP1 YET
}

void init_timer2(void) //one second counter to enable/disable ICP1
{
TCCR2A = 0x00; //no outputs, normal mode
TCCR2B |= (1 << CS22) | (1 << CS21) | (1 << CS20); //prescaler = 1024 (15625 Hz)
TIMSK2 |= (1 << TOIE2); //interrupt on overflow (~61 Hz)
}

I should try this. You you are not clearing ICF flag now, why ?
Also you saving ICR1 value in the main function may be better to save it in temporary value in the interrupt routine ?
You are using your own UART transmit function that is why it is not interfere with other code ?

You can send project to me t.parashchuk@gmail.com

Aso you can send float to UART if change compiler settings.
https://startingelectronics.org/articles/atmel-AVR...

I have seen projects with and without D-FF, some also using external TXCO, this one is interesting also and similar, but code is partly in asm.

http://www.mshopf.de/proj/avr/freq_meter.html May be key to success is in this supporting note. I have not read it yet.

You got nice result by the way :-)

Okay, I should stop now....

I found some improvements

1 - change the optimization to (-O3)

2 - add TIFR1 |= (1 << ICF1);
to ISR(TIMER2_OVF_vect)
just before enabling the ICP1 interrupt

3 - add the same line to
ISR (TIMER1_CAPT_vect)
just after disabling the ICP1 interrupt

At 750 kHz it is still spot on.