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.");
}