Issues with saving images and sensor data onto an SD card simultaneously

To put it into a little bit of context, I'm building a mini satellite which will be dropped from ~1.5km altitude, that measures atmospheric, rotation and location data. I also have a camera because during the fall, it will take images of the ground, which I will then post-process using python scripts. By stopping writing sensor data while the image is being saved, I'm losing ~3s of data, which means approximately 1/4th of the data that could be recorded.

My board is an ESP-Wroom-32, and I'm using an Arducam Mega 5mp, an SD card, and a bunch of sensors (TMP117, DPS310, SAM-M10Q, SGP30, BNO085, BME680, all of which are wired through I2C). I have written a program that measures with the sensors as fast as it can, and saves each measurement onto the SD card into a csv file along with a timestamp (This is the code snippet below). Every 5 seconds, I also want to take a picture and save it to the SD card.

Even if I have the camera and the SD card on different SPI buses it seems that it cannot save the image and sensor data simultaneously, so while it's saving the image I have thought of saving the sensor data into the ESP's buffer until the image is saved and then clear it, but after weeks of trying to make it work I just couldn't.
Are there any better options? Should I just give up on the idea of writing into separate files simultaneously? Any comments are appreciated.

#include <Wire.h>
#include <SdFat.h>
#include <Adafruit_TMP117.h>
#include <Adafruit_DPS310.h>
#include <Adafruit_SGP30.h>
#include <Adafruit_BME680.h>
#include <Adafruit_BNO08x.h>
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
#include <sh2.h>

// SD Card Configuration
#define SD_CS 15
#define SD_SCK 14
#define SD_MISO 25
#define SD_MOSI 13
SdFat sd;
SdFile dataFile;
SPIClass spiSD(HSPI);

// Sensor Objects
Adafruit_TMP117 tmp117;
Adafruit_DPS310 dps310;
Adafruit_Sensor *dps_pressure;
Adafruit_SGP30 sgp30;
Adafruit_BME680 bme680;
Adafruit_BNO08x bno08x;
SFE_UBLOX_GNSS gps;

// CSV filename
const char* filename = "sensor_data1.csv";

void setup() {
    Serial.begin(115200);
    Wire.begin(21, 22);
    spiSD.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);

    // Initialize SD Card
    if (!sd.begin(SdSpiConfig(SD_CS, SHARED_SPI, SD_SCK_MHZ(10), &spiSD))) {
        Serial.println("SD Card initialization failed!");
        while (1);
    }

    // Write CSV Header if file does not exist
    if (!sd.exists(filename)) {
        dataFile.open(filename, O_RDWR | O_CREAT | O_APPEND);
        if (dataFile) {
            dataFile.println("Timestamp,TMP117_C,DPS310_C,DPS310_hPa,BME680_C,BME680_hPa,BME680_H%,BME680_VOC,SGP30_TVOC_ppb,SGP30_eCO2_ppm,BNO_QuatW,BNO_QuatX,BNO_QuatY,BNO_QuatZ,BNO_AccX,BNO_AccY,BNO_AccZ,BNO_GyroX,BNO_GyroY,BNO_GyroZ,BNO_MagX,BNO_MagY,BNO_MagZ,Latitude,Longitude,Altitude");
            dataFile.close();
        }
    }

    // Initialize Sensors
    if (!tmp117.begin()) Serial.println("TMP117 not found!");
    if (!dps310.begin_I2C(0x77)) Serial.println("DPS310 not found!");
    else dps_pressure = dps310.getPressureSensor();
    if (!bme680.begin(0x76)) Serial.println("BME680 not found!");
    if (!sgp30.begin()) Serial.println("SGP30 not found!");
    if (!bno08x.begin_I2C(0x4A)) Serial.println("BNO08x not found!");
    else {
        bno08x.enableReport(SH2_GAME_ROTATION_VECTOR);
        bno08x.enableReport(SH2_ACCELEROMETER);
        bno08x.enableReport(SH2_GYROSCOPE_CALIBRATED);
        bno08x.enableReport(SH2_MAGNETIC_FIELD_CALIBRATED);
    }

    gps.begin();
}

void loop() {
    sensors_event_t temp_event;
    tmp117.getEvent(&temp_event);
    float tempTMP = temp_event.temperature;

    sensors_event_t pressure_event, temp_event_dps;
    float pressure = -1.0, tempDPS = -1.0;
    if (dps310.temperatureAvailable() && dps310.pressureAvailable()) {
        dps310.getEvents(&temp_event_dps, &pressure_event);
        tempDPS = temp_event_dps.temperature;
        pressure = pressure_event.pressure;
    }

    float tempBME = -1, presBME = -1, humBME = -1, vocBME = -1;
    if (bme680.performReading()) {
        tempBME = bme680.temperature;
        presBME = bme680.pressure / 100.0;
        humBME = bme680.humidity;
        vocBME = bme680.gas_resistance / 1000.0;
    }

    sgp30.IAQmeasure();
    uint16_t tvoc = sgp30.TVOC;
    uint16_t eco2 = sgp30.eCO2;

    float quatW = 0, quatX = 0, quatY = 0, quatZ = 0;
    float accX = 0, accY = 0, accZ = 0;
    float gyroX = 0, gyroY = 0, gyroZ = 0;
    float magX = 0, magY = 0, magZ = 0;
    sh2_SensorValue_t sensorValue;
    while (bno08x.getSensorEvent(&sensorValue)) {
        if (sensorValue.sensorId == SH2_GAME_ROTATION_VECTOR) {
            quatW = sensorValue.un.gameRotationVector.real;
            quatX = sensorValue.un.gameRotationVector.i;
            quatY = sensorValue.un.gameRotationVector.j;
            quatZ = sensorValue.un.gameRotationVector.k;
        } else if (sensorValue.sensorId == SH2_ACCELEROMETER) {
            accX = sensorValue.un.accelerometer.x;
            accY = sensorValue.un.accelerometer.y;
            accZ = sensorValue.un.accelerometer.z;
        } else if (sensorValue.sensorId == SH2_GYROSCOPE_CALIBRATED) {
            gyroX = sensorValue.un.gyroscope.x;
            gyroY = sensorValue.un.gyroscope.y;
            gyroZ = sensorValue.un.gyroscope.z;
        } else if (sensorValue.sensorId == SH2_MAGNETIC_FIELD_CALIBRATED) {
            magX = sensorValue.un.magneticField.x;
            magY = sensorValue.un.magneticField.y;
            magZ = sensorValue.un.magneticField.z;
        }
    }

    // Read GPS Data
    double latitude = 0.0, longitude = 0.0, altitude = 0.0;
    String gpsData = "No GPS fix"; // Default message if no fix

    if (gps.getFixType() >= 3) {
        latitude = gps.getLatitude() / 10000000.0;
        longitude = gps.getLongitude() / 10000000.0;
        altitude = gps.getAltitude() / 1000.0;
        gpsData = "GPS fix acquired";
    }

    // Save to SD Card
    dataFile.open(filename, O_WRITE | O_APPEND);
    if (dataFile) {
        dataFile.printf("%lu,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.6f,%.6f,%.2f,%s\n",
                        millis(), tempTMP, tempDPS, pressure, tempBME, presBME, humBME, vocBME, tvoc, eco2,
                        quatW, quatX, quatY, quatZ, accX, accY, accZ, gyroX, gyroY, gyroZ, magX, magY, magZ,
                        latitude, longitude, altitude, gpsData.c_str());
        dataFile.flush();
        dataFile.close();
        Serial.println("Data saved to SD");
    } else {
        Serial.println("Error opening file for writing!");
    }
}

Since that is a reasonable approach (except for trying to use two SPI buses to write to one SD card), describe in detail what goes wrong and forum members may be able to help.

Should I just give up on the idea of writing into separate files simultaneously?

With a single process, nothing happens "simultaneously". All actions are sequential.

By the way, opening a file, writing a few bytes of data, and closing it again is a common beginner mistake that vastly increases the power consumption, the time it takes to write the data and the SD card error rate.

How can I safely close the file in case the program unexpectedly stops running though?

Accidents happen, and data are often lost. Life has certain risks.

Well no, the SD-card can only do one thing at a time.
As long as you are not using the 2nd core, you are probably not able to do more than 1 thing at a time, which probably means that you are not gathering data while you are writing to the SD card.

I think the SD card can run a bit quicker than that.

You are never doing that simultaneously.

I don't see the transfer from the camera to the SD card, but i don't think that should take 3s

On a single core machine, i would say, chop the image into sections to be saved, but the camera probably holding the process flow back. Ideally 1 core will read-write from camera to SD-card, and i would suggest to use SPIFFS to store the sensor data (it uses a different SPI bus again) and let another core take care of that. I have no experience with the multi-core usage, but the ESP32 does have 2 cores.

There's an example sketch in your SdFat library called LowLatencyLogger.ino. The sketch sets up multiple 128MB (I think) files in advance, their data sections consisting of consecutive sectors on the card. During operation, the sketch simply writes the logged data directly to the card sectors, successively, but since the file has already been defined, there's no need to open or close anything, or update the directory entry, or update the FAT, or really anything else. Then when the file is closed at the end of the run, all those file system entries are updated to reflect what was actually stored. And if something prevents that, all the data is still on the card, in a file. The only thing that's wrong is that the file isn't actually 128MB in size. But a simple hex editor can help you find the actual end of the file. This method has the added advantage of really speeding up writing to the card.

Another thought. Devin Darrah is posting new videos again, and in a recent one:

https://youtu.be/KUvkIH0voLw?si=gDuvdO2fNlygNxjc&t=820

he shows a new design with I believe an ESP32-S3 which uses a full speed 4-bit data connection to the SD card. I didn't think this was possible for hobbyists since that method is a State secret, but apparently it can be done, and I think he provides the software too.

And finally, for something special like this, I wonder if it might make sense to add a second card, so you could have completely separate streams for both data types, and if the dual cores of the ESP32 still can't handle it, maybe even a separate processor for the second card. You could even do video. In fact, you could use one of those 1080P keyfob cameras with its own SD card and battery built in.

https://www.ebay.com/itm/156558864125?_skw=mobius+32s

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