Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 13 of 13

Thread: Teensy 3.6, SD Card, missing time in milliseconds

  1. #1
    Junior Member
    Join Date
    May 2020
    Posts
    1

    Teensy 3.6, SD Card, missing time in milliseconds

    Hi,
    I am using this datalogger tutorial as the basis for a.. well, datalogger.
    https://www.arduino.cc/en/tutorial/datalogger

    I'm losing time (millis) at a pretty consistent interval(s).

    In the tutorial, 3 analog pins are being read, written to sd card and spit out the the serial monitor

    in my test.. i am just writing millis() to the sd card, no serial monitor and no pins.

    Each write takes about 3-4 milliseconds.. however, after 6.8 seconds (varies per test - but are constant) i get a time drop. example: 1000 milliseconds was recorded, then the next line would be 1070 and the millis count continues from there... 6.8 seconds later.. another 50 milli jump and continues to count from there.

    I'm using a teensy 3.6, with a class 10 san disk 32g uSD drive.

    What could be the issue? with 8 analog sensors, spitting to the serial monitor, I get the same issue

    I was investigating using an external RTC, but the resolution is to low

    Any Ideas?

  2. #2
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,586
    I guess that small delays at each write add up until every 6.8s, a kind of overflow occurs, especially since this Arduino tutorial code is highly inefficient.
    An improved approach could be:
    - not opening and closing the file each time you write, but defining a data acquisition period before and then opening the file once, write during the whole data acquisition period, and then closing the file once.
    - pre-allocate disk space for your file at the beginning, so that the SD file system will not have to allocate new blocks during the data acquisition period, that pulls regularly the handbrake during continuous write operations.
    - Cumulate the ADC readings in memory until you reach the block size of 512 Bytes and write these at once.

  3. #3
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,677
    For a possible time reference the T_3.6 has a cycle counter that can be enabled.

    Once enabled it counts each processor cycle based on clock speed of F_CPU.

    So regular reference with a uint32_t will give the passing clock cycles - with 180M ( or whatever the compiled clock speed is ) passing between each second - or partial second. It will have error from true time based on the crystal/clocking of the Teensy at hand - maybe 500 or 2500 cycles off per second - but it will be relatively consistent - with some changes over time with change in crystal output.

    Activate the cycle counter - not started by default on T_3.6:
    Code:
      ARM_DEMCR |= ARM_DEMCR_TRCENA; // Assure Cycle Counter active
      ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
    Read the cycle counter:
    Code:
      static uint32_t LastCnt = 0;
    
      LastCnt = ARM_DWT_CYCCNT;
    Doing math or compares against two readings like { ARM_DWT_CYCCNT - LastCnt } with unit32_t vars will work for the difference in the readings.

    That doesn't solve the problem - but should give a true measure to compare against the millis() time slipping. If the 'datalogger' code disables interrupts across those long I/O periods that would explain the lost millis().

    There are other posts on ARM_DWT_CYCCNT usage if the above leaves questions.

    Also there are other 'fast data loggers' posted on forum.pjrc that may use better SD access methods that won't disable interrupts and cause time loss or other side effects.

  4. #4
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,677
    crossposted with @Theremingenieur who makes great points to look for or implement. Ideally there are posted data loggers that will implement such things.

    There is a Teensy included sample that will at least use Teensy hardware aware software : T:\arduino-1.8.12\hardware\teensy\avr\libraries\SD\examples\D atalogger\Datalogger.ino

    To run that on T_3.6 use :: chipSelect = BUILTIN_SDCARD;

    It still opens and closes the file each time and does not buffer the write data into 512 byte logical blocks, but it might be a starting point to implement that.

  5. #5
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    243
    This data logger is missing so many of the elements of a true data logger that it really doesn't deserve the name "data logger"---but that's another discussion altogether.

    The intermittent longer write times that you see are a common occurrence with SD cards. The file system buffers up your input and only writes to the SD card when it has accumulated a full sector of 512 bytes. The file open and close messes all this up, of course, and the file system has to write a partial sector at close and then read that sector on the next open, add your new data, then write the sector on the following close. That is terribly inefficient. The SD card internal controller may help by buffering the sectors and writing to a remapped and erased sector. However, at some point the SD card controller has to erase a new block of sectors. That is probably what is taking the extra 50milliseconds.

    In a real data logger, you accumulate your data in an interrupt handler called from an interval timer and write the data to a large buffer. The loop() function monitors the state of that buffer and writes the data to the SD card when needed. It is important that the buffer be able to hold at least enough data to cover the longest time taken to write to the file. In your case that would be at least 50mSec worth of data.

    There are two general ways to handle the buffering: Queues and ping-pong buffers. Both these techniques work better with binary data of fixed length than they do with the strings in the simplistic example.

  6. #6
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    243
    Since I was less than complimentary about the capabilities of that simple logger example, I decided to add some code to back up my comments.

    The following data logger example has the elements I consider necessary for a simple logger:

    1. Commands to start and stop logging.
    2. A command to play back the logged data (just lines of text sent to Serial in this program).
    3. Interrupt-driven collection timing to collect at a specified sampling rate.
    4. Buffering of data so that SD Card writes do not change the timing of sample collection.

    The sample code lacks such niceties as setting proper times in the file directory entries and the
    automatic generation of new file names based on the starting time---things I include in my more
    sophisticated loggers. The code doesn't take advantage of the significant improvements
    in SD file reading and writing in Bill Greiman's SDFat 2.0 Beta. It also skips the significant advantages
    of the ADC object library written by PEDVIDE.

    Code:
    /**************************************************************
      Simple SD card datalogger for Teensy 3.6
    
      This example shows how to log data from three analog sensors
      to an SD card using the SD library.  As much as possible, the
      code uses the simplest Arduino functions, adding Teensy-specific
      code only where necessary (such as IntervalTimer). As a result, it doesn't 
      take advantage of options like the ADC library or the SDFat library.
    
      The circuit:
       analog sensors on analog ins A0, A1, and A2
       SD card attached to built-in SD Card slot on t3.6
    
    
      This example uses  buffering and interval timer to collect at regular
      intervals without collection timing jitter caused by SDC writes.
      A time stamp is saved with each record.
    
      A minimal user interface is added with three one-letter commands:
          'r'   Open file and start recording data
          'q'   Quit logging data and close file
          'p'   Play back data in CSV format
    
    
    NOTES:
        1. A lot of the data handling routines could be shortened by broader
           use of pointers.  I stuck with indices into arrays because it 
           better illustrates some of the principles and the speed difference
           is minimal with the ARM GCC compiler.
        2. A lot of the code is overly simplified--lacking in parameters to 
           functions, etc. etc.
        3. More descriptive output, such as bytes written, timing data, etc,
           would be nice, but the example is long enough as it is.
        4. I used printf() and sprintf() to output multiple variables with
           a single line of code.
        5. I used the Arduino String object when reading the data file. 
           There are many reasons to avoid this object on smaller Arduino
           systems, as it can crash your program if it tries to read a malformed
           file.  Since I'm reading only strings that I have generated, I hope
           to get away with this and avoid many more lines of code to read the
           file for playback.
           
    
      
    Written for Teensy 3.6 by M. Borgerson  May 6, 2020
    ********************************************************************/
    
    // SPI library not needed with Teensy built-in SD card
    #include <SD.h>
    
    IntervalTimer ADCTimer;
    
    // keep sample rate less than 1000 for now
    #define SAMPLERATE 100
    const char *logfilename = "DATALOG.CSV";
    
    // A simple structure to hold time stamp and three analog values
    // The structure takes up 12 bytes to align ltime on 4-byte boundary
    struct datrec {
      uint32_t ltime;
      uint16_t a0, a1, a2;
      uint16_t spare;
    };
    
    #define BUFFSIZE  128
    // allocate two buffers, each of which holds 128 records
    // If you collect faster than about 200 samples/second, you may
    // need to allocate larger buffers.
    struct datrec dbuff0[BUFFSIZE];
    struct datrec dbuff1[BUFFSIZE];
    
    File dataFile;
    
    #define DEBUGPRINT true
    
    // these variables are declared volatile because they are changed or used in the
    // interrupt handler
    volatile uint16_t saveidx = 0;
    volatile int16_t writebuffnum = -1;
    volatile uint16_t savebuffnum = 0;
    volatile uint32_t filestartmilli = 0;
    
    const int chipSelect = BUILTIN_SDCARD;
    const int ledpin = 13;
    
    #define LEDON  digitalWriteFast(ledpin, HIGH);
    #define LEDOFF digitalWriteFast(ledpin, LOW);
    
    const char compileTime [] = "Simple Data Logger  Compiled on " __DATE__ " " __TIME__;
    void setup() {
      pinMode(ledpin, OUTPUT);
      // Open serial communications and wait for port to open:
      Serial.begin(9600);
      while (!Serial) {
        ; // wait for serial port to connect. Needed for native USB port only
      }
      Serial.println(compileTime);
      Serial.print("Initializing SD card...");
    
      // see if the card is present and can be initialized:
      if (!SD.begin(chipSelect)) {
        Serial.println("Card failed, or not present");
        // don't do anything more except fast blink LED
        while (1) {
          LEDON
          delay(50);
          LEDOFF
          delay(50);
    
        }
      }
      Serial.println("card initialized.");
      Serial.println("Enter command selected from (r, q, p)");
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      char ch;
      if (Serial.available()) {
        ch = Serial.read();
        if (ch == 'r') StartLogging();
        if (ch == 'q') StopLogging();
        if (ch == 'p') PlaybackLog();
      }
      // Now check to see if a buffer is ready to be written to SD
      // writebuffnum will be set in the interrupt handler
      if (writebuffnum == 0) { //is dbuff0 ready?
        LEDON
        writebuffnum = -1;
        if (dataFile) { //if the file is open write dbuff0
          WriteCSV(&dbuff0[0], BUFFSIZE);
          if (DEBUGPRINT) {
            Serial.print("Writing dbuff0 to data file.  tmilli = ");
            Serial.printf(" %lu\n", dbuff0[0].ltime);
          }
        }
        LEDOFF
      }
      if (writebuffnum == 1) { // is dbuff1 ready?
        LEDON
        writebuffnum = -1;
        if (dataFile) { //if the file is open write dbuff0
          WriteCSV(&dbuff1[0], BUFFSIZE);
          if (DEBUGPRINT) {
            Serial.print("Writing dbuff1 to data file.  tmilli = ");
            Serial.printf(" %lu \n", dbuff1[0].ltime);
          }
        }
        LEDOFF
      }
      delay(5);
    }  // end of the loop() function
    
    // write a buffer to the file as CSV lines. This  takes more time and file space than
    // binary format, but can be directly read on PC.
    //
    // On a T3.6 AT 168MHz, this usually takes about 25mSec, but that can increase to
    // over 150mSec when the SD Card has to erase a new block.
    // That's not a problem with 100Hz sampling, since we have over 1 second of buffered data.
    void WriteCSV(volatile struct datrec *drp, size_t numlines){
    uint16_t i;  
    char outline[100];
      for(i= 0; i< numlines; i++){
        sprintf(outline,"%6lu, %5u, %5u, %5u\n", drp->ltime,drp->a0,drp->a1, drp->a2);
        dataFile.print(outline);
        drp++;  // advance pointer to next record in buffer
      }
    }
    
    // This is the interrupt handler for the interval timer.
    // It saves the data in the data buffer structures.  The output is converted
    // to CSV text when it is saved to the output file in the main loop.
    // We don't want to do the conversion to a string in the interrupt handler
    // since it takes a lot more time and many of the string routines are not
    // reentrant. (Programmer talk for "They don't play nicely when interrupted")
    void ADCChore(void) {
      uint32_t tmilli;
      tmilli = millis() - filestartmilli;
      // save in  the proper buffer--defined by savebuffnum
      if (savebuffnum == 0) { // put data in dbuff0
        dbuff0[saveidx].ltime = tmilli;
        dbuff0[saveidx].a0 = analogRead(A0);
        dbuff0[saveidx].a1 = analogRead(A1);
        dbuff0[saveidx].a2 = analogRead(A2);
        saveidx++;
        if (saveidx >= BUFFSIZE) { // mark buffer for write to  SD
          writebuffnum = 0;
          savebuffnum = 1;   // start saving in other buffer on next interrupt
          saveidx = 0;  // start at beginning of next buffer
        }
      } else {  // must be saving to dbuff1
        dbuff1[saveidx].ltime = tmilli;
        dbuff1[saveidx].a0 = analogRead(A0);
        dbuff1[saveidx].a1 = analogRead(A1);
        dbuff1[saveidx].a2 = analogRead(A2);
        saveidx++;
        if (saveidx >= BUFFSIZE) { // mark buffer for write to  SD
          writebuffnum = 1;
          savebuffnum = 0;   // start saving in other buffer on next interrupt
          saveidx = 0;   // start at beginning of next buffer
        }
      }
    
    }
    
    void StartLogging(void) {
      if (dataFile) {
        Serial.println("Already collecting!");
        return;
      }
      // we open in a mode that creates a new file each time
      // instead of appending as in the Arduino example
      if(SD.exists(logfilename)) SD.remove(logfilename);
      dataFile = SD.open(logfilename, FILE_WRITE);
      // if the file can't be opened, say so
      if (!dataFile) {
        Serial.println("Could not open output file!");
        return;
      }
      Serial.println("Starting logging");
      dataFile.println("  TIME,   A0,    A1,    A2");  // put header line in file
      // initialize some variables for the buffers
      saveidx = 0;   // start saving at beginning of buffer
      savebuffnum = 0;  // start saving in dbuff0
      writebuffnum = -1;  // indicates no buffer ready yet
      //  start the interval timer to begin logging
      filestartmilli = millis();
      ADCTimer.begin(ADCChore, 1000000 / SAMPLERATE); //begin() expects timer period in microseconds
    }
    
    void StopLogging(void) {
      Serial.println("Stopping logging");
      ADCTimer.end();
      if (DEBUGPRINT) Serial.println("ADCTimer halted");
      delay(10);
      if (dataFile) {
        Serial.println("Data file closed.");
        dataFile.close();  // if file was open, close it.
      } else {
        if (DEBUGPRINT) Serial.println("dataFile was not open!");
      }
      writebuffnum = -1;
      // in the interest of simplicity, we ignore any partial buffer at the end
    }
    
    void PlaybackLog(void) {  
      String datline;
     
      if(dataFile) StopLogging(); // if still recording, stop and close file
      dataFile = SD.open(logfilename, FILE_READ);
      if(!dataFile){
        Serial.println("Could not open data file for reading.");
        return;
      }
      while (dataFile.available()) {
        datline = dataFile.readStringUntil('\n');
        Serial.println(datline); 
      }
      dataFile.close();
      Serial.println("\nPlayback complete.");
    }

  7. #7
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,677
    Quote Originally Posted by mborgerson View Post
    Since I was less than complimentary about the capabilities of that simple logger example, I decided to add some code to back up my comments.

    ...
    Nice looking example and follow up.

    <edit>: It compiles and Runs! Easy to understand and change!

    Made the three uint16_t in 32 bits and stored millis, micros and ARM_DWT_CYCCNT, and removed the spare. Swapped those first two ms and us between the two buffs [0]and [1] to see them toggle.

    Compiled first time and ran to T_4.1 SD card with just short LED blinks after command 'r', then 'q' worked, and so did 'p' to see the results!

    And added a LED on time check - where 'took' is us:
    Code:
    Starting logging
    Writing dbuff0 to data file.  tmilli =  10 took 11734
    Writing dbuff1 to data file.  tmilli =  1290 took 21199
    Writing dbuff0 to data file.  tmilli =  2570 took 12102
    Writing dbuff1 to data file.  tmilli =  3850 took 10900
    Writing dbuff0 to data file.  tmilli =  5130 took 12170
    Writing dbuff1 to data file.  tmilli =  6410 took 20940
    Writing dbuff0 to data file.  tmilli =  7690 took 12049
    Writing dbuff1 to data file.  tmilli =  8970 took 93407
    Writing dbuff0 to data file.  tmilli =  10250 took 79691
    Writing dbuff1 to data file.  tmilli =  11530 took 40186
    Writing dbuff0 to data file.  tmilli =  12810 took 11898
    Writing dbuff1 to data file.  tmilli =  14090 took 11988
    …
    Writing dbuff0 to data file.  tmilli =  43530 took 11053
    Writing dbuff1 to data file.  tmilli =  44810 took 500645
    Writing dbuff0 to data file.  tmilli =  46090 took 489097
    Writing dbuff1 to data file.  tmilli =  47370 took 10848
    …
    Writing dbuff1 to data file.  tmilli =  131850 took 619179
    Writing dbuff0 to data file.  tmilli =  133130 took 312047
    Writing dbuff1 to data file.  tmilli =  134410 took 35150
    Writing dbuff0 to data file.  tmilli =  135690 took 32091
    Last edited by defragster; 05-07-2020 at 09:52 AM.

  8. #8
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    243
    Wow! Those long times at 44810 and 131850 indicate that there must be some significant block erase and remap times. The faster times on the low end (~11-12mSec) show the increased speed of the T4.1 in doing the conversion from binary structure to string output. I've got a T4.0 with an SD socket and I'm going to try the same thing with SDFat 2.0B using a pre-allocated EXFAT file.

    Delays like those in the > 500mSec range are the kind of thing that reinforce the warning: "SD Cards can have long and variable write delays!" Luckily, a T3.6 has enough RAM to buffer over a second of data even at high collection rates. If I extend the data structure to be 4 x 32Bit values, logging at 10KHz will require that each buffer be 160KBytes in length. In that case the two buffers are going to use about 125% of the T3.6 RAM! Not Good. Looks like that is a job for a T4.1!

    Logging at that high rate with the standard SD library gets problematic. Even worse, you end up with a LOT of data to sift through and text in the Serial monitor is not the way to handle that. For high-rate, long-term logging I usually save the more efficient binary records and add a file upload function so that I can analyze with MatLab. The problem with that approach for example code is that you need a PC Host program that knows how to talk to the Teensy and receive the upload data. Requiring the distribution of a PC executable file with the Teensy example code adds a whole new can of worms. On the plus side is the fact that the T4.0 can upload binary files at ~22MBytes/second. The T3.6 maxes out the USB link at about 1.08MBytes/second. The other alternative is to pull the SD Card and use a MatLab function to read the binary data on a PC. Since that requires MatLab, or some other program, to read the binary data, it is also awkward for demo code.

    The large buffers needed for high-rate collection with the SD library may be a good argument for using a queue rather than ping-pong buffers. In many cases, one queue of 160KB can do the same job as two ping-pong buffers of 160KB each. The problem with queues is that the code gets more complex if you want to optimize for multi-sector writes as can be done with SDFat 2.0B. (The extra code is needed to handle the wrap around at the end of the queue buffer.)

    Today's project will be to extend the example code to use SDFat 2.0B and EXFAT files. I'm going to focus more on the SD write delays and eliminate the ADC inputs, while saving just timing data. The playback function will spit out just the write delay after a pause to allow switching to the graphing mode of the serial monitor.

  9. #9
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,677
    Those delay times on SD write numbers are typical - and ugly - not sure if they are any better with the way SDFat 2.0B does I/O - but generally SDFat writes are indeed much faster. Will look for SDFat version here - saves me from doing it to see.

    I was considering adding a data element to record the prior 'SD write time' in the logged data so it could be tracked/plotted offline.

    There are plans to get USB file access to 'Teensy Media' - was just mentioned on the T_4.1 Beta thread. Also USBHost function allows write to Media { FLASH, HDD } ...

  10. #10
    Senior Member
    Join Date
    Feb 2018
    Location
    Corvallis, OR
    Posts
    243
    I've included an updated version of the timing logger. This one has a single #define USE_EXFAT
    and some #ifdefs sprinkled through the code where there are differences between SD file access and
    EXFat file access with SDFat 2.0b. To test EXFat, you need to start with a card formatted for that file
    system.

    In general, the EXFat file writes are faster---on the order of 0.88mSec on a T3.6 when writing to a pre-allocated
    contiguous file. This makes sense since there are no writes to the directory sector until the file is closed and there
    are no reads and writes to the FAT, since all the FAT clusters are marked as used during the pre-allocation process.

    However, if you use the sync() function to put the SD card into low-power mode, write times go up to about 5mSec
    since the file system updates the directory block and that has to wait until the background SD card write of the data
    block is finished.

    I also tried this program on a T4.0 with an attached uSD card socket. Times were only about 20% shorter, indicating
    that most of the time is taken up in the SDIO hardware and SD card. I haven't yet experimented with different SD
    cards. The most recent tests with EXFat were done with a 16GB Samsun EVO card.

    Code:
     
    /**************************************************************
      Simple SD card timing datalogger for Teensy 3.6
    
      This example shows how to log timing data to an SD card using the SD library
      and SDFat 2.0b  
      As much as possible, the code uses the simplest Arduino functions, adding 
      teensy-specific code only where necessary (such as IntervalTimer). 
     
      The circuit:
    
       SD card attached to built-in SD Card slot on t3.6
    
    
      This example uses  buffering and interval timer to collect at regular
      intervals without collection timing jitter caused by SDC writes.
      A time stamp is saved with each record.
    
      A minimal user interface is added with three one-letter commands:
          'r'   Open file and start recording data with verbose output
          'g'   Open file and start recording data with only block write delay output
          'q'   Quit logging data and close file
          'p'   Play back data in CSV format
    
    
      NOTES:
        1. Unlike the Simple Logger, this version saves only the binary data.
           Conversion to strings is done on playback.
        2. Playback is done one record at a time.  This is less efficient,
           but output is going to the Serial port, so speed isn't as much
           of an issue.
        3. if you uncomment #define USE_EXFAT the program will use the EXFat
           file system of SDFat 2.0beta.  You will need an SD Card formatted
           for the EXFat file system.  That can be done with the tools
           in the SDFat examples
    
    
    
    
      Written for Teensy 3.6 by M. Borgerson  May 7, 2020
    ********************************************************************/
    
    // SPI library not needed with Teensy built-in SD card
    
    //#define USE_EXFAT
    
    #ifdef USE_EXFAT
    
    #include "SdFat.h"
    #include "sdios.h"
    #include "ExFatlib\ExFatLib.h"
    SdExFat sd;
    SdioCard sdc;
    ExFile dataFile;
    #define SD_CONFIG SdioConfig(FIFO_SDIO)
    
    #else
    
    #include <SD.h>
    File dataFile;
    
    #endif
    
    IntervalTimer ADCTimer;
    
    
    #define SAMPLERATE 1000
    const char *logfilename = "TIMELOG.DAT";
    
    // A simple structure to hold time stamp and three analog values
    // The structure takes up 16 bytes to align millitime on 4-byte boundary
    struct datrec {
      uint32_t millitime;
      uint32_t microtime;
      uint32_t DWTCount;
      uint32_t byteswritten;
    };
    // Allocate two buffers, each holding over 1 second of data.
    // For efficiency, the total length should be a multiple of 512.
    // Note that the T3.6 will run out of RAM at about 8000Hz sampling
    // as each buffer would have to be 8000 * 16 or 128,000 bytes, leaving
    // very little room for the stack, heap, and USB buffers.
    #define BUFFSIZE  1024
    // allocate two buffers, each of which holds 1024 records
    // each buffer is 1024 * 16 or 16KBytes
    struct datrec dbuff0[BUFFSIZE];
    struct datrec dbuff1[BUFFSIZE];
    
    
    
    #define DEBUGPRINT true
    
    
    // when graphout is true, during recording, only the block write
    // interval is displayed.  This allows graphing with the Arduino
    // Serial Plotter display.
    bool graphout = false;
    
    // these variables are declared volatile because they are changed or used in the
    // interrupt handler
    volatile uint16_t saveidx = 0;
    volatile int16_t writebuffnum = -1;
    volatile uint16_t savebuffnum = 0;
    volatile uint32_t filestartmilli = 0;
    volatile uint32_t byteswritten = 0;
    
    #ifndef USE_EXFAT
    const int chipSelect = BUILTIN_SDCARD;
    #endif
    const int ledpin = 13;
    
    #define LEDON  digitalWriteFast(ledpin, HIGH);
    #define LEDOFF digitalWriteFast(ledpin, LOW);
    
    const char compileTime [] = "Timing Data Logger  Compiled on " __DATE__ " " __TIME__;
    void setup() {
      // activate ARM cycle counter
      ARM_DEMCR |= ARM_DEMCR_TRCENA; // Assure Cycle Counter active
      ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
      pinMode(ledpin, OUTPUT);
      // Open serial communications and wait for port to open:
      Serial.begin(9600);
      while (!Serial) {
        ; // wait for serial port to connect. Needed for native USB port only
      }
      Serial.println(compileTime);
      // see if the card is present and can be initialized:
      Serial.print("Initializing SD card...");
    #ifdef USE_EXFAT
      StartEXFat();
    #else
      StartSD();
    #endif
    
      Serial.println("Enter command selected from (r, g, q, p)");
    }
    
    
    #ifdef USE_EXFAT
    void StartEXFat(void) {
      if (!sd.cardBegin(SD_CONFIG)) {
        Serial.print("cardBegin failed");
      }
      if (!sd.volumeBegin()) {
        Serial.print("  volumeBegin failed");
      }
      if (!sd.begin(SdioConfig(FIFO_SDIO))) {
        Serial.println("\nSD File  system initialization failed.\n");
        FastBlink();
      } else  Serial.print("EXFat initialization done.");
    
      if (sd.fatType() == FAT_TYPE_EXFAT) {
        Serial.println("  Card Type is exFAT");
      } else {
        FastBlink();
      }
      Serial.println("card initialized.");
    }
    
    #else
    void StartSD(void) {
      if (!SD.begin(chipSelect)) {
        Serial.println("Card failed, not FAT32, or not present");
        // don't do anything more except fast blink LED
        FastBlink();
      }
      Serial.println("card initialized.");
    }
    #endif
    
    void FastBlink(void) {  // blink forever
      while (1) {
        LEDON
        delay(50);
        LEDOFF
        delay(50);
      }
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      char ch;
      uint32_t microselapsed;
      elapsedMicros wrtelapsed = 0;
    
      if (Serial.available()) {
        ch = Serial.read();
        if (ch == 'r') {
          graphout = false;
          StartLogging();
        }
        if (ch == 'g') {
          graphout = true;
          StartLogging();
        }
        if (ch == 'q') StopLogging();
        if (ch == 'p') PlaybackLog();
      }
      // Now check to see if a buffer is ready to be written to SD
      // writebuffnum will be set in the interrupt handler
      if (writebuffnum == 0) { //is dbuff0 ready?
        LEDON
        writebuffnum = -1;
        if (dataFile) { //if the file is open write dbuff0
          wrtelapsed = 0;
          WriteBinary(&dbuff0[0], BUFFSIZE);
          microselapsed = wrtelapsed;
          if (DEBUGPRINT) {
            if (graphout) {
              Serial.println((float)microselapsed / 1000.0);
            } else {
              Serial.print("Writing dbuff0 to data file.  tmilli = ");
              Serial.printf(" %lu  took %6.2f mSec\n", dbuff0[0].millitime, (float)microselapsed / 1000.0);
            }
          }
        }
        LEDOFF
      }
      if (writebuffnum == 1) { // is dbuff1 ready?
        LEDON
        writebuffnum = -1;
        if (dataFile) { //if the file is open write dbuff0
          wrtelapsed = 0;
          WriteBinary(&dbuff1[0], BUFFSIZE);
          microselapsed = wrtelapsed;
          if (DEBUGPRINT) {
            if (graphout) {
              Serial.println((float)microselapsed / 1000.0);
            } else {
              Serial.print("Writing dbuff1 to data file.  tmilli = ");
              Serial.printf(" %lu  took %6.2f mSec\n", dbuff1[0].millitime, (float)microselapsed / 1000.0);
            }
          }
        }
        LEDOFF
      }
      delay(5);
    }  // end of the loop() function
    
    // write a buffer to the file as binary record. This  takes more time and file space than
    // binary format, but can be directly read on PC.
    // Note that we have to cast datrec pointer to a uint8_t pointer
    // to keep the SD libraryhappy
    void WriteBinary(volatile struct datrec *drp, size_t numstructs) {
      dataFile.write((uint8_t *)drp, numstructs * sizeof(datrec));
      
    #ifdef USE_EXFAT
     // sync() updates directory and puts SD card in low-power idle mode
     // it increases the block write time to 6-10mSec, since the file system
     // must wait for the long block write to finish before it can update the
     // directory and put the SD card into idle mode
     // Without the sync, the block write returns in 0.88mSec on T3.6 at 168MHz
     // because all the multi-block write stuff happens in interrupt routines in 
     // the SDFat driver.
     // Sync() will reduce power consumption by about 20 to 40mA in the ~1 second
     // between block writes. That can be significant for low-power loggers.
     
    //    dataFile.sync(); // uncomment for slower writes, but reduced SD power
    #endif
    }
    
    // This is the interrupt handler for the interval timer.
    // It saves the data in the data buffer structures.  The output is converted
    // to CSV text when it is saved to the output file in the main loop.
    // We don't want to do the conversion to a string in the interrupt handler
    // since it takes a lot more time and many of the string routines are not
    // reentrant. (Programmer talk for "They don't play nicely when interrupted")
    void ADCChore(void) {
      uint32_t tmilli;
      tmilli = millis() - filestartmilli;
      byteswritten += sizeof(datrec); // update global bytes written count
      // save in  the proper buffer--defined by savebuffnum
      if (savebuffnum == 0) { // put data in dbuff0
        dbuff0[saveidx].millitime = tmilli;
        dbuff0[saveidx].microtime =  micros();
        dbuff0[saveidx].DWTCount = ARM_DWT_CYCCNT;
        dbuff0[saveidx].byteswritten = byteswritten;
        saveidx++;
        if (saveidx >= BUFFSIZE) { // mark buffer for write to  SD
          writebuffnum = 0;
          savebuffnum = 1;   // start saving in other buffer on next interrupt
          saveidx = 0;  // start at beginning of next buffer
        }
      } else {  // must be saving to dbuff1
        dbuff1[saveidx].millitime = tmilli;
        dbuff1[saveidx].microtime =  micros();
        dbuff1[saveidx].DWTCount = ARM_DWT_CYCCNT;
        dbuff1[saveidx].byteswritten = byteswritten;
        saveidx++;
        if (saveidx >= BUFFSIZE) { // mark buffer for write to  SD
          writebuffnum = 1;
          savebuffnum = 0;   // start saving in other buffer on next interrupt
          saveidx = 0;   // start at beginning of next buffer
        }
      }
    }
    
    void StartLogging(void) {
      if (dataFile) {
        Serial.println("Already collecting!");
        return;
      }
      // we open in a mode that creates a new file each time
      // instead of appending as in the Arduino example
    #ifdef USE_EXFAT
    
      if (!dataFile.open(logfilename, O_WRITE | O_CREAT | O_TRUNC)) {
        Serial1.println("Open File failed");
        return;
      }
      // pre-allocate 1GB
      uint64_t alloclength = (uint64_t)(1024l * 1024l * 1024l);
      Serial1.print("Pre-allocating 1GB file space ");
      if (!dataFile.preAllocate(alloclength)) {
        Serial.println("Allocation failed. Proceeding anyway.");
      } else {
        Serial.println("Allocation succeeded.");
      }
    
    #else
    
      if (SD.exists(logfilename)) SD.remove(logfilename);
      dataFile = SD.open(logfilename, FILE_WRITE);
    
    #endif
      // if the file can't be opened, say so
      if (!dataFile) {
        Serial.println("Could not open output file!");
        return;
      }
    
      Serial.println("Starting logging");
      // initialize some variables for the buffers
      saveidx = 0;   // start saving at beginning of buffer
      savebuffnum = 0;  // start saving in dbuff0
      writebuffnum = -1;  // indicates no buffer ready yet
      //  start the interval timer to begin logging
      filestartmilli = millis();
      byteswritten = 0;
      ADCTimer.begin(ADCChore, 1000000 / SAMPLERATE); //begin() expects timer period in microseconds
    }
    
    void StopLogging(void) {
      Serial.println("Stopping logging");
      ADCTimer.end();
      if (DEBUGPRINT) Serial.println("ADCTimer halted");
      delay(10);
      if (dataFile) {
        Serial.println("Data file closed.");
    #ifdef USE_EXFAT
        dataFile.truncate();  // Truncate down to data actually written
    #endif
        dataFile.close();  // if file was open, close it.
      } else {
        if (DEBUGPRINT) Serial.println("dataFile was not open!");
      }
      writebuffnum = -1;
      // dont' write tottal bytes if graphing--it messes up the plot!
      if(!graphout){
        Serial.printf("%lu KBytes written to file.\n", byteswritten / 1024);
      }
      // in the interest of simplicity, we ignore any partial buffer at the end
    }
    
    void PlaybackLog(void) {
      struct datrec temprec;
      if (dataFile) StopLogging(); // if still recording, stop and close file
    #ifdef USE_EXFAT
      if (!dataFile.open(logfilename, O_READ)) {
        Serial1.println("Open File failed");
        return;
      }
    #else
      dataFile = SD.open(logfilename, FILE_READ);
    #endif
      if (!dataFile) {
        Serial.println("Could not open data file for reading.");
        return;
      }
      while (dataFile.available()) {
        dataFile.read(&temprec, sizeof(datrec));
        Serial.printf("%lu, %lu, %lu\n", temprec.millitime, temprec.microtime, temprec.DWTCount);
      }
      dataFile.close();
      Serial.println("\nPlayback complete.");
    }
    Here's ascreen capture showing why it's a good idea to have at least 1 second of buffer space. Times on the Y axis are milliseconds to complete a block write of 16KBytes on a T3.6 with a FAT32 SD Card using the Arduino SD library.

    Click image for larger version. 

Name:	timingCapture.JPG 
Views:	25 
Size:	49.2 KB 
ID:	20015

  11. #11
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    12,677
    Great work - thanks for posting - look to try it soon. I've seen other fast data logger sketches posted or around but never bothered to look - will give this a try.

    It shows why a big enough buffer is needed for sure as they generally write FAST - but not always. Also shows the import of having an _isr() doing the data logging so as to not miss timing intervals.

    I ran prior edited code overnight at a slower log rate {#define SAMPLERATE 10} and stopped it and printed and all seems well and uncorrupted for proper writes from both buffers as presented.

    Will have to try it on pending Teensy 4.x with logging to 8MB PSRAM and see if SD writes work at the same time without crashing on colliding QSPI access during SD write when _isr() triggers new log entries. Maybe even onto the 16MB added Flash chip could work - if I can get the code right for that.

    <EDIT> Copied code COMPILED! and RUNS! - this SD card was FAT32.

    Changed to exFAT and much faster now - on native T_4.1 SD - 13.16 is the slowest time so far:
    Code:
    T:\tCode\sd\SDfat_Logger\SDfat_Logger.ino May  7 2020 13:36:18
    Timing Data Logger  Compiled on May  7 2020 13:36:18
    Initializing SD card...EXFat initialization done.  Card Type is exFAT
    card initialized.
    Enter command selected from (r, g, q, p)
    Allocation succeeded.
    Starting logging
    Writing dbuff0 to data file.  tmilli =  1  took   1.20 mSec
    Writing dbuff1 to data file.  tmilli =  1025  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  2049  took   0.72 mSec
    Writing dbuff1 to data file.  tmilli =  3073  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  4097  took   1.12 mSec
    Writing dbuff1 to data file.  tmilli =  5121  took   2.75 mSec
    …
    Writing dbuff1 to data file.  tmilli =  56321  took   1.12 mSec
    Writing dbuff0 to data file.  tmilli =  57345  took  13.16 mSec
    Writing dbuff1 to data file.  tmilli =  58369  took   1.24 mSec
    …
    Writing dbuff1 to data file.  tmilli =  136193  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  137217  took   1.83 mSec
    Writing dbuff1 to data file.  tmilli =  138241  took   1.23 mSec
    Writing dbuff0 to data file.  tmilli =  139265  took   0.72 mSec
    Writing dbuff1 to data file.  tmilli =  140289  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  141313  took   0.72 mSec
    Writing dbuff1 to data file.  tmilli =  142337  took  13.07 mSec
    Writing dbuff0 to data file.  tmilli =  143361  took   1.31 mSec
    Writing dbuff1 to data file.  tmilli =  144385  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  145409  took   0.72 mSec
    Writing dbuff1 to data file.  tmilli =  146433  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  147457  took   1.18 mSec
    Writing dbuff1 to data file.  tmilli =  148481  took   2.07 mSec
    …
    Writing dbuff1 to data file.  tmilli =  228353  took  12.58 mSec
    Writing dbuff0 to data file.  tmilli =  229377  took   1.24 mSec
    Writing dbuff1 to data file.  tmilli =  230401  took   0.72 mSec
    Writing dbuff0 to data file.  tmilli =  231425  took   0.72 mSec
    Only 3 double digit times in 5 minutes.

    Some follow up on T_4.1 Beta thread :: pjrc.com/threads/60532-Teensy-4-1-Beta-Test
    20K samples/sec with twin 64 KB buffer to exFAT:
    Code:
    Writing dbuff0 to data file.  tmilli =  691814  took   2.88 mSec
    Writing dbuff1 to data file.  tmilli =  692019  took   2.88 mSec
    Writing dbuff0 to data file.  tmilli =  692224  took   2.88 mSec
    Writing dbuff1 to data file.  tmilli =  692429  took   2.88 mSec
    Writing dbuff0 to data file.  tmilli =  692633  took   2.88 mSec
    Writing dbuff1 to data file.  tmilli =  692838  took   2.88 mSec
    Writing dbuff0 to data file.  tmilli =  693043  took   2.88 mSec
    Writing dbuff1 to data file.  tmilli =  693248  took   2.88 mSec
    Writing dbuff0 to data file.  tmilli =  693453  took   2.88 mSec
    Stopping logging
    ADCTimer halted
    Data file closed.
    216780 KBytes written to file.
    <edit> This above code also working on T_4.0 on a FRDM 4236 breakout to the same exFAT SD card.
    Last edited by defragster; 05-07-2020 at 10:21 PM.

  12. #12
    Junior Member
    Join Date
    Oct 2020
    Location
    Oregon
    Posts
    2
    Thanks a bunch for this sample code. I used your first example from 05-06-2020, 08:42 PM as it was easier for me to understand. I have one TeensyLC device, but have not worked my way up to it yet, so I ported this onto an Atmega326P. But not after making a few modifications. For example I'm using Timer-1 as my interrupt handler since the 328 doesn't understand the ADCTimer function. Your code works great!

    But I have a question: your data structure has 12 bytes and the variable BUFFSIZE is assigned to 128. Wouldn't that make a 1528 byte buffer when the previously discussed SD card page-write limit is 512 bytes at a time? The index variable "saveidx" is indexed one at a time until it exceeds BUFFSIZE. At this point I had to make my buffers much smaller due to the limits of the Amtega chip. But soon, I will grow onto the Teensy perhaps out of necessity.

    Aside: I'm playing with datalogging of home water pressure wave forms. Professor Patel from UW describes how one sensor can discriminate amongst individual valves in a home. Each valve has its own fingerprint. Once you identify the valve, you can estimate the water consumption. Alternatively, the water pressure drop itself is an indication but the relationship is not linear.

  13. #13
    Junior Member
    Join Date
    Oct 2020
    Location
    Oregon
    Posts
    2

    Teensy 3.6, SD Card, missing time in milliseconds

    @ mborgerson: Again, thanks for the code example.

    Quote Originally Posted by Craig Larson View Post
    Wouldn't that make a 1528 byte buffer ... ?
    My mistake: strike 1528 and replace with 1536.

    For others following this thread: I also found that writes to the SD card caused a voltage sag on connected circutry despite using a large capacitor. This caused a dent in my ADC readings. To solve this I did two things: 1. The SD Card reader was given a dedicated power supply. 2. Each ADC reading was adjusted by by a modified VCC based on AREF. Andreas Spiess shows how to do this in his video #10.

    But now I've got silky smooth ADC readings indicating that the sensors have very little noise.

Posting Permissions

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