Forum Rule: Always post complete source code & details to reproduce any issue!
Page 1 of 2 1 2 LastLast
Results 1 to 25 of 48

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

  1. #1

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

    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.

  2. #2
    Thought I'd add a picture because everyone likes pictures

    Click image for larger version. 

Name:	Untitled.jpg 
Views:	39 
Size:	57.1 KB 
ID:	19570

  3. #3
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    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 by mborgerson; 04-02-2020 at 05:35 PM. Reason: removed unused boolean variable

  4. #4
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    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:

    Click image for larger version. 

Name:	T4_sdio2.jpg 
Views:	36 
Size:	142.3 KB 
ID:	19572

    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.

  5. #5
    hehe no problem, thanks for that. I love how you say "this mornings project" this would have taken me at least 10 mornings .

    Cheers

  6. #6
    "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!

  7. #7
    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!

    Click image for larger version. 

Name:	Capture.PNG 
Views:	12 
Size:	17.9 KB 
ID:	21324

  8. #8
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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.

  9. #9
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    > 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.

  10. #10
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    7,663
    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...

  11. #11
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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;
    }

  12. #12
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    Quote Originally Posted by Maximiljan View Post

    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.

  13. #13
    Quote Originally Posted by jonr View Post
    > 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

  14. #14
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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}

  15. #15
    Quote Originally Posted by mborgerson View Post
    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.

  16. #16
    Quote Originally Posted by jonr View Post
    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.

  17. #17
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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.

  18. #18
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    7,663
    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.

  19. #19
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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.

  20. #20
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    Quote Originally Posted by jonr View Post
    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:
    Click image for larger version. 

Name:	1MSample_12bit.jpg 
Views:	20 
Size:	58.2 KB 
ID:	21329

    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

  21. #21
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    adc->adc0->setAveraging(1 ); // set number of averages
    adc->adc0->setResolution(12); // set bits of resolution
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIG H_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?

  22. #22
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    Quote Originally Posted by jonr View Post
    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.

  23. #23
    Quote Originally Posted by KurtE View Post
    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.

  24. #24
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    644
    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.

  25. #25
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    206
    Quote Originally Posted by jonr View Post
    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").

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •