Optimizing SD Card Writes

I'm working on creating a data acquisition system for my collegiate racing team and have run into a bit of a problem. My Teensy 4.1 is quite happy to sample and save data at over 20ksps when there's only a couple values

outputFile.printf("%llu,%lu,%d,%d,%d,%d,%d,%d,%d\n", now(), micros()
  , binaryValues[0], binaryValues[1], binaryValues[2], analogValues[0], analogValues[1], 
  analogValues[2], analogValues[3]);

but as soon as I add some more data in there everything breaks.

outputFile.printf("%llu,%lu,%d,%d,%d,%d,%d,%d,%d,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%d,%d,%d,%d\n", now(), micros()
  , binaryValues[0], binaryValues[1], binaryValues[2], analogValues[0], analogValues[1], 
  analogValues[2], analogValues[3],orientationData.orientation.x, orientationData.orientation.y, 
  orientationData.orientation.z, linearAccelData.acceleration.x, linearAccelData.acceleration.y, linearAccelData.acceleration.z,
  accelerometerData.acceleration.x, accelerometerData.acceleration.y, accelerometerData.acceleration.z,
  quatW, quatX, quatY, quatZ, BNO05System, gyro, accel, mag);

Normally the approximately 700sps we can reach with this much larger amount of data would be fine, but that unfortunately puts us well under the nyquist rate for our rpm readings causing some pretty significant problems. My question is as follows: Should I be writing in binary instead of text, and, if so, can someone help me figure out how best to do that here? I'm not super familiar with how SD.write() works and how I'd reconstruct my data on the other side. I've posted the entire code below for context.

//Note: Also could maybe decrease bit resolution
//Note: Should get an external powersupply to get better VCC stability
//sd card stuff

//try to change this to use interupts instead of constantly checking each pin (there's apparently a seperate piece of hardware that geenrates the interupt signal so using interupts should save clock cycles)
#include "SD.h"
#include "SPI.h"
//time stuff
#include <TimeLib.h>
#include <Adafruit_ADS1X15.h>
#include <Wire.h>
#include <Adafruit_BNO055.h>


#define BAUD 230400

#define serialMonitor Serial

File outputFile;

bool isRecording = false;

//the index in this matches up to the index of binary data in the printf
int binaryValues[3] = {0, 0, 0};

Adafruit_ADS1115 ads;
//stores current values for sensors connected to adc0
int analogValues[4] = {0, 0, 0, 0};
//current analog sensor number being polled
int currentAnalogSensor = 0;

//creates a new instance of the BNO055 class
Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x29, &Wire);

//varaibles for data from BNO05
sensors_event_t orientationData, linearAccelData, accelerometerData;
uint8_t BNO05System, gyro, accel, mag = 0;
double quatW, quatX, quatY, quatZ;
imu::Quaternion quat;
//saves the last time data was saved 
ulong lastSaveTimeInMillis = 0;

void setup() {
  if (CrashReport) {
    /* print info (hope Serial Monitor windows is open) */
    Serial.print(CrashReport);
  }
  // set the Time library to use Teensy 3.0's RTC to keep time
  pinMode(8, OUTPUT); //white LED (powered on)
  Serial.begin(115200);
  setSyncProvider(getTeensy3Time);
  if (timeStatus()!= timeSet) {
    Serial.println("Unable to sync with the RTC");
  } else {
    Serial.println("RTC has set the system time");
  }
  serialMonitor.begin(BAUD);
  SD.begin(BUILTIN_SDCARD);
  delay(500);
  String time =  String(year()) + "-" + String(month()) + "-" + String(day()) + " " + String(hour()) + "_" + String(minute()) + "_" + String(second());
  Serial.println(time.c_str());
  outputFile = SD.open(time.c_str(),  FILE_WRITE);
  //sets top leds to output
  pinMode(9, OUTPUT); //red LED (recording)
  digitalWrite(8, HIGH); //turn on white LED
  pinMode(7, INPUT_PULLDOWN); //pull down input for record stop/start button (NEEDED TO MAKE IT WORK)
  //set the values in the digital value array to their initial values (likely doesn't matter but feels better to do it this way)
  binaryValues[0] = digitalRead(20);
  binaryValues[1] = digitalRead(21);
  binaryValues[2] = digitalRead(22);
  //sets up interupts for binary values
  attachInterrupt(digitalPinToInterrupt(20), updateRearDiff, CHANGE); //rear diff
  attachInterrupt(digitalPinToInterrupt(21), updateFrontLeftHalleffect, CHANGE); // front left halleffect
  attachInterrupt(digitalPinToInterrupt(22), updateFrontRightHalleffect, CHANGE); // front right halleffect
  attachInterrupt(digitalPinToInterrupt(7), changeRecordingState, CHANGE); // monitor record button
  //sets up interupt pin for ADS1115 ADC (have to pullup alert pin in accordance with ADS1115 datasheet)
  pinMode(15, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(15), readAnalogValues, FALLING);
  //set up ADC on seperate bus to hopefully make them play nice
  ads.begin(ADS1X15_ADDRESS, &Wire1);
  //set data rate to max 
  ads.setDataRate(RATE_ADS1115_860SPS);
  isRecording = true;
  digitalWrite(9, HIGH); //turn on red LED
  //start the BNO05
  bno.begin();
  //start first ADC reading to begin the cycle
  ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, false);
}
//writes data to SD card
void loop() {
  CrashReport.breadcrumb(2, 1111111);
  //try testing with an oscilloscope
  //potentially reduce print buffer time?
  //use flush?
  /**outputFile.printf("%llu,%lu,%d,%d,%d,%d,%d,%d,%d,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%d,%d,%d,%d\n", now(), micros()
  , binaryValues[0], binaryValues[1], binaryValues[2], analogValues[0], analogValues[1], 
  analogValues[2], analogValues[3],orientationData.orientation.x, orientationData.orientation.y, 
  orientationData.orientation.z, linearAccelData.acceleration.x, linearAccelData.acceleration.y, linearAccelData.acceleration.z,
  accelerometerData.acceleration.x, accelerometerData.acceleration.y, accelerometerData.acceleration.z,
  quatW, quatX, quatY, quatZ, BNO05System, gyro, accel, mag);**/
  outputFile.printf("%llu,%lu\n", now(), micros());
  //CrashReport.breadcrumb(2, 2222222);
  /**orientationData.orientation.x, orientationData.orientation.y, 
  orientationData.orientation.z, linearAccelData.acceleration.x, linearAccelData.acceleration.y, linearAccelData.acceleration.z,
  accelerometerData.acceleration.x, accelerometerData.acceleration.y, accelerometerData.acceleration.z,
  quatW, quatX, quatY, quatZ, BNO05System, gyro, accel, mag**/
  //update the BNO05 data values once every 5 milliseconds (the sensor updates at about 100hz but we poll at double it to make sure we dont miss any data)
  if(millis() % 20 == 0) {
    bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER);
    bno.getEvent(&linearAccelData, Adafruit_BNO055::VECTOR_LINEARACCEL);
    bno.getEvent(&accelerometerData, Adafruit_BNO055::VECTOR_ACCELEROMETER);
    bno.getCalibration(&BNO05System, &gyro, &accel, &mag);
    quat = bno.getQuat();
    quatW = quat.w();
    quatX = quat.x();
    quatY= quat.y();
    quatZ = quat.z();
  }
  CrashReport.breadcrumb(2, 3333333);
  //doing it this way instead of using interupts since it seems to be more stable
}
//method needed to get time
time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}
//I think you need a seperate one for some reason :(
void updateRearDiff() {
  binaryValues[0] = !binaryValues[0];
}
void updateFrontLeftHalleffect() {
  binaryValues[1] = !binaryValues[1];
}
void updateFrontRightHalleffect() {
  binaryValues[2] = !binaryValues[2];
}
void readAnalogValues() {
  CrashReport.breadcrumb(2, 44444444);
  switch (currentAnalogSensor) {
      case 0:
        analogValues[0] =  ads.getLastConversionResults();
        currentAnalogSensor = 1;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_1, false);
        break;
      case 1:
        analogValues[1] =  ads.getLastConversionResults();
        currentAnalogSensor = 2;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_2, false);
        break;
      case 2:
        analogValues[2] =  ads.getLastConversionResults();
        currentAnalogSensor = 3;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_3, false);
        break;
      case 3:
        analogValues[3] =  ads.getLastConversionResults();
        currentAnalogSensor = 0;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, false);
        break;
  }
  CrashReport.breadcrumb(2, 55555555);
}
void changeRecordingState() {
  if (lastSaveTimeInMillis + 1000 < millis()) {
    noInterrupts();
    CrashReport.breadcrumb(3, 66666666);
    if(isRecording == true) {
      while(digitalRead(7) == 1) {
        delay(1);
      }
      outputFile.close();
      digitalWrite(9, LOW); //turn off red LED
      Serial.println("Data Recording Stopped");
      isRecording = false;
    }
    else {
      while(digitalRead(7) == 1) {
        delay(1);
      }
      String time =  String(year()) + "-" + String(month()) + "-" + String(day()) + " " + String(hour()) + "_" + String(minute()) + "_" + String(second());
      Serial.println(time.c_str());
      //turn on red LED
      digitalWrite(9, HIGH);
      outputFile = SD.open(time.c_str(),  FILE_WRITE);
      isRecording = true;
    }
    lastSaveTimeInMillis = millis();
    CrashReport.breadcrumb(3, 77777777);
    interrupts();
  }
}

Thanks in advance for the help!

To get max speed create a struct of the data you want to save.

struct 
{
  int analogValues[4];
  int binaryValues[3];
} myData;

....

// fill the stuct
myData.binaryValues[0] = digitalRead(20);
myData.binaryValues[1] = digitalRead(21);
myData.binaryValues[2] = digitalRead(22);

...

// write the struct in one call
SD.write( &myData, sizeof(myData));

Get the idea?


There are ways to compact the data, e.g. the 1 bit digitalReads could be compacted into a single bit but first get the above working :wink:

1 Like

Sick! Thanks for the advice. One question, how do I pull the structs back out on the other side and how do I distinguish between the end of one struct and the beginning of another?

Edit: Why is the SD.write being passed a pointer by the way? I thought it called for a buffer? (SD - write() - Arduino Reference)

A pointer is the simplest way to point (sic) to the buffer....

Gotcha, makes sense! Now I just need to figure out how to recompile on the other side. Gonna take me a bit to write that program since I haven't many file operations in c before.

Edit: Nvm! Python to the rescue. I'll figure out how to make something faster in c/c++ later.

It's working! Thanks everyone! Note to anyone replicating something similar to this, arduino likes to add some padding a little differently than pythons struct library so make sure to check the sizeof() function in arduino against the calcsize() function in python. If they're different you can add padding with (num_bytes)x.

If possible post your solution on both side,
thanks

Here's the updated code (I made some other changes to better facilitate my use of interrupts). Note the struct and the outputFile.write() statement. I chose to replace the sizeof() with a global variable since it tehcnically reduces the amount of function calls in my loop.

//Note: May be able to go faster by modifying core Teensy 4.1 code to reduce the amount of averages that it takes when doing an analog read
//Note: Also could maybe decrease bit resolution
//Note: Should get an external powersupply to get better VCC stability
//sd card stuff

//try to change this to use interupts instead of constantly checking each pin (there's apparently a seperate piece of hardware that geenrates the interupt signal so using interupts should save clock cycles)
#include "SD.h"
#include "SPI.h"
//time stuff
#include <TimeLib.h>
#include <Adafruit_ADS1X15.h>
#include <Wire.h>
#include <Adafruit_BNO055.h>

#define BAUD 230400

#define serialMonitor Serial

struct {
  unsigned long long int seconds;
  unsigned long int micros;
  int binaryValues[3];
  int analogValues[4];
  float orientation[3];
  float acceleration[3];
  float linearAcceleration[3];
  float quaternionCoords[4];
  int calibration[4];
} dataStruct;


File outputFile;

bool isRecording = false;

Adafruit_ADS1115 ads;

//current analog sensor number being polled
int currentAnalogSensor = 0;

//create an interval timer for BNO05
IntervalTimer BNO05Timer;

//creates a new instance of the BNO055 class
Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x29, &Wire);

//varaibles for data from BNO05
sensors_event_t orientationData, linearAccelData, accelerometerData;
uint8_t BNO05System, gyro, accel, mag = 0;
imu::Quaternion quat;

volatile bool analogValueFlag = false;
volatile bool BNO05flag = false;

int sizeOfStruct = sizeof(dataStruct);

//saves the last time data was saved 
ulong lastSaveTimeInMillis = 0;

void setup() {
  pinMode(8, OUTPUT); //white LED (powered on)
  Serial.begin(115200);
  //set up time stuff for rtc
  setSyncProvider(getTeensy3Time);
  if (timeStatus()!= timeSet) {
    Serial.println("Unable to sync with the RTC");
  } else {
    Serial.println("RTC has set the system time");
  }
  serialMonitor.begin(BAUD);
  SD.begin(BUILTIN_SDCARD);
  delay(500);
  String time =  String(year()) + "-" + String(month()) + "-" + String(day()) + " " + String(hour()) + "_" + String(minute()) + "_" + String(second())+".bin";
  Serial.println(time.c_str());
  outputFile = SD.open(time.c_str(),  FILE_WRITE);
  //sets top leds to output
  pinMode(9, OUTPUT); //red LED (recording)
  digitalWrite(8, HIGH); //turn on white LED
  pinMode(7, INPUT_PULLDOWN); //pull down input for record stop/start button (NEEDED TO MAKE IT WORK)
  //set the values in the digital value array to their initial values (likely doesn't matter but feels better to do it this way)
  dataStruct.binaryValues[0] = digitalRead(20);
  dataStruct.binaryValues[1] = digitalRead(21);
  dataStruct.binaryValues[2] = digitalRead(22);
  //sets up interupts for binary values
  attachInterrupt(digitalPinToInterrupt(20), updateRearDiff, CHANGE); //rear diff
  attachInterrupt(digitalPinToInterrupt(21), updateFrontLeftHalleffect, CHANGE); // front left halleffect
  attachInterrupt(digitalPinToInterrupt(22), updateFrontRightHalleffect, CHANGE); // front right halleffect
  BNO05Timer.begin(updatBNO05Flag, 5000); //BNO05 polling flag
  //sets up interupt pin for ADS1115 ADC (have to pullup alert pin in accordance with ADS1115 datasheet)
  pinMode(15, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(15), updateAnalogValueFlag, FALLING);
  //set up ADC on seperate bus to hopefully make them play nice
  ads.begin(ADS1X15_ADDRESS, &Wire1);
  //set data rate to max 
  ads.setDataRate(RATE_ADS1115_860SPS);
  isRecording = true;
  digitalWrite(9, HIGH); //turn on red LED
  //start the BNO05
  bno.begin();
  //start first ADC reading to begin the cycle
  ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, false);
  updateBNO05Readings();
}
//writes data to SD card
void loop() {
  dataStruct.seconds = now();
  dataStruct.micros = micros();
  outputFile.write(&dataStruct, sizeOfStruct);
  if(BNO05flag) {
    updateBNO05Readings();
    BNO05flag = false;
  }
  if (analogValueFlag) {
    readAnalogValues();
    analogValueFlag = false;
  }
  if (digitalRead(7) && lastSaveTimeInMillis + 1000 < millis()) {
    changeRecordingState();
  }

}
//method to update BNO05 readings
void updateBNO05Readings() {
  bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER);
  bno.getEvent(&linearAccelData, Adafruit_BNO055::VECTOR_LINEARACCEL);
  bno.getEvent(&accelerometerData, Adafruit_BNO055::VECTOR_ACCELEROMETER);
  bno.getCalibration(&BNO05System, &gyro, &accel, &mag);
  quat = bno.getQuat();
  dataStruct.orientation[0] = orientationData.orientation.x;
  dataStruct.orientation[1] = orientationData.orientation.y;
  dataStruct.orientation[2] = orientationData.orientation.z;
  dataStruct.linearAcceleration[0] = linearAccelData.acceleration.x;
  dataStruct.linearAcceleration[1] = linearAccelData.acceleration.y;
  dataStruct.linearAcceleration[2] = linearAccelData.acceleration.z;
  dataStruct.acceleration[0] = accelerometerData.acceleration.x;
  dataStruct.acceleration[1] = accelerometerData.acceleration.y;
  dataStruct.acceleration[2] = accelerometerData.acceleration.z;
  dataStruct.calibration[0] = BNO05System;
  dataStruct.calibration[1] = gyro;
  dataStruct.calibration[2] = accel;
  dataStruct.calibration[3] = mag;
}
//method needed to get time
time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}
void updateAnalogValueFlag() {
  analogValueFlag = true;
}
void updatBNO05Flag() {
  BNO05flag = true;
}
void updateRearDiff() {
  dataStruct.binaryValues[0] = !dataStruct.binaryValues[0];
}
void updateFrontLeftHalleffect() {
  dataStruct.binaryValues[1] = !dataStruct.binaryValues[1];
}
void updateFrontRightHalleffect() {
  dataStruct.binaryValues[2] = !dataStruct.binaryValues[2];
}
void changeRecordingState() {
    noInterrupts();
    if(isRecording == true) {
      while(digitalRead(7) == 1) {
        delay(10);
      }
      outputFile.close();
      digitalWrite(9, LOW); //turn off red LED
      Serial.println("Data Recording Stopped");
      isRecording = false;
    }
    else {
      while(digitalRead(7) == 1) {
        delay(10);
      }
      String time =  String(year()) + "-" + String(month()) + "-" + String(day()) + " " + String(hour()) + "_" + String(minute()) + "_" + String(second());
      Serial.println(time.c_str());
      //turn on red LED
      digitalWrite(9, HIGH);
      outputFile = SD.open(time.c_str(),  FILE_WRITE);
      isRecording = true;
    }
    lastSaveTimeInMillis = millis();
    interrupts();    
}
void readAnalogValues() {
  switch (currentAnalogSensor) {
      case 0:
        dataStruct.analogValues[0] =  ads.getLastConversionResults();
        currentAnalogSensor = 1;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_1, false);
        break;
      case 1:
        dataStruct.analogValues[1] =  ads.getLastConversionResults();
        currentAnalogSensor = 2;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_2, false);
        break;
      case 2:
        dataStruct.analogValues[2] =  ads.getLastConversionResults();
        currentAnalogSensor = 3;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_3, false);
        break;
      case 3:
        dataStruct.analogValues[3] =  ads.getLastConversionResults();
        currentAnalogSensor = 0;
        ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, false);
        break;
  }
}

And here's the python code snippet I got chatgpt to spit out to convert the .bin files to the .txt format I was using before.

import struct

# Define the format for the binary data (including padding to match Arduino's structure size)
data_format = "Q L 3i 4i 3f 3f 3f 4f 4i 4x"  # Add 4 bytes of padding (x4)
data_size = struct.calcsize(data_format)

# Main function
def main():
    input_file_name = "2025-1-19 22_17_35.bin"
    output_file_name = "output.txt"

    try:
        with open(input_file_name, "rb") as binary_file, open(output_file_name, "w") as text_file:

            # Read and process binary data
            while chunk := binary_file.read(data_size):
                if len(chunk) != data_size:
                    print("Incomplete data chunk encountered. Skipping.")
                    continue

                # Unpack the binary data
                data = struct.unpack(data_format, chunk)

                # Write the unpacked data as a comma-separated line
                text_file.write(",".join(map(str, data)) + "\n")

        print(f"Data successfully written to {output_file_name}")

    except FileNotFoundError:
        print(f"File not found: {input_file_name}")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    main()

The changeover to .bin has approximately doubled the sampling rate I'm able to achieve with this setup. Thanks everyone!

sizeof() is resolved compile time,
you should not be able to solve the difference.

From maintenance point of view sizeof() works even if you change the struct contents.

To separate structs you could add a field int size in the struct as first byte
Another way is to define separator fields you could search for.

How is the performance?
Did it improve?

Gotcha, I moved sizeof() back into the loop. As for performance it's wayyyyyy better! Close to 2x sampling rate. And the python program manages to recompile the data just fine without a separator.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.