К осени в корне карты логгера теплицы накопилось 240 файлов. Лог за 14 марта я искал прокруткой минут четыре — и это на ноутбуке, где есть сортировка. А теперь представьте тот же поиск через Serial: список из 240 строк бежит в монитор, глаза скользят по именам, и нужная дата проскакивает между двумя показами. Именно после той прокрутки я переехал на папки: лог за каждый день живёт в своей директории, и поиск — это один заход в /logs/d240314 вместо ленты из пары сотен имён.
Папки на SD заводятся одной командой SD.mkdir, а пишутся в них обычным SD.open с путём. Хорошая новость в том, что большая часть логики уже у вас в руках: папка открывается и читается теми же функциями, что и файл, из уроков про оглавление и режимы. Разберём, как mkdir создаёт вложенность за раз, как собрать архив измерений, который не стыдно открыть через полгода, и где у FAT проходят границы: корень, глубина, имена. В конце — модель архива в симуляторе, где дни сменяются прямо в Serial.
mkdir и путь в open
SD.mkdir("/logs") создаёт папку в корне. Сюрприз для тех, кто привык к mkdir в консоли: библиотека создаёт и промежуточные каталоги — SD.mkdir("/logs/d240314") на пустой карте заведёт обе папки одной строкой, весь путь целиком. Пишется в папку через путь в open: SD.open("/logs/d240314/log.csv", FILE_WRITE) — и дальше всё как обычно, print и close из урока про запись. SD.exists работает и для папок: перед созданием дневной директории спрашивать не обязательно, mkdir на существующей папке вернёт false без последствий, — но проверка помогает вести счётчик дней.
Главная грабля: открытие файла в несуществующей папке молча возвращает «ложь». SD.open("/logs/d240315/log.csv", FILE_WRITE) при отсутствии d240315 не создаст ни папку, ни файл — просто false, и скетч пропустит запись. Порядок всегда один: mkdir раньше open. Для шапки CSV в новом файле помните правило из урока про CSV и конфиг: печатаем её, только если файла ещё не было.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;unsigned long startedAt = 0;unsigned long lastLog = 0;unsigned long lastDay = 0;int dayNum = 0; String denSD(int n) { // имя папки дня: d000, d001, d002... String s = "d"; if (n < 10) { s = s + "00"; } else if (n < 100) { s = s + "0"; } return s + n;} void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } startedAt = millis(); SD.mkdir("/logs"); // корень архива SD.mkdir("/cfg");} void loop() { // условный «день» для отладки на столе: минута аптайма if (millis() - lastDay >= 60000UL) { lastDay = millis(); dayNum = dayNum + 1; String dir = "/logs/" + denSD(dayNum); bool ok = SD.mkdir(dir); // создаст и папку дня за один вызов Serial.print("mkdir dir="); Serial.print(dir); Serial.print(" ok="); Serial.println(ok); } if (millis() - lastLog >= 2000) { lastLog = millis(); String path = "/logs/" + denSD(dayNum) + "/log.csv"; File f = SD.open(path, FILE_WRITE); if (f) { unsigned long sec = (millis() - startedAt) / 1000UL; f.print(sec); f.print(","); f.println(analogRead(A0)); f.close(); } else { Serial.println("open false: papka dnya eshe ne sozdana?"); } }}Разбор по цифрам. Папка на карте — та же 32-байтовая запись каталога, что и файл, так что директории почти ничего не стоят. В скетче день назван denSD: имя папки растёт до трёх цифр (d000, d007, d031), и за счёт нулей слева список дней сортируется компьютером честно — без нулей d10 встал бы между d1 и d2. Условный день из минуты нужен только для отладки на столе; в реальном устройстве дату даёт либо RTC, либо счётчик аптайма — про каркас таймеров без delay напомню ссылкой на урок про millis вместо delay.
Читается такой архив тем же openNextFile из предыдущих уроков, только сначала для корня, потом для папки дня: открыли /logs, выбрали нужный день по имени, открыли его, внутри читаете log.csv FILE_READ-циклом. Учтите, что в списке из папок размеры уже не помогут: у директории size() вернёт ноль, и выбирать день придётся по имени, а не по весу. Пара практических мелочей: имя текущей папки держите в String глобально — собирать путь на каждую запись из кусков дороже, чем один раз при смене дня; и печатайте путь в Serial при каждой смене дня — через неделю жизни устройства эти строки скажут вам, сколько дней оно честно отработало, без всяких часов.
Глубину держите на двух уровнях: /logs/d240314 — мой потолок. Имена звеньев всё равно 8.3, буфер под путь в библиотеке не резиновый, а третий уровень ни разу не окупался: за ним не скрыто ничего, кроме шансов опечататься в пути и искать потерянный файл двумя вложениями дольше. Это мнение с границей: если у вас три прибора пишут на одну карту, четвёртый уровень типа /well/d240314/raw оправдан — но тогда у каждого прибора и префикс свой, о префиксах шла речь в предыдущем уроке про remove.
| Путь | Что лежит | Зачем |
|---|---|---|
| /cfg/config.txt | пороги и интервалы | правится блокнотом без перепрошивки |
| /logs/d240314/log.csv | сегодняшние измерения | пишется скетчем, одна папка на день |
| /logs/d240313/log.csv | вчерашний день | чистится по ротации: снёс папку — и всё |
| /bak/run.old | бэкап прошлого запуска | rename перед стартом, откат одним вызовом |
Лимиты FAT: корень, глубина, имена
Почему архив начинается с папки, а не лежит плоско в корне: на маленьких картах в FAT16 корень — таблица фиксированного размера, вмещающая максимум 512 записей, и каждая папка в корне съедает одну из них наравне с файлами. Двести сорок логов из моей сцены — это половина корня; логи по файлу на день добили бы таблицу за год с хвостом, и карта отказалась бы принимать новые имена при пустом на вид месте. Подпапки от этого свободны: каталог /logs растёт, сколько ему дадут сектора, поэтому всё, что множится, живёт внутри папок, а в корне — только фиксированный набор: cfg, logs, bak.
8.3 действует и на папки: /logs — хорошо, /Данные за 2024 — папка, которую Arduino увидит кривым псевдонимом из тильды, а создать из скетча её и вовсе не получится. Про псевдонимы вида OTCH~1.TXT и про то, как openNextFile показывал их в списке, — урок про оглавление. На картах в FAT32 корень устроен мягче — он растёт, как обычная папка, — но чистый корень я держу и там: при переносе карты на другую платформу или в кардридер старые карты с фиксированным корнем встречаются чаще, чем кажется, а привычка стоит ноль. Формат самой карты, на которой всё это живёт, мы разбирали в уроке про FAT16 и FAT32.
Последний вопрос — стоит ли заводить папки вообще. Моя граница: до сотни тысяч строк на карту один log.csv окупается, меньше движений по каталогу и проще читать; папки начинаются тогда, когда либо есть ротация по дням, либо на одной карте пишут несколько приборов, либо архив должен открываться без сортировки. Прикинем объём: строка 12 байт раз в две секунды — это около 43 тысяч строк и полмегабайта в сутки, то есть карта 4 ГБ по объёму живёт годами, и папки здесь добавляют порядок, а не экономят место. Папки окупаются по-настоящему, когда пишут двое: датчик колодца в /well и термометр теплицы в /greenhouse — перепутать логи уже нельзя, а чистка одного прибора не трогает второй. Метки времени для имён папок с настоящими часами — готовая схема в логгере со штампами времени, с часами DS3231 даты будут точными даже после месячного простоя.
Три урока этого хвоста складываются в полный цикл самообслуживания карты: список файлов — что есть, remove и rename — что с этим делать, mkdir — куда складывать новое. Соберите три скетча в один блок самообслуживания: печать оглавления по кнопке, ротация по префиксу, дневные папки для записи — и карта перестанет быть чёрным ящиком, который вскрывают с отвёрткой. Осталось последнее: убедиться, что логика имён и папок, которую вы построили, действительно работает — поэтому в симуляторе ниже архив живёт моделью, где дни сменяются прямо в Serial, а пути печатаются на каждый mkdir и каждую запись.
- 1. Что сделает SD.mkdir("/logs/d240314"), если /logs ещё не существует?
- 2. Вызвали SD.open("/logs/d240315/log.csv", FILE_WRITE) до создания папки. Что будет?
- 3. Почему логи по дням не держат плоско в корне карты FAT16?
- 4. Какая глубина вложенности — разумный потолок для архива логгера на Uno?
- 5. Что сделает SD.rmdir("/logs"), если в ней лежат дневные папки с логами?
- SD.mkdir создаёт весь путь за раз: промежуточные папки достраиваются сами — вызывайте mkdir раньше open
- Файл в несуществующей папке не создаётся: open молча вернёт «ложь», и скетч потеряет запись
- Папка — та же 32-байтовая запись каталога; корень FAT16 — фиксированная таблица на 512 записей, в корне держите только cfg, logs, bak
- Глубина — два уровня: имена звеньев 8.3, буфер пути ограничен, третий уровень оправдан только для нескольких приборов на карте
- rmdir берёт только пустую папку: чистка идёт в два шага, и случайный снос всего архива из скетча невозможен
Частые вопросы по теме урока
- Что сделает SD.mkdir("/logs/d240314"), если /logs ещё не существует?
- Библиотека создаёт весь путь за раз: недостающие промежуточные каталоги появляются вместе с конечным. Всё же mkdir в setup для корня архива я оставляю явно. Создаст обе папки — промежуточные каталоги достраиваются сами
- Вызвали SD.open("/logs/d240315/log.csv", FILE_WRITE) до создания папки. Что будет?
- Файл в несуществующей папке не создаётся: open молча вернёт false. Порядок всегда один: mkdir раньше open, и проверяйте результат open. Open вернёт «ложь» молча — ни папка, ни файл не создадутся
- Почему логи по дням не держат плоско в корне карты FAT16?
- У FAT16 корневой каталог имеет фиксированный объём, записи кончаются раньше свободных секторов. Подпапки растут свободно — множащееся живёт внутри них. Корень — таблица фиксированного размера: максимум 512 записей
- Какая глубина вложенности — разумный потолок для архива логгера на Uno?
- Каждое звено — имя 8.3, весь путь живёт в ограниченном буфере, а навигация глубже двух уровней не окупается. Исключение — несколько приборов на одной карте. Два уровня: имена звеньев 8.3, буфер пути ограничен, третий не окупается
- Что сделает SD.rmdir("/logs"), если в ней лежат дневные папки с логами?
- rmdir берёт только пустую папку. Сначала — удаление файлов по списку, потом сама папка. Двухшаговая чистка защищает архив от случайного сноса. Вернёт false: папка не пуста