T4 ADC high speed sampling - how to?

Rezo

Well-known member
I need some guidance on a sketch I want to put together.
My goal is to be able to calculate true RMS voltage of a sine wave with a frequency range of 10Hz to 20Khz.

Here are my requirements:
1. I only need to sample a half wave at a time
2. I need to start the data collection at the zero crossing from negative to positive cycle, and stop it at the zero crossing from positive to negative cycle
3. I need to square each sample and add it to a sum of samples variable, as well as count the number of sample so that I can calculate RMS after the zero crossing
4. I need to capture the peak value
5. I don't need any scaling right now. Can keep it at 0-3.3v and offset it by half for a positive/negative output value

I want to compare cycle samples RMS to peak value RMS at the end of each half cycle capture - that's it

Can someone guide me on the best approach towards this?
Thanks!
 
There are some issues you need to consider:

1. How noisy is your input signal? This will determine how much hysteresis you will need in determining the zero crossing.
2. How often do you need to collect this data?
3. Will the results be used for real-time control of other processes? If so, the problem becomes much more difficult.
4. How fast do you expect the amplitude and frequency to change while you are sampling?

You may want to consider using the T4 analog comparators to do the zero-crossing detection. I used them in a frequency-counting application some years ago.

https://forum.pjrc.com/index.php?th...ter-using-analog-comparator.66725/post-274645

Using the comparators might simplify the problem by doing the zero-crossing detection in hardware.

Your very wide range of input frequencies will add some complexity. At the 20KHz end, you will need very fast ADC sampling to get a good peak value and you may need to minimize processing during the collection cycle, which is only 25 microseconds for a half-cycle. It is possible to sample and store data at 1MSamples/second, but it can be challenging. There is a long thread on the challenges of collecting data at 1MSamples/second at

High speed ADC sampling with T4

Picking the right combination of ADC resolution, sampling speed, and conversion speed can be challenging.

Next, you may need adaptive sampling rates to handle the low end of your frequency range. 1/2 cycle at 10Hz is 50 milliseconds. You may not want to tie up your processor for that long, so interrupt-based collection and processing may be required--particularly if the answer to question #3 above is 'yes'.
 
@mborgerson thank you for taking the time to respond!

Here are the answers to your questions:
1. Class D audio amplifier output, mostly car audio applications, so it might have some Alternator noise and any class D topology related noises and harmonics.
2. I need to collect the data all the time
3. The result will be used to attune the signal if needed using a PGA2310 digital pot connected over SPI
4. Since the bigger application is for subwoofer amplifiers, the frequency range is ALOT smaller (10-100Hz) and so the change might be rapid but not a whole lot off. For a full range output the frequency would swing across the spectrum.

I did see your comparator code, but I am clueless on how to modify it (or what to modify actually)

would it be possible to setup two ADC channels to sample the same pin? Perhaps one channel samples slower, the other faster, then using the zero crossing detection decided which one has the more adequate amount of data to process?

BTW you used the same URL for both links
 
BTW you used the same URL for both links
Sorry about that. I'm in the middle of a cold caught on return from a family vacation trip.

This link may be more useful
Fast ADC sampling


I'll look at your other questions the first morning after I get more than four hours of sleep!
 
If you want to sample the same signal with two different ADC channels, or even connect one signal to both an ADC input and a comparator input, you may need a couple of amplifiers to divide the signal into two (or more) parts, each having a low output impedance. The problem with simple splitters using only a few resistors, or just two wires, is that when the ADC samples a signal, it draws current from the source to charge an internal capacitor and that can drop the voltage at the source a few micro or millivolts (depending on the source output impedance). If a second channel samples the same signal before the source recovers, it will get an error in its sensed voltage. This is usually a small problem, but it becomes worse as your overall sampling rate increases, as the source has less time to recover. If the two channels are sampling at different rates, you may get harmonic interference in your data.

A better approach may be to sample the at your desired high rate, then duplicate the results and do a digital low-pass filter on one part and resample that result at a lower rate. However one of the problems with digital low-pass filters is that they can introduce phase shift in the output. There are ways to avoid the phase shift, but they may exceed the capabilities of the T4.1 for real-time collection and processing.

I'll take another look at your original post and see what other suggestions I can make, now that I don't have to reach for a kleenex after every fifth word!
 
The suggestion to read the same signal on two channels was just an idea, but seems like external hardware needs to be added (active buffers etc) and I want to keep the circuit as simple as possible

What if we reduce the frequency range down to 1Khz. So 10Hz to 1Khz.
Even 500Hz top limit would be okay to start with.

My only concern is the noise pickup from whatever is powering the Teensy. At home
I was getting 50Hz noise from the USB power (Hub connected to my monitor/macbook)

Perhaps an isolated DC-DC power supply could help with that?
 
@mborgerson anything you can suggest?

The comparator rout seems like a very good trigger option so start/stop ADC samples.
Perhaps use two buffers, so while one is being filled via DMA the other can be read and calculated.
Just need some guidance or some base sketch with a breakdown and I think I can learn from playing around with it.
 
@mborgerson anything you can suggest?

The comparator rout seems like a very good trigger option so start/stop ADC samples.
Perhaps use two buffers, so while one is being filled via DMA the other can be read and calculated.
Just need some guidance or some base sketch with a breakdown and I think I can learn from playing around with it.
I've take a break from another project to look at your problem. There are several significant issues that need to be cleared up before I can come up with an algorithm:

1. What is the level of the signal output? I assume that, since it is an audio amplifier output, it has low output impedance (8 Ohms is a common speaker impedance). If the speakers are to be connected while measuring, you have to watch out for changes in their impedance with input frequency.

2. What is the input to the amplifier? A true sine wave with low noise makes things simpler.

3. What is your desired accuracy and precision?

Determining the 'zero' for zero crossings will be complicated unless input has a stable offset and is a sine wave. Using a capacitive coupling circuit to the teensy gets really complicated for frequencies around 10Hz, as you need a very large capacitor. At 10Hz, a 100-microfarad capacitor has a reactance of 159 ohms. The voltage divider that you use to center the output at 1.65V should use resistors of at least 47K Ohms to get errors under 1%. For a simple circuit, the capacitor has to be non-polar, as the input will be both above and below the 1.65V output.

If you add an operational amplifier to the input circuit, you can use smaller capacitors and resistors. I'll follow up on this part when I've done a bit of analog circuit modeling.


If you know the zero-signal offset at the Teensy pins, you can use the comparator to set the zero-crossing points and trigger a collection session. If there is some drift in the offset, you can estimate the zero-level as half-way between the signal maximum and minimum. Doing it that way may involve more samples and calculations.
 
I did some simple LTSpice modeling, and it looks like a simple capacitor and resistor divide may be adequate.

For the 20 micro-farad capacitor could parallel a couple of 10uF ceramic caps.
The output graph is hard to read, as the lines turned out to be too narrow. I used a relative voltage scale on the plot, as I hate converting plots in dB to what I see on my oscilloscope.

Simple Offset copy.png
Simple offset frequency response.png
 
I've take a break from another project to look at your problem
Thank you, I appreciate the time spent on helping out!

1. What is the level of the signal output? I assume that, since it is an audio amplifier output, it has low output impedance (8 Ohms is a common speaker impedance). If the speakers are to be connected while measuring, you have to watch out for changes in their impedance with input frequency.

2. What is the input to the amplifier? A true sine wave with low noise makes things simpler.

3. What is your desired accuracy and precision?
1. The signal will be attuned, but the target is 5-15kW amplifiers, so peak to peak voltages of up to 400-500 volts. Plan is to use a voltage divider to get the levels down to around 3vpp, feed it into a buffer, then into a differential to single ended stage as some of the power amps might be full bridge technology.
2. The input of the amplifier is dynamic audio (music), although these amps in most cases won't be playing anything above 100Hz. But I do have a friend that would love to hook this tool up to his front stage amp, that plays between 200Hz and 20Khz
3. It's hard to describe accuracy here, but I would like to detect clipping. The method here is to compare true RMS to peak RMS. When true RMS is higher than peak RMS, that's when clipping is occurring. The ADC resolutions is what will determine how accurate that will be. I would like to stay on 10 bit resolution.

If you know the zero-signal offset at the Teensy pins, you can use the comparator to set the zero-crossing points and trigger a collection session
I want to use two voltage followers to create the reference voltage. One is on the ADC input to DC bias the AC signal, the other will be fed into the comparator for zero crossing detection.
I can then do an initial calibration as I can read the exact offset voltage with the ADC input.

I hope the above provides more context
 
After thinking about your goal--collecting RMS voltage data, I decided to try a software aproach that relies on a single ADC channels collecting data at 50KSamples/second. Other than the ADC channel (using timer-controlled ADC collection), and a few resistors and a capacitor, there is no other required hardware. There is a lot of arithmetic and housekeeping in the interrupt handler-----but it still runs in about 100nanoSeconds.

My test setup has a fall-off in the low frequencies as the largest non-polar capacitor I could find is only 0.47uF. The attached sketch will display the signal frequency and RMS volts. Noise in RMS voltage is pretty low--about a millivolt. The frequency result is within a few parts per thousand of the value I see on my 22-year-old Tektronix scope.
Code:
/*******************************************************
  RMS Voltage calculation using LOA (Lots of Arithmetic)
  Detection of zero crossing and accumulation of RMS data
  is all done with software using a single ADC channel collecting
  at 50KHz.
Input circuit:  80K-80K voltage divider and capacitively-coupled input
                                           |--80K Ohm Resistor---3.3V     
                            0.5 uF         |
   Signal Generator ->-------||------------|--------------Analog input A9
                                           |
                                           |--80K Ohm Resistor---GND
  Signal Generator ->----------------------|
  MJB   03/27/2024
***************************************************************/
#include <ADC.h>
// instantiate a new ADC object
ADC *adc = new ADC(); // adc object;
const char compileTime [] = "RMS Voltage with LOA algorithm  Compiled " __DATE__ " " __TIME__;
const int adcpin     = A9;
const int ADCMARKpin = 2;
const int ledpin     = 13;
// ADCMARKHI and ADCMARKLO are used to observe ADC IRQ  timing on oscilloscope
#define  ADCMARKHI digitalWriteFast(ADCMARKpin, HIGH);
#define  ADCMARKLO digitalWriteFast(ADCMARKpin, LOW);
#define  LEDON digitalWriteFast(ledpin, HIGH); // Also marks IRQ handler timing
#define  LEDOFF digitalWriteFast(ledpin, LOW);
#define  LEDTOGGLE digitalToggleFast(ledpin);
// buffers for histogram data
#define SAMPRATE 50000  // Sampling at 50KHZ
//  Variables used by ADC interrupt handler to communicate
//  with the foreground loop()
volatile uint32_t filtered_mean;  // filtered mean in ADC counts
volatile uint32_t freq_samples;   // number of ADC samples in full cycle
volatile uint32_t num_samples;    // number of samples in RMS data
volatile uint32_t sum_squares;    // sum of squared voltage counts
volatile bool data_ready;         // set after high-to-low zero crossing
void setup() {
  Serial.begin(9600);
  delay(500);
  LEDON
  Serial.println(compileTime);
  pinMode(ADCMARKpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(adcpin, INPUT_DISABLE);
  InitADC();
}
void loop() {  // just one option:  show the collected data
  char ch;
  if (Serial.available()) {
    ch = Serial.read();     
    if (ch == 'd')  {
      Serial.println("\nCollection Results");
      ShowData();
    }
  }
}
void ShowData(void){
float period, frequency;
float rmscounts, rmsvolts;
  while(data_ready){} // wait while data_ready
  while(!data_ready){} // wait until next data_ready
  Serial.printf("Filtered Mean: %lu\n", filtered_mean);
  period = (float)freq_samples/SAMPRATE;
  if(period >0) frequency = 1.0/period; else frequency = -1.0;
  Serial.printf("Period: %8.2fmSec   Frequency: %8.3fHz\n", 1000.0 *period,frequency);
  Serial.printf("num_samples: %lu   sum_squares: %lu\n",num_samples, sum_squares);
  rmscounts = sqrt((float)sum_squares/num_samples);
  rmsvolts =  3.32 * rmscounts/4096.0;
  Serial.printf("RMS counts:  %8.1f    RMS Volts:  %8.3f\n", rmscounts, rmsvolts);
}
/******************************************************
Initialize the ADC to automatically collect data
using a dedicated hardware timer.
 *****************************************************/
void InitADC(void) {
 
  Serial.println("Initializing ADC");
  adc->adc0->setAveraging(1 ); // set number of averages
  adc->adc0->setResolution(12); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED); // change the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::MED_SPEED); // change the sampling speed
  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(adcpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();
  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz
  filtered_mean = 2048;  // starting guess for filtered mean
  adc->adc0->enableInterrupts(adc0_isr);
}
/***************************************************************************
  This is the ADC interrupt handler.  The ADC Timer hardware starts collecting
  an ADC sample at the rate specified with the ADCTimer frequency (50KHz).
  This handler is called at the end of the ADC collection.  It has to do the
  follwing arithmetic while collecting ADC Values continuously:
  1. Note the sample at which the ADC value exceeds the filtered_mean of a complete
     input cycle. Mean values are calculated as the signal mean between
     two successive downward zero crossings.
     NOTE: Positive zero-crossing is  the point where the input exceeds the mean.
  2. After the positive zero crossing,  reset the sample counter,
     NSamples.  Also reset the sum of squared values, SUMSQUARES. Subtract the
     filtered_mean value from the sample and square the difference.
     Add this variable   to the sum of squared values,SumSquares.
  3. When in the top half of the input signal (above the mean), subtract filtered_mean
     from the input value and square the result. Add that squared value to SumSquares
     increment the sample counter, NSamples (includes the values just after zero crossing).
  4. Continue step 3 until the input voltage descends below the mean.  At this time
     you have collected NSamples and the sum of the squared sample values.
  5  Calculate the new_mean value from mean_samples and mean_sum.
     Save the value of mean_samples in a separate variable, freq_samples.
     Initialize the counter, mean_Count, to 1  and a sum, mean_Sum, to the ADC
     value
  6. Apply a simple low pass filter to the generated new_mean values to reduce
     noise on the mean value. Save the filtered value in filtered_mean.
     This updated value is not used until the next positive zero crossing.
  7. Set a boolean flag, DataReady, to indicate to the foreground loop() that new
     values of NSamples and SumSquares are available.  The foreground loop will
     use these values to calculate the RMS value for the top half of the input
     cycle.  The foreground can also use the values of filtered_mean and freq_samples
     to display the mean value and the signal period (or frequency: 1/period).
     NOTE: variables local to ISR are in form: VariableName (called CamelCase).
           variables communicating outside ISR are in form:  variable_name.
   When collecting data, the ISR runs in about 100nanoSeconds (66 clock cycles).
   The T4X can do a LOT of Arithmetic very quickly!
  ******************************************************************************/
void adc0_isr()  {
  uint32_t AdcVal, PlusValue;
  static bool InUpperHalf = false;  // used in detection of zero-crossing
  static bool LastwasUpper = false; // used in detection of zero-crossing
  static bool RisingEdge = false;
  static bool FallingEdge = false;
  static uint32_t NSamples = 1;
  static uint32_t Hysteresis = 15;  // static variable retain their values from
  static uint32_t SumSquares = 0;   // one interrupt to the next.
  static uint32_t MeanSamples = 0;
  static uint32_t MeanSum = 0;
  static uint32_t FilteredSum = 0;
  static uint32_t NewMean  = 2048;
    // Collect ADC value and do all the arithmetic
    AdcVal = adc->adc0->readSingle();
    MeanSamples++;
    MeanSum += AdcVal;
    RisingEdge = ((AdcVal > (NewMean + Hysteresis)) && !LastwasUpper);
    FallingEdge = ((AdcVal < (NewMean - Hysteresis )) && LastwasUpper);
    if(RisingEdge){
      InUpperHalf = true;
      ADCMARKHI  // for oscilloscope measurement of time in handler
      LastwasUpper = true;
      NSamples = 0;
      SumSquares =  0;
      data_ready = false; // let the foreground know new data is being collected
    }
    if(InUpperHalf){// Now do the arithmetic for samples after the transition to upper half
      NSamples++;
      PlusValue =  AdcVal - filtered_mean;
      SumSquares += PlusValue * PlusValue;
    }
    if(FallingEdge){ // reached transition to lower half
      LastwasUpper = false;
      ADCMARKLO
      InUpperHalf = false;
      if(MeanSamples > 2){
        NewMean =  MeanSum / MeanSamples; 
        freq_samples = MeanSamples;  // Set value for foreground use
      }
      MeanSum = 0; MeanSamples = 1;  // set up for next sample
        // Now do a simple filter that 'averages' means over 16 cycles
      FilteredSum = (filtered_mean * 15)  + NewMean;
      filtered_mean =  FilteredSum/16;
      num_samples = NSamples; NSamples = 0; // Set external variable and reset NSamples
      sum_squares = SumSquares; SumSquares = 0; // Set external variable and reset SumSquares
      data_ready = true; // let the foreground know new data is available in num_samples and sum_squares
    }
    // after the transition to lower half, we don't do anything except collect data to
    // update the mean.   That is done at the very beginning.
// The following instruction makes sure that everything is
// completed before exiting the IRQ handler
#if defined(__IMXRT1062__)  // Teensy 4.1
    asm("DSB");
#endif
}
 
Back
Top