SD-карта: папки, пути, листинг на Arduino
SD-карта на Arduino поддерживает файловую систему FAT16 или FAT32, и в ней есть полноценные папки. Метод SD.mkdir создаёт директорию, SD.open принимает пути с разделителем, а File.openNextFile перечисляет содержимое папки. Для логгера или файловой системы это ровно тот набор, который нужен.
В симуляторе карта виртуальная, но папки в ней работают по-настоящему: mkdir действительно создаёт директорию, openNextFile перечисляет файлы и подпапки, а remove и rmdir удаляют содержимое. Это удобно, чтобы отладить логику структуры каталогов до того, как вы вставите карту в модуль.
Откройте симулятор, выберите пример «SD: Логи в папке + листинг». Код создаст папку /logs, запишет в неё файлы и перечислит их через openNextFile.
Пути и нормализация
Библиотека SD принимает пути обычными строками, и разделителем служит прямой слэш. Имена можно писать как со слэшем в начале, так и без него: SD.open("/logs/2024.txt") и SD.open("logs/2024.txt") указывают на один и тот же файл. В симуляторе этим занимается функция normPath, которая приводит путь к единому виду с ведущим слэшем и убирает повторные разделители.
Грабль: путь без ведущего слэша может быть интерпретирован как файл в корне, а не как папка. Поэтому в рабочем коде лучше всегда писать абсолютные пути вида /logs/2024.txt. И ещё: если вы создаёте вложенную структуру, родительская папка должна существовать заранее. Один вызов mkdir для пути /logs/2024/jan не создаст промежуточные каталоги, их нужно создавать по одному.
Про формат имён. FAT16 поддерживает только формат 8.3: восемь символов имени и три символа расширения, только заглавные буквы, без пробелов. Это ограничение самой файловой системы, а не библиотеки: имя temperature_log.csv физически не поместится в FAT16, и библиотека его обрежет или откажется создавать файл. FAT32 ограничений на длину почти не имеет, там имена до 255 символов, и современная библиотека SD их поддерживает. Практический вывод: если планируете длинные имена, берите карту SDHC от 4 ГБ, она форматируется в FAT32.
Листинг папки через openNextFile
#include <SD.h>File root;File dataFile;void setup() { Serial.begin(9600); SD.begin(4); SD.mkdir("/logs"); dataFile = SD.open("/logs/2024.txt", 1); dataFile.println("t=0 temp=23"); dataFile.close(); Serial.println("Файлы в /logs:"); root = SD.open("/logs"); while (true) { File entry = root.openNextFile(); if (!entry) break; Serial.print(" "); Serial.print(entry.name()); if (entry.isDirectory()) Serial.print("/"); else { Serial.print(" ("); Serial.print(entry.size()); Serial.print("b)"); } Serial.println(); entry.close(); } root.close();}void loop() {}Сначала файл открывается на запись флагом FILE_WRITE, который в библиотеке равен 1. Метод println дописывает строку в конец, а close закрывает файл и сбрасывает буфер. Важно не забывать close: пока файл открыт, часть данных может не попасть на карту, и при выключении питания файл останется неполным.
Потом открывается сама папка, и из неё вытягиваются элементы по одному. Метод openNextFile возвращает следующий файл или подпапку, а когда элементы кончились, возвращает пустое значение, и цикл заканчивается. Для каждого элемента доступны name() с именем, size() с размером в байтах для файлов и isDirectory(), который отвечает на вопрос, папка это или файл. У папок в выводе принято добавлять слэш, чтобы её было видно глазом.
После цикла каталог закрывается методом close. Если нужно перечитать содержимое папки заново, вызовите rewindDirectory: он сбрасывает указатель на начало, и следующий openNextFile вернёт первый элемент, а не продолжит с последнего. Это же полезно для функции, которая дважды проходит по файлам: сначала считает, потом обрабатывает.
Подробнее про SD.begin и open — в лендинге симулятора SD.
| Метод | Назначение | В симуляторе |
|---|---|---|
| SD.mkdir(path) | Создать папку | ✓ добавляет в dirs |
| SD.open(path, mode) | Открыть файл/папку | ✓ isDir если путь в dirs |
| SD.exists(path) | Проверить файл/папку | ✓ проверяет files + dirs |
| SD.remove/rmdir(path) | Удалить (рекурсивно) | ✓ удаляет содержимое |
| File.openNextFile() | Следующий элемент в папке | ✓ возвращает File или 0 |
| File.isDirectory() | Папка или файл | ✓ проверяет dirs |
| File.rewindDirectory() | Сброс указателя | ✓ nextIdx = 0 |
Что сначала не получилось
Первый листинг показывал только файлы, а папки не появлялись. Я полчаса проверял, создан ли каталог, и в итоге обнаружил, что в цикле я вызывал openNextFile на объекте файла, а не на объекте папки. Родительский объект нужно открыть как SD.open("/logs"), и только у него есть метод openNextFile.
Второй затык был с вложенностью. Я создал папку /logs/2024 одним вызовом mkdir, и ничего не получилось: родительский каталог /logs в этот момент ещё не существовал. Пришлось создавать каталоги по одному, от корня вниз. Отдельно я заметил, что openNextFile показывает только прямых потомков, поэтому для обхода дерева нужна рекурсия.
Личный опыт: логгер с разбором по дням
Первое устройство на SD было логгером: Arduino собирает показания датчиков раз в минуту и складывает их в файлы, разделённые по дням. Структура получается такая: в корне папка data, внутри неё папка с датой в имени, а внутри уже файлы с измерениями за час. Такой расклад удобен тем, что старые дни можно удалить одним вызовом rmdir, не перебирая файлы.
Проверял я это в симуляторе: создавал папки, записывал в них по три файла, прогонял openNextFile и сверял количество элементов. Первый вариант показывал 0 элементов, потому что я забывал вызвать rewindDirectory перед вторым проходом, и указатель оставался в конце списка. После исправления счётчик элементов совпадал с ожидаемым. На настоящей карте добавил проверку: перед записью в файл вызываю SD.exists, и если папки за текущий день ещё нет, создаю её. И отдельно проверяю результат mkdir, потому что создание уже существующей папки возвращает false, и это нормальная ситуация, а не ошибка.
Как организовать логи на карте
Раскладка папок определяется тем, как вы потом будете эти данные читать. Для логгера с датчиков удобно разделение по дате: в корне папка data, в ней каталоги вида 2024-06, а внутри файлы по часам. Плюс такой схемы в том, что месяц удаляется одним вызовом rmdir, а не перебором файлов, и карта не распухает.
- Имя файла содержит дату и время, чтобы сортировка шла в правильном порядке
- Один файл на час или на смену, а не один файл на весь день
- В начале каждой записи пишите заголовок с датой и единицами измерения
- Держите разделитель строки в конце каждой записи, иначе строки слипнутся
- Старые каталоги удаляйте по расписанию, иначе карта забьётся
Отдельно про единицы. Записывайте в файл ровно то, что выводите в Serial, с указанием единиц измерения. Через месяц вы будете разбирать этот файл, и потерянная единица превращает столбцы чисел в бессмыслицу. Тот же совет касается и времени: метка времени в начале записи избавляет от необходимости сверяться с часами компьютера.
Авторская позиция: что взять новичку
Берите модуль SPI, а не UART: у SPI заметно выше скорость, и при записи логгеров разница чувствуется. Карту берите SDHC от 4 ГБ, чтобы не упереться в ограничение FAT16 и не возиться с именами в формате 8.3. И не пытайтесь сразу строить сложную иерархию: сначала один каталог и один файл, потом листинг, и только когда это заработает, добавляйте вложенность. Всё это целиком проверяется в симуляторе, где папки создаются мгновенно и ничего не стоит.