82. High-Frequency Measurement

Objective: Measure the frequency of a square wave greater than 100 kHz.

Methods:

  1. pulseIn() function: Measures pulse duration (HIGH or LOW) on a digital pin, but its range (10µs to 3 minutes) is too slow for pulses under 10µs.
  2. micros() with digitalRead(): Tracks time using micros() around digitalRead(), but digitalRead() itself introduces a delay ~(4-12µs), making it unsuitable for very short pulses.

Conclusion: Neither method works for this scenario.

 

What’s the solution?

There are two approaches that we can use 

  1. Measure the period of one cycle using Input Capture Mode.
  2. Counting the number of pulses in one second using a Timer counter (external pulse counter)

 

Let us explore them below:

Approach 1 (Input Capture Mode) 

Input Capture Mode (ATmega328P - Arduino UNO)

  • What It Does: Captures the exact timer value (TCNT)when an external signal edge occurs on PD6 (ICP1 pin).
  • How It Works:
    1. A hardware timer (Timer1) runs continuously.
    2. When a signal edge (rising/falling) happens, the timer value is saved in ICR1.
    3. By capturing two consecutive rising edges, we get the signal period (T).
    4. Frequency is calculated as: F=1/T.
  • Why Use It?: More precise frequency measurement than basic timing functions.

Hardware Connection 

  • Input Capture Mode 
    • Signal Source: Connect the high-frequency signal (greater than 100 kHz) to Pin 8 (ICP1) of the Arduino UNO. This pin is the input capture pin for Timer1.
    • Arduino UNO: Ensure proper grounding between the signal source and the Arduino.
       

Firmware

  • In this approach, Timer1 is configured in Input Capture Mode to measure the period T of the signal. 
  • The frequency F is then calculated using the formula:     F = 1/T                                                
  • The Input Capture Mode captures the timestamp of two consecutive rising edges of the signal, and the difference between these timestamps gives the period T.

Code


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
}


Code Explanation

Setup (setup())

  • Initializes serial communication (115200 baud rate) to display frequency values.

Main Loop (loop())

  • countSignal() is called to calculate the signal period.
  • Measures and prints frequency, then waits 1000 ms

Signal Measurement (countSignal())

  •  Setup Timer1 
    • Disable interrupts → cli(); 
    • clear flags (overflow flag and input capture flag)
      •  TIFR1 = (1 << ICF1) | (1 << TOV1);  
    • To configure rising edge detection and start Timer 
      •  TCCR1A = 0;  
      •   TCCR1B = (1 << ICES1) | (1 << CS10);                  
  •  First Rising Edge 
    • Wait, record firstCapture, and clear the flag                                                                                                           uint16_t firstCapture = ICR1;  
  •  Second Rising Edge
    •  Wait while counting overflows, record secondCapture, then stop Timer1                                                     uint16_t secondCapture = ICR1; 
  •  Calculate Time 
    • Compute t using overflows and capture values.                                                                                 t = (overflowCount * 65536UL) + (secondCapture - firstCapture);
  • Finish 
    • Re-enable interrupts and return true if a signal was detected.                                                              sei();  

Conclusion

  • In Arduino UNO, the maximum clock frequency is 16MHz. So, 62.5 nanoseconds is for executing one instruction. That is why it is not possible to measure a signal >16MHz.
  • Interrupt Handling Limitation (attachInterrupt())
    • Interrupts introduce latency (~5 µs per ISR call).
    • If an interrupt is triggered at 16 MHz, the CPU wouldn’t have time to handle it before the next pulse arrives.
  • So in the given approach, we used a polling method for capturing rising edge. Which can calculate the frequency of 1.14MHz accurately without any error.

Why 1 MHz is the Maximum Detectable Frequency?

1️. After First Capture:

  • Reads ICR1 (16-bit) → 2 cycles (125 ns)
  • Clears ICF1 flag → 1 cycle (62.5 ns)

2️. Waiting for Second Capture:

  • The while loop checks ICF1 every 8 cycles (500 ns @ 16 MHz)
  • If an overflow occurs, handling it adds 4 more cycles (250 ns)

3️. Limit on Frequency Detection:

  • To detect a full waveform (one period), the second capture must be accurate.
  • Since the loop takes approximately 8 cycles (500 ns) per iteration, the system can reliably detect signals ≥ 1 µs period (≤ 1 MHz frequency).
  • For frequencies > 1 MHz, the loop may miss transitions, causing incorrect measurements.

 Conclusion: The approach is accurate up to 1 MHz but fails for higher frequencies due to the loop execution time. 

Approach 2 (Counting Pulses using Timer counter)

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.

  • Counting Pulses in 1 Second
    • Signal Source: Connect the high-frequency signal (greater than 100 kHz) to Digital Pin 5 (ICP1) of the Arduino UNO.
    • Arduino UNO: Ensure proper grounding between the signal source and the Arduino.
       

Hardware Connection


Firmware

  • In this approach, Timer1 is used as a counter to count the number of pulses on Digital Pin 5, while Timer2 is used to generate a 1-second time interval. 
  • The frequency is calculated as the number of cycles counted in 1 second.
    • Frequency = No of cycles in 1 sec interval.
    • Time Period = 1 / Frequency.

Code

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++;
}

 

Code Explanation

  1. setup()
    • Initializes serial communication at 115200 baud to display output.
  2. loop()
    • Timer0 is disabled to avoid conflicts with delay() and millis().
    • Measurement starts using startCount().
    • Prints the frequency & time period on the Serial Monitor.
    • Re-enables Timer0 after measurement.
    • Waits for 500 milliseconds before starting again.
  3. startCount().
    • Disables interrupts (cli()) for safe configuration.
    • Configures Timer2 to interrupt every 1/100th of a second.
    • Configures Timer1 to count pulses from an external signal (on D5).
    • Resets counters (overflowCount = 0, TCNT1 = 0).
    • Enables interrupts (sei()).
  4.  disableTimer0().
    • Disable the timer0.
  5. enableTimer0().
    • Enable the timer0.
  6. ISR(TIMER1_OVF_vect)
    • It increments the count after the timer gets overflown.
  7. ISR(TIMER2_COMPA_vect)
    • Timer2 Interrupts 100 Times → 1 Second Elapsed.
    • cli(); → Disable interrupts to prevent data corruption.
    • timerTick = ((uint32_t)overflowCount * 65536) + TCNT1;
      • Calculates total number of pulses counted in 1 second.
    • Disables Timer1 & Timer2 (Stops counting).
    • Computes frequency & time period:
      • frequency = timerTick; → Pulses per second.
      • timePeriod = 1.0 / frequency; → Time period (seconds per pulse).
    • Sets countValue = true; → Tells loop() that measurement is complete.
    • sei(); → Re-enables interrupts.

How Code works

  • Timer1 counts signal pulses on pin D5.
  • Timer1 Overflow ISR increments overflowCount when Timer1 overflows.
  • Timer2 ISR tracks elapsed time.
  • After 1 second, Timer2 ISR:
    • Stops all timers.
    • Calculates frequency & time period.
    • Prints results to Serial Monitor.
  • Loop repeats every second.

Conclusion:

  1. The Pulse Counting Method allows measurement of signals up to 8 MHz.
  2. For an 8 MHz signal, the error is approximately ±0.1 , making this approach highly accurate for a wide range of frequencies.

 

Output

Hardware Setup

 

Input Capture Mode

 

Counting Pulse Counting using Timer Counter

 

Video

Input Capture Mode

 

Counting Pulse Counting using Timer Counter



 

Submit Your Solution

Note: Once submitted, your solution goes public, helping others learn from your approach!