choochoo22
Active member
Using TeensyThreads to alleviate SD write latency issues works very well when it works but it only seems to work within a sweet spot of write block size of 256-1024 characters. If 2048 or higher is selected SD.h won't write at all. If 128 or smaller is selected SD.h writes but sporadically with errors. Without using threads, blocks as small as 64 and at least as large as 32k can be written.
The intended project captures serial data on a Teensy 3.5, processes it, and logs it to an SD card. This works for the most part but SD write latency is a problem as it blocks capture of incoming data. Serial buffering can compensate for this up to a point but occasionally the write latency is extreme, the buffers overflow and data is corrupted. Using TeensyThreads is an attempt to isolate the write operation as a separate thread so the input capture and processing can continue during SD write blocking. As mentioned this works quite well with a block size of 1024 and a slice time of 1ms but fails completely with larger block sizes. It would seem that using threads should work with any block size that works without threads, although performance may differ.
To test the feasibility of applying threads to this project a simulator was created that just generates random data that approximates actual project data in size and sample rate and writes to the SD card. The following code is the simulator. In particular, the use of threads can be turned on or off with a switch and the block size can be specified. With this code and no threads, block sizes will work from 64 up, exhibiting the latency issues the threading is intended to prevent. With threading enabled only block sizes of 256, 512, or 1024 will work, the sweet spot.
Actually the project seems so far to be working fine with settings of 1024 and 1ms. I am raising this as a question to see if perhaps there is something that needs to be tweaked in the libraries to allow them to work together better, or if perhaps there is a way to code this better to permit a wider range of options?
If you would like to try this for yourself I would suggest using a block length of 2048 with Threads=false for 30sec. Ideally this should result in zero total and max latency recorded in columns 4 and 5 and about 4100 lines, give or take depending on the random numbers. If there is latency and significantly less than 4100 lines, this is the problem threading is intended to avoid. Now, changing only turning Threads=true, run it again. On my card and T3.5 it will fail immediately as the SD won't write. Now run it again with a block size of 1024 to demonstrate that the threading works with the block size in the sweet spot and greatly reduces the SD latency issue.
The intended project captures serial data on a Teensy 3.5, processes it, and logs it to an SD card. This works for the most part but SD write latency is a problem as it blocks capture of incoming data. Serial buffering can compensate for this up to a point but occasionally the write latency is extreme, the buffers overflow and data is corrupted. Using TeensyThreads is an attempt to isolate the write operation as a separate thread so the input capture and processing can continue during SD write blocking. As mentioned this works quite well with a block size of 1024 and a slice time of 1ms but fails completely with larger block sizes. It would seem that using threads should work with any block size that works without threads, although performance may differ.
To test the feasibility of applying threads to this project a simulator was created that just generates random data that approximates actual project data in size and sample rate and writes to the SD card. The following code is the simulator. In particular, the use of threads can be turned on or off with a switch and the block size can be specified. With this code and no threads, block sizes will work from 64 up, exhibiting the latency issues the threading is intended to prevent. With threading enabled only block sizes of 256, 512, or 1024 will work, the sweet spot.
Actually the project seems so far to be working fine with settings of 1024 and 1ms. I am raising this as a question to see if perhaps there is something that needs to be tweaked in the libraries to allow them to work together better, or if perhaps there is a way to code this better to permit a wider range of options?
If you would like to try this for yourself I would suggest using a block length of 2048 with Threads=false for 30sec. Ideally this should result in zero total and max latency recorded in columns 4 and 5 and about 4100 lines, give or take depending on the random numbers. If there is latency and significantly less than 4100 lines, this is the problem threading is intended to avoid. Now, changing only turning Threads=true, run it again. On my card and T3.5 it will fail immediately as the SD won't write. Now run it again with a block size of 1024 to demonstrate that the threading works with the block size in the sweet spot and greatly reduces the SD latency issue.
Code:
#include <SD.h>
#include <TeensyThreads.h>
// Tuneable parameters
unsigned int blockLength = 2048; // Number of characters to log in a block
unsigned int frameRate = 7; // Rate new lines are generated (millisec) In actual use this could be several values from 6.7 to 22.
int lineLength = 45; // Number of values in a line. In actual use this is about 25 or 45 depending if one or two inputs are logged.
unsigned int runTime = 30; // Runtime for the test trial (Sec). In actual use logging must succeed at one hour.
bool Threads = false; // Do or don't use a threaded solution.
byte slice = 1; // Slice time for a threaded solution only (millisec).
// Misc other variables
String dataString = "Count,Millis,Latency,Total Latency, Max Latency,String Length,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ";
elapsedMillis frameTime = 0;
int count = 1;
double lastLine = millis();
const int chipSelect = BUILTIN_SDCARD; // Set SD pin for this card
File dataFile ; // Define file for SD write
char fileName[12] = "TestLog.csv";
void setup() {
Serial.begin(9600);
while(!Serial);
SD.begin(chipSelect) ; // Start SD driver
SD.remove(fileName); // If the filename exists, erase it
dataFile = SD.open(fileName, FILE_WRITE); // Open the data file and leave open
delay(10);
if(!dataFile) End("Could not open file: ");
lastLine = millis() - frameRate;
if(Threads){
threads.setSliceMillis(slice);
threads.addThread(writeLog);
}
}
void loop() {
if(frameTime >= frameRate){ // Generate a new line at the prescribed frame rate
if(Threads)threads.suspend(1);
dataString += "\n";
dataString += count++;
dataString+= ",";
dataString += millis();
dataString += ",";
int Latency = millis() - lastLine - frameRate;
static int totLatency = 0;
static int maxLatency =0;
totLatency+= Latency;
if(Latency > maxLatency) maxLatency = Latency;
dataString += Latency;
dataString += ",";
dataString += totLatency;
dataString += ",";
dataString += maxLatency;
lastLine = millis();
dataString += ",";
dataString += dataString.length();
for(int i = 0; i < lineLength; i++){
dataString += ",";
dataString += random(175,1875);
}
frameTime = 0;
if(Threads)threads.restart(1);
}
// If not using threads, this section does the write to the SD card when a new block of text is full
if(!Threads){
if(dataString.length() > blockLength){
Serial.print(dataString.substring(0, blockLength));
if(!dataFile.print(dataString.substring(0, blockLength))) End("SD write failed: ");
dataString.remove(0,blockLength);
dataFile.flush();
}
}
if(millis() > runTime*1000) End("Run complete: ");
if(Threads && (frameTime < frameRate/2)) threads.yield(); // (Only for threads) If no new frame is expected soon, yield some time to the SD process.
}
// Shut off the logging when the proscribed time expires or an error occurs.
void End(String message){
threads.stop();
Serial.print("\n\n");
Serial.print(message);
Serial.print(millis()/1000);
Serial.println(" Seconds");
while(1);
}
// If threads are used, this section writes the data to the SD card when a text block is complete
void writeLog(){
while(1){
if(dataString.length() > blockLength){
Serial.print(dataString.substring(0, blockLength));
if(!dataFile.print(dataString.substring(0, blockLength))) End("SD write failed: ");
dataString.remove(0,blockLength);
dataFile.flush();
}
threads.yield();
}
}