У меня был логгер теплицы, который писал в log.csv полгода — четыреста тысяч строк, каждая с секундой от старта и температурой. Полгода истории в одном файле. Потом я переносил скетч на ESP32: код почти один в один, компиляция зелёная, а к вечеру в файле осталось девять строк текущего дня. Секрет сидел в одной константе: на классической библиотеке для Uno FILE_WRITE открывает существующий файл с курсором в конце и дописывает, а в библиотеке для ESP32 тот же FILE_WRITE усекает файл в ноль. Одна буква в описании режима — и полгода лога ушли в пустоту.
С тех пор любое место, где скетч открывает файл, обязано отвечать мне на два вопроса. Первый — существует ли файл, и это проверка SD.exists до открытия: шапка пишется один раз, свежие имена подбираются без коллизий. Второй — что произойдёт с существующим файлом при открытии: дописывание или очистка. Ответ на второй вопрос зависит не от здравого смысла, а от платформы, и таблица режимов ниже — мой чек-лист при переносе скетчей между Uno и ESP32. В том месте спокойнее держать в коде и комментарий: «я хочу дописать, а не переписать» — год спустя он сэкономит вечер.
И третья часть урока — ротация: лог, который растёт без ограничений, к четвёртому гигабайту упирается в предел FAT, а выгружать файл на четыреста мегабайт через Serial — значит занять ноутбук на вечер. Лог по частям — файл на один-два мегабайта, при превышении закрыли, переименовали, открыли новый — снимает обе беды разом. Циклическую перезапись, когда старый лог затирается новым, я разбирал в уроке про циклическую запись; здесь — вариант с сохранением истории: переименование по счётчику.
exists(): спросить карту, прежде чем писать
SD.exists("log.csv") отвечает одним битом: файл есть или нет. Вызов дешёвый — проход по каталогу без открытия дескриптора — и живёт у меня в трёх местах. Первое: шапка CSV. Писать sec,temp безусловно при каждом старте — гарантированные десять шапок после десяти перезагрузок; проверка exists отличает первый запуск от продолжения, и шапка встаёт ровно один раз. С путями exists тоже честен: exists("data/log.csv") проверит файл в папке, не открывая и не закрывая ничего ради одной проверки. Приём знакомый по уроку про CSV-лог, здесь я просто выношу его в обязательные.
Второе место — подбор свободного имени. Ротация требует архива с номерами: log001.csv, log002.csv — и наивный счётчик в RAM рано или поздно назовёт файл существующим именем: плата пережила перезагрузку, счётчик обнулился, а карта старые имена помнит. Цикл с условием while (SD.exists(name)) и инкрементом номера отдаёт имя, которого на карте точно нет, — коллизий нет по построению. Третье место — стартовый номер архива: exists подсказывает, первый это запуск или продолжение, и от этого зависит, с какой цифры вести нумерацию.
Один нюанс честности: между exists и open на плате проходит микросекунда, и многозадачности на Uno нет — никто файл в этот промежуток не перехватит, проверка честная. И оговорка про имена: файл с длинным именем, положенный на карту компьютером, exists по оригиналу не найдёт — искать придётся коротким двойником, о котором подробно говорил урок про имена 8.3 и лимит корня.
| Режим | Платформа | Существующий файл | Когда брать |
|---|---|---|---|
| FILE_WRITE | Uno, классическая SD.h | курсор в конец — дозапись | дефолт для логов на AVR |
| FILE_WRITE | ESP32 | файл усекается в ноль | осознанная перезапись, и только |
| FILE_APPEND | ESP32 | курсор в конец — дозапись | перенос логгера с Uno без потерь |
| FILE_READ | обе | курсор в начало, записи нет | чтение конфига и выгрузка лога |
Перезапись без сюрпризов: remove и свежий файл
Обратная задача — начать файл заново, затерев прошлое, — на классической библиотеке решается в два шага: SD.remove("log.csv"), затем open с FILE_WRITE — файл создастся пустым. Методов усечения в духе truncate у SD.h нет, и это не беда, а ясность: либо дописываешь в конец, либо стираешь и создаёшь заново — середины нет, и код читается однозначно. seek(0) вместо этого не годится: курсор встанет в начало, а старые байты останутся дальше по файлу, и лог превратится в слоёный пирог. Порядок действий при перезапуске лога проверен до тривиальности: remove вернул true — open создаст новый файл; remove вернул false — файла и не было, open создаст его так же. Обе ветки сходятся в одну точку, отдельно разбирать результат remove не нужно.
Механику режимов с позициями курсора и разбором, куда встаёт указатель при каждом способе открытия, я оставляю уроку про File и режимы открытия — там же таблица поведения при первом и повторном открытии. А за обязательность close() при дозаписи — буфер на 512 байт дожимается на карту именно им — отвечает урок про close и потерю строк: без close дозапись существует только в вашем воображении, карта о ней не знает.
Ротация по размеру: закрыл, переименовал, начал новый
Механика ротации помещается в четыре действия после каждой записи: смотрю f.size() — дорос ли файл до лимита; если да — close, подбираю свободное имя архива, SD.rename("log.csv", имя), открываю новый log.csv и пишу в него шапку. Лимит у меня обычно один-два мегабайта: такой файл выгружается через Serial за минуты и открывается в табличном редакторе без задумчивости. Проверку размера я делаю на открытом дескрипторе, а не отдельным открытием ради размера — дешевле на один проход по каталогу. Параллельно размер — ранний индикатор здоровья: файл, который за сутки не подрос ни на байт при живом периоде записи, подсказывает, что open начал возвращать false, — и это повод глянуть в монитор раньше, чем лог молча закончится.
Номер архива ведётся с ведущими нулями — log001.csv, а не log1.csv: без нулей сортировка в проводнике ставит log10.csv сразу после log1.csv, и хронология в списке ломается. Форматирование номера — три строки кода один раз навсегда. Имя архива подбирается циклом с exists: счётчик в RAM может обнулиться от перезагрузки, а карта помнит старые имена, — цикл перешагивает занятые номера, и дублей не возникает. Правда, цикл не бесконечен: на тысячах архивов поиск свободного имени замедляется с каждым новым файлом, поэтому глубину архива я держу соразмерной задаче, а не формально максимальной.
Про скорость переименования полезно знать вот что: rename правит запись в таблице каталога, байты файла на карте остаются где лежали, — ничего не копируется. Ротация получается мгновенной и не изнашивает карту, в отличие от любой схемы с ручным копированием. Границы честности: rename на классической библиотеке не перезапишет существующую цель — вернёт false, отсюда и цикл с exists вместо прямого имени по счётчику. Проверку раз в секунду держу на millis без блокировки — рецепт многозадачного каркаса в уроке про millis вместо delay.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const unsigned long LIMIT = 1000000UL; // ротация: 1 МБ на файл int fileNum = 1;unsigned long lastLog = 0;unsigned long startedAt = 0; // Свободное имя для архива: log001.csv, log002.csv ...String nextName() { while (true) { String name = "log"; if (fileNum < 10) { name = name + "00"; } else if (fileNum < 100) { name = name + "0"; } name = name + fileNum + ".csv"; if (!SD.exists(name)) { return name; } // занято - шагаем дальше fileNum = fileNum + 1; }} void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } startedAt = millis(); if (!SD.exists("log.csv")) { // свежая карта - шапка один раз File f = SD.open("log.csv", FILE_WRITE); f.println("sec,temp"); f.close(); } lastLog = millis();} void loop() { if (millis() - lastLog < 1000UL) { return; } lastLog = millis(); File f = SD.open("log.csv", FILE_WRITE); // на Uno - курсор в конец if (!f) { Serial.println("open false"); return; } if (f.size() >= LIMIT) { // дорос до лимита - ротация f.close(); // закрыли String arc = nextName(); if (SD.rename("log.csv", arc)) { // переименовали: байты не копируются Serial.print("rotaciya: log.csv -> "); Serial.println(arc); } f = SD.open("log.csv", FILE_WRITE); // свежий файл f.println("sec,temp"); // и своя шапка } int temp = map(analogRead(A0), 0, 1023, 0, 100); unsigned long sec = (millis() - startedAt) / 1000UL; f.print(sec); f.print(","); f.println(temp); f.close(); // закрыли - буфер на карте}Что смотреть в симуляторе. Модель пишет строку каждые две секунды, и при лимите 120 байт ротация наступает на пятой строке — весь цикл виден за полминуты: пять записей, печать переименования с ведущими нулями, сброс на 11 байт шапки. Отправьте size три раза подряд между ротациями — числа растут с шагом 24 и падают до 11 на новом файле. Уменьшите LIMIT до 60 — ротация пойдёт на каждой записи, и счётчик файлов побежит: наглядное напоминание, что лимит — компромисс между размером файла и частотой переименований.
На плате счётчик заменяется f.size(), а переименование — SD.rename из скетча выше; проверка размера — на открытом дескрипторе сразу после open. И держите в голове цену сбоя: что именно потеряется при пропадании питания между записями и как этот хвост дожимается на карту, разобрано в уроке про sync и питание — ротация не отменяет ни close после каждой записи, ни разумного интервала между ними.
- 1. Логгер с Uno перенесли на ESP32 без правок. Что FILE_WRITE сделает с существующим log.csv на ESP32?
- 2. Зачем шапку sec,temp писать только после проверки SD.exists?
- 3. Как на классической SD.h начать log.csv с чистого листа?
- 4. Почему имена архива ведут с ведущими нулями: log001.csv, а не log1.csv?
- 5. Сколько стоит переименование log.csv в log012.csv с точки зрения записи на карту?
- SD.exists перед записью: шапка один раз, свободное имя без коллизий, первый запуск отличён от продолжения.
- FILE_WRITE на Uno — дозапись; на ESP32 — усечение; перенос скетча между платформами проверяйте по таблице режимов.
- Перезапись на классической библиотеке — remove плюс новый open; truncate-режима в SD.h нет.
- Ротация по размеру: f.size() >= лимит — close, rename с нумерацией log001.csv, новый файл с шапкой.
- Rename не копирует байты — только запись каталога; цикл с exists перешагивает занятые номера после перезагрузки.
Частые вопросы по теме урока
- Логгер с Uno перенесли на ESP32 без правок. Что FILE_WRITE сделает с существующим log.csv на ESP32?
- В библиотеке для ESP32 FILE_WRITE открывает файл с усечением, а за дозаписью там стоит FILE_APPEND. Один и тот же литерал на разных платформах означает разное. файл усечётся в ноль — первая же запись затрёт историю
- Зачем шапку sec,temp писать только после проверки SD.exists?
- Шапка нужна ровно одна. Exists отличает первый запуск от продолжения, и после десятой перезагрузки файл остаётся валидной таблицей. иначе каждая перезагрузка допишет ещё одну шапку, и колонки в таблице поедут
- Как на классической SD.h начать log.csv с чистого листа?
- Методов усечения у классической библиотеки нет: либо дозапись, либо удалить и создать заново. Seek(0) оставит старые байты дальше по файлу. SD.remove("log.csv"), затем SD.open с FILE_WRITE — создастся пустой файл
- Почему имена архива ведут с ведущими нулями: log001.csv, а не log1.csv?
- Сортировка по имени без нулей ломает хронологию: log2 уезжает за log10. Три строки форматирования — и список архива читается по порядку. иначе сортировка в проводнике ставит log10.csv сразу после log1.csv
- Сколько стоит переименование log.csv в log012.csv с точки зрения записи на карту?
- Rename правит только запись в таблице каталога — файл никуда не копируется. Ротация выходит мгновенной и не изнашивает карту. почти ничего: меняется запись каталога, байты остаются на месте