After analyzing the task, we need to measure a square wave frequency above 100 kHz, which can be done using two approaches:
So, by considering the above points, we can implement the task.
Below are the solutions to the given task using different microcontrollers
Note: Although the task mentions using a timer, as per our understanding, ESP32’s general-purpose timers don't have the ability to count external pulses. So use the PCNT (Pulse Counter) peripheral, which is dedicated hardware for reliable external pulse counting.
Circuit Connection

#include <Arduino.h>
#include "driver/pcnt.h"
#define PCNT_INPUT_GPIO 4 // Input pin where pulses are received
#define PCNT_UNIT_USED PCNT_UNIT_0 // Use hardware pulse counter Unit 0
#define SAMPLE_TIME_MS 10 // Measurement window in milliseconds
int16_t count = 0;
void setup() {
Serial.begin(115200);
delay(200);
Serial.println("Frequency Measurement ");
// Configure PCNT hardware
pcnt_config_t pcnt_config = {}; // Initialize config structure
pcnt_config.pulse_gpio_num = PCNT_INPUT_GPIO; // Pulse input pin
pcnt_config.ctrl_gpio_num = PCNT_PIN_NOT_USED; // No direction control pin
pcnt_config.channel = PCNT_CHANNEL_0; // Use channel 0 of the unit
pcnt_config.unit = PCNT_UNIT_USED; // Use PCNT unit 0
pcnt_config.pos_mode = PCNT_COUNT_INC; // Increment count on rising edge
pcnt_config.neg_mode = PCNT_COUNT_DIS; // Ignore falling edges
pcnt_config.lctrl_mode = PCNT_MODE_KEEP; // Keep mode (no direction change)
pcnt_config.hctrl_mode = PCNT_MODE_KEEP; // Keep mode (no direction change)
// Set counter upper and lower limits (16-bit signed range)
pcnt_config.counter_h_lim = 32767;
pcnt_config.counter_l_lim = -32768;
// Apply configuration to PCNT hardware
ESP_ERROR_CHECK(pcnt_unit_config(&pcnt_config));
// Initialize counter
pcnt_counter_pause(PCNT_UNIT_USED); // Stop counter during setup
pcnt_counter_clear(PCNT_UNIT_USED); // Reset count to zero
pcnt_counter_resume(PCNT_UNIT_USED); // Start counting pulses
Serial.println("PCNT initialized on GPIO4.");
}
void loop() {
int16_t count = 0;
// Measure pulse count in fixed time window
pcnt_counter_clear(PCNT_UNIT_USED); // Reset count
delay(SAMPLE_TIME_MS); // Wait for sampling period
pcnt_get_counter_value(PCNT_UNIT_USED, &count); // Read count after delay
// Calculate frequency
// Convert counts per sample time to frequency in Hz
float freq = (float)count * (1000.0f / SAMPLE_TIME_MS);
// Display result
Serial.printf("Frequency: %.2f Hz\n",freq);
delay(1000); // Wait 1 second before next update
}void setup()
void Loop()
float freq = (float)count * (1000.0f / SAMPLE_TIME_MS); Serial.printf("Frequency: %.2f Hz\n",freq);ESP32 PCNT practical frequency range: ~1 MHz to 3–4 MHz (typical), in good hardware conditions with no heavy filtering.
We are using the Arduino UNO development board and programming it using the Arduino IDE.
Input Capture Mode (ATmega328P - Arduino UNO)

volatile uint8_t overflowCount = 0;
volatile uint32_t t = 0;
uint16_t firstCapture = 0;
uint16_t secondCapture = 0;
void setup() {
Serial.begin(115200);
TCCR1A = 0;
TCCR1B = (1 << ICES1) | (1 << CS10); // Rising edge, No prescaler (16 MHz)
}
void loop() {
if (countSignal()) {
float timeMicroseconds = t * 0.0625; // Convert clock cycles to microseconds (1/16 MHz)
float frequency = 1e6 / timeMicroseconds; // Convert period to frequency
Serial.print("Frequency: ");
Serial.print(frequency);
Serial.print(" Hz, Time: ");
Serial.print(timeMicroseconds);
Serial.println(" µs");
}
delay(500);
}
bool countSignal() {
cli(); // Disable interrupts
overflowCount = 0; // Reset overflow counter before starting
TIFR1 |= (1 << ICF1) | (1 << TOV1); // Clear input capture & overflow flags
// Wait for first rising edge
while (!(TIFR1 & (1 << ICF1)));
firstCapture = ICR1; // Store first capture value
TIFR1 = (1 << ICF1); // Clear flag
// Wait for second rising edge while counting overflows
while (!(TIFR1 & (1 << ICF1))) {
if (TIFR1 & (1 << TOV1)) {
overflowCount++;
TIFR1 = (1 << TOV1); // Clear overflow flag
}
}
secondCapture = ICR1; // Store second capture
sei(); // Re-enable interrupts
// Correct time calculation
t = (overflowCount * 65536UL) + (secondCapture - firstCapture);
return t > 0; // Return true if valid measurement
}
Setup (setup())
Main Loop (loop())
countSignal() is called to calculate the signal period.Signal Measurement (countSignal())
cli();TIFR1 = (1 << ICF1) | (1 << TOV1); TCCR1A = 0; TCCR1B = (1 << ICES1) | (1 << CS10); firstCapture, and clear the flag uint16_t firstCapture = ICR1; uint16_t secondCapture = ICR1; t = (overflowCount * 65536UL) + (secondCapture - firstCapture); sei(); attachInterrupt())Why 1 MHz is the Maximum Detectable Frequency?
1️. After First Capture:
2️. Waiting for Second Capture:
3️. Limit on Frequency Detection:
Conclusion: The approach is accurate up to 1 MHz but fails for higher frequencies due to the loop execution time.
Alternatively, we can count the number of rising edges of the signal in a fixed time interval (e.g., 1 second).
The frequency F is directly equal to the number of pulses counted in 1 second.

uint8_t OldTimerA0, oldTimerB0, oldTimerMask;
volatile uint32_t timerTick = 0;
volatile uint16_t overflowCount = 0;
volatile bool countValue = false;
float frequency = 0, timePeriod = 0;
uint16_t count = 0;
void setup() {
Serial.begin(115200);
}
void loop() {
disableTimer0(); // Stop Timer0 to avoid conflicts
startCount(); // Start frequency measurement
while (!countValue); // Wait for measurement to complete
Serial.print("Frequency: ");
Serial.print(frequency);
Serial.println(" Hz");
Serial.print("Time Period: ");
Serial.print(timePeriod, 10);
Serial.println(" Sec");
enableTimer0(); // Restore Timer0
delay(1000); // Wait before next measurement
}
void disableTimer0() {
OldTimerA0 = TCCR0A;
oldTimerB0 = TCCR0B;
oldTimerMask = TIMSK0;
TCCR0A = TCCR0B = TIMSK0 = 0; // Stop Timer0
}
void enableTimer0() {
TCCR0A = OldTimerA0;
TCCR0B = oldTimerB0;
TIMSK0 = oldTimerMask;
}
void startCount() {
cli(); // Disable interrupts
// Configure Timer2 for 1-second measurement
TCCR2A = (1 << WGM21); // CTC mode
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20); // Prescaler 1024
OCR2A = 156; // 1-second interrupt
TIMSK2 = (1 << OCIE2A); // Enable Timer2 Compare Match Interrupt
// Configure Timer1 for external pulse counting
TCCR1A = 0;
TCCR1B = (1 << CS12) | (1 << CS11) | (1 << CS10); // External clock, rising edge
TIMSK1 = (1 << TOIE1); // Enable Timer1 overflow interrupt
countValue = false;
overflowCount = 0;
TCNT1 = 0;
sei(); // Enable interrupts
}
ISR(TIMER1_OVF_vect) {
overflowCount++; // Increment overflow count
}
ISR(TIMER2_COMPA_vect) {
if (count == 100) { // 100 interrupts = 1 second
cli(); // Disable interrupts for calculation
timerTick = ((uint32_t)overflowCount * 65536) + TCNT1; // Total pulses
frequency = timerTick; // Frequency = total pulses in 1 second
timePeriod = 1.0 / frequency; // Time period
// Stop timers
TCCR1A = TCCR1B = TIMSK1 = 0;
TCCR2A = TCCR2B = TIMSK2 = 0;
TCNT1 = TCNT2 = 0;
countValue = true; // Set measurement complete flag
count = 0; // Reset counter
sei(); // Re-enable interrupts
}
count++;
}setup()loop()delay() and millis().startCount().startCount().(overflowCount = 0, TCNT1 = 0).sei()).disableTimer0().enableTimer0().ISR(TIMER1_OVF_vect)ISR(TIMER2_COMPA_vect)cli(); → Disable interrupts to prevent data corruption.timerTick = ((uint32_t)overflowCount * 65536) + TCNT1;frequency = timerTick; → Pulses per second.timePeriod = 1.0 / frequency; → Time period (seconds per pulse).loop() that measurement is complete.sei(); → Re-enables interrupts.overflowCount when Timer1 overflows.Input Capture Mode

Counting Pulse Counting using Timer Counter

Input Capture Mode
Counting Pulse Counting using Timer Counter