Arduino

Модуль чтения SD-карт: запись данных на карту памяти

Введение

Модуль чтения SD-карт даёт Arduino-проекту возможность сохранять данные на съёмный носитель большого объёма — это принципиально отличается от внутренней EEPROM/Flash микроконтроллера (ограниченных по объёму до нескольких килобайт) и открывает возможность долговременного логирования показаний датчиков, аналогично логированию из STM32-цикла, но на платформе Arduino через SPI-интерфейс.

Концепция

Модуль SD-карты подключается через SPI (аналогично другим SPI-устройствам основного цикла) и используется через стандартную библиотеку SD.h, дающую файловую абстракцию (открытие, запись, чтение, закрытие файлов) поверх физической карты, отформатированной в FAT16/FAT32 — это значительно удобнее работы с сырыми блоками памяти напрямую, давая привычный, файловый API, аналогичный (хотя значительно более ограниченный) QFile из основного цикла статей про работу с файлами.

Пример кода

// Базовая запись показаний датчика в CSV-файл на SD-карте
#include <SPI.h>
#include <SD.h>

const int SD_CS_PIN = 4; // Chip Select пин — зависит от конкретной распайки модуля

void setup() {
    Serial.begin(9600);

    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("Ошибка инициализации SD-карты");
        return;
    }
    Serial.println("SD-карта инициализирована");

    // Запись заголовка CSV-файла при первом запуске (если файла ещё не существует)
    if (!SD.exists("log.csv")) {
        File dataFile = SD.open("log.csv", FILE_WRITE);
        if (dataFile) {
            dataFile.println("timestamp_ms,temperature,humidity");
            dataFile.close();
        }
    }
}

void logSensorReading(float temperature, float humidity) {
    File dataFile = SD.open("log.csv", FILE_WRITE); // FILE_WRITE добавляет в конец, не перезаписывает
    if (dataFile) {
        dataFile.print(millis());
        dataFile.print(",");
        dataFile.print(temperature);
        dataFile.print(",");
        dataFile.println(humidity);
        dataFile.close(); // ОБЯЗАТЕЛЬНОЕ закрытие — без него данные могут не записаться на карту физически
    } else {
        Serial.println("Ошибка открытия файла лога");
    }
}

void loop() {
    float temperature = 23.5; // в реальном проекте — показание реального датчика
    float humidity = 45.0;
    logSensorReading(temperature, humidity);
    delay(5000);
}
// Чтение ранее записанных данных — например, для отправки накопленного лога через UART
void dumpLogFile() {
    File dataFile = SD.open("log.csv");
    if (dataFile) {
        while (dataFile.available()) {
            Serial.write(dataFile.read());
        }
        dataFile.close();
    } else {
        Serial.println("Не удалось открыть файл для чтения");
    }
}

Пояснения к коду

SD.begin(SD_CS_PIN) инициализирует SPI-связь с модулем и монтирует файловую систему карты — параметр Chip Select специфичен для конкретной распайки используемого модуля и должен соответствовать реальному подключению. Явный, обязательный вызов dataFile.close() после каждой записи (а не только в конце всей программы) — критично важная деталь, отличающая работу с SD-картой от многих других операций Arduino: библиотека SD.h буферизует часть записываемых данных, и без явного закрытия файла часть данных может остаться неотправленной физически на карту (особенно опасно при внезапном отключении питания между записями, если файл остаётся открытым длительное время) — паттерн «открыть, записать, сразу закрыть» для каждой отдельной записи (как в примере) значительно надёжнее, чем держать файл открытым на всё время работы программы.

Подводные камни

  • Удержание файла открытым на длительное время между отдельными операциями записи (вместо паттерна «открыть-записать-закрыть» для каждой записи, как в примере) — при внезапной потере питания (отключение или разряд батареи проекта) во время длительного периода, пока файл остаётся открытым, накопленные, но ещё не сброшенные физически на карту данные могут быть потеряны, тогда как файл, закрытый сразу после каждой записи, минимизирует это окно потенциальной потери данных до одной единственной записи в худшем случае.
  • Использование длинных имён файлов или путей с глубокой вложенностью директорий, если используется устаревшая версия библиотеки SD.h, поддерживающая только короткие имена в формате FAT16 8.3 (восемь символов имени, три символа расширения) — попытка создать файл с более длинным именем на такой конфигурации может привести к неожиданному усечению имени или ошибке; для надёжности стоит использовать короткие, простые имена файлов (как log.csv в примере), если нет явной уверенности в поддержке длинных имён конкретной используемой версией библиотеки и форматом карты.
  • Неограниченный рост единственного лог-файла без какой-либо ротации или ограничения размера (в отличие от подходов с ротацией из spdlog-цикла, статья 336, или STM32-цикла) — для проекта, работающего длительное время непрерывно, единственный, постоянно растущий файл может со временем заполнить всю доступную ёмкость карты или стать неудобным для последующего анализа из-за чрезмерного размера; для долговременного логирования стоит реализовать собственную простую ротацию (например, создание нового файла с именем, включающим дату/номер, при достижении определённого размера или периода времени).
  • Использование некачественной или несовместимой SD-карты без проверки её реальной работоспособности с конкретным модулем и библиотекой — не все карты (особенно карты большой ёмкости, использующие более новые стандарты SDXC, или карты сомнительного происхождения) одинаково хорошо работают с относительно простыми, недорогими SD-модулями для Arduino и базовой библиотекой SD.h; при необъяснимых проблемах инициализации или записи стоит протестировать с другой, заведомо простой и качественной картой меньшей ёмкости (как правило, более старые стандарты SD/SDHC небольшой ёмкости дают наибольшую совместимость с такими простыми модулями).