Measuring ADC voltage & Frequency on the same pin?


Well-known member
I am building a power meter for audio on a Teensy 4.x and I would like to measure AC volts on an analog pin, AC amperes on an analog pin, but I would also like to measure the frequency of each sample, as well as the phase/time difference between them (for power factor calculation)

Sampling range is 10Hz-20Khz. I was thinking of running the ADC using a timer at around 400Khz to get a decent amount of samples at the upper range of the spectrum.
But what do I do for calculating frequency? I though of using FreqMeasureMulti but it's recommended for 0.1-1000Hz only and uses the pin as a PWM input(?)

Any other suggestions on how I could achieve the voltage sampling as well as calculating frequency? (It does not need to be accurate to a decimal point)
Usually the first step, if you have just raw ADC samples, is to convert to signed integers. If you know the DC offset, you can just subtract it. If the DC offset isn't precisely known, you can find it by just taking the average of many samples, or implementing a moving average algorithm. Or you could implement a high pass filter in any number of traditional ways (biquad, FIR, etc) to remove the DC and end up with signed numbers... though pretty much any approach that dynamically finds and subtracts the DC level can be considered a high pass filter.

Once you have signed data, AC voltage is a pretty simple matter of just multiplying each sample by itself, adding all those up over the period of time you want to know the voltage, and then take the square root.

Frequency is pretty easy if you have a simple waveform like a sine, square, triangle or sawtooth. Just scan through the samples and look for the number of places where you have a negative number followed by a positive number. Then divide that count by the amount of time for all the samples you analyzed to get (approximately) the frequency.

If you want to get fancy, you can look at the actual negative and positive numbers and try to estimate the sub-sample timing when the signal went from negative to positive. For example, if the samples were -25 and +200, you could assume the waveform crossed zero close the beginning of the time between those 2 samples. Maybe you do this for just the first and last pairs, so you can end up with higher resolution of the total time.

This very simple frequency algorithm can give unreliable results if you have a slowly varying waveform like a sine or triangle and also some high frequency signal or noise added. As the slow waveform crosses from negative to positive, you can end up counting several crossings if the high frequency component causes the signal to cross back and forth more than once. The simple way to deal with this is hysteresis, where you set a threshold slightly below zero to consider the signal negative and slightly above zero to consider it positive.

But if you have a very complicated signal with a lot of harmonics (like the sound of a tuba or other large horn instrument) or a lot of higher frequencies mixed in (like the sound of most plucked string instruments), then simple analysis almost never gives good results. Very advanced analysis like the YIN algorithm is needed to find the fundamental frequency for those really complicated sounds.
Do you really need to measure the frequency, or do you know you have nominal frequency of 50 or 60 Hz? If you know, you can compute apparent, real, and reactive power without measuring phase angle between AC voltage (vac) and AC current (iac). Create a ring buffer that contains samples representing 1 or more complete cycles. For example, if your frequency is 50 Hz and your sample rate is 10 kHz, you can use 200 samples (1 cycle), 400 samples (2 cycles) etc. If your frequency is 60 Hz and sample rate is 10 kHz, you can measure over 3 complete cycles with 500 samples. On each sample, update your buffer and a runnning sum and sum-of-squares, and use them to compute:

instantaneous power = iac*vac
apparent power (P) = rms(iac) * rms(vac)
real power (S) = average(iac*vac)
reactive power (Q) = sqrt(P^2 - S^2)

You do need to know whether current is leading or lagging voltage to get the sign of Q
Thank you both for the responses.

The bare basic need I have it so calculate the RMS voltage from multiple samples by multiplying, averaging and then square root, as Paul mentioned.
But calculating frequency and phase shift between v/a etc is something I would like to add on as well as I don't need much more additional hardware for it.

So the signal is music, so it's dynamic and not nominal. As mentioned, range is 10Hz all the way up to 20KHz or however higher I can get up to 20KHz.

If you guys have any code to reference for some of the methods provided above, I'd love to study them
If you guys have any code to reference for some of the methods provided above, I'd love to study them
I don't have any code, but if you google "zero crossing detection algorithm" you will find many references, and Paul's previous reply gives a good outline of this method.
The audio library has RMS analysis. This is the official documentation (right side panel)

And here is a link to the source code.

But if you're writing new code from scratch and you only need to use Teensy 4.x, might be better and certainly will be easier to just use float. This fixed point code is highly optimized, which mattered more on Teensy 3.2 which didn't have a FPU and run under 100 MHz.
Would something like this be a good start to find calculate the frequency with zero crossing points? (Used Bard and some of my brain for this)

#include <ADC.h>
#include "circular_buffer.h"

// Define the ADC pin and ADC instance
const int adcPin = 14;
ADC *adc = new ADC();

// Create a Circular_Buffer object to store ADC values and timestamps
Circular_Buffer<float, 1024 >adcValues;
Circular_Buffer<uint32_t, 1204>timestamps;

IntervalTimer adcTimer;
// Volatile flag to indicate a detected zero-crossing
volatile bool zeroCrossingDetected = false;

void setup() {
  pinMode(adcPin, INPUT_DISABLE);
  adc->adc0->setResolution(10); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // change the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // change the sampling speed
  adcTimer.begin(adcTimerInterrupt, 4); //250Khz sample rate

void loop() {
  // ...

void adcTimerInterrupt() {
  // Read the ADC value
  float adcValue = (((adc->adc0->analogRead(adcPin)) / 1024) / 3.3) - 1.65; // Detect -1.65 v to +1.65v

  // Store the ADC value and timestamp in the ring buffers

  // Check for zero-crossing
  if (zeroCrossingDetected) {
    // Check for the next zero-crossing
    if ((adcValue > 0 && adcValues.peek_front()[0] <= 0) || (adcValue <= 0 && adcValues.front()[0] > 0)) {
      // Detect zero-crossing
      zeroCrossingDetected = false;

      // Estimate the exact zero-crossing time using interpolation
      uint32_t previousTimestamp = timestamps.pop_back();
      uint32_t currentTimestamp = timestamps.peek_front()[0];
      float previousValue = adcValues.pop_back();
      float currentValue = adcValues.peek_front()[0];

      // Calculate the slope of the line segment connecting the two ADC values
      float slope = (currentValue - previousValue) / (currentTimestamp - previousTimestamp);

      // Estimate the zero-crossing time based on the slope and the zero-crossing timestamp
      uint32_t estimatedZeroCrossingTimestamp = currentTimestamp - (currentValue - 0) / slope;

      // Calculate the frequency based on the estimated zero-crossing time
      uint32_t timeInterval = estimatedZeroCrossingTimestamp - previousTimestamp;
      float frequency = 1000000.0 / timeInterval;

      Serial.printf("Freqency: %.1f \n",frequency);
  } else {
    // No zero-crossing detected yet
    if ((adcValue <= 0 && adcValues.peek_front()[0] > 0) ||
        (adcValue > 0 && adcValues.peek_front()[0] <= 0)) {
      // Detect zero-crossing
      zeroCrossingDetected = true;