High Resolution Frequency Counter

19,984

67

77

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.

1 Person Made This Project!

Recommendations

  • Anything Goes Contest

    Anything Goes Contest

77 Comments

0
mabartibin
mabartibin

4 months ago

Thanks for the great explanation of this method!
I’m making one with an ATtiny84 and no external flipflop. Basically the same idea as the 328 code posted in the comments. Bonus: it also captures a 1Hz pulse from a DS3231 RTC module, so the clock frequency is known at every measurement (up to the precision of the RTC). Breadboard now, will post pictures when it looks presentable.

What puzzles me about your circuit: you use a MAX7219 module for the display. You power it from 3.3V. The data sheet of the MAX7219 says it wants at least 4V, and indeed, the module I have stays dark at 3.3V. Anything mysterious happening that makes it work?

0
WilkoL
WilkoL

Reply 4 months ago

Well, I guess I simply was lucky with the MAX7219. Because I didn't do anything special to it. It is an eight digit seven segment module bought via ebay from some seller in China and I have another one that I just tested on 3.3V. That one also works without any problems.
Now 3.3V isn't that much lower than 4V so my idea is that either you are unlucky with this MAX7219 or that you are even more unlucky and have a fake one... Did you try to use it on 5V, eg with an Arduino?

0
mabartibin
mabartibin

Reply 4 months ago

Yeah, mine works fine on 5V. Looking at the thing, I notice that they included a protection diode on Vcc in case I get the polarity wrong. So it’s getting 3.3V minus one diode drop really. I’ll try to bypass that and see what happens. (Hopefully, what will happen is not me getting the polarity wrong ;-)

If it fails I can still run it on 5V.

IMAG3364.jpg
0
mabartibin
mabartibin

Reply 4 months ago

I tried it once more (diode still included) and it works at 3.3V. Hmm. I must have done something stupid in my original tests.

0
WilkoL
WilkoL

Reply 4 months ago

That's the part of building things I like best, finding my own errors. I'm almost sad if a device works first time.. :-)

0
mabartibin
mabartibin

Reply 4 months ago

True :-)
Next challenge: the input stage. What works for a square wave doesn’t necessarily work for a sine. What works for 4MHz doesn’t necessarily work for 0.1Hz.

0
WilkoL
WilkoL

Reply 4 months ago

Yes, everything works fine on 5V. I wanted to power it with a Li-ion battery so the choice for 3.3V was a logical one. Succes!

0
r09876
r09876

10 months ago

Hi, I tried to file an issue on GitLab, but it showed: "Your issue has been recognized as spam and has been discarded". Maybe someone else has already tried to interact there and failed.

0
WilkoL
WilkoL

Reply 10 months ago

Well, I'm not an experienced user of Gitlab, I just use it to post code and some other information, so I have no idea what causes the spam-message.
Just post the info here, as you did :-)
I'll take a look at it.. and see if I can replicate the issue. It may take a while though, I'm somewhat busy with other things (not related to electronics)

0
r09876
r09876

Reply 10 months ago

Hello,

I just made a few changes to the code to show 512.55552 instead of 051.2555. I added a "zero" number to each "multiplier" value.

I had to change the multiplication value to show 512Hz correctly (Need to review, I haven't tested it with other frequencies).

Thank you.

```
if (input_freq < 10) //number of received pulses in ~1 second
{
multiplier = 100000000;
dig_point = 8;
}
else if (input_freq < 100) //number of received pulses in ~1 second
{
multiplier = 10000000;
dig_point = 7;
}
else if (input_freq < 1000) //etc.
{
multiplier = 1000000;
dig_point = 6;
}
else if (input_freq < 10000)
{
multiplier = 100000;
dig_point = 5;
}
else if (input_freq < 100000)
{
multiplier = 10000;
dig_point = 4;
}
else if (input_freq < 1000000)
{
multiplier = 1000;
dig_point = 3;
}
else
{
multiplier = 100;
dig_point = 2;
}
```

0
t.parashchuk
t.parashchuk

2 years ago

Hello.
I do not understand my second d-trigger with output connected to "TestPoint 5Mhz".
If you are not using external generator than i have to quess. And where the "TestPoint 5Mhz".connected ?

0
WilkoL
WilkoL

Reply 2 years ago

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.

0
t.parashchuk
t.parashchuk

Reply 2 years ago

I understand your algorithm.
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.

0
WilkoL
WilkoL

Reply 2 years ago

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.

0
t.parashchuk
t.parashchuk

Reply 2 years ago

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 ?

0
WilkoL
WilkoL

Reply 2 years ago

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.

0
t.parashchuk
t.parashchuk

Reply 2 years ago

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.

0
WilkoL
WilkoL

Reply 2 years ago

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)

0
t.parashchuk
t.parashchuk

Reply 2 years ago

Hi. I tried to adapt your code to atmega328 and without
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 ?

0
WilkoL
WilkoL

Reply 2 years ago

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.