Логгер на насосной станции отработал на объекте три месяца, и в выходной я доехал до него с одной целью — понять, что он накопил. Карту вынул, вставил в ноутбук: в корне 94 файла — по одному на каждый запуск скетча, потому что в ранних версиях я клал в имя номер запуска. Лог за нужную неделю нашёл минут через десять: прокрутка, сортировка по дате, ещё прокрутка. И тут дошло: на объекте-то ноутбука нет. Если бы скетч умел печатать оглавление карты в Serial, я бы увидел все 94 имени с размерами за пять секунд — хоть через терминал на телефоне с OTG-переходником.
SD.open() из первого урока раздела открывает файл по имени — но имя сначала надо узнать. Каталог, в котором живут эти имена, для библиотеки тоже файл: его можно открыть SD.open("/") и читать по одной записи за раз, как ленту.
План такой: как устроена запись каталога и что возвращает openNextFile(); скетч, который печатает оглавление корня с размерами и суммой; почему список не по алфавиту и почему обрывается на десятой строке; рекурсия по папкам — упрощённо; и ловушка 8.3-имён, на которую я наступил с картой, записанной на ноутбуке. В конце — модель оглавления в симуляторе.
Каталог — это тоже файл: по записи за раз
Одна запись каталога FAT занимает 32 байта: имя, атрибуты, даты, размер и номер первого кластера. В 512-байтовом секторе их шестнадцать. SD.open("/") возвращает дескриптор корневого каталога — обычный объект File, тот же тип, что и у открытого лога. У этого дескриптора есть метод dir.openNextFile(): каждый вызов отдаёт следующий элемент каталога, тоже как объект File. Записи кончились — вернётся «ложный» объект, который в if (f) даст false. Отсюда весь цикл: while (f) { сделать что-то; f.close(); f = dir.openNextFile(); } — идёте по ленте записей, пока она не кончится.
Каждый элемент — полноценный файловый дескриптор, поэтому у него работают знакомые методы: name() отдаёт имя без пути, size() — размер в байтах, isDirectory() — папка или файл (у папки size() вернёт ноль). Моё правило: close() в каждой итерации, без пропусков. Почему именно «без пропусков» — покажет note ниже, там история на полчаса поиска.
Две практические детали, которые экономят время. Первая: name() появился в библиотеке не сразу — на старых версиях IDE компилятор скажет, что у File нет такого метода; обновите библиотеку через менеджер, и метод появится. Вторая: оглавление — бесплатный тест записи. Открыли список после недели работы логгера, нашли свой log.csv, сверили size() с ожиданием — строка 12 байт при записи раз в две секунды даёт примерно полмегабайта в сутки, и цифра в каталоге либо сходится, либо вы узнали о проблеме до того, как она съела неделю данных.
#include <SPI.h>#include <SD.h> const int chipSelect = 10; void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } File dir = SD.open("/"); int files = 0; unsigned long total = 0; File f = dir.openNextFile(); while (f) { if (f.isDirectory()) { Serial.print("[DIR] name="); Serial.println(f.name()); } else { Serial.print("name="); Serial.print(f.name()); Serial.print(" bytes="); Serial.println(f.size()); files = files + 1; total = total + f.size(); } f.close(); // каждый элемент занимает дескриптор f = dir.openNextFile(); } dir.close(); Serial.print("files="); Serial.print(files); Serial.print(" total bytes="); Serial.println(total);} void loop() { }Первое, что бросается в глаза при запуске: имена идут не по алфавиту. FAT кладёт записи в каталог в порядке физического заполнения — это порядок создания, а после удаления освободившуюся дырку занимает следующий новый файл. Сортировать список в скетче некуда: на Uno 2 КБ оперативки, и массив из девяноста четырёх имён уже съел бы ощутимую долю вместе с накладными расходами String. Мой обход — искать не глазами, а префиксом: все мои логи носят имена вроде d240314.log, и цикл с проверкой name().startsWith("d2403") печатает только нужную неделю. Сортировку я оставил компьютеру — на тот случай, когда карта всё-таки доехала до него.
Итоговая сумма total — мой любимый вопрос к чужой карте. Карта 4 ГБ, где 94 файла дают total около 110 МБ, проживёт ещё годы; total под 3,9 ГБ — это «чистить прямо сейчас», и оглавление тут же превращается в очередь на удаление. Чем удалять из скетча и как не снести лишнего — тема следующего урока.
Второй сценарий использования списка — найти самого жирного жильца. Пробегаете каталог с условием в цикле: если size() больше текущего рекордсмена — запоминаете имя. На моей карте-рекордсмене однажды оказался не лог, а чужой файл снимков, забытый после теста камеры: один прогон списка — и лишние 300 МБ нашлись без картридера. Мнение с границей: если файлов больше пары сотен, простой перебор всё ещё работает, но каждая печать строки в Serial стоит миллисекунды при 9600 бод — сотня строк это пара секунд тишины в loop, так что на объекте запускайте оглавление по кнопке, а не в каждом обороте.
Чтение самих файлов по напечатанным именам — ровно тот FILE_READ-цикл, что разобран в уроке про режимы File. Если оглавление хочется красивым, с колонками и выравниванием, приёмы печати таблиц лежат в уроке про таблицу данных в Serial.
| Метод | Что делает | Когда нужен |
|---|---|---|
| dir.openNextFile() | следующий элемент каталога как объект File | цикл по оглавлению: while (f) { ... } |
| f.name() | имя без пути, как записано в каталоге | печатать список, искать по префиксу |
| f.size() | размер в байтах; у папки — ноль | найти самые жирные логи, посчитать сумму |
| f.isDirectory() | true для папки, false для файла | отделить папки и зайти в них рекурсией |
| dir.rewindDirectory() | перематывает каталог на первую запись | пройти список заново без переоткрытия |
Папки внутри папок и ловушка 8.3
Вложенные каталоги обходятся той же парой openNextFile() и isDirectory(): встретили папку — открыли её и вызвали ту же функцию. Функция, вызывающая сама себя, — рекурсия, и на Uno у неё жёсткая граница: стек всего 2 КБ. Глубина два-три уровня проходит спокойно, четвёртый уже на грани, глубже не лезет — и не надо. Уровень передаю аргументом depth и печатаю отступом, чтобы в Serial было видно, кто в какой папке лежит.
Мнение с границей: рекурсию на Uno я включаю только для отладки, на столе. В работающем устройстве она лишний риск: одна потерянная папка-петля с ссылкой на саму себя — и стек кончится раньше списка. Рекурсии там, где хватает плоского корня, я избегаю по той же причине, по которой не ставлю таймер без нужды: чем проще обход, тем меньше путей упасть в поле. Для архива с двумя уровнями из следующего урока хватает двух вложенных циклов без единого вызова функции из самой себя.
#include <SPI.h>#include <SD.h> const int chipSelect = 10; void printDir(File dir, int depth) { File f = dir.openNextFile(); while (f) { for (int i = 0; i < depth; i++) Serial.print(" "); if (f.isDirectory()) { Serial.print("[DIR] name="); Serial.println(f.name()); printDir(f, depth + 1); // внутрь вложенной папки } else { Serial.print("name="); Serial.print(f.name()); Serial.print(" bytes="); Serial.println(f.size()); } f.close(); f = dir.openNextFile(); }} void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } File root = SD.open("/"); printDir(root, 0); root.close();} void loop() { }Теперь грабли с именами, на которые я наступил, воткнув в логгер карту, начисто переписанную на ноутбуке. Библиотека SD понимает только короткие имена DOS: до восьми символов до точки, до трёх после, и всё верхним регистром. Длинные имена — надстройка над FAT: каждое живёт рядом со своим коротким псевдонимом и съедает несколько 32-байтовых записей каталога. Файл «отчёт за январь.txt», записанный с компьютера, Arduino в списке увидит псевдонимом вида OTCH~1.TXT — прочитать можно, угадать, что внутри, уже нет. Библиотека SdFat из менеджера библиотек длинные имена понимает, но у неё другой API, и код из этого раздела придётся переписывать.
Отсюда имена моих логов: d240314.log — восемь символов ровно, латиница. Библиотека при записи поднимет регистр, и в списке он выглядит как D240314.LOG — на поиск префиксом это не влияет, но загляните в свой список глазами до того, как строить на регистре логику. Расширение выбирайте по смыслу, но не длиннее трёх букв: .csv для таблиц, .txt для конфигов, .log для сырых журналов — на устройство оно не влияет, зато на компьютере файл открывается правильной программой по двойному клику. Если полезли в кластеры и хвосты, съедающие карту на множестве мелких файлов, — мы их считали в уроке про FAT16, FAT32 и размеры.
Список у вас теперь в кармане. Что с ним делать дальше — удалять лишнее и переименовывать ценное — предмет следующего урока про remove и rename. А в симуляторе ниже — модель оглавления: настоящей карты в нём нет, каталог задан массивом имён и размеров прямо в скетче, но порядок печати и итоговая сумма совпадают с тем, что печатает код выше.
- 1. Что вернёт dir.openNextFile(), когда записи каталога кончились?
- 2. Почему имена в оглавлении идут не по алфавиту?
- 3. Сколько байт занимает одна запись каталога FAT?
- 4. Оглавление обрывается ровно после десяти строк. Что забыли в цикле?
- 5. Файл «отчёт за январь.txt» записали на карту с компьютера. Что напечатает f.name()?
- dir.openNextFile() отдаёт записи каталога по одной как объекты File; кончились — «ложный» объект, while (f) завершается; name(), size(), isDirectory() — в помощь
- close() в каждой итерации обязателен: библиотека держит до 10 открытых дескрипторов, забытый close молча обрывает список на десятой строке
- Порядок записей — порядок создания, не алфавит; сортировку на Uno не вытянуть — ищите по префиксу имени вроде d2403
- Имена — только 8.3 верхним регистром: до 8 символов до точки, 3 после; длинные имена с компьютера видны псевдонимом вида OTCH~1.TXT
- Рекурсия по папкам — вызов той же функции для isDirectory(); глубина два-три уровня, стек Uno — 2 КБ
Частые вопросы по теме урока
- Что вернёт dir.openNextFile(), когда записи каталога кончились?
- Дескриптор каталога читается по записи за раз; записи кончились — while (f) завершится сам. Вернуть указатель к началу умеет rewindDirectory(). «Ложный» объект File, который в if даёт false
- Почему имена в оглавлении идут не по алфавиту?
- Записи лежат в каталоге в порядке появления; освободившиеся после удаления дырки занимают новые файлы. Сортировать на Uno нечем: 2 КБ RAM. FAT отдаёт записи в порядке физического заполнения — то есть в порядке создания
- Сколько байт занимает одна запись каталога FAT?
- 32 байта: имя, атрибуты, даты, размер и номер первого кластера. В 512-байтовом секторе их шестнадцать. 32 байта
- Оглавление обрывается ровно после десяти строк. Что забыли в цикле?
- Каждый элемент из openNextFile — открытый файл, а библиотека держит до десяти одновременно. Без close одиннадцатая итерация молча вернёт ложь. f.close() — лимит открытых дескрипторов исчерпан
- Файл «отчёт за январь.txt» записали на карту с компьютера. Что напечатает f.name()?
- SD.h понимает только имена 8.3; длинное имя живёт рядом как надстройка, а библиотека показывает его короткий DOS-псевдоним. Держите имена до восьми символов. Короткий псевдоним вида OTCH~1.TXT