Building an arduino frequency counter project is one of the most practical and educational electronics projects you can undertake. A frequency counter lets you measure the oscillation rate of any periodic signal — from slow 1 Hz sensor pulses to fast 8 MHz crystal oscillators — using nothing more than an Arduino and a few passive components. This guide walks you through the complete design, from theory to a working circuit with an LCD display, capable of measuring signals up to 8 MHz.
Table of Contents
- How a Frequency Counter Works
- Hardware You Need
- Circuit Design and Input Conditioning
- Arduino Frequency Counter Code
- Adding a 16×2 LCD Display
- Extending the Range Beyond 8 MHz
- Calibration and Accuracy
- Frequently Asked Questions
How a Frequency Counter Works
Frequency is defined as the number of complete cycles per second, measured in Hertz (Hz). A frequency counter works by counting the number of rising edges (or pulses) on a signal within a precise time window — typically one second — then displaying that count as frequency in Hz.
The Arduino achieves this using its hardware timer and external interrupt system:
- Timer1 (16-bit) generates a precise 1-second gate window using the 16 MHz crystal oscillator on the board.
- INT0 or INT1 (digital pins 2 and 3 on Uno) count the rising edges from the incoming signal during that window.
- After each gate window, the count equals the frequency in Hz.
The theoretical maximum frequency the Arduino can count is limited by the interrupt latency. Each interrupt takes approximately 4 CPU cycles to service. At 16 MHz clock speed, the absolute maximum is around 8 MHz — though 4-6 MHz is more reliable in practice without prescaling tricks.
Hardware You Need
For a basic frequency counter capable of measuring up to 8 MHz, you need:
- Arduino Uno or compatible board (16 MHz)
- 16×2 LCD display (HD44780 compatible) or LCD with I2C backpack
- 10 kΩ potentiometer (LCD contrast)
- 100 kΩ resistor and 1N4148 signal diode (input protection)
- 0.1 µF decoupling capacitor
- BNC connector (optional, for proper signal connections)
- Breadboard and jumper wires (for prototyping)
For a polished kit build, an all-in-one solution is available:
Circuit Design and Input Conditioning
The input stage is the most critical part of the frequency counter. Raw signals from oscillators, function generators, or other circuits may have voltage levels incompatible with Arduino’s 5V logic. Connecting a 12V signal directly to an Arduino pin will destroy the microcontroller.
Input Protection Circuit
The simplest safe input conditioning circuit uses three components:
- A 100 kΩ resistor in series with the signal (limits current to the clamping diodes).
- A 1N4148 diode from the signal pin to VCC (clamps overvoltage to ~5.6V).
- A second 1N4148 diode from GND to the signal pin (clamps undervoltage to –0.6V).
This configuration protects the pin while allowing signals between 0V and 5V to pass correctly. The 100 kΩ resistor does introduce a voltage divider effect at high frequencies — but for signals already in the 0–5V range, this is acceptable up to 8 MHz.
Signal Conditioning for Non-TTL Signals
Sine waves, triangular waves, or any analogue signal from a function generator must be converted to a clean digital square wave before counting. Use a 74HC14 Schmitt trigger inverter between your input and the Arduino pin. The hysteresis built into the Schmitt trigger prevents false triggering on slow-rise signals or noisy inputs.
Complete Connection Summary
- Signal input → 100 kΩ → Arduino Pin 5 (ICP1, Timer1 input capture pin) or Pin 2 (INT0)
- Clamping diodes connected between the Arduino pin, VCC, and GND
- LCD: RS→D12, EN→D11, D4→D5, D5→D4, D6→D3, D7→D2 (if not using I2C backpack)
- Potentiometer wiper → LCD V0 pin (contrast)
Arduino Frequency Counter Code
Method 1: External Interrupt Counter (Simple, Up to ~6 MHz)
// Arduino Frequency Counter using External Interrupt
// Measures up to ~6 MHz on Pin 2 (INT0)
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
volatile unsigned long pulseCount = 0;
unsigned long lastTime = 0;
float frequency = 0;
void countPulse() {
pulseCount++;
}
void setup() {
lcd.begin(16, 2);
lcd.print("Freq Counter");
attachInterrupt(digitalPinToInterrupt(2), countPulse, RISING);
lastTime = millis();
}
void loop() {
unsigned long currentTime = millis();
if (currentTime - lastTime >= 1000) { // 1-second gate
detachInterrupt(digitalPinToInterrupt(2));
frequency = pulseCount / ((currentTime - lastTime) / 1000.0);
pulseCount = 0;
lastTime = currentTime;
// Display result
lcd.setCursor(0, 1);
if (frequency >= 1000000) {
lcd.print(frequency / 1000000.0, 3);
lcd.print(" MHz ");
} else if (frequency >= 1000) {
lcd.print(frequency / 1000.0, 3);
lcd.print(" kHz ");
} else {
lcd.print(frequency, 1);
lcd.print(" Hz ");
}
attachInterrupt(digitalPinToInterrupt(2), countPulse, RISING);
}
}
Method 2: Timer1 Input Capture (Accurate, Up to 8 MHz)
For higher accuracy and frequencies up to 8 MHz, use the Timer1 Input Capture Unit (ICU) on pin D8 of the Arduino Uno. The ICU captures the exact timer value on each rising edge, eliminating interrupt latency errors.
// High-accuracy frequency counter using Timer1 ICP
// Measures period of input signal, calculates frequency
// Input: Pin 8 (ICP1) on Arduino Uno
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, A2);
volatile uint16_t captureVal[2];
volatile bool newCapture = false;
volatile uint8_t captureIdx = 0;
ISR(TIMER1_CAPT_vect) {
captureVal[captureIdx & 1] = ICR1;
captureIdx++;
if (captureIdx >= 2) {
newCapture = true;
captureIdx = 0;
}
}
void setup() {
lcd.begin(16, 2);
lcd.print("Freq Counter");
// Configure Timer1 for input capture
TCCR1A = 0;
TCCR1B = (1 << ICNC1) | (1 << ICES1) | (1 << CS10); // No prescale, rising edge
TIMSK1 = (1 << ICIE1); // Enable input capture interrupt
sei();
}
void loop() {
if (newCapture) {
noInterrupts();
uint16_t period = captureVal[1] - captureVal[0];
newCapture = false;
interrupts();
if (period > 0) {
float freq = 16000000.0 / period; // F_CPU / period_counts
lcd.setCursor(0, 1);
if (freq >= 1000000) {
lcd.print(freq / 1000000.0, 4);
lcd.print(" MHz ");
} else if (freq >= 1000) {
lcd.print(freq / 1000.0, 3);
lcd.print(" kHz ");
} else {
lcd.print(freq, 2);
lcd.print(" Hz ");
}
}
}
}
Adding a 16×2 LCD Display
The 16×2 LCD (HD44780) is the standard display for this project. Connect it in 4-bit mode to save Arduino pins. If you want to save even more pins, use an I2C LCD backpack module — it reduces the connection to just two wires (SDA and SCL), freeing up most of the Arduino’s I/O for other uses.
Format the frequency display to auto-scale between Hz, kHz, and MHz based on the measured value. Always display at least three significant figures for usability. Add a second line showing measurement period in microseconds for oscilloscope-like functionality.
Extending the Range Beyond 8 MHz
The Arduino Uno’s 16 MHz clock limits direct counting to roughly 8 MHz. For higher frequencies, two approaches work well:
Prescaler Divider (74HC390 or 74HC4040)
Insert a frequency divider IC between your input signal and the Arduino. A 74HC390 divides by 2, 5, or 10; a 74HC4040 provides binary division up to ÷4096. Divide the incoming frequency by 10, count the result on the Arduino, then multiply by 10 in software. This extends the range to 80 MHz with a ÷10 prescaler.
Arduino Nano RP2040 Connect
The RP2040 microcontroller runs at 133 MHz and has hardware PIO (Programmable I/O) state machines that can count at speeds approaching 100 MHz without burdening the CPU. If you need a single-chip solution for high-frequency measurement, upgrade to the RP2040.
Calibration and Accuracy
The accuracy of your frequency counter depends directly on the accuracy of the Arduino’s crystal oscillator. Standard Arduino Uno crystals are accurate to ±50 ppm — that is ±0.005%, or a maximum error of 50 Hz when measuring 1 MHz signals. This is adequate for most hobbyist applications.
Improving Accuracy
- Reference calibration: Measure a known-accurate reference signal (GPS 1 PPS output = exactly 1 Hz) and apply a software correction factor.
- TCXO: Replace the ceramic resonator on cheap boards with a temperature-compensated crystal oscillator (TCXO) module for 1–5 ppm accuracy.
- Longer gate time: Increase the gate window from 1 second to 10 seconds. Accuracy improves proportionally, but updates become slower.
- Multiple measurements: Average 5–10 readings and discard outliers for stable, accurate results on noisy signals.
Understanding Resolution
With a 1-second gate, resolution is 1 Hz — you cannot distinguish between 1000 Hz and 1001 Hz. With a 10-second gate, resolution improves to 0.1 Hz. For audio frequency work (20 Hz to 20 kHz), a 1-second gate gives 0.005% to 5% resolution — perfectly acceptable.
Frequently Asked Questions
What is the maximum frequency the Arduino Uno can measure?
Using the external interrupt method, reliably up to about 6 MHz. Using the Timer1 Input Capture Unit (pin D8), up to approximately 8 MHz. Beyond that, you need a prescaler divider IC or a faster microcontroller like the RP2040.
Can I measure the frequency of an AC mains supply (50 Hz) with this?
Yes, but you need a mains-safe isolation circuit — never connect mains voltage directly to an Arduino. Use a small signal transformer or optocoupler to generate a 0–5V TTL-level replica of the 50 Hz waveform, then feed that to the frequency counter input.
Why does my frequency reading fluctuate?
Fluctuation is usually caused by noise on the input signal triggering false edges. Add a Schmitt trigger (74HC14) in the input stage, add a 100 pF capacitor across the input to GND for RF noise filtering, and increase the Schmitt trigger hysteresis threshold if you are working with slow-rise signals.
Can I use this to tune a crystal oscillator?
Yes. Measure the crystal oscillator output, compare to the nominal frequency, and adjust the series/parallel capacitors to pull the frequency to specification. This is a standard crystal trimming procedure in radio equipment.
Does this project work with the Arduino Nano?
Yes. The Arduino Nano uses the same ATmega328P chip as the Uno and runs at 16 MHz, so all the code and timing is identical. The input capture pin is still D8 on the Nano.
Find all the components you need for your frequency counter project in our Arduino boards and kits collection at Zbotic.in — with fast delivery across India.
Add comment