Assistance with High-Speed Frequency Measurement on Teensy 4.1

derlev

Member
Hello Teensy Community,

I am currently engaged in a project requiring precise frequency measurement using a Teensy 4.1. The project involves measuring the interval of a sine wave generated from an induced voltage. After conducting initial tests with professional equipment, we've determined that we need to achieve a sampling rate of at least 30 measurements per millisecond to accurately capture the waveform.

Challenge:

  • Objective: To accurately measure the intervals of a sine wave of induced voltage using ADC on a Teensy 4.1.
  • Required Sampling Rate: Minimum of 30 samples per millisecond.
Current Progress:

  • In our current setup, we are only able to achieve an average of about 11 measurements per millisecond over a span of 10,000 samples, which falls short of our requirement.
  • We understand that achieving such a high sampling rate might involve optimizing the ADC settings or possibly employing advanced techniques.
Questions:

  1. Could you provide insights or suggestions on how to optimize the Teensy 4.1's ADC to achieve our target sampling rate of 30 measurements per millisecond?
  2. Are there specific configurations, libraries, or coding practices with Teensy 4.1 that would facilitate reaching this high frequency measurement capability?
  3. Would implementing techniques like Direct Memory Access (DMA) or other advanced methods be beneficial in this context?
  4. Any tips on managing data buffering and processing at such high sampling rates would also be greatly appreciated.
I understand that this is a challenging task, and any advice, code snippets, or pointers to relevant resources would be extremely helpful. Thank you in advance for your time and assistance.
 
Even with no optimization, the program below achieves about 55 kHz sampling. If this approach is not fast enough, you can do much better using the ADC library, which has methods to optimize ADC conversions, use interrupts, etc. For example, as opposed to the blocking analogRead(), you can start a conversion, do something else, then check to see if the conversion is complete, etc. The number one rule on this forum is to show what you're doing. You'll get much better help that way. If you haven't done so yet, look through the examples in the ADC library. That will give you a good idea of the possibilities, but if I remember correctly, you can pretty easily get up to 200 kHz, perhaps higher.

Code:
elapsedMillis ms;
uint32_t count;
uint32_t prevCount;

void setup() {
  Serial.begin( 9600 );
  while (!Serial) {}
  ms = 0;
}

void loop() {
  uint16_t input = analogRead( 10 );
  count++;
  if (ms >= 1000) {
    Serial.println( count - prevCount );
    prevCount = count;
    ms -= 1000;
  }
}
 
Hey! Thank you for your response! I am sorry, I forgot to paste my code in here:
#include <ADC.h>

ADC *adc = new ADC(); // adc object

void setup() {
Serial.begin(9600);

adc->adc0->setAveraging(32); // no averaging
adc->adc0->setResolution(16); // 8 bits resolution

// Try these settings
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // fastest conversion
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // fastest sampling

pinMode(A0, INPUT);
}

void loop() {
uint32_t start = micros();

int numSamples = 1000;
for (int i = 0; i < numSamples; i++) {
volatile int value = adc->adc0->analogRead(A0); // volatile to prevent compiler optimization
}

uint32_t duration = micros() - start;
Serial.print("Time for ");
Serial.print(numSamples);
Serial.print(" samples: ");
Serial.print(duration);
Serial.println(" us");
delay(1000);
}

This is the highest sampling rate /ms I could achieve.
 
averaging by 32 means you are timing 32000 ADC readings, not 1000.

You don't want averaging for high speed acquisition. You do want accurate sample timing though, which likely means using interrupts or DMA to drive the ADC.
 
Yeah, I was testing something and I forgot to change it back to
adc->adc0->setAveraging(0);
adc->adc0->setResolution(10);
But when I print it to the serial monitor (I know it slows the sample time/ms but it measures around 100 when there is no impulse...
 
I took your setup() and merged it with the logic in my previous example to get the program below. It consistently reports 892849 samples per second. You may find that you don't want to use VERY_HIGH_SPEED sampling and conversion, because they do affect the accuracy of individual readings, but clearly you have a lot of leeway. You can set up an interval timer to execute at 30 kHz or higher, and get your data. Doesn't that answer your question?

Code:
#include <ADC.h>

ADC *adc = new ADC(); // adc object

void setup() {
  Serial.begin(9600);

  adc->adc0->setAveraging(1); // no averaging
  adc->adc0->setResolution(10); // 10 bits resolution

  // Try these settings
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // fastest conversion
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // fastest sampling

  pinMode(A0, INPUT);
}

void loop() {
  uint32_t count = 0;
  elapsedMillis ms = 0;
  while (ms < 1000) {
    volatile int value = adc->adc0->analogRead(A0); // volatile to prevent compiler optimization
    count++;
  }
  Serial.println( count );
}
 
Thank you for your help! I need to measure voltages up to 3V with an accuracy of 0.01V. However, I'm getting non-zero readings even with no input signal. Could this be due to noise or a configuration error? Any tips for accurate measurements in this voltage range?
 
If you're polling the Teensy ADC from loop() as in code examples above, then expect trouble if you're targeting 30.0 kHz deterministic sampling.

A better approach would be to have a hardware timer trigger ADC conversions, with ADC results pushed into a huge FIFO via DMA. That way, you will not get gaps when there's higher priority activity that is blocking. Blocking stuff that may bite you is for example:
* Using emulated EEPROM (expect up to 1 second for writes)
* Setting the real time clock (expect 100 us gaps)
* USB Serial() activity
* SD card activity

The "Audio Shield" ADC route might be a better choice for the task at hand?

But what actually is your approach to do "precise frequency measurement"? How precise? How fast do you need answers on what the frequency was? And is it ok that you only look at the signal every now and then, so that the Teensy could do other stuff while ignoring the analog input for say 1 second or so? Do you want to know the frequency each and every cycle or just once per second or so?

Maybe you aim to capture say 10000 ADC samples, then do FFT and then find a peak in the spectrum? But maybe you just make the signal AC in software (=subtract the moving average) and then look for zero crossings?

If you really want 'accurate' and 'fast', as in say 10 ns -ish resolution, each and a score for every cycle of your say ~0.1 to 10 kHz induced sine wave input signal then it might be smarter to use hardware timers. That is AC couple through a capacitor, amplify/cap if/as needed, probably some hysteresis enabled, and connect to a Teensy digital input pin. So forget ADC altogether...
 
My project involves using a microcontroller to accurately measure the frequency of a waveform, which is crucial for determining water flow through a turbine. It can only stop measuring when there is no impulse, there is no water flowing through the turbine. I don't need the answer instantly. My idea was, that it stores the measured data in its memory, and later on, I'll use Wi-Fi to transmit it. When there is no impulse the teensy can be in a sleep mode, but when there is, it needs to constantly measure it.

I tried what zero crossing

The frequency is estimated based on the count of zero crossings. Since each complete wave cycle has two zero crossings (upward and downward), the frequency is half the number of crossings.

C++:
#include <ADC.h>

ADC *adc = new ADC(); // adc object

void setup() {
    Serial.begin(115200);

    adc->adc0->setAveraging(1); // no averaging
    adc->adc0->setResolution(10); // 10 bits resolution
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // fastest conversion
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // fastest sampling

    pinMode(A0, INPUT);
}

void loop() {
    const int numSamples = 10000; // Number of samples in a burst
    for (int i = 0; i < numSamples; i++) {
        int value = adc->adc0->analogRead(A0); // Read from ADC
        Serial.println(value);
    }
    delay(1000); // Delay before next burst of samples to avoid overwhelming the serial buffer
}

And for python:

Python:
import serial

# Initialize serial connection
ser = serial.Serial('COM4', 115200)  # Update COM port and baud rate as needed

def read_adc_data():
    data = ser.readline().decode().strip()
    return int(data)

def zero_crossing_detection(samples):
    crossings = 0
    for i in range(1, len(samples)):
        if samples[i-1] * samples[i] < 0:
            crossings += 1
    return crossings

# Main loop
samples = []
while True:
    value = read_adc_data()
    samples.append(value)
   
    # Perform zero-crossing detection over a certain number of samples
    if len(samples) >= 1000:  # Adjust the sample size as needed
        crossings = zero_crossing_detection(samples)
        frequency = crossings / 2  # Frequency estimation
        print("Estimated Frequency:", frequency, "Hz")
       
        samples.clear()  # Reset the sample list for the next batch
 
Last edited:
Also, I tried your suggestion:


C++:
#include <ADC.h> // 

ADC *adc = new ADC(); // Create an ADC object

void setup() {
    Serial.begin(115200); // Start serial communication at a fast baud rate

    adc->adc0->setAveraging(1); // 
    adc->adc0->setResolution(10); // Set 10-bit resolution
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);

    pinMode(A0, INPUT); // Set the analog pin as input
}

void loop() {
    int value = adc->adc0->analogRead(A0); // Read from the ADC
    Serial.println(value); // Send the value over Serial
    delayMicroseconds(10); // Short delay for stable sampling
}

Python:
import serial
import numpy as np
from scipy.fft import fft
from scipy.signal import find_peaks

# Initialize serial connection
SERIAL_PORT = 'COM4'
BAUD_RATE = 115200
NUM_SAMPLES = 1000  # Number of samples for FFT

ser = serial.Serial(SERIAL_PORT, BAUD_RATE)

def read_samples(num_samples):
    data = []
    for _ in range(num_samples):
        line = ser.readline().decode('utf-8').strip()
        data.append(int(line))
    return data

def perform_fft(data):
    # Perform Fast Fourier Transform
    fft_result = fft(data)
    fft_magnitude = np.abs(fft_result)
    fft_freq = np.fft.fftfreq(len(fft_magnitude), 1.0 / SAMPLING_RATE)
    return fft_freq[:len(data)//2], fft_magnitude[:len(data)//2]

def detect_peaks(fft_freq, fft_magnitude):
    peaks, _ = find_peaks(fft_magnitude)
    peak_freq = fft_freq[peaks]
    peak_magnitude = fft_magnitude[peaks]
    return peak_freq, peak_magnitude

# Main process
samples = read_samples(NUM_SAMPLES)
fft_freq, fft_magnitude = perform_fft(samples)

peak_freq, peak_magnitude = detect_peaks(fft_freq, fft_magnitude)

# Output the peaks
for f, m in zip(peak_freq, peak_magnitude):
    print(f"Frequency: {f:.2f} Hz, Magnitude: {m:.2f}")
 
Thank you for your help! I need to measure voltages up to 3V with an accuracy of 0.01V. However, I'm getting non-zero readings even with no input signal. Could this be due to noise or a configuration error? Any tips for accurate measurements in this voltage range?
If you are using a pin with the ADC. you should not use "pinMode(A0, INPUT);" That makes internal connections appropriate for digital inputs. You should use "pinMode(A0,INPUT_DISABLE);" which removes resistors that can affect signals near the digital transition voltage.
 
I guess it is a bit late but I am interested if your 2nd approach worked. I tried something similar in the past where using a teensy 4.1 effectively as an ADC for my computer. I ran into the issue that the python serial connection could only handle a few kB/s. Unfortunately it didn't throw any errors but just started recieving mangled data (buffer overflow or something similar). Curious if you were able to stream at a higher rate :)

I ended up performing all the computation on my teensy and only transmitting the results to python. In my case it was a convolution calculation which requires fourier transformation.

I found this forum post for the convolution calculation. Have a special look at the line:
Code:
  // Perform FFT
  arm_cfft_radix4_q15(&fft_inst, fft_buf);

The arm processor on the teensy seem to have a very low level and very fast implementation for FFT. The output still needs to be processed, because there does not seem to be a native implementation of complex numbers.
 
I thought CMSIS had all the relevant vector operations like complex magnitude? I do know T4 only out of the box supports a certain version of CMSIS, perhaps that's the issue?
 
I thought CMSIS had all the relevant vector operations like complex magnitude? I do know T4 only out of the box supports a certain version of CMSIS, perhaps that's the issue?
I think I just had another definition of native implementation of complex numbers. In python there is an actual type for them, with most arithmetic operations working without having to think about it anymore.
I am not very familiar with cpp, but it seems to me like complex numbers are usually handled by doubling the length of the array and having real and imag values alternatingly stored.
 
Back
Top