1 MSPS on a T4? Is this possible...

Maximiljan

Active member
Hello All,

I want to record 300 ms of data from the internal ADC at 1 MSPS in 8 bit resolution, this should require 300 kB (1 000 000 (samples) x 8 (bits) x 0.3 (seconds)) of memory and the T4 will still have plenty to spare.

Is this possible? And if so where do I start? I have spent some time with the Pedvide ADC library but have yet to figure out how to record this data and then read it afterwards. I'm not an embedded systems specialist or a very good programmer so I don't mind spending the time looking for the solution, I just want to know that I am looking in the right place.
 
Thought I'd add a picture because everyone likes pictures :)

Untitled.jpg
 
Thanks for providing this morning's "stay at home and stay busy" project.

This code worked fine for recording at 10KHz sine wave. It's a bit too simplified to be up to my professional programming standards (global variables, no parameters to functions, etc. etc.), but it is easy to read and it works.
I ran it on at T4.0 at 600MHz.

Code:
/*******************************************************
   1MegaSample T4.0 ADC
   MJB   4/2/20

   NOTE:  This is a proof-of-concept program written for
          a novice programmer.  In the interest of simplicity
          it uses global variables and simple functions without
          parameters.

          The sampling runs fast enough at 600MHz that some 
          oversampling may be possible to reduce noise

          There may be occasional timing glitches if other
          processes such as the tick interrupt, run at higher
          priority than the ADC interval timer.

          The program uses the ADC library from Pedvide, which
          is one of the libraries installed by  Teensyduino.
          If you plan to do high-speed ADC acquisition, learning
          how this library works should be high on your TO-DO
          list.
***************************************************************/
#include <ADC.h>

IntervalTimer ADCTimer;

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




const int admarkpin  = 1;

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

#define ADMAX 300000
uint8_t adcbuffer[ADMAX];
volatile uint32_t adcidx;
volatile bool doneflag = false;

void setup() {
  // put your setup code here, to run once:
  while (!Serial) {}
  Serial.begin(9600);
  Serial.println("\nOne MSample ADC test");
  pinMode(admarkpin, OUTPUT);
  pinMode(A0, INPUT);
  adc->adc0->setAveraging(1); // set number of averages
  adc->adc0->setResolution(8); // 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
}
void loop() {
  // put your main code here, to run repeatedly:
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'r')  ReadADC();
    if (ch == 'p')  SendPlotADC();
    if (ch == 's')  ShowADC();
  }
}


/*****************************************************
 * This is the intervaltimer interrupt handler
 ******************************************************/
void ADCChore(void){
  uint8_t adval;
  ADMARKHI
  if(adcidx < ADMAX){  // sample until end of buffer
    adc->adc0->startSingleRead(A0); // start a single conversion
    adcbuffer[adcidx] = adc->adc0->readSingle();
    adcidx++;   
  }
  ADMARKLO
  // ADMARHI to ADMARKLO takes about 180nS at 600MHz
  // So some oversampling may be possible
}

/******************************************************
   Read MAXSAMPLES from ADC at 1 microsecond intervals
   Store the results in adcbuffer;
 *****************************************************/
void ReadADC(void) {

  Serial.println("Reading ADC Samples");
  adcidx = 0;
  ADCTimer.begin(ADCChore,1.0);  // start timer at 1 microsecond intervals
  delay(1000);  // plenty of time for 300,000 samples
  ADCTimer.end();  // stop the timer
  Serial.println("ADC Read ");
  Serial.print(adcidx);
  Serial.println(" samples");
}

/******************************************************
   Send MAXSAMPLES from adcbuffer at 1 millisecond
   intervals.  Output is slowed down to allow plotting
   with the Arduino serial plotter
 *****************************************************/
void SendPlotADC(void) {
  uint32_t sendidx;

  Serial.println("Switch to plotter now.");
  delay(5000);  //Wait for user to switch
  // Sending the full 300,000 samples would take 5 minutes
  // at 1mSec/ sample. So I send only the first fifth
  for (sendidx = 0; sendidx < ADMAX/5; sendidx++) {
    Serial.println(adcbuffer[sendidx]);
    delay(1);
  }
}

/******************************************************
*  Display data from adcbuffer in lines of 20 values
*  Only the first NUMTOSHOW values are displayed
 *****************************************************/
#define NUMTOSHOW   1000  // change to alter number output
void ShowADC(void) {
  uint32_t sendidx;

  Serial.println("ADC Data");
  // Sending the full 300,000 samples would  a long time!
  for (sendidx = 0; sendidx < NUMTOSHOW; sendidx++) {
    
    Serial.printf("% 4u", adcbuffer[sendidx]);
    if((sendidx % 20) == 19) Serial.println();
  }
}
 
Last edited:
This afternoon's project was to see if the 1MSample example could be modified to log the 1MSample ADC values to an SD Card. Some time ago, I coerced a micro SD socket into clinging to the bottom of a T4.0 by twisting its pins and applying dabs of molten tin/lead alloy. The result is shown in this photo:

T4_sdio2.jpg

With that socket in place, I added code to the example to save 10 seconds of data collected at 1MSamples/second.

Code:
/*******************************************************
   1MegaSample T4.0 ADC
   MJB   4/2/20

   NOTE:  This is a proof-of-concept program written for
          a novice programmer.  In the interest of simplicity
          it uses global variables and simple functions without
          parameters.

          The sampling runs fast enough at 600MHz that ther is
          time for the native SDIO interface to write the data
          to an sd card.

          In this case, the SD card is a 128GB SanDisk card formatted
          as an EXFAT volume.  A pre-allocated EXFat file greatly reduces
          write overhead as the system doesn't have to update the FAT when
          moving to a new write cluster.
     

          The EXFAT formatter and file routines are part of the SDFAT 2.0
          beta version of SDFAT. You can download the library from
          Bill Greiman's  GITHUB site.
          
          The program uses the ADC library from Pedvide, which
          is one of the libraries installed by  Teensyduino.
          If you plan to do high-speed ADC acquisition, learning
          how this library works should be high on your TO-DO
          list.


***************************************************************/
#include "SdFat.h"
#include "sdios.h"
#include "FreeStack.h"
#include "ExFatlib\ExFatLib.h"
#include <time.h>
#include <TimeLib.h>
#include <ADC.h>

IntervalTimer ADCTimer;

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


#define SD_FAT_TYPE 2
// file system
SdExFat sd;
SdioCard sdc;
ExFile logFile;


#define SD_CONFIG SdioConfig(FIFO_SDIO)
const char compileTime [] = "MegaSample logger Compiled on " __DATE__ " " __TIME__;
const int admarkpin = 1;
const int wrmarkpin = 0;
const int ledpin    = 13;

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

// WRMARKHI and WRMARKLO are used to observe SDC Write timing on oscilloscope
#define  WRMARKHI digitalWriteFast(wrmarkpin, HIGH);
#define  WRMARKLO digitalWriteFast(wrmarkpin, LOW);

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


#define ADBLOCKSIZE  131072

uint8_t DMAMEM adcbuff0[ADBLOCKSIZE];// 128K Buffer
uint8_t DMAMEM adcbuff1[ADBLOCKSIZE];

volatile uint16_t inbuffnum = 0;


uint8_t *inbuffptr, *sdbuffptr;
volatile uint32_t adcidx, totalbytes;

void setup() {
  // put your setup code here, to run once:
  while (!Serial) {}
  Serial.begin(9600);
  Serial.println(compileTime);
  pinMode(admarkpin, OUTPUT);
  pinMode(wrmarkpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(A0, INPUT);
  adc->adc0->setAveraging(1 ); // set number of averages
  adc->adc0->setResolution(8); // 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

  if (!StartSDCard()) {
    // do fast blink forever

    do { // hang with blinking LED
      LEDON
      delay(100);
      LEDOFF
      delay(100);
    } while (1);

  }// end of  if (!StartSDCard())
  setSyncProvider(getTeensy3Time); // helps put time into file directory data
}



void loop() {
  // put your main code here, to run repeatedly:
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'l')  LogADC();
    if (ch == 's')  ShowADC();
    if (ch == 'd')  sd.ls(LS_SIZE | LS_DATE | LS_R);
  }
}

// define the number of samples to collect as 10 megabytes
#define MAXSAMPLES  10*1024l*1024l
/*****************************************************
   This is the intervaltimer interrupt handler
 ******************************************************/
void ADCChore(void) {
;
  ADMARKHI
  if (totalbytes < MAXSAMPLES) { // sample until enough collected
    adc->adc0->startSingleRead(A0); // start a single conversion
    inbuffptr[adcidx] = adc->adc0->readSingle();
    //inbuffptr[adcidx] = micros() & 0xFF;  // uncomment for timing test
    adcidx++;
    if (adcidx >= ADBLOCKSIZE) {  // switch buffers at end
      sdbuffptr = inbuffptr; // set up block for output
      if (inbuffnum == 0) { // swap input to other buffer
        inbuffptr = &adcbuff1[0];
        inbuffnum = 1;
      } else {
        inbuffptr = &adcbuff0[0];
        inbuffnum = 0;
      }
      adcidx = 0;
    }// end of  if (adcidx < ADMAX)
    ADMARKLO
    // ADMARHI to ADMARKLO takes about 180nS at 600MHz
    // So some oversampling may be possible
  }  // end of if (totalbytes < MAXSAMPLES)
}

  /******************************************************
     Read MAXSAMPLES from ADC at 1 microsecond intervals
     Store the results in adcbuffer;
   *****************************************************/

  void LogADC(void) {
    uint32_t totalbytes = 0;
    Serial.println("Reading ADC Samples");
    inbuffnum = 0;
    inbuffptr = &adcbuff0[0];
    sdbuffptr = NULL;
    adcidx = 0;
    if (!OpenLogFile()) {
      Serial.print("Could not open log file.");
      return;
    }
    ADCTimer.priority(50); // medium-high priority
    ADCTimer.begin(ADCChore, 1.0); // start timer at 1 microsecond intervals
    do {
      if (sdbuffptr != NULL) { // when data in buffer, write to SD card
        WRMARKHI
        logFile.write(sdbuffptr, ADBLOCKSIZE);
        sdbuffptr = NULL;
        totalbytes += ADBLOCKSIZE;
        Serial.print("."); // mark each 128KB block written
        WRMARKLO
      }
    }  while (totalbytes < MAXSAMPLES);
    ADCTimer.end();  // stop the timer
    logFile.truncate();  //truncate to amount actually written
    logFile.close();
    Serial.print("\nADC Read ");
    Serial.print(totalbytes);
    Serial.println(" samples");
  }


  bool OpenLogFile(void) {
    uint64_t alloclength;

    if (!logFile.open("Log1MS.dat",  O_RDWR | O_CREAT | O_TRUNC)) {
      return false;
    }
    alloclength = (uint64_t)200 * (uint64_t)(1024L * 1024l); //200MB

    if (!logFile.preAllocate(alloclength)) {
      Serial.println("Pre-Allocation failed.");
      return false;
    } else {
      Serial.println("Pre-Allocation succeeded.");
    }
    return true;

  }


  /******************************************************
     Display data from adcbuffer0 in lines of 20 values
     Only the first NUMTOSHOW values are displayed
   *****************************************************/
#define NUMTOSHOW   1000  // change to alter numbers output
  void ShowADC(void) {
    uint32_t sendidx;

    Serial.println("ADC Data");
    // Sending the full 128K samples would  a long time!
    for (sendidx = 0; sendidx < NUMTOSHOW; sendidx++) {

      Serial.printf("% 4u", adcbuff0[sendidx]);
      if ((sendidx % 20) == 19) Serial.println();
    }
  }


  bool StartSDCard() {
    if (!sd.cardBegin(SD_CONFIG)) {
      Serial.println("cardBegin failed");
    }
    if (!sd.volumeBegin()) {
      Serial.println("volumeBegin failed");
    }
    if (!sd.begin(SdioConfig(FIFO_SDIO))) {
      Serial.println("\nSD File initialization failed.\n");
      return false;
    } 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);
    return true;
  }
/*****************************************************************************
   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());
}

The result demonstrates the feasibility of continuously logging ADC data to the SD card at 1M samples/second. Before this example is ready for exposure to the real world, it needs a lot more work:
* a way to upload the data at the end of sampling.
* a user interface beyond the single-character selectors
* verification that the SD writing doesn't unduly disturb the sampling interval
* a measurement of the degree to which the block write and erase currents of the SD writes affect the ADC values.
(The T4.0 uses the 3.3V supply as the ADC reference. Noise on that supply affects the ADC results)
* a test to see if it is possible to collect 12-bit ADC data and write it as uint16_t values.

Theoretically, a 128GB SD card could log about 1.5 days of data at 1M samples/second. You'd better have crazy Matlab skills if you're going to analyze that much data!

OTOH, collecting the data seems to take only about 60% of the CPU cycles, so perhaps you can analyze it on the fly and save only the results of the analysis.
 
hehe no problem, thanks for that. I love how you say "this mornings project" this would have taken me at least 10 mornings :p.

Cheers
 
"An Afternoon project", you're killing me ;)

Looking through the code you've already answered 2 other questions that I haven't yet been able to figure out until now, thanks!
 
Hi Mborgerson,

The first code (no SD Card) you posted above is amazing, since our Covid lockdown I have been doing tests using a trigger wire to capture ADC input this has worked amazingly well.

I Then bought a T4.1 and planned to use the second code you posted above which included writing to SD card, everything seems to work however the file generated on the SD card (pic below) only contains brackets and no ADC data. Am I doing something wrong or did your comment above "a way to upload the data at the end of sampling" mean that writing to the SD card was not yet implemented, in which case I will try and implement. Thanks again!

Capture.PNG
 
Do some analysis of the data - its likely that you will want to use some different settings and both ADCs to get the accuracy that you would expect from 8 bits. Even "12 bits" is possible.

> pinMode(A0, INPUT)
This isn't a good idea - it sets up the pin as a digital input.


AFAIK, 1Msps is only possible by alternating between two ADCs.
 
> ADMARHI to ADMARKLO takes about 180nS at 600MHz

But this says nothing about how long a conversion takes to complete. I find that they take about 1.1 usec - so I think your code is over-reading the ADC. A sinewave as input would detect it.

It might just work using VERY_HIGH_SPEED. But alternating ADCs will allow lower speed for better results.
 
Just wondering if you looked at the ADC example sketch:
adc_timer_dma.ino?

It uses a Quad Timer to automatically trigger the ADC, and results go out using DMA...

I have not tried the timer at 1mhz, I think the Quad timer set frequency code in PWM.c will allow it...

As for can you actually do this fast of a conversion? I don't know. If I remember correctly in the example I was only reading something like 3000 samples per second as I am just trying to get a rough RMS value for 60hz AC and figured that was fast enough.

The code use two DMA structures which are linked to each other, where you get an interrupt each time each buffer is filled. Assuming that ADC unit can actually go fast enough (Max for PDF - Analog-Digital-Converter (ADC) - Supports up to 1MS/s sampling rate).


Note: depending on if you are really just wanting to run the ADC at absolutely fastest speed, there is another example like this without the name timer in it, that runs the ADC as fast as it can storing results in DMA...
 
Here is an example of alternating to allow twice the speed. Would be better if it verified (vs assumed) that AD conversion was complete.

Code:
void ADCChore(void) {
  static unsigned flipflop = 0;
  unsigned value;
  static unsigned count = 0;   // skip using first two samples

  if (flipflop++ & 1) {
    value = adc->adc1->readSingle();     // read 1
    adc->adc0->startSingleRead(A0);     // start 0
  } else {
    value = adc->adc0->readSingle();    // read 0
    adc->adc1->startSingleRead(A0);    // start 1
  }

  if (adcidx < ADMAX && count > 2)
    adcbuffer[adcidx++] = value;
  else
    ++count;
}
 
I Then bought a T4.1 and planned to use the second code you posted above which included writing to SD card, everything seems to work however the file generated on the SD card (pic below) only contains brackets and no ADC data. Am I doing something wrong or did your comment above "a way to upload the data at the end of sampling" mean that writing to the SD card was not yet implemented, in which case I will try and implement. Thanks again!
Note that the sample code writes the ADC data in binary format as a series of bytes. Part of the process of uploading the binary data would be to convert the series of binary bytes to human-readable format. Simply looking at the SD files as if they were ASCII codes could give you the kind of display you showed. You need to implement something like the ShowADC() function, but using the data on the SD card.

I think the comments by jonr concerning the pitfalls of trying to read the ADC at 1MHz are relevant. I will play around with the code on my T4.1 with a sine-wave input and let you know what I see.
 
> ADMARHI to ADMARKLO takes about 180nS at 600MHz

But this says nothing about how long a conversion takes to complete. I find that they take about 1.1 usec - so I think your code is over-reading the ADC. A sinewave as input would detect it.

It might just work using VERY_HIGH_SPEED. But alternating ADCs will allow lower speed for better results.

Hi Jonr.

Ok thanks will try to implement the second ADC. I did hook the single ADC up to an oscilloscope and got what I considered to be decent results a 1MSPS but in this case faster is better and your idea to use both ADCs makes sense. Thanks
 
Interesting. Once I added code to detect incomplete conversions, I got occasional errors, even at rates that should be possible. Ie, interval timer doesn't guarantee that it won't be called with less interval. But it usually worked with a single ADC, which supports your results. Probably timer triggered DMA is the way to go for low jitter and no errors. In any case, your program is quite nice.


Code:
if (adc->adc0->isConverting())
    error = 1;
  value = adc->adc0->readSingle();  // read previously started conversion
  adc->adc0->startSingleRead(A0); // start next conversion}
 
Note that the sample code writes the ADC data in binary format as a series of bytes. Part of the process of uploading the binary data would be to convert the series of binary bytes to human-readable format. Simply looking at the SD files as if they were ASCII codes could give you the kind of display you showed. You need to implement something like the ShowADC() function, but using the data on the SD card.

I think the comments by jonr concerning the pitfalls of trying to read the ADC at 1MHz are relevant. I will play around with the code on my T4.1 with a sine-wave input and let you know what I see.

Thank you, I thought I was converting the data from .bin to .csv but clearly my method wasn't working. Will have a look at "ShowADC()". As always thank you.
 
Interesting. Once I added code to detect incomplete conversions, I got occasional errors, even at rates that should be possible. Ie, interval timer doesn't guarantee that it won't be called with less interval. But it usually worked with a single ADC, which supports your results. Probably timer triggered DMA is the way to go for low jitter and no errors. In any case, your program is quite nice.


Code:
if (adc->adc0->isConverting())
    error = 1;
  value = adc->adc0->readSingle();  // read previously started conversion
  adc->adc0->startSingleRead(A0); // start next conversion}

Thanks but credit where credit is due, that is 100% mborgerson's excellent work.
 
Whoops, my code in #11 isn't right. To interleave the ADCs, it should read from adc0 and immediately start another on adc0 (not adc1). And vice versa.

Done right, I get 2.55 Msps with VERY_HIGH_SPEED. 2 Msps with HIGH_SPEED and 10 bits resolution.
 
Sorry, I know I am probably missing something here and maybe suggesting some more hardcore things than what you actually need or want.

But I am confused on what is the goal? To do as many fast reads as possible? Or to read at some specific fast speed? How accurate do you want the timings to be?

Maybe I am missing something but if your goal is to read as fast as the ADC will allow? Why not just turn on continuous sampling and with this you have a few different options:
a) Interrupts - I believe you can get an interrupt when a conversion completes, which you can query.
b) Poll again I believe you can sample say is there a conversion ready, yes, read it...
c) DMA - I pointed to a couple of examples.

Now if you wish to read N samples at a specific speed like 1MBS and the hardware supports that fast... Again you have a few options, with plus and minus.
a) You can run your own timing in a loop and say, this much time has expired, so do next read.

b) IntervalTimer - I have done the approach mentioned in this thread, before where I would read one result and startup the next one. For me I did it this way as I was reading 4 ADC pins to try to detect if a circuit was running by doing an RMS like calculation)... Works reasonably well. BUT: you are at the mercy of Interrupts. That is you start up some SDCard write and either I higher priority interrupt happens or someone disables interrupts and your IntervalTimer interrupt does not happen at consistent timing.

c) Setup to use a Timer to trigger the ADC at a specific rate as I mentioned there are examples of it in ADC. The Advantage of this is once setup, it is all handled by hardware, that is the in this case QTimer trigger happens at some specific time, and through the XBAR system, it triggers the ADC module. So this is no longer tied to timing issues of other interrupts happening. Especially if again setup to use DMA to move the results out of the ADC into your own buffer. Note, the current code has limitations on how large these buffers are that ADC does the DMA operation to. That is I believe I setup only one DMA data structure for each buffer, so I believe limit is something like 64K bytes.
But it is setup to handle 2 of them round robin, and has the ability to call you when a buffer is full, so you can then move results out of queue into your own buffer if needed...

Sorry again if I am in left field.
 
That's a good summary. Some glitches at 1Mhz leads to "so how fast can it go". With interleaving, more than fast enough to always complete even with some interrupt jitter.
 
That's a good summary. Some glitches at 1Mhz leads to "so how fast can it go". With interleaving, more than fast enough to always complete even with some interrupt jitter.

I modified my sample code to use interleaved ADC collection per jonr's sample code and added some timing data collection using the ARM cycle counter. I also tested with the resolution set to 12 bits. With the new setup, the time spent in the intervaltimer service routine is about 220nsec.

I connected the ADC to a signal generator and tested various frequencies from 100Hz to 10Khz and got good results:
1MSample_12bit.jpg

That is a ~10KHz signal---which takes about 100 samples per cycle.

I found the following points interesting:

1. There can be up to 0.42 microseconds of jitter in the start of the timer interrupt handler. I presume that this is due to other drivers blocking interrupts while handling Serial output and SD Card writing. I Tested without SD writes and Serial output and the max jitter decreased to 0.13uSec. Better--but still not perfect--perhaps due to things in Serial and the system tick.

2. It didn't seem to make any difference whether I put my SD card buffers in regular RAM or DMAMEM.

3. Both EXFat and FAT32 SD cards worked about the same when pre-allocation was used.

Here's the updated code

Code:
/*******************************************************
   One MegaSample T4.0 ADC  12-bit
   MJB   4/2/20
         updated 8/9/2020
   NOTE:  This is a proof-of-concept program written for
          a novice programmer.  In the interest of simplicity
          it uses global variables and simple functions without
          parameters.

          The sampling runs fast enough at 600MHz that ther is
          time for the native SDIO interface to write the data
          to an sd card.

          The program uses the ADC library from Pedvide, which
          is one of the libraries installed by  Teensyduino.
          If you plan to do high-speed ADC acquisition, learning
          how this library works should be high on your TO-DO
          list.


***************************************************************/
#include "SdFat.h"
#include "sdios.h"
#include <TimeLib.h>
#include <ADC.h>

/*******************************************************/
// when USEMTP is defined, you can upload file with MTP,
// but you have to have the MTP library and modified USB files
#define USEMTP   

#ifdef USEMTP
#include "MTP.h"
#include <Storage.h>
#include <usb1_mtp.h>

MTPStorage_SD storage;
MTPD       mtpd(&storage);
#endif
/*******************************************************/

IntervalTimer ADCTimer;

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

// use an elapsedmillis object to control collection time
elapsedMillis collectmillis;


// This version uses SdFs, which can be either FAT32 or EXFat
SdFs sdf;
SdioCard sdc;
FsFile logFile;


#define SD_CONFIG SdioConfig(FIFO_SDIO)
const char compileTime [] = "T4.1 MegaSample 12-bit logger Compiled on " __DATE__ " " __TIME__;
const int admarkpin = 32;   // Changed to end pins on T4.1
const int wrmarkpin = 33;
const int ledpin    = 13;

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

// WRMARKHI and WRMARKLO are used to observe SDC Write timing on oscilloscope
#define  WRMARKHI digitalWriteFast(wrmarkpin, HIGH);
#define  WRMARKLO digitalWriteFast(wrmarkpin, LOW);

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


#define ADBLOCKSIZE  (1024 * 100)   //  2 x 200KBytes to leave some room for other users of DMAMEM

uint16_t  adcbuff0[ADBLOCKSIZE];
uint16_t  adcbuff1[ADBLOCKSIZE];

volatile uint16_t inbuffnum = 0;
volatile bool logging = false;

uint16_t *inbuffptr, *sdbuffptr;
volatile uint32_t adcidx;
uint32_t totalbytes;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(500);  // wait for Serial to open
  Serial.println(compileTime);
  pinMode(admarkpin, OUTPUT);
  pinMode(wrmarkpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(A0, INPUT_DISABLE); // disable digital keeper resistors
  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::HIGH_SPEED); // change the sampling speed
  
  adc->adc1->setAveraging(1 ); // set number of averages
  adc->adc1->setResolution(12); // set bits of resolution
  adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // change the conversion speed
  adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED); // change the sampling speed
  if (!StartSDCard()) {
    // do fast blink forever

    do { // hang with blinking LED
      LEDON
      delay(100);
      LEDOFF
      delay(100);
    } while (1);

  }// end of  if (!StartSDCard())
  setSyncProvider(getTeensy3Time); // helps put time into file directory data
  #ifdef USEMTP  
  StartMTP();

  ARM_DEMCR |= ARM_DEMCR_TRCENA;
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
  #endif
}



void loop() {
  // put your main code here, to run repeatedly:
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'l')  LogADC();
    if (ch == 's')  ShowADC();
    if (ch == 'd')  sdf.ls(LS_SIZE | LS_DATE | LS_R);
  }
  #ifdef USEMTP
  mtpd.loop();
  #endif
}



/*****************************************************
   This is the intervaltimer interrupt handler
   8/9/2020   Simplified to have main thread control
              logging.
              Changed per jonr to alternate ADCs
              This code takes about 220nS when logging
 ******************************************************/
volatile uint32_t dwtlast; 
volatile uint32_t maxinterval;
volatile uint16_t overflows;
void ADCChore(void) {
  uint16_t value;
  uint32_t dwt;
  dwt = ARM_DWT_CYCCNT; 
  ADMARKHI
  if (adcidx & 0x01) { //Read ADC0 and restart it   
    value = adc->adc0->readSingle();
    adc->adc0->startSingleRead(A0);    
 } else  { //Read ADC1 and restart it 
    value = adc->adc1->readSingle();
    adc->adc1->startSingleRead(A0); 
  }
  if (logging) { // Save result to buffer
    inbuffptr[adcidx] = value;
    adcidx++;
    if (adcidx >= ADBLOCKSIZE) {  // switch buffers at end
      if(sdbuffptr != NULL) overflows++;
      sdbuffptr = inbuffptr; // set up block for output
      if (inbuffnum == 0) { // swap input to other buffer
        inbuffptr = &adcbuff1[0];
        inbuffnum = 1;
      } else {
        inbuffptr = &adcbuff0[0];
        inbuffnum = 0;
      }
      adcidx = 0;
    }  // end of if(logging)
    ADMARKLO
    // ADMARHI to ADMARKLO takes about 180nS at 600MHz
    // So some oversampling may be possible
  } 
  //keep track of max clock cycles between interrupts
  if(dwt-dwtlast > maxinterval) maxinterval = (dwt-dwtlast);
  dwtlast = dwt;
}

/******************************************************
   Read 7 seconds of data from ADCs at 1 microsecond intervals
   Store the results in adcbuffer;
   note that MTP loop is not called during logging
 *****************************************************/

void LogADC(void) {
  uint32_t totalbytes = 0;

  Serial.println("Reading ADC Samples");
  inbuffnum = 0;
  inbuffptr = &adcbuff0[0];
  sdbuffptr = NULL;
  adcidx = 0;
  if (!OpenLogFile()) {
    Serial.print("Could not open log file.");
    return;
  }
  ADCTimer.priority(50); // medium-high priority
  ADCTimer.begin(ADCChore, 1.0); // start timer at 1 microsecond intervals
  delay(1);  // wait 1msec before starting logging
  dwtlast = ARM_DWT_CYCCNT;
  maxinterval = 0;
  logging = true;
  collectmillis = 0;  // reset the elapsedmillis timer
  do {
    if (sdbuffptr != NULL) { // when data in buffer, write to SD card
      WRMARKHI
  //    logFile.write(sdbuffptr, ADBLOCKSIZE*2);// save block of 16-bit words
      sdbuffptr = NULL;
      totalbytes += ADBLOCKSIZE;
    // Serial.print("."); // mark each 128KB block written
      WRMARKLO
    }
  }  while(collectmillis  < 7200); // A bit of extra time to make sure last buffer is written
  logging = false;
  ADCTimer.end();  // stop the timer
  logFile.truncate();  //truncate to amount actually written
  logFile.close();
  Serial.printf("\nADC Read %lu samples\n", totalbytes);
  Serial.printf("Maximum Sampling interval was %6.2f microseconds\n", (float)maxinterval/600.0);
}


bool OpenLogFile(void) {
  uint64_t alloclength;

  if (!logFile.open("Log1MS12B.dat",  O_RDWR | O_CREAT | O_TRUNC)) {
    return false;
  }
  alloclength = (uint64_t)200 * (uint64_t)(1024L * 1024l); //200MB

  if (!logFile.preAllocate(alloclength)) {
    Serial.println("Pre-Allocation failed.");
    return false;
  } else {
    Serial.println("Pre-Allocation succeeded.");
  }
  return true;

}


/******************************************************
   Display data from adcbuffer0 in lines of 20 values
   Only the first NUMTOSHOW values are displayed
 *****************************************************/
#define NUMTOSHOW   1000  // change to alter numbers output
void ShowADC(void) {
  uint32_t sendidx;

  Serial.println("ADC Data");
  // Sending the full 128K samples would  a long time!
  for (sendidx = 0; sendidx < NUMTOSHOW; sendidx++) {

    Serial.printf("% 5u", adcbuff0[sendidx]);
    if ((sendidx % 20) == 19) Serial.println();
  }
}


bool StartSDCard() {
  if (!sdf.cardBegin(SD_CONFIG)) {
    Serial.println("cardBegin failed");
  }
  if (!sdf.volumeBegin()) {
    Serial.println("volumeBegin failed");
  }
  if (!sdf.begin(SdioConfig(FIFO_SDIO))) {
    Serial.println("\nSD File initialization failed.\n");
    return false;
  } else  Serial.println("initialization done.");

  if (sdf.fatType() == FAT_TYPE_EXFAT) {
    Serial.println("Type is exFAT");
  } else {
    Serial.printf("Type is FAT%d\n", int16_t(sdf.fatType()));
  }
  // set date time callback function
  SdFile::dateTimeCallback(dateTime);
  return true;
}
/*****************************************************************************
   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());
}

#ifdef USEMTP
void StartMTP(void){
  Serial.println("Starting MTP Responder");
  usb_mtp_configure();
  if(!Storage_init(&sdf)) {
    Serial.println("Could not initialize MTP Storage!");    
   } 
}
#endif
 
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::HIGH_SPEED); // change the sampling speed

Say one wants the best possible accuracy within some amount of time. Is there a process other than "test all combinations" to find the optimal ADC settings?
 
Say one wants the best possible accuracy within some amount of time. Is there a process other than "test all combinations" to find the optimal ADC settings?

To make that kind of decision you need to specify very carefully what you mean by 'accuracy'.

If your sensor has significant output impedance, or you add an RC filter at the ADC input, if you sample too quickly your signal may not be stable by the time the ADC starts converting.

If you don't have any filters at the input, you may pick up noise at the input. This is particularly true near SD Cards that can radiate a lot of RF noise that can be picked up by high impedance sensors or proto board wiring.

If you are only able to use the 3.3V supply as the reference (as is the case with the Teensy 4.x), your result may have noise due to devices, like SD Cards, that have intermittent high current spikes that pull down the 3.3V rail by a few millivolts.

If you want good DC results, you will probably need a full system calibration. In that case, you have to pick good standards for your calibration data.

If your plan to do spectral analysis of the recorded signals, low sampling jitter is important.

In the end, achieving the best result for a given sensor suite is as much an art as a science. In the end most good art requires talent, and practice.
 
Sorry, I know I am probably missing something here and maybe suggesting some more hardcore things than what you actually need or want.

But I am confused on what is the goal? To do as many fast reads as possible? Or to read at some specific fast speed? How accurate do you want the timings to be?

Hello KurtE, apologies did not really expect this rabbit hole to go as deep as it did, so I don't think that I properly explained what I was trying to do. I am measuring the rise time of shock-waves in a hazardous environment as part of my studies, by using the T4.1 I can collect this data and afford to lose a few T4.1's along the way (but trying not to). A sampling rate of 1 msps is a minimum requirement any faster is better but not a prerequisite, the accuracy of the timing however is important in order to be able to generate an accurate impulse graph of the rise time.

Anyway to Jonr, KurtE and Mborgerson, thanks for all the effort, this is a serious learning curve so really appreciate it, I am going to spend this week fumbling with your codes and suggestions to see if I can get some data.
 
I implemented the sampling using polling of ARM_DWT_CYCCNT with interrupts off and while jitter was normally < 2nsec, it had occasional huge jitter - I think the ADC routines turn interrupts back on (this is poor behavior). But, changing from:

Serial.printf("Starting collection\n"); Serial.flush();

to

Serial.printf("Starting collection\n"); Serial.flush();
delay(1000);

resolved the issue. Conclusion - the serial port driver causes some time consuming interrupts even after a flush should, IMO, have caused it to completely finish.
 
I implemented the sampling using polling of ARM_DWT_CYCCNT with interrupts off and while jitter was normally < 2nsec, it had occasional huge jitter - I think the ADC routines turn interrupts back on (this is poor behavior). But, changing from:

Serial.printf("Starting collection\n"); Serial.flush();

to

Serial.printf("Starting collection\n"); Serial.flush();
delay(1000);

resolved the issue. Conclusion - the serial port driver causes some time consuming interrupts even after a flush should, IMO, have caused it to completely finish.

I can understand that. Serial.flush() starts a series of USB transactions to send the data. How fast that can be done probably depends on how busy the computer on the other end of the USB cable happens to be at the time. IIRC there is a 1-millisecond polling loop in the USB transactions somewhere---but I'm not an expert on USB timing.

I like the idea of polling for the end of continuous ADC conversions and recording DWT cycles for time stamps. I also plan to see how the 8MB PSRAM works as a large buffer.

The idea of capturing shock wave data intrigues me. It takes me back to my first years out of college in the early 1970s. I was in the Navy and on a ship that spent about 3 months each year in '72 and '73 sailing in circles near Mururoa Atoll. Every few weeks, there was a large shock wave generated over the atoll. We were 20 miles away, so the shock wave had degraded to a loud "BOOM" by the time it reached us. (You can decode this diversion by Googling "Mururoa").
 
Back
Top