analogRead sampling

frohr

Well-known member
Hey all,
I want read from analog pin every 50 micros.

My code:

Code:
void READ_BUFFER()
{
   analogReadResolution(12);
   period = 50;
   float val;
   float time = 0;
   for(int i= 0;i < BUFFER_SIZE ; i++)
   {
    micros_0 = 0;  
    BUFFER[i] = analogRead(A1);
    val = (((BUFFER[i] * 3.0) / 4095));// - (3.0 / 2)); // convert to volts
    time = micros_0;
    while(micros_0 <=  period) 
    { 
    Serial.print(val,4);
    Serial.print(" - ");
    Serial.println(time); 
    } 
   }
}

BUT time is 20 - 21 micros.

1.5788 - 21
1.5788 - 21
1.5766 - 20
1.5766 - 20

I need have this time exactly 50 micros = read one sample every 50 micros.
Any way how to do it?
Thanks a lot for help.

Michal
 
Do you mean something like this:


Code:
long tim = 0;


void setup() {
  // put your setup code here, to run once:
  tim = micros();
}


void loop() {
  // put your main code here, to run repeatedly:
  doSomethingEvery50();
}


void doSomethingEvery50() {
  //wait until 50 micros have passed
  while (micros() - tim < 50) {
    //waste time until 50 micros have passed 
  }
  Serial.println(micros() - tim);
  tim = micros();
  //Do the think you want to here so if it uses some time then the while loop will be shorter to accomodate it
}
 
The correct way to run something every 50us is this:
Code:
#define PERIOD 50

unsigned long prevtime = 0L;

void loop ()
{
  if (micros() - prevtime >= PERIOD)
  {
    prevtime += PERIOD ;  // update the target timestamp precisely by PERIOD.
    ....  do the thing ....
  }
}

You definitely do not update the variable by calling micros() again, since the time can have changed by then.

However all the software methods like this will have jitter, and this is usually bad news for a sampling system -
using DMA is preferred, even though is more complicated to set up.
 
Another possibility is to use the ADC library and the hardware ADC timer. It is also more complex in that you have to know how to set up and use an interrupt to collect the data-- but it gets you jitter-free sampling.
 
Another possibility is to use the ADC library and the hardware ADC timer. It is also more complex in that you have to know how to set up and use an interrupt to collect the data-- but it gets you jitter-free sampling.

Hi, could you show me example how read data to buffer via ADC? In one buffer I need value from analogread and in second buffer I need have time in micros (0,50,100,150,200,....) - its because FFT.
I see many examples but I am not familiar with ADC for now - I never used.
Thank you for your help.
Michal
 
Hi, could you show me example how read data to buffer via ADC? In one buffer I need value from analogread and in second buffer I need have time in micros (0,50,100,150,200,....) - its because FFT.
I see many examples but I am not familiar with ADC for now - I never used.
Thank you for your help.
Michal

I've got lots of example code that might be applicable, but your question is a bit too vague. To pull out the right example I need to know:

1. Which Teensy are you using?
2. How many samples do you need to collect at 20KHz (50 uSec sample interval)?
3. Will a sample set fit in a buffer in your Teensy memory, or does it need to be written to SD Card?
4. Will you process the data on the Teensy or transfer it to a PC for analysis?

If you use the ADC timer, there is really no need to save the sample time. The samples will be collected at 50uSec intervals and you can generate the times after collection with something like:
Code:
for(i=0; i<numsamples; i++){
   Serial.printf("%6d \t %4d\n", i * 50, samplebuffer[i]);
}

For an FFT, you don't necessarily need, sample times, just the sampling rate.
 
I've got lots of example code that might be applicable, but your question is a bit too vague. To pull out the right example I need to know:

1. Which Teensy are you using?
2. How many samples do you need to collect at 20KHz (50 uSec sample interval)?
3. Will a sample set fit in a buffer in your Teensy memory, or does it need to be written to SD Card?
4. Will you process the data on the Teensy or transfer it to a PC for analysis?

If you use the ADC timer, there is really no need to save the sample time. The samples will be collected at 50uSec intervals and you can generate the times after collection with something like:
Code:
for(i=0; i<numsamples; i++){
   Serial.printf("%6d \t %4d\n", i * 50, samplebuffer[i]);
}

For an FFT, you don't necessarily need, sample times, just the sampling rate.

1. I use Teensy 4.0
2. I want 16384 samples (maybe less but I have to try it)
3. I will use just Teensy memory
4. I will proceed data on Teensy

With this one buffer I will work in many ways - make calculations and fft. FFT I want have "real time" - update on displej as fast as possible (10-1000Hz and 500-10000Hz) + rms calculations.

What is important for me now is read data to buffer correctly. I will work in many ways - make calculations and fft. FFT I will have "real time" - update on displej as fast as possible (10-1000Hz and 500-10000)
 
I have used the simple loop with micros() to time intervals in my data logging examples and assumed lots of jitter. I did a test using the cycle counter in a Teeny 4.1. The result is far better than I hoped for.

Here is the test code that take 20,000 samples and prints the min and max time in usec between samples.
Code:
// Will warn data[] not used.
void setup() {
  Serial.begin(9600);
  while (!Serial) {}
}


int n = 0;
void loop() {
  int32_t cycle[20000];
  uint16_t data[20000];
  uint32_t logTime = micros();
  for (int i = 0; i < 20000; i++) {
    logTime += 50; // log every 50 us.
    while ((int)(micros() - logTime) < 0) {}
    cycle[i] = ARM_DWT_CYCCNT;
    data[i] = analogRead(A0);
  }
  float usMin = 999;
  float usMax = 0;
  for (int i = 0; i < 19999; i++) {
    // F_CPU is 600 MHz.
    float us = (float)(cycle[i+1] - cycle[i])/600;
    if (us < usMin) {
      usMin = us;
    }
    if (us > usMax) {
      usMax = us;   
    }
  }
  Serial.print(usMin);
  Serial.print(' ');
  Serial.println(usMax);
  if (n++ > 50) {
    Serial.println("done");
    while(true) {}
  }
}
Here is the output. The worst case is less than 0.2 usec. This is for 50 sets of 20,000 samples.

49.93 50.04
49.95 50.05
49.98 50.04
49.98 50.03
49.95 50.05
49.82 50.16
49.97 50.04
49.95 50.05
49.97 50.03
49.98 50.04
49.98 50.05
49.95 50.05
49.98 50.03
49.97 50.03
49.95 50.05
49.98 50.03
49.98 50.03
49.95 50.05
49.95 50.05
49.97 50.03
49.98 50.04
49.95 50.05
49.98 50.03
49.97 50.04
49.95 50.05
49.97 50.03
49.98 50.03
49.98 50.04
49.95 50.05
49.97 50.04
49.97 50.04
49.95 50.05
49.98 50.04
49.98 50.04
49.97 50.05
49.95 50.05
49.97 50.04
49.97 50.03
49.95 50.05
49.98 50.04
49.98 50.04
49.95 50.05
49.97 50.04
49.97 50.03
49.93 50.03
49.95 50.05
49.98 50.03
49.97 50.04
49.95 50.05
49.97 50.04
49.98 50.03
49.95 50.05
done
 
Here is a simple demo program to collect 16384 samples using the ADC timer.
Code:
// Sample program to Collect ADC data with ADC timer
// Using the ADC timer to collect minimizes sampling jitter
// Targeted to T4.0 at 600MHz
// mborgerson 7/6/2021

const int ledpin = 13;

#define LEDON digitalWriteFast(ledpin, HIGH);
#define LEDOFF digitalWriteFast(ledpin, LOW);
#define LEDTOGGLE digitalToggleFast(ledpin);

#include <ADC.h>
const int readpin = A9; //   Connected to AA Alkaline cell

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

#define SAMPRATE 20000
#define AVERAGING 4
#define RESOLUTION 12

#define ADCSAMPLES 16384 // number of samples to collect
uint16_t adcbuffer[ADCSAMPLES]; // data in DTCM so no cache updates required 
const char compileTime [] = "ADC Timer test Compiled on " __DATE__ " " __TIME__;

void setup() {
  delay(500);
  Serial.begin(115200);
  delay(1000);
  pinMode(ledpin, OUTPUT);
  pinMode(readpin, INPUT_DISABLE);
  delay(1500);
  Serial.print("\n\n");
  Serial.println(compileTime);
  Serial.println("Press <n> to collect and display data");

  adc->adc0->setAveraging(AVERAGING); // set number of averages
  adc->adc0->setResolution(RESOLUTION); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);  // set the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);      // set the sampling speed
}

void loop() {
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'n') GetADCData();
  }
}

volatile uint32_t adcidx;  // must be volatile as it is changed in interrupt handler
// This ISR runs in about 60nSec on T4.0 at 600MHz.
void adc0_isr()  {
  uint16_t adc_val;
  LEDTOGGLE;  // So you can verify sample rate on oscilloscope
  // call readSingle() to clear the interrupt.
  // and store the ADC data
  adc_val = adc->adc0->readSingle();

  if (adcidx < ADCSAMPLES) {  // storage stops when enough samples collected
    adcbuffer[adcidx] = adc_val;
    adcidx++;
  }
}

//Collect the ADC data
void GetADCData(void) {
  delay(100); // wait for usb serial to finish
  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(readpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();
  adcidx = 0;
  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz
  adc->adc0->enableInterrupts(adc0_isr);

  do {
    // You could process a sample here with multiple buffers or copying to a new buffer
    // for processing while collection is in progress
  } while (adcidx < ADCSAMPLES);

  adc->adc0->stopTimer();
  Serial.println("Collection complete.");
  // The data is in the global adcbuffer array.
  ShowADC();
}

// Display the start of data in counts.  Send 10 values per line
// data is in the global variable adcbuffer.
void ShowADC(void) {
  uint32_t i;
  Serial.println("ADC Data in  Counts");
  for (i = 0; i < 100; i++) {
    if ((i % 10) ==  0) {
      Serial.printf("\n% 3d: ", i);
    }
    Serial.printf("% 5d", adcbuffer[i]);
  }
  Serial.println();
}

Similar code that collected timing data for a histogram display showed jitter of less than one ARM clock cycle if no other operations were running during the collection. This actually seems hard to believe, since I thought that the systick interrupt would extend the interrupt response time every millisecond.
 
I'd collect 100K+samples to catch another ISR's effect on timing. Periods of disabled interrupts (common in teensy code) will also cause jitter.
 
@Bill Greiman noted timing with ARM_DWT_CYCCNT is more accurate ( takes 3 cycles to read instead of ~36 for micros() )

... seems I edited the code to use that and did 100,000 cycles:
Code:
T:\tCode\ADC\AnalogTimedRead\AnalogTimedRead.ino Jul  6 2021 13:42:12
50.00 50.00
49.99 50.01
49.99 50.01
50.00 50.00
49.99 50.00
50.00 50.01
50.00 50.00
49.99 50.01
...
49.99 50.01
50.00 50.00
49.99 50.01
50.00 50.00
50.00 50.00
50.00 50.00
49.99 50.00
49.99 50.00
done

Code:
// Will warn data[] not used.
void setup() {
  Serial.begin(9600);
  while (!Serial) {}
  Serial.println("\n" __FILE__ " " __DATE__ " " __TIME__);
}

#define NUM_READS 100'000
#define NUM_CYCLES 600'000'000/(1'000'000/50)
int n = 0;
void loop() {
  int32_t cycle[NUM_READS];
  uint16_t data[NUM_READS]; 
  uint32_t logTime = ARM_DWT_CYCCNT;
  for (int i = 0; i < NUM_READS; i++) {
    logTime += NUM_CYCLES; // log every 50 us.
    [B]while ((int)(ARM_DWT_CYCCNT - logTime) < 0) {}[/B]
    cycle[i] = ARM_DWT_CYCCNT;
    data[i] = analogRead(A0);
  }
  float usMin = 999;
  float usMax = 0;
  for (int i = 0; i < NUM_READS-1; i++) {
    // F_CPU is 600 MHz.
    float us = (float)(cycle[i+1] - cycle[i])/600;
    if (us < usMin) {
      usMin = us;
    }
    if (us > usMax) {
      usMax = us;   
    }
  }
  Serial.print(usMin);
  Serial.print(' ');
  Serial.println(usMax);
  if (n++ > 50) {
    Serial.println("done");
    while(true) {}
  }
}

Went back and added Min and Max on the actual cycle counts. The mix is diff each time but always between 4 and 6:
Code:
T:\tCode\ADC\AnalogTimedRead\AnalogTimedRead.ino Jul  6 2021 14:49:35
50.00 50.00		29998 30002		4
49.99 50.01		29997 30003		6
49.99 50.01		29997 30003		6
49.99 50.01		29997 30003		6
50.00 50.00		29998 30002		4
49.99 50.01		29997 30003		6
49.99 50.01		29997 30003		6
 
Last edited:
I'd collect 100K+samples to catch another ISR's effect on timing. Periods of disabled interrupts (common in teensy code) will also cause jitter.

With the OP's specification of 50uSec sampling, I think there is a low probability that the SysTick interrupt will affect the ADC timing. Both the ADC timer and the Systick timer are generated from the same system clock. Since the 50USec ADC interval divides evenly into the 1000uSec Systick interval, if the SysTick interrupt occurs 5uSec after the ADC interrupt, it will ALWAYS occur 5uSec after the ADC interrupt and the SysTic will NEVER extend the ADC interrupt interval.

If there is an interrupt handler that takes more than 30 to 40 uSec to execute, it might extend the ADC servicing interval enough to cause problems. If the ADC interval doesn't divide evenly into the SysTick interval, you could also see occasional changes in the interrupt response time. However, it is important to remember that variations in the Interrupt service response DO NOT change the ADC sampling interval--which is controlled by the hardware timer. Only when other interrupts delay the collection of the ADC data past the 50uSec sampling interval will you see jitter in the ADC data. Very few Teensy interrupts block other interrupts for more than a few microseconds, so ADC sampling jitter should be VERY rare. That's the beauty of having the ADC sampling controlled by directly by the hardware timer---without software intervention.

If I'm going to collect ADC data for spectral analysis, the ADC Timer mode is my first choice. It offers the highest probability of sampling intervals accurate to a 0 to 1 clock cycles. Using other methods, as Bill Greiman demonstrated, allows jitter of up to 4 to 5 ARM clock cycles. The other major advantage of the ADC timer method is that the CPU is available to process the last sample with about 99% of the CPU bandwidth.
 
Agreed, but many other interrupts aren't synchronized like this. Good point about the advantage of hardware timing.

With this software timed code (derived from defragster code) I get jitter of 2.3 usec or .07 usec with interrupts disabled. Both strike me as terrible - a coding mistake?


Code:
#define NUM_READS 100'000
#define NUM_CYCLES 600'000'000/(1'000'000/60)


void loop() {

  volatile int data;
  uint32_t tmax = 0;
  uint32_t tmin = 1E9;
  uint32_t prevtime;
  uint32_t logTime = ARM_DWT_CYCCNT;

  noInterrupts();  // optional, try with and without

  for (int i = 0; i < NUM_READS; i++) {

    logTime += NUM_CYCLES; // log every 60 us.
    while ((int)(ARM_DWT_CYCCNT - logTime) < 0) {}

    data = analogRead(A0);
    uint32_t curtime = ARM_DWT_CYCCNT;
    uint32_t deltime = curtime - prevtime;
    prevtime = curtime;

    if (i > 0) {
      if (deltime > tmax)
        tmax = deltime;
      if (deltime < tmin)
        tmin = deltime;
    }

  } // for

  interrupts();

  Serial.print(tmin);
  Serial.print(" ");
  Serial.print(tmax);
  Serial.print(" jitter = ");
  Serial.println((tmax - tmin) / 600.0);

}
 
Last edited:
Indeed having ADC use a timer better than this tight loop polling for time to sample. It's DMA background reading it much better.

Given the test and loop time where reading the ARM_CYCCNT alone takes 3 cycles - skipping by 4,5, or 6 cycles consistently suggest it isn't getting caught much by any _isr()'s as it was running 100K times and none longer seem in some hundreds of those seen.

An update to SYSTICK for millis() does the inc++ after storing the ARM_CYCCNT to reproduce micros() timing offset from that point.
 
Agreed, but many other interrupts aren't synchronized like this. Good point about the advantage of hardware timing.

With this software timed code (derived from defragster code) I get jitter of 2.3 usec or .07 usec with interrupts disabled. Both strike me as terrible - a coding mistake?
Sounds entirely as expected - ISRs taking a couple of microseconds aren't normally a problem, and 70ns jitter on a machine with a
complex CPU with caching isn't a surprize, cache-misses are expensive. The only processor I know that can deliver precise software
timing is the Propeller and that was designed explicitly for this (and doesn't use interrupts).
 
The ADC conversion time alone is also quite variant.
Code:
66.5.4.5 Sample Time and Total Conversion Time
The total conversion time depends upon the following:
• the sample phase time (as determined by ADLSMP and ADSTS bits in ADC_CFG
register),
• the compare phase time (determined by MODE bits)
• the frequency of the conversion clock (fADCK).
• the MCU bus frequency (for Handshaking and selection of clock)
From reference manual.
The chapter has much more information, so I'd suggest to read it :)
 
Last edited:
Yes timer triggered ADCs are best for jitter. Too bad there are not simple Teensy functions for the ADC to take a 16K point sample using DMA.

I have been playing with the Raspberry Pi SDK for the RP2040 and it has a simple way to take such a sample with DMA. The ADC is capable of 500 ksps and has a register to space samples in free running mode. "If non-zero, CS_START_MANY will start conversions at regular intervals rather than back-to-back."

But what are the requirements for jitter for this application?

Analog Devices has lots of app notes on all the issues of sampling errors for discrete Fourier analysis. An example of 12-bit samples of a 260 kHz signal sampled at 4M samples per second with a sample size of 64K points required less than about 6 ns of jitter. For 20K samples per second with 16K points, jitter of 1 microsecond is probably good enough.

Jitter in triggering the ADC may be a small issue. The ADC in a micro-controller is generally not great. Especially with raw input to the sample and hold and all the noise generated by the chip and power supply.

You really need to know what the requirements are for this application then do a test of the entire system by recording high quality pure sine test signals and look at the results of the Fourier analysis to see what the real problems are.
 
I made a few measurements and comparisons:

ADC, 16384 samples, sampling 20000Hz, averaging 4, resolution 12 - used source code by mborgerson
analogRead, 16384 samples, read every 50 micros, averaging 4, resolution 12, code below.

Compare values - seems very similar, converted to Volts, Value = (BUFFER[x] * 3) / 4095;

analog-adc.jpg


Execute time:

ADC: 0,92548 sec
analogRead: 0,835585
Time should be 0,8192 second (16384 samples / 20000Hz)

I measure time this way:
Code:
time1 = micros();
READ_BUFFER();
//GetADCData();
time2 = micros();
Serial.println(time1);
Serial.println(time2);

analogRead code:

Code:
void READ_BUFFER()
{
   analogReadResolution(12);
   analogReadAveraging(4);
   period = 50;
   for(int i= 0;i < BUFFER_SIZE ; i++)
   {
    micros_0 = 0;  
    BUFFER[i] = analogRead(A1);
    vImag[i] = period *i;
    while(micros_0 <=  period) 
    { 
    } 
   }
}

In vImag I have micros fro every sample.
1.5663 - 0.00
1.5648 - 50.00
1.5619 - 100.00
...
1.5678 - 819050.00
1.5663 - 819100.00
1.5692 - 819150.00

Now it seems that analogRead is faster. Is it correct?
What are main benefits of using ADC?

Anyway, thanks all for suggestions.

Michal
 
If you simply called my GetADC function without change and captured the time before and after the call, the interval is going to be higher by 101 milliseconds due to the delay() calls in the function. If you left in the call to display the data, that also added some time. If you correct for the delay() calls, you would get (0.92548 - 0.101) or 0.82358 seconds. The difference between that and 0.8192 seconds may be the time to display the data.

Since you did not post complete code, I don't know what "micros_0" is. Please note the "Always post complete source code" at the top of the forum.

The main benefit of using the ADC timer is the guaranteed low jitter and the fact that 99+ percent of the processor cycles are available to do other work while the data is being collected.
Your sample code is not going to show any jitter because you are computing the time of sample, not actually measuring the time at which sampling occurs.
 
Apparently some installed-by-default interrupt service routine takes up to 2.5 usec. That's about 2000 ISR instructions, which seems like a lot for a T4 that appears to be idle. Any idea what is causing this?
 
Here is my cleaned code without any other calculations - this part is very important for futher calculations - to have pin A1 value (in adcbuffer) and sample time duration - expected every 50 micros (in vImag). I am not sure, how to measure time - I know, now I am wrong in my code.
Or I have to measure between adc->adc0->startTimer(SAMPRATE) and adc->adc0->stopTimer();?

Code:
#include <ADC.h>
#define BUFFER_SIZE 16384
float vImag[BUFFER_SIZE]; // buffer for sample time
const int readpin = A1;
volatile uint32_t adcidx;
float calc_time1;
float calc_time2;
float sample_time;
float value;

ADC *adc = new ADC(); // adc object;
#define SAMPRATE 20000
#define AVERAGING 16
#define RESOLUTION 12
#define ADCSAMPLES 16384
uint16_t adcbuffer[ADCSAMPLES]; 

void setup() {
  Serial.begin(115200);
  adc->adc0->setAveraging(AVERAGING); // set number of averages
  adc->adc0->setResolution(RESOLUTION); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);  // set the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);      // set the sampling speed
}

void adc0_isr()  {
  uint16_t adc_val;
  // call readSingle() to clear the interrupt.
  // and store the ADC data
  adc_val = adc->adc0->readSingle();
  if (adcidx < ADCSAMPLES) {  // storage stops when enough samples collected
    calc_time1 = micros(); // ????start time
    adcbuffer[adcidx] = adc_val;
    calc_time2 = micros(); // ???? end time
    vImag[adcidx] = calc_time2 - calc_time1; // ???? sample time to buffer
    adcidx++;
  }
}


void GetADCData(void) {
  delay(100); // wait for usb serial to finish
  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(readpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();
  adcidx = 0;
  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz
  adc->adc0->enableInterrupts(adc0_isr);
  do {
    // You could process a sample here with multiple buffers or copying to a new buffer
    // for processing while collection is in progress
  } while (adcidx < ADCSAMPLES);
  adc->adc0->stopTimer();
  // The data is in the global adcbuffer array.

  
}


void loop() {
 
   GetADCData();
   
   for(int i= 1;i < BUFFER_SIZE; i++)
   { 
     value = adcbuffer[i];
     value = (value * 3) / 4095;
     sample_time = vImag[i];
     Serial.print(value,8); // value from A1 in volts
     Serial.print(" - ");
     Serial.println(sample_time,16); // sample time
   }
}
 
I ran my histogram analyzer that does 1,000,000 samples at 50uSec intervals for two cases: The first collects the ADC values for histogram analysis without SD card writes, the second collects the data and writes a dummy buffer to SD card. This allows me to evaluate the interrupt delays without and with SD writes. It also allows me to evaluate the change in ADC performance with SD Writes. For the T4.1, I expected and observed significant degradation of the ADC performance because it uses the 3.3V supply as the ADC reference. That supply sees some dips when SD card writes are in progress.
Code:
Histogram data without SD Card activity
Timing Histogram Data in ARM Cycle Counts
30000 999998
Maximum at sample 0
ADC Histogram Data in  Counts
 1824 2
 1825 246
 1826 6618
 1827 6338
 1828 10261
 1829 26632
 1830 743634
 1831 170613
 1832 13476
 1833 20008
 1834 2168
 1836 2

Histogram data with SD Card active
  2.00 MBytes written to SD Card.
Collection complete.
Timing Histogram Data in ARM Cycle Counts
29985 25
29989 14
29993 3
29996 1
29997 8
29999 1
30000 999881
30001 1
30003 22
30007 3
30008 3
30011 10
30012 11
30015 15

ADC Histogram Data in  Counts
 1822 1
 1823 2
 1824 6
 1825 15
 1826 2586
 1827 7728
 1828 7349
 1829 13118
 1830 242240
 1831 674076
 1832 22896
 1833 19150
 1834 10306
 1835 365
 1836 100
 1837 34
 1838 12
 1839 6
 1840 3
 1841 1
 1842 4


Note that, without SD card activity, there are no extensions to the interrupt response time. The SYSTick interrupt is higher priority, but is synchronized with the ADC timer and doesn't interfere. The USB Serial port is also active, but also doesn't interfere--either because is is lower priority than the ADC timer, or because the activity occurs between the ADC interrupts.

When the SD card is active, it does extend the ADC interrupt response time, but never for long enough to cause missed data. (Due to the algorithm used, there should be a shorter interval to match each extended response interval.) The SD card activity also adds significant noise to the ADC data. However, that's an issue that has been discussed in many other forum posts.

Here is the source code for the histogram analyzer:

Code:
// Get timing histograms for ADC Timer with and without SD Card writing. 
// Targeted to T4.1 at 600MHz  Compiled with ARDUINO 1.8.15 and TD 1.54
// mborgerson 7/9/2021

#include "SdFat.h"
#include <TimeLib.h>

SdFs sd;
FsFile outfile;

const int ledpin = 13;
const int tmpin = 30;

#define TMLOW digitalWriteFast(tmpin, LOW);
#define TMHIGH digitalWriteFast(tmpin, HIGH);

#define LEDON digitalWriteFast(ledpin, HIGH);
#define LEDOFF digitalWriteFast(ledpin, LOW);

#define SD_CONFIG SdioConfig(FIFO_SDIO)
#define TMHISTOMAX 40960
#define ADCMAX 4096  // for 12-bit samples

#include <ADC.h>
#include <ADC_util.h>

const int readpin = A9; // My 2.5V precision reference

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

#define SAMPRATE 20000
#define AVERAGING 4
#define RESOLUTION 12

uint8_t sdcbuffer[32*1024]; // 32K SD card buffer

uint32_t tm_histobuffer[TMHISTOMAX];  // for timing intervals up to 40.96mSec
uint32_t adc_histobuffer[ADCMAX];

const char compileTime [] = "ADC Timer histogram Compiled on " __DATE__ " " __TIME__;
void setup() {
  delay(500);
  Serial.begin(115200);
  delay(1000);
  // activate ARM cycle counter
  ARM_DEMCR |= ARM_DEMCR_TRCENA; // Assure Cycle Counter active
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
  pinMode(tmpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(readpin, INPUT_DISABLE);
  delay(2500);
  Serial.print("\n\n");
  Serial.println(compileTime);
  setSyncProvider(getTeensy3Time);

  if (timeStatus() != timeSet) {
    Serial.println("Unable to sync with the RTC");
  } else {
    Serial.println("RTC has set the system time");
    Serial.print("Initializing SD card...");
  }
  if (!sd.begin(SD_CONFIG)) {
    Serial.println("\nSD initialization failed.\n");
  } else  Serial.println("initialization done.");

  if (sd.fatType() == FAT_TYPE_EXFAT) {
    Serial.println("Type is exFAT");
  } else {
    Serial.printf("Type is FAT%d\n", int16_t(sd.fatType()));
  }
  // set date time callback function
  SdFile::dateTimeCallback(dateTime);

  adc->adc0->setAveraging(AVERAGING); // set number of averages
  adc->adc0->setResolution(RESOLUTION); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED); // change the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED); // change the sampling speed

}

elapsedMillis blocktimer;
volatile uint32_t byteswritten;
volatile uint32_t totalsamples;
volatile uint32_t lastcycles;

void loop() {
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'n') GetHistoData(false);
    if (ch == 'f') GetHistoData(true);
    if (ch == 'a') ShowADCHisto();
    if (ch == 'd') sd.ls(LS_SIZE | LS_DATE | LS_R);
  }

}

/*****************************************************************************
   Read the Teensy RTC and return a time_t (Unix Seconds) value
 ******************************************************************************/
time_t getTeensy3Time() {
  return Teensy3Clock.get();
}
/*
   User provided date time callback function.
   See SdFile::dateTimeCallback() for usage.
*/
void dateTime(uint16_t* date, uint16_t* time) {
  // use the year(), month() day() etc. functions from timelib

  // return date using FAT_DATE macro to format fields
  *date = FAT_DATE(year(), month(), day());

  // return time using FAT_TIME macro to format fields
  *time = FAT_TIME(hour(), minute(), second());
}
volatile uint16_t maxidx;
volatile uint32_t tmax;

// This ISR runs in about 100nSec on T4.1 at 600MHz.
// It doesn't actually save the data, but only updates
// timing and adc histogram values
void adc0_isr()  {
  uint32_t tmdiff, thiscycles;
  uint16_t adc_val;
  LEDON;

  thiscycles =  ARM_DWT_CYCCNT;
  tmdiff = thiscycles - lastcycles;
  if (tmdiff >= TMHISTOMAX) tmdiff = TMHISTOMAX - 1;

  lastcycles = thiscycles;
  if(tmdiff > tmax) {
    tmax = tmdiff;
    maxidx = totalsamples;
  }

  totalsamples++;
  // signal the main loop to write a 16KB block every 8192 samples
   // call readSingle() to clear the interrupt. 
   // and update the ADC Histogram data
   adc_val = adc->adc0->readSingle();

   if (adc_val >= 4095) adc_val = 4095;
   // Skip the first two samples, as they often have
   // weird timing values. Update histogram data
   if(totalsamples > 2){
    tm_histobuffer[tmdiff]++;
    adc_histobuffer[adc_val]++;
   }

#if defined(__IMXRT1062__)  // Teensy 4.0
  asm("DSB");
#endif
  LEDOFF;
}


// get timing  and ADC histogram data, optionally writing to SD card while
// collecting.
void GetHistoData(bool writesdc) {
  bool sdfileopen = false;
  byteswritten = 0;
  totalsamples = 0;
  uint32_t numtowrite;
  uint16_t seccount = 0;
  elapsedMillis dmillis;

  memset(adc_histobuffer, 0, sizeof(adc_histobuffer));  // clear adc histogram counts
  memset(tm_histobuffer, 0, sizeof(tm_histobuffer));  // clear  timing histogram counts

  if (writesdc) {
    sdfileopen = outfile.open("dummyfile.dxx",  O_RDWR | O_CREAT | O_TRUNC);
  }
  ShowSetup(writesdc);
  delay(10); // wait for usb serial to finish

  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(readpin); // call this to setup everything before the Timer starts, differential is also possible
  maxidx = 0;
  tmax = 0;
  delay(1);
  adc->adc0->readSingle();

  dmillis = 0;

  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz 
  
  lastcycles =  ARM_DWT_CYCCNT;
  adc->adc0->enableInterrupts(adc0_isr);
  // Write out the elapsed seconds during collection
  // The USB Serial writes occasionally block ADC interrupts
  // for a microsecond or so.
  do {
    if (dmillis > 1000) {
      seccount++;
      dmillis = 0;
      Serial.printf("%4u: %lu\n", seccount, totalsamples);
    }
    // write an appropriate number of bytes from SD Buffer
    if(byteswritten < (totalsamples *2)){
      if (writesdc) {
        numtowrite = (totalsamples * 2) - byteswritten;
        if (sdfileopen)outfile.write(sdcbuffer, numtowrite); // Write bytes when needed
        byteswritten += numtowrite;
      }
    } // end of if(byteswritten < (totalsamples *2)
    
  } while (totalsamples < 1000000);  // stop after 1 million samples

  adc->adc0->stopTimer(); 
  if (writesdc) {
    if (sdfileopen)outfile.close();
    Serial.printf("%6.2f MBytes written to SD Card.\n", (float)byteswritten / (1000000));
  }
  Serial.println("Collection complete.");
  ShowTmHisto();
}

void ShowSetup(bool writesdc) {
  Serial.printf("Collecting Data  ");
  if (writesdc) {
    Serial.printf("with SD Card Writes \n");
  } else {
    Serial.println("with no SD Card writes\n");
  }
}

void ShowTmHisto(void) {
  uint32_t i;
  Serial.println("Timing Histogram Data in ARM Cycle Counts");
  for (i = 0; i < TMHISTOMAX; i++) {
    if (tm_histobuffer[i] > 0) {
      Serial.printf("%5lu %lu\n", i, tm_histobuffer[i]);
    }
  }
  Serial.println();
  Serial.printf("Maximum at sample %lu\n",maxidx);
}

void ShowADCHisto(void) {
  uint32_t i;
  Serial.println("ADC Histogram Data in  Counts");
  for (i = 0; i < ADCMAX; i++) {
    if (adc_histobuffer[i] > 0) {
      Serial.printf("%5lu %lu\n", i, adc_histobuffer[i]);
    }
  }
  Serial.println();
}
 
Here is my cleaned code without any other calculations - this part is very important for futher calculations - to have pin A1 value (in adcbuffer) and sample time duration - expected every 50 micros (in vImag). I am not sure, how to measure time - I know, now I am wrong in my code.
Or I have to measure between adc->adc0->startTimer(SAMPRATE) and adc->adc0->stopTimer();?

Code:
void adc0_isr()  {
  uint16_t adc_val;
  // call readSingle() to clear the interrupt.
  // and store the ADC data
  adc_val = adc->adc0->readSingle();
  if (adcidx < ADCSAMPLES) {  // storage stops when enough samples collected
    calc_time1 = micros(); // ????start time
    adcbuffer[adcidx] = adc_val;
    calc_time2 = micros(); // ???? end time
    vImag[adcidx] = calc_time2 - calc_time1; // ???? sample time to buffer
    adcidx++;
  }
}

Your timing code will probably return 0 for all values in vImag;


You could try something like this:


Code:
void adc0_isr()  {
  uint16_t adc_val;
  // call readSingle() to clear the interrupt.
  // and store the ADC data
  adc_val = adc->adc0->readSingle();
  if (adcidx < ADCSAMPLES) {  // storage stops when enough samples collected
    adcbuffer[adcidx] = adc_val;
    vImag[adcidx] = adcidx *(1000000/SAMPRATE); // ???? sample time to buffer
    adcidx++;
  }
}

For a software loop, that calculation of the sample time would not be valid. However, for the timer-controlled sampling it works. The histogram tests have shown that no data will be missed due to other interrupts.

You could also skip that timing generation in the interrupt handler and calculate the timing array before or after collecting the data---and you would only need to do it once in the setup function.

To get a verification, you could collect the start and end times in GetADCData:
Code:
uint32_t starttime, endtime;
void GetADCData(void) {
  delay(100); // wait for usb serial to finish
  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(readpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();
  adcidx = 0;
  starttime = micros();
  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz
  adc->adc0->enableInterrupts(adc0_isr);
  do {
    // You could process a sample here with multiple buffers or copying to a new buffer
    // for processing while collection is in progress
  } while (adcidx < ADCSAMPLES);
  adc->adc0->stopTimer();
  // The data is in the global adcbuffer array.
  endtime = micros();
  Serial.printf("Collection time was %lu microseconds.\n", endtime-starttime);
  
}
 
Back
Top