Precise frequency measurements with Arduino microcontroller

Frequency measurements are necessary in many projects. Many years ago, when microprocessors in measurement instruments were a rarity, frequency meters usually were based on digital counters. With a long enough measurement frame (typically 0.1 second, 1 second or even more) and sufficient number of serially connected binary-decimal counters it was possible to conduct very precise measurements, with relative error about several ppms (mostly determined by the internal quartz clock parameters).

Nowadays everything is measured by microcontrollers. Surprisingly, time and frequency measurement algorithms sometimes may give poor results, especially when the microcontroller is doing several tasks in parallel (as usual), so one should always thoroughly test the solution. There are several libraries available for Arduino that allow frequency measurements (such as FreqMeasure, FreqCounter or FreqPeriod) but unlike system function pulseIn() they are less portable, have some limitations, e.g. provide good accuracy only at high or low frequencies, and certainly take some space in the small microcontroller memory. Here I will discuss another couple of extremely simple algorithms, assuming that input signal is in kHz range and has standard TTL level so that no amplification and threshold detector circuitry is required, and I want to obtain at least 4 significant digits.

Using pulseIn() for time/frequency measurements

My first candidate was this standard function from Arduino library. Unfortunately in this case duty cycle of the input signal is essential, because pulseIn() takes working level (HIGH or LOW) as an input parameter. One can use external D-type trigger to ensure symmetric signal shape or measure both levels independently, but in the following sketch, for simplicity, I assume duty cycle = 0.5. Also I will accumulate and average several period values within given time frame (0.1 second in my case) as in Voltmeter application.

#define MainPeriod 100  // measurement frame, milliseconds
#define f_input_pin 2   // input pin for pulseIn

long previousMillis = 0;
unsigned long duration=0; // receive pulse width
long pulsecount=0;

void setup() 
{
  pinMode(f_input_pin, INPUT);
  Serial.begin(19200);
}

void loop()
{
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= MainPeriod) 
  {
    previousMillis = currentMillis;   
    // write current time and F values to the serial port
    Serial.print(currentMillis);
    Serial.print(" "); // separator!
    float Freq=0.5e6/float(duration); // assume pulse duty cycle 0.5
    Freq*=pulsecount;
    Serial.print(Freq);
    Serial.print(" "); 
    Serial.print(pulsecount);
    Serial.print(" ");
    Serial.println(duration);
    duration=0;
    pulsecount=0;
  }
  // instead of single measurement per cycle - accumulate and average
  duration += pulseIn(f_input_pin, HIGH, MainPeriod*900);
  pulsecount++;
}

The results of measurements of 1 kHz test signal presented on this plot (red circles, data just copied from the Arduino port monitor):

Frequency measurements by standard Arduino function pulseIn()

Frequency measurements using interrupt handler

Although the algorithm works, the observed dispersion of points is unexpectedly large and far above the "theoretical limit" which can be estimated as a quotient of time resolution of pulseIn() function and the total measurement time:

maximal noise/signal ratio = ~ 2 microseconds / 100 milliseconds = 0.002%

The value of approximately 2 microseconds was obtained from the comments in the source code, also the Arduino documentation claims that minimal time resolution for time - related services is about 2..4 microseconds, or so. Since I would like to obtain more than order of magnitude better values of noise-to-signal ratio, another algorithm should be developed. The following algorithm is self-explaining (I hope) and uses interrupt handler to count pulses and their total duration, the same 0.1 second measurement frame is used:

// period of pulse accumulation and serial output, milliseconds
#define MainPeriod 100
long previousMillis = 0; // will store last time of the cycle end
volatile unsigned long duration=0; // accumulates pulse width
volatile unsigned int pulsecount=0;
volatile unsigned long previousMicros=0;

void setup()
{
  Serial.begin(19200); 
  attachInterrupt(0, myinthandler, RISING);
}

void loop()
{
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= MainPeriod) 
  {
    previousMillis = currentMillis;   
    // need to bufferize to avoid glitches
    unsigned long _duration = duration;
    unsigned long _pulsecount = pulsecount;
    duration = 0; // clear counters
    pulsecount = 0;
    float Freq = 1e6 / float(_duration);
    Freq *= _pulsecount; // calculate F
    // output time and frequency data to RS232
    Serial.print(currentMillis);
    Serial.print(" "); // separator!
    Serial.print(Freq);
    Serial.print(" "); 
    Serial.print(_pulsecount);
    Serial.print(" ");
    Serial.println(_duration);
  }
}

void myinthandler() // interrupt handler
{
  unsigned long currentMicros = micros();
  duration += currentMicros - previousMicros;
  previousMicros = currentMicros;
  pulsecount++;
}

The results of the second sketch (blue squares, plotted on the previous plot for comparison) are much more reasonable, so the whole frequency range should be tested. In this case I used special application for DM2003 instead of port monitor, input signal was generated by PicoScope 2205 as in previous experiment. I plotted F(t) dependence and for each interval with fixed frequency the noise-to-signal ratio was calculated as a function of input frequency (top inset). As one can conclude, the accuracy is good up to 10 kHz. At higher frequencies the number of pulses (and corresponding interrupt handler overheads) increase, as well as the measurement error. It should be noticed however that in the worst cases (see bottom inset) the main origin of the large error is a single defective point, so that I think that this algorithm may be improved.

Frequency measurements using interrupt handler

<html style="width: 200px">

<head>

<!--
Warning: you must correct language settings if you 
will use localized resources in the scripts and HTML!
-->

<meta http-equiv="X-UA-Compatible" content="IE=5">
<meta http-equiv="Content-Language" content="en-us">
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<meta http-equiv="MSThemeCompatible" content="Yes">
<meta name="GENERATOR" content="DM Script Editor">
<meta name="Author" content="RRR">
<meta name="vs_targetSchema" content="HTML 4.0">

<title>Frequency Measurement</title>

<!-- This script process data stream generated by FMeasurement.ino -->

<style>
<!--

body
{
color: "BUTTONTEXT";
background-color: "THREEDFACE";
}

p, td, button, input, select, label, div, object, legend, fieldset
{
font-family: Arial;
font-size: 8pt;
}

/* change element styles or/and add your custom styles here */

-->
</style>

</head>

<body topmargin="10" leftmargin="10" scroll="no">

<script id="clientEventHandlersVBS" language="VBScript">
<!--

Option Explicit

dim Server
set Server=window.external 
' Use Server variable to access DM application object

dim wnd ' as IDMDocument2

Sub window_onload
' Application initialization code may be here
  Port.Open "1,19200"
  set wnd = Server.CreateDocument("")
  wnd.Worksheet.ColumnLabels(1)="Time, ms"
  wnd.Worksheet.ColumnLabels(2)="F, Hz"
  wnd.Worksheet.ColumnLabels(3)="pulse count"
  wnd.Worksheet.ColumnLabels(4)="total width, us"
  wnd.Worksheet.ColumnLabels(5)="F<sub>calc</sub>, Hz"
  wnd.Worksheet.Expression(5)="1e6*c/d"
  wnd.Worksheet.Font.Name="Arial"
  wnd.Worksheet.RowHeight=18
  wnd.Plot.XAxis.Font.Name="Arial"
  wnd.Plot.XAxis.Format="4.1e"
  wnd.WindowState=1 ' dwsPlot
  wnd.IsRecording=true
  wnd.Plot.CurrentSerie.XColumn=1
  wnd.Plot.CurrentSerie.YColumn=2
  wnd.Plot.CurrentSerie.IsRecording=true
End Sub

Sub window_onunload
' Application termination code may be here
  Port.Close
End Sub

Function window_onerror(msg, url, line) ' global error handler
  window_onerror=true
  MsgBox "Unexpected error with message: " & vbCrLf & msg & vbCrLf & "Url: " _
    & url & vbCrLf & "Line: " & line, vbCritical, document.title
End Function

' Place your script code here

dim buffer : buffer=""

' Fired when incoming data are ready
sub Port_OnRead(Data)
  ' enter your code here
  buffer=buffer & Data
  if InStr(buffer, vbCrLf)>0 then
    Display.innerText=buffer
    if RecordCB.checked then
      wnd.Container.AddItem(buffer)
      wnd.Container.Modified=false
      wnd.Plot.CurrentSerie.AddPoint
    end if
    buffer=""
  end if
end sub

-->
</script>

<!-- Place your HTML code here -->

<p id="Display"></p>

<input type="checkbox" id="RecordCB" checked> <label for="RecordCB">
Enable recording</label>

<object classid="clsid:673AAF16-9A0B-11D4-B2A4-FD6847C75367" id="Port">
  <span style="color: red">Unable to instantiate control!
  (type library: DMForms)</span>
</object>

</body>

</html>