Логгер из прошлого урока каждую сессию питания заводит новый файл — и через месяц на карте начинается склад: dht01.csv рядом с dht31.csv, между ними пара файлов-призраков по ноль байт от обрывов питания и tmp-хвосты прерванных записей. Пока карту не трогают, всё живёт; а потом приходит человек и спрашивает, что из этого можно удалить руками, а что трогать нельзя. Мне надоело быть диспетчером чужого архива, и скетч получил собственную уборку: старый журнал переезжает в архивное имя, мусор удаляется, всё — из loop, без вынимания карты.
Инструментов три, и все они работают с каталогом, а не с содержимым. SD.exists(имя) отвечает, есть ли файл на карте, — без него любая уборка стреляет по пустоте. SD.rename(старое, новое) переименовывает запись каталога: байты файла никуда не копируются, операция моментальная, и старый лог мгновенно становится arc01.csv. SD.remove(имя) вычёркивает файл из каталога и освобождает кластеры — вот и вся чистка. Базовые операции с файлами уже показывались в уроке про remove и rename, ротацию по размеру — в уроке про дозапись и ротацию; здесь собираю из них ночной регламент.
Список файлов для ревизии тоже можно получить из скетча: каталог обходится openNextFile — его разбирали в уроке про список файлов. Здесь список не нужен: имена, с которыми работает уборка, скетч и так знает, потому что сам их завёл. Это важное различие: уборка по известным именам — это регламент, а разбор чужой карты — уже другая работа, и руками она делается лучше.
Сколько мусора даёт месяц работы — считается грубо, но полезно: при обрыве питания раз в неделю возникает один файл-призрак или потерянный кластер; за месяц их четыре-пять, за полгода — пара десятков. На карте в гигабайт это не про место: кластер на FAT32 занимает до тридцати двух килобайт, зато пустой файл почти ничего не весит. Проблема в порядке, а не в байтах: человек, открывший карту, не отличит призрака от живого журнала, и единственная защита — чтобы живых журналов было мало, а мусор убирался до того, как карту откроют.
rename: рабочий лог уезжает в архив
Сценарий переименования один, и он повторяется каждую полночь или каждую сессию: журнал log.csv отработал своё, имя надо освободить для нового дня. Файл закрывается, SD.rename(log.csv, arc07.csv) переносит запись в архивное имя — и можно открывать свежий log.csv. Архивный номер я веду так же, как счётчик дней в прошлом уроке: arc01, arc02, две цифры с ведущим нулём, чтобы сортировка по именам читалась хронологией. Если хочется календаря, номер меняет дата из RTC — механика переименования от этого не меняется ни на строку.
Возврат rename обязан проверяться, потому что промах здесь молчалив. Функция возвращает true только при удаче; если целевое имя уже занято, файл открыт или его нет вовсе — вернётся false, и скетч, не посмотревший на ответ, спокойно продолжит работать с вчерашним журналом как с сегодняшним. Мне хватает двух проверок: exists() для нового архивного имени до переименования и проверка возврата после. Когда имя занято, я не перезаписываю архив, а беру следующий номер: затирать вчерашние данные ради сегодняшнего порядка — та сделка, на которую я не согласен ни при каком удобстве.
Отдельная страница истории: в старых версиях библиотеки SD переименования не было вовсе, и советы той эпохи предлагали копировать файл по байтам и удалять оригинал. На свежих ядрах Arduino SD.rename есть и работает штатно, но если проект собирается на старом тулчейне, первое, что стоит проверить при отсутствующей функции, — версию ядра. Признак второй, смежной проблемы, — rename, который компилируется, но стабильно возвращает false: почти всегда это открытый файл, и к нему вернёмся в грабле ниже.
Тот же регламент полезен и на старте прибора. Если питание пропало вчера посреди дня, на карте остался журнал без признака завершения: лучше не дописывать в него молча, а сначала переименовать в архивный номер и открыть свежий — тогда одна сессия питания равна одному журналу, и вчерашний файл всегда цельный. Счётчик архивных номеров храню в той же ячейке EEPROM, что и счётчик дней: одна запись в сутки ресурс не греет, а после перезапуска нумерация продолжается, а не начинается с arc01, пытаясь занять занятое.
remove, exists и когда форматировать
Мусор от записей выглядит по-разному, и удалять стоит не всё подряд. Файлы-призраки по ноль байт — след обрыва питания во время создания файла: каталог запись получил, кластеры не заполнил. tmp-файлы — хвосты прерванных сессий, если скетч когда-нибудь писал через временные имена. Битые имена с мусорными символами — самая неприятная категория: каталог читается, а имя не соответствует никакому набору букв. Правило у меня простое: удаляю то, что сам же и создал, по известным именам, — tmp-файлы и нулевые файлы своей номенклатуры; всё остальное на карте чужого происхождения требует ручного разбора, потому что за каждым именем может стоять чей-то конфиг.
exists() перед удалением — не паранойя, а экономия веток: remove несуществующего файла вернёт false, и отличить «файла нет, всё чисто» от «файл есть, но удалить не вышло» без exists невозможно. С exists ветка честная: нет файла — печатаем «чисто», есть — удаляем и проверяем возврат. Имена подгоняю под короткий формат: arc01.csv, tmp0001.tmp — всё в рамках 8.3, как и весь раздел про каталоги и лимит корня в соответствующем уроке. Новое имя тоже подчиняется тем же правилам FAT: кириллица, пробелы и длинные имена на классической библиотеке — лишний источник молчаливых неудач.
Ревизию по размерам удобно гонять через openNextFile: каталог обходится по файлам, и каждый нулевой файл своей номенклатуры отправляется в очередь на удаление. Я держу эту процедуру в скетче выключенной — вызов раз в месяц кнопкой, а не при каждом старте: обход каталога на большой карте занимает секунды, и делать его молча при каждом включении незачем. Зато раз в месяц он приносит честный отчёт: сколько файлов, сколько байтов, что из этого призраки — это отчёт о здоровье карты, а не уборка вслепую.
Когда всё-таки форматировать: карта — не вечный носитель, и после месяцев обрывов питания на ней копятся потерянные кластеры, которые не видны в каталоге, но съедают место. Признаки — место кончается само, хотя файлов мало, или запись начала спотыкаться на пустой карте. Форматировать нужно в FAT32 для карт до 32 гигабайт и полной, а не быстрой, операцией — разбор форматов был в уроке про FAT16 и FAT32. После форматирования я прогоняю тот же регламент: залить settings.txt, завести новый журнал. Пара народных советов с форумов — например, создать и удалить дюжину пустых файлов после форматирования, чтобы FAT выделила таблицы заранее, — я записываю как эмпирику: хуже не делает, но и обещанного ускорения на моих картах не показал.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10; char workName[9] = "log.csv"; // текущий журналchar arcName[10]; // архивное имя, собирается со счётчикомint arcN = 0; void makeArcName(int n, char* buf) { buf[0] = 'a'; buf[1] = 'r'; buf[2] = 'c'; buf[3] = '0' + (n / 10) % 10; // ведущий ноль: arc07 buf[4] = '0' + n % 10; buf[5] = '.'; buf[6] = 'c'; buf[7] = 's'; buf[8] = 'v'; buf[9] = 0;} bool rotateLog() { // полночь: старый лог -> архив if (SD.exists(workName)) { arcN = arcN + 1; makeArcName(arcN, arcName); if (SD.exists(arcName)) { // имя занято вчерашним архивом Serial.print(arcName); Serial.println(" уже есть - беру следующий номер"); return false; // затирать архив не буду } if (!SD.rename(workName, arcName)) { Serial.println("rename вернул false: он где-то открыт?"); return false; } Serial.print(workName); Serial.print(" -> "); Serial.println(arcName); // архив готов, имя свободно } File f = SD.open(workName, FILE_WRITE); // свежий журнал на новый день if (!f) return false; if (f.size() == 0) f.println("sec,temp_c,hum_pct"); f.close(); return true;} void cleanTemp() { // мусор от прерванных записей for (int i = 1; i <= 3; i++) { char tmp[12] = "tmp0001.tmp"; tmp[3] = '0' + (i / 1000) % 10; tmp[4] = '0' + (i / 100) % 10; tmp[5] = '0' + (i / 10) % 10; tmp[6] = '0' + i % 10; if (!SD.exists(tmp)) continue; // файла нет - чисто if (SD.remove(tmp)) { Serial.print(tmp); Serial.println(" удалён"); } else { Serial.print(tmp); Serial.println(" не удалился - закрыть все File и повторить"); } }} void setup() { Serial.begin(9600); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } cleanTemp(); rotateLog();} void loop() { // регламент вызывается по счётчику суток или смене дня RTC delay(1000);}Нажмите кнопку и следите за порядком шагов: переименование стоит первым после close, потому что оно самое дешёвое и освобождает имя; проверка exists стоит перед rename, потому что занятое имя надо заметить до, а не после; tmp-мусор уходит последним, когда архив уже в безопасности. Номер архива растёт с каждым нажатием — arc01, arc02, arc03 — и на плате он же хранится в EEPROM, чтобы после перезапуска счёт не начинался заново и не пробовал занимать чужие имена.
Чего модель не показывает — скорость и объём. Переименование на реальной карте занимает единицы миллисекунд и не зависит от размера файла: архивный лог на мегабайт переезжает так же мгновенно, как пустышка, потому что меняется только запись каталога. Удаление такое же мгновенное. Отсюда и стоимость регламента: ночные операции не мешают ни замерам, ни питанию, и их можно позволять себе каждую полночь без всякой жалости к ресурсу карты.
Отдельно про страх испортить архив переименованием. Rename на FAT меняет пару байтов записи каталога, и если питание пропадёт ровно в этот момент, худший исход — две копии каталога разойдутся во мнении об имени, что проверка диска разрулит с потерей имени, но не данных: кластеры остаются на месте. По сравнению с риском дописать новый день в вчерашний журнал через FILE_WRITE — а это смешанные данные, которые потом не разделить, — цена rename пренебрежима. Операции каталога аккуратнее любых манипуляций с содержимым, и этим стоит пользоваться.
| Операция | Что делает | Стоимость | Когда вызывать |
|---|---|---|---|
| exists(имя) | отвечает, есть ли файл | микросекунды | перед каждым remove и rename |
| rename | переносит запись каталога на новое имя | единицы мс, байты не копируются | полночь: рабочий лог -> архив |
| remove | вычёркивает файл, освобождает кластеры | единицы мс | tmp-мусор и нулевые файлы своей номенклатуры |
| openNextFile | обход каталога со списком файлов | секунды на большой карте | месячная ревизия призраков |
| формат карты | чистая FAT32, полная, не быстрая | минуты, всё стирается | потерянные кластеры копятся, место исчезает |
- 1. Почему rename большого лога происходит мгновенно?
- 2. rename вернул false. Самая вероятная причина?
- 3. Зачем exists() перед remove()?
- 4. Файл-призрак по ноль байт после обрыва питания. Что это?
- 5. Когда форматирование карты оправдано?
- Уборка работает с каталогом: exists отвечает на вопрос, rename переносит запись мгновенно, remove освобождает кластеры.
- Полночный регламент: close, exists для нового имени, rename в архив со счётчиком, remove tmp-мусора, свежий журнал с шапкой.
- Возврат rename и remove проверяется всегда: обе функции молчат false, и самый частый виновник — открытый где-то File.
- Удаляю то, что сам создал, по известным именам; чужие имена и битые названия — ручной разбор, не скетч.
- Форматирование — крайняя мера: исчезающее место и спотыкающаяся запись; полная FAT32, после неё конфиг и новый журнал.
Частые вопросы по теме урока
- Почему rename большого лога происходит мгновенно?
- Rename правит запись в каталоге FAT: имя сменилось, кластеры с данными остались на месте. Поэтому архивация лога на мегабайт стоит те же единицы миллисекунд, что и пустышка. Меняется запись каталога, байты файла не копируются
- rename вернул false. Самая вероятная причина?
- Открытый File блокирует переименование и удаление — false возвращается молча. Первое действие: закрыть все файлы, которые могли его держать, потом повторить операцию. Файл сейчас открыт каким-то объектом File
- Зачем exists() перед remove()?
- remove несуществующего файла тоже возвращает false, и без exists эти два случая неразличимы. С exists ветки честные: чисто — напечатали, неудача — разбираемся. Чтобы отличить «файла нет — чисто» от «файл есть, но не удалился»
- Файл-призрак по ноль байт после обрыва питания. Что это?
- При обрыве во время создания файла запись в каталоге остаётся, а данные не успевают лечь. Такие файлы своей номенклатуры скетч удаляет по известным именам через exists и remove. Каталог успел получить запись, кластеры не заполнились
- Когда форматирование карты оправдано?
- Признак — исчезающее место при малом числе файлов и спотыкающаяся запись: на карте копятся потерянные кластеры после обрывов. Форматировать полной операцией в FAT32, затем залить конфиг и завести новый журнал. Когда место кончается само, а файлов мало