ADC with sampling rate of 4Msps with Teensy

samshurp

Member
Hey Everyone,

I have a sine wave of 300Khz, and I would like to sample that. How possible is it for Teensy 4 to sample a 300Khz signal. Does Teensy ADC has that capability? if not, can I use a separate ADC module and use I2C or SPI to sample at 4Msps. I have not built anything yet as I am currently brainstorming. You help is necessary and appreciated.
 
How many bits? How many samples? That sort of rate suggests using a parallel interface ADC, which will need quite a few pins and some low level
programming to read the pins as a group via DMA I think. You will be limited by the amount of RAM as to the max number of samples I think.
I2C cannot handle that sort of datarate, and I doubt there are any 4MSPS SPI ADCs either - typically if SPI is clocked at 32MHz you don't
have time for even 8 bits plus protocol overhead.

What's the bandwidth of the signal - you may only need to sample around 1MSPS if there's little above 300kHz to alias with it...
 
The LTC2315-12 needs 14 clocks to read each sample. You need 40ns
idle between SPI transaction and that leaves 210ns to handle 14 bits, if you want 4MSPS - something like 75MHz SPI
clock is needed for that. You'll need a good layout to handle that sort of high speed signal on a PCB, and it probably
won't work reliably on a breadboard. You also need the individual conversion start edges to be jitter-free, suggesting
DMA driven SPI (I've never tried this myself).

This is why a parallel ADC is usually used at higher speeds, I found the MAX1184 for instance, dual 10bit parallel flash
ADC with 20MSPS max sample rate.

If you don't need 12 bits, don't pick a 12 bit ADC - price and availability tend to follow the specs, 8 bit fast ADCs are
much more plentiful than 12 bit, or even 10 bit.

What S/N ratio are you looking for? What are these signals?
 
Hey Mark, I am using ADC to read the max and min voltage. The main reason is because I want to get the peak to peak voltage of Sine wave that has a frequency of 300Khz to 400Khz, and a voltage range of 0 to 4V.
When it comes to S/N ration, I don't particular know how to answer that. I just want to make sure that the peak to peak voltage is accurate down to its second decimal point. I will look into MAX1184.
 
I will not be able to get the min and max voltage when I under sample. The sine wave has a frequency of 300khz to 400Khz, and if I were to undersample, I will be getting sampling at less or 1 sample per sine wave. I am interested to why you would recommend to undersample.
 
I will not be able to get the min and max voltage when I under sample. The sine wave has a frequency of 300khz to 400Khz, and if I were to undersample, I will be getting sampling at less or 1 sample per sine wave. I am interested to why you would recommend to undersample.


Unless you subsample at a frequency that is an exact sub multiple of the input sine wave, you will eventually take a sample at a maximum and minimum point. The key to getting this to work is to know the frequency of the sine wave and how much the frequency changes over time.

Under-sampling won't work if you need the P-P voltage on a cycle-by-cycle basis. If you need the P-P voltage over the last 100mSec, it should work just fine.
 
I will not be able to get the min and max voltage when I under sample. The sine wave has a frequency of 300khz to 400Khz, and if I were to undersample, I will be getting sampling at less or 1 sample per sine wave. I am interested to why you would recommend to undersample.

For undersampling you'd use an ADC with a short aperture time. It takes effectively instantaneous samples so your
signal aliases down unattenuated into the Nyquist band. You may have to buffer the signal of interest to get this
to work well. The sampling clock timing is critical for under-sampling, it usually needs to be cycle-precise and quartz-derived.

Undersampling is just like using a strobe.
 
The ADC on the T4.0 can be set up with an aperture time of 100nSec or less. @Samsurp specified an accuracy of 0.01Volts in a 4.5 Volt signal, so a 10-bit resolution should be sufficient. I think you could set up the ADC for very high speed sampling and conversion and collect samples at 250KHZ for 50 to 100mSec and get a pretty good value.

As @MarkT pointed out, you probably need a buffer on the input to make sure that the internal sampling capacitor gets fully charged in 100nSec.

Whether this method will work out depends a lot on the unspecified rate at which the user needs to measure the P-P voltage and how quickly the voltage can change.

If you're going to need a buffer amplifier anyway, you might be able to add a few components and make an analog peak detector as shown here: https://sound-au.com/appnotes/an012.htm

Once again, knowing how often you need a result is a key piece of design information.
 
100ns for 300kHz is an aperture of 0.2 radians - that might be just small enough?

One technique used for RF amplitude measurements is a simple diode detector followed by table look up
to correct for forward voltage of the particular diodes/detector used.
 
On the basis that 100 lines of code is often worth a thousand lines of discussion, I put together a simple program to test over and under-sampling an ~300KHz sine wave and calculating the Peak-to-Peak voltage. The T4.0 ADC input was driven with a signal generator with a 50-Ohm output impedance and with the signal offset to fit within the 0 to 3.3V limits of the ADC. In a real-world application, you would have to add some input electronics to offset and scale a 4.0V AC input. The data was collected with the smallest sampling aperture and highest conversion speed. 12-bit ADC resolution was specified.
Here is the code:
Code:
/*******************************************************
    Peak to Peak voltage measurement
    ADC collection controlled by ADC timer for no jitter
    and precise sample timing.

    This simple test program assumes that the input signal
    has been shifted and scaled to fit within the 0.0 to 
    3.3V range  of the T4.0 ADC

    For testing purposes, the program was run with input from
    a signal generator with a 50-Ohm output impedance  
    
    Results were compared with an 18-year-old Tek TDS210 
    digital oscilloscope.
    
    MJB 3/23/2021

***************************************************************/
#include <ADC.h>

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

const char compileTime [] = "\n\nPk-Pk test Compiled on " __DATE__ " " __TIME__;
const int admarkpin = 1;
const int ledpin    = 13;
const uint adcpin = A9;

// ADMARKHI and ADMARKLO are used to observe ADC Collectiom timing on oscilloscope
#define  ADMARKHI digitalWriteFast(admarkpin, HIGH);
#define  ADMARKLO digitalWriteFast(admarkpin, LOW);

#define  LEDON digitalWriteFast(ledpin, HIGH); // Also marks IRQ handler timing
#define  LEDOFF digitalWriteFast(ledpin, LOW);
#define ADCMAX 4096  // for 12-bit samples

float vRef = 3.298;  // measured for my T4.0
float pk_pkSamples[1000];
uint16_t numsamples;

// keep the sample rate out of the 300 to 400KHz band
#define DEFAULTRATE 298000

volatile bool sampling = false;
volatile uint16_t adcmax, adcmin;



void setup() {

  Serial.begin(9600);
  delay(500);
  Serial.println(compileTime);
  pinMode(admarkpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(adcpin, INPUT_DISABLE);
  adc->adc0->setAveraging(1 ); // set number of averages
  adc->adc0->setResolution(12); // 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

  ADCSetFrequency(DEFAULTRATE);
}

void loop() {
  // put your main code here, to run repeatedly:
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == '0')Pk_PkDisplay(10, 1);
    if (ch == '1')Pk_PkDisplay(10, 10);
    if (ch == '2')Pk_PkDisplay(10, 100);
    if (ch == 'a')ADCSetFrequency(210000);
    if (ch == 'b')ADCSetFrequency(290000);
    if (ch == 'c')ADCSetFrequency(420000);
  }
}
/*****************************************************
   This is the ADC timer interrupt handler
   The ISR runs in about 75nSec on T4.1 at 600MHz
   when collecting max and min data
 ******************************************************/
void adc0_isr()  {
  uint16_t adc_val;
  ADMARKHI
  if (sampling) {
    // Collect ADC value and update the ADC Histogram data
    adc_val = adc->adc0->readSingle();
    if (adc_val >= adcmax) adcmax = adc_val;
    if (adc_val <= adcmin) adcmin = adc_val;

#if defined(__IMXRT1062__)  // Teensy 4.0
    asm("DSB");
#endif
  }  // end of if( sampling)
  ADMARKLO;
}

// Set the ADC Sample timer to the desired frequency
void ADCSetFrequency(uint32_t freq){
  // Now start the ADC timer.  It runs continuously, but we don't always collect the data
  sampling = false;
  adc->adc0->stopTimer();
  delay(2);
  adc->adc0->startSingleRead(adcpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();

  adc->adc0->startTimer(freq); //frequency in Hz
  adc->adc0->enableInterrupts(adc0_isr);
  Serial.printf("\nADC0 timer rate set to %lu\n", adc->adc0->getTimerFrequency());
}

// Display 10 samples of Pk-Pk data
void Pk_PkDisplay(uint16_t nsamples, uint16_t msec) {
  uint16_t i;
  for (i = 0; i < nsamples; i++) {
    pk_pkSamples[i] = GetPk_Pk(msec);
  }
  Serial.printf("\nSample Data with %u mSec window:\n", msec);
  for (i = 0; i < nsamples; i++) {
    Serial.printf("%6.3f\t",pk_pkSamples[i]);
  }
  Serial.println();
}

// Collect max and min data for msec milliseconds
// then calculate pk-pk volts;
float GetPk_Pk(uint16_t msec) {
  uint16_t pkpkcounts;
  float volts;
   sampling = false;
  // now  reset adcmax and adcmin, then start the ADC collection timer
  adcmax = 0;  adcmin =  ADCMAX;
  sampling = true;
  
  delay(msec);  // collect max and min for requested milliseconds

  sampling = false;
  pkpkcounts = adcmax - adcmin;
  volts = vRef * pkpkcounts / ADCMAX;

  return volts;
}

And here are the results:
Code:
Pk-Pk test Compiled on Mar 23 2021 10:56:55

ADC0 timer rate set to 298210

Sample Data with 10 mSec window:
 2.179	 2.185	 2.191	 2.184	 2.184	 2.188	 2.180	 2.183	 2.173	 2.179	

ADC0 timer rate set to 210084

Sample Data with 10 mSec window:
 2.180	 2.182	 2.184	 2.180	 2.176	 2.183	 2.177	 2.172	 2.180	 2.181	

ADC0 timer rate set to 290135

Sample Data with 10 mSec window:
 2.176	 2.181	 2.198	 2.182	 2.178	 2.179	 2.184	 2.178	 2.185	 2.178	

ADC0 timer rate set to 420168

Sample Data with 1 mSec window:
 2.171	 2.180	 2.173	 2.176	 2.169	 2.183	 2.168	 2.186	 2.173	 2.163	

Sample Data with 10 mSec window:
 2.185	 2.189	 2.188	 2.186	 2.185	 2.196	 2.187	 2.196	 2.183	 2.185	

Sample Data with 100 mSec window:
 2.198	 2.189	 2.205	 2.212	 2.198	 2.205	 2.197	 2.198	 2.204	 2.190	

Signal generator turned off

Sample Data with 1 mSec window:
 0.021	 0.023	 0.019	 0.021	 0.027	 0.021	 0.024	 0.020	 0.027	 0.019	

Sample Data with 10 mSec window:
 0.030	 0.024	 0.029	 0.029	 0.029	 0.027	 0.027	 0.031	 0.028	 0.028	

Sample Data with 100 mSec window:
 0.032	 0.029	 0.031	 0.031	 0.031	 0.031	 0.032	 0.031	 0.032	 0.031

A few things are immediately apparent:
1. Both undersampling and oversampling seem to work equally well
2. Longer sample windows yield higher pk-pk voltages. This is expected to some extent, since more samples mean that you are more likely to collect a sample at exactly the maximum or minimum of the input signal + noise.
3. With the input signal generator off, there is about 30mV of noise.

It might be possible to get results in less time by using the T4.0 input comparator to detect zero crossings and determine the input period. You could then wait for the next zero crossing, delay a quarter of the period and collect a sample, then delay another 1/2 of the period and collect another sample. For the necessary timing resolution, you would probably need to have a wait loop based on the 600MHz ARM cycle counter running with interrupts disabled. If this algorithm is workable, you should be able to collect PK-PK data in two or three cycles of the 300KHz input. The time between the two peak samples would be about 1.67 microseconds, well within the capability of the ADC at 10 or 12-bit resolution. I expect the timing would need some tweaking to account for the difference between the 600MHz CPU clock and the ADC clock which is many times slower.
 
What was your voltage source? I wanted the measurement within an accuracy of 0.01. Looking at your data, it doesn't look like it is that good. I may be wrong here.
 
The voltage source was a signal generator with a 50-ohm output impedance.

I think we need to make sure we are thinking of the same things. An accuracy of 0.01V means that the measured voltage is within 0.01V of the actual value. You can't really determine that unless you have a way to calibrate your system against a reference value.

The data I showed illustrates the PRECISION of the method: How well it gets the same value with repeated measurements. How ACCURATE it is, I don't know because I don't have a 3-volt highly accurate Pk-PK voltage source.

The precision seems to be about 0.1Volts. There are many ways to measure precision: Standard deviation of a number of readings, Pk-Pk noise in the readings etc. etc. Not all these methods work very well with the very quantized data you get with a 12-bit ADC which has only 4096 possible values. With the possibility of noise on both the maximum and minimum voltage values, I think getting a precision of 10mV is asking a lot for a single reading. You could certainly get better precision by averaging a number of readings. All the averaging in the world won't fix the ACCURACY unless you have a good source of calibration data. Even then, you will have to worry about changes in the T4.1 reference voltage with temperature and other loads on the voltage regulator.
 
I have no problems using a 10 Msps parallel output ADC on a teensy 4. Timing suggests that 20 Msps would also work fine.
 
It might be possible to get results in less time by using the T4.0 input comparator to detect zero crossings and determine the input period. You could then wait for the next zero crossing, delay a quarter of the period and collect a sample, then delay another 1/2 of the period and collect another sample. For the necessary timing resolution, you would probably need to have a wait loop based on the 600MHz ARM cycle counter running with interrupts disabled.
Or, if it is possible to wait for multiple cycles of the input wave to calculate the period, then using timer/counters and averaging many results to get sub-count resolution. (That way one can do other processing while waiting, interrupts shouldn't matter and thus timing (in)accuracy should be known/fixed.)

If very high accuracy/precision/resolution is needed and quickly (within two input cycles or such), I'd combine some of the fancier analog peak-detector circuits using the "ideal diode"-things (gives basically "infinite" timing resolution to catch the peaks) with crude measurement for the period and phase (or simply another "normal" ADC) for resetting the detectors. With such arrangement one can use basically something like little faster than that 300..400kSps ADC chip for the conversion (plenty of choices up to an overkill 24-bit (ENOB ~19..20 bits, IIRC) with SPI. .. For one peak polarity; another ADC etc. for the other polarity.

Or a choice to use single twice as fast, less than 24 bits, 2-channel ADC to convert both negative and positive peaks interleaved (naturally).

Edit: ideas keep crawling... If the peak value of interest is not the exact peak (including noises, glitches, etc.) of that particular wave, and it is ok to get the wave's overall shape's peak (in practice "amplitude"), then... To get better precision, if going for high sample rates, and less bits than needed for directly getting the needed resolution/precision, and having more noise than desired, then one could try to calculate a curve-fitting of the sine-wave vs. samples. This basically does both noise reduction and resolution/precision improvement, no matter where the samples are along the wave. The peak is then calculated from the curve's parameters, not from the sample/samples at/near the peak. Caveat, I have not done such for non-linear signals (like sine), no direct idea on processing requirements, but I have done it with linear signals.
 
Last edited:
Hi group,

If you need ever a high speed ADC I have tested the LTC2315-12 on a Teensy 4.1.



Code:
// We use SPI MISO = 12, SCK = 13 CS= 10
// Tested on Teensy 4.1 measured with scope SPI clock at 5MHz
#include <SPI.h>
#define CS 10
static uint8_t LTC2315_shift = 1;
float LTC2315_vref = 3.315;
float adc_voltage;

void setup() {
  Serial.begin(115200);
  SPI.begin();
  pinMode(CS, OUTPUT);
  digitalWrite(CS, HIGH);// Pull Chip Select High
}
void loop() {
  adc_voltage = readADC(CS, LTC2315_shift, LTC2315_vref);
  Serial.println(adc_voltage, 4);
  
}
  float readADC(uint8_t cs_pin, uint8_t shift, float vref) {
  uint16_t adc_code;
  uint8_t b[2];
  SPI.beginTransaction(SPISettings(5000000, MSBFIRST, SPI_MODE0));
  digitalWrite(cs_pin, LOW);
  b[1] = SPI.transfer(0x00);  //Read MSB and send MSB 
  b[0] = SPI.transfer(0x00);  //Read LSB and send LSB
  digitalWrite(cs_pin, HIGH);  
  SPI.endTransaction();
  adc_code = (uint16_t((b[1] << 8)) | b[0]) <<  shift;
  return ((float)adc_code / (pow(2,16)-1)) * vref;
}

LTC2315.jpeg

Best regards,

Johan
 
Back
Top