Логгер котельной в нашем кооперативе писал температуру и подачу раз в две минуты и прожил на одной карте восемь месяцев. Приехав за архивом, я нашёл в мониторе вечное open failed: карта кончилась недели за две до этого, и всё это время скетч исправно тикал, пробовал открыть файл, получал отказ и шёл дальше. Две недели измерений ушли не из-за железа — я просто не продумал, что делать с файлом, который растёт без ограничений.
Второй сюрприз ждал на компьютере: файл разросся до 180 МБ. Excel молча обрезал его до первого миллиона строк, блокнот открывался полминуты, а листать такое без поиска по дате невозможно. Большой лог — не достижение, а проблема: и для карты, и для того, кому его читать.
Разберём, как ограничить файл по размеру через myFile.size(), как устроена ротация закрыл — переименовал — открыл новый, где держать номер файла, чтобы он переживал любой сброс питания, и когда циклическая запись вообще нужна. Каркас записи с закрытием файла мы проходили в уроке про println и close — теперь надстраиваем поверх него.
Лимит размера: какой и зачем
Размер спрашивают у дескриптора: myFile.size() возвращает текущую длину файла в байтах. Строка моего котельного лога — метка, две температуры, разделители — выходит на 14 байт вместе с переводом строки. Один мегабайт — это около 75 000 таких строк: при записи раз в две минуты файл добирается до лимита за 52 суток. Отсюда мой рабочий ориентир: лимит 1 МБ закрывает и читаемость — такой файл открывается мгновенно и печатается на десятке листов, — и ритм устройства: ротация случается пару раз в месяц, а не каждый час.
Число 1 МБ — мнение, а не закон, и граница у него условная. Если лог едят скрипты, а не глаза, смело ставьте 10 МБ: FAT32 переваривает. Если пишете по десять записей в секунду, мегабайт набежит за сутки — тогда лимит подбирают под паузу, в которую ротация встанет без ущерба. Готового ограничения в библиотеке нет: размер файла может дорасти до четырёх гигабайт — столько адресует файловая система на Uno, — и никто не предупредит. Лимит вводите вы, одной константой в скетче.
Второй предел — количество файлов. На FAT16 в корневом каталоге ровно 512 слотов под имена, насыпать по файлу на каждый запуск скетча не выйдет. Во что обходятся хвосты кластеров при множестве мелких файлов, мы считали в уроке про формат карты — ротация по суткам умножает эти хвосты на количество дней наблюдений. Моё правило: меньше десяти файлов на карте, у каждого осмысленное имя и номер.
| Триггер ротации | Ориентир | Когда выбираю |
|---|---|---|
| Размер: size >= лимита | 1 МБ | лог читают глазами: файл-страничка открывается мгновенно |
| Сутки: новый файл в полночь | RTC, имя вида d2512.csv | сравнение показателей по дням, файл сам себя датирует |
| Количество строк | 50 000 строк | лог едят скрипты, для них размер вторичен |
| Кольцо из N файлов | 3-5 файлов | карта маленькая, старое не жалко — перезаписывается само |
Закрыл — переименовал — открыл новый
Механика ротации скучна, и это её достоинство. После каждой записи спрашиваем size(); доехали до лимита — close() (незакрытый файл держит имя занятым и не зафиксирует размер), присваиваем текущему логу архивное имя log_003.csv, открываем свежий log.csv — и дозапись продолжается уже в него. Номера веду с ведущими нулями: log_003 сортируется правильно и в проводнике, и в скрипте, а log_10 встанет между log_1 и log_2.
Слово «переименовал» на Uno — самое коварное в этой схеме: в SD.h функции rename нет. Обходных пути два. Первый — библиотека SdFat из менеджера библиотек: у её файлов rename() есть, и вся ротация сжимается до одного вызова, но её API не совпадает с SD.h, скетч придётся переписывать. Второй путь — остаться в SD.h: открыть старый файл FILE_READ, открыть архивный FILE_WRITE, перелить байты, закрыть оба, удалить оригинал через SD.remove(). Это те самые режимы из урока про File и режимы открытия. Цена копирования — время: мегабайт по SPI на Uno перекачивается за пять-десять секунд, и всё это время лог молчит. Ставьте ротацию в паузу: между измерениями, в ночные часы, перед плановым выключением.
Осталось слабое место — номер. Локальная переменная fileNo живёт в оперативной памяти: один сброс питания, и нумерация стартует с нуля. Первая же ротация после перезагрузки откроет log_000 в режиме дозаписи — поверх самого ценного, самого старого архива.
EEPROM решает проблему за три строки. При старте fileNo = EEPROM.read(0) — нумерация продолжается с того места, где оборвалась. После каждой ротации EEPROM.update(0, fileNo) — и сброс питания для номера перестаёт существовать. Update нужен именно здесь, а не write: он сравнивает байт с тем, что уже записан, и трогает ячейку только при изменении. Ресурс EEPROM — около 100 000 циклов записи на ячейку: при ротации раз в месяц это запас на восемь тысяч лет, при суточной — на двести семьдесят. Износ ячеек и дисциплину записи мы разбирали в уроке про EEPROM.update, а счётчик как класс задач — в уроке про счётчик включений.
Адрес 0 держите зарезервированным под номер: заведите список, что на каком адресе живёт, и не кладите туда ничего другого. Через полгода кто-нибудь — включая вас — добавит в скетч новый флаг и небрежно поставит его на первый свободный адрес, который окажется вашим.
#include <SPI.h>#include <SD.h>#include <EEPROM.h> const int chipSelect = 10;const unsigned long LIMIT = 1048576UL; // лимит 1 МБconst char* CURRENT = "log.csv"; // пишем всегда сюда unsigned long lastLog = 0;int fileNo = 0; void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD not ready"); while (true) { } } fileNo = EEPROM.read(0); // продолжаем нумерацию после сброса Serial.print("next archive: log_"); Serial.println(fileNo);} void rotate() { // в SD.h нет rename - копируем FILE_READ -> FILE_WRITE String arcName = "log_"; arcName += fileNo; arcName += ".csv"; File src = SD.open(CURRENT, FILE_READ); File dst = SD.open(arcName, FILE_WRITE); // перезапишет старый архив if (src && dst) { while (src.available()) { dst.write(src.read()); // для скорости - порциями по 512 байт } } src.close(); dst.close(); SD.remove(CURRENT); // оригинал больше не нужен fileNo = (fileNo + 1) % 10; // кольцо из десяти файлов EEPROM.update(0, fileNo); // номер переживает любой сброс Serial.print("rotated to "); Serial.println(arcName);} void loop() { if (millis() - lastLog >= 2000) { lastLog = millis(); File f = SD.open(CURRENT, FILE_WRITE); // курсор в конец if (f) { f.println("t=21.4,h=58"); f.close(); // размер фиксируется именно здесь } else { Serial.println("open failed - karta zbita ili voobshe ne ta"); } File probe = SD.open(CURRENT, FILE_READ); if (probe) { unsigned long sz = probe.size(); probe.close(); if (sz >= LIMIT) rotate(); } }}По скетчу три ремарки. Размер проверяю отдельным чтением после close: файл закрыт, длина зафиксирована каталогом, и число приходит из файловой системы, а не из памяти дескриптора — так проверка не зависит от версии библиотеки. Перелив байтов по одному через read/write поставлен ради ясности; если пауза на ротацию критична, читайте порциями в буфер на 512 байт — ровно размер сектора карты, — и время сократится примерно вдвое. Кольцо на десять файлов — компромисс: старейший архив перезаписывается, но запас истории остаётся.
Есть и форумный приём для FAT16: сразу после форматирования создать на карте шестнадцать пустых файлов и удалить их. Слоты корневого каталога пометятся занятыми, каталог перестанет двигаться по карте, и у логгера закрепится место в его начале. Это эмпирика с форумов Arduino, а не документация, — но для логгеров, которые пишут годами, я приём взял в привычку: стоит минуту, а целый класс фрагментации каталога уходит.
Когда ротация нужна в принципе? Если карту читаете руками — нужна, и лимит подбирается под вашу читалку. Если данные едят скрипты — ротация по суткам с датой в имени удобнее нумерованных файлов: сравнение по дням ложится прямо на имена. Если файлы и вовсе не должны выживать — буфер телеметрии, который всегда свежий, — кольцо из трёх-пяти файлов, как в скетче, и ничего придумывать не надо. Каркас millis() для всего этого берите из урока про millis вместо delay. В симуляторе SD не эмулируется — ниже модель ротации: строки прирастают по 64 байта, лимит 384, кольцо из трёх архивов, номер — в настоящей EEPROM модели платы.
- 1. Что возвращает myFile.size()?
- 2. В SD.h на Uno переименовать файл можно:
- 3. Зачем номер архивного файла хранить в EEPROM, а не в переменной?
- 4. Почему для счётчика ротации берут EEPROM.update, а не EEPROM.write?
- 5. Когда проверять размер файла для ротации?
- SD.h не ставит лимитов: файл растёт хоть до 4 ГБ — лимит 1 МБ вводите константой сами
- size() у дескриптора — текущая длина в байтах; проверяйте после записи и close
- Ротация: close — архивное имя log_003.csv — свежий log.csv; в SD.h rename нет: копировать и удалить либо SdFat
- Номер архива — в EEPROM.update(0, fileNo): переменная в RAM после сброса начнёт затирать старые архивы
- Копирование 1 МБ по SPI занимает 5-10 секунд — ставьте ротацию в паузу между измерениями
Частые вопросы по теме урока
- Что возвращает myFile.size()?
- size() у дескриптора отдаёт длину файла в байтах. С ней сравнивают лимит — готового ограничения в SD.h нет, файл растёт хоть до 4 ГБ. Текущую длину файла в байтах
- В SD.h на Uno переименовать файл можно:
- Функции rename в SD.h нет. Путь в рамках библиотеки — копировать и удалить; SdFat умеет rename, но у неё другой API. Никак: скопировать FILE_READ в FILE_WRITE и удалить оригинал, либо взять SdFat
- Зачем номер архивного файла хранить в EEPROM, а не в переменной?
- После сброса питания переменная станет нулём, и первая же ротация перезапишет log_000 — самый старый архив. EEPROM живёт без питания. Переменная обнуляется при сбросе, и нумерация начнёт затирать старые архивы
- Почему для счётчика ротации берут EEPROM.update, а не EEPROM.write?
- update сравнивает байт с записанным и трогает ячейку только при изменении. При ресурсе 100 000 циклов на ячейку счётчика хватает на века. update пишет байт только при изменении и бережёт ресурс ячейки
- Когда проверять размер файла для ротации?
- Размер спрашивают после закрытия файла: число приходит из каталога FAT и не зависит от версии библиотеки. Лимитов сама библиотека не ставит. После записи и close, когда размер зафиксирован каталогом