Логгер погреба провёл в работе всю ночь: Serial каждые шесть секунд печатал свежую строку, скетч не падал, питание не мигало. Утром я вынул карту, вставил в ноутбук — файл log.csv нулевой длины. Ни одной строки из четырнадцати тысяч. С этой карты ещё вечером я читал конфиг, то есть карта живая, а файл — пустой. Виновник нашёлся в третьей строке скетча от «работает же»: я открыл файл в setup, всю ночь печатал в него print-ами и ни разу не вызвал close(). Все байты ночи просидели в буфере оперативной памяти, каталог FAT честно считал файл пустым, а утром питание выдернули — и буфер умер вместе с платой.
Это самая дорогая ошибка раздела: она не шумит, не роняет скетч и не зажигает ошибок в Serial — просто однажды вы остаётесь без данных, ради которых всё затевалось.
Разберём механику: куда попадает байт после println, зачем библиотеке буфер ровно на 512 байт, что делает close() и чем отличается от flush(), и как выбрать схему записи — открывать файл на каждую строку или держать открытым всю ночь.
Буфер на 512 байт между println и картой
println в объект File делает три вещи: превращает число в текст, добавляет перевод строки — два байта, \r\n, приплюсуйте их к любому подсчёту объёма, — и копирует байты в буфер. Буфер ровно 512 байт, потому что 512 байт — это один сектор карты, минимальная порция физической записи. Писать сектор карта может за единицы миллисекунд, а вот дешёвые и изношенные карты иногда затягивают один сектор записи до сотни миллисекунд — этот паразитный прыжок называется latency spike, и библиотека старается дёргать карту пореже: пока буфер не заполнен, карта вообще не трогается.
Заполнился буфер — сектор уехал на карту, буфер обнулился, печать продолжается. Прикинем на моей строке лога: sec,temp в формате 3661,24.7 — это 9 символов плюс два на перевод строки, итого 11 байт. В сектор влезает около 46 таких строк: первые 45 строк существуют только в оперативной памяти. Всё это время файл на карте с точки зрения компьютера пуст — размер в каталоге обновляется отдельно, и этим отдельно занимается close().
Задержки удобно измерить самому: оберните open-print-close в печать millis() до и после. На моей связке Uno с картой 8 ГБ Class 4 цикл занимает 12-18 мс, а раз в сотню записей прилетает прыжок до 90-110 мс — контроллер карты в этот момент внутри себя занимается выравниванием износа ячеек. Для записи раз в две секунды это незаметно, а для потока по каждому обороту loop — уже причина дырок: интервал между метками в логе прыгает со 100 мс до 200 мс без всякой причины в вашем коде.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;unsigned long lastLog = 0;unsigned long startedAt = 0;int count = 0; void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } startedAt = millis();} void loop() { if (millis() - lastLog >= 2000) { lastLog = millis(); unsigned long sec = (millis() - startedAt) / 1000; File f = SD.open("log.csv", FILE_WRITE); // курсор в конец if (f) { f.print(sec); f.print(","); f.println(count); // строка целиком плюс перевод строки f.close(); // без этой строки данных на карте нет count = count + 1; } else { Serial.println("open failed"); } }}close() делает два дела сразу. Первое: дожимает буфер — недозаполненный сектор всё равно уходит на карту, хоть там и три байта. Второе — и это то, что теряли мои ночные строки: close обновляет запись в каталоге FAT, где хранится размер файла и конец цепочки кластеров. До close файл для компьютера имеет ту длину, что была зафиксирована при прошлом закрытии; данные в недописанных секторах, может, и лежат физически, но никто не знает, что они принадлежат этому файлу. Поэтому файл ночной записи без close выглядел пустым, хотя байты в карте, возможно, остались.
Собрат close — метод flush(): сбрасывает буфер на карту, но файл оставляет открытым и каталог не трогает до следующего sync или close.Flush нужен в схеме с долго открытым файлом — как страховочный сброс каждые несколько секунд. А вот миф, что flush заменяет close, я проверял на себе: после flush файл всё ещё занят дескриптором, и повторный open вернёт ложь. Закрывать файл всё равно надо.
| Вызов | Что делает | Когда байты на карте | Размер файла в каталоге FAT |
|---|---|---|---|
| print / println | текст и числа в буфер 512 байт | при заполнении сектора или по close | обновится только по close |
| write(байт) | сырой байт без преобразований | так же, через буфер | так же |
| write(буфер, len) | пачка байт одним куском | так же, но быстрее для больших порций | так же |
| flush() | принудительный сброс буфера | сразу после вызова | при следующем sync или close |
| close() | сброс буфера + обновление каталога | сразу после вызова | фиксируется немедленно |
Теперь стратегии. Схема «открыл — записал — закрыл» на каждую порцию стоит денег: открытие, запись и закрытие занимают порядка 10-50 мс с учётом задержек карты. При записи раз в две секунды это ноль целых два процента времени — ничто. Зато при сбое питания вы теряете максимум одну строку: всё предыдущее уже зафиксировано каталогом. Для логгеров с частотой до примерно одной записи в секунду я выбираю всегда эту схему — она же стоит в коде выше, и каркас millis() для неё берите из урока про millis вместо delay.
Держать файл открытым имеет смысл, когда записей много: десять и больше в секунду, где 10-50 мс на открытие уже заметны. Тогда файл открывается один раз, а буфер подстёгивается flush() каждые 2-5 секунд — потеря при сбое ограничивается этими двумя-пятью секундами, зато каталог обновится только при финальном close. Второй риск этой схемы я уже проходил в первом уроке раздела: при сбое питания посреди физической записи FAT может повредиться, и пострадает весь файл, а не последний сектор. Watchdog из урока про таймеры и сторожевого пса подстрахует зависший скетч, но от потери буфера при выдёргивании питания он, увы, не спасает.
После симулятора проверьте свой логгер на прочность прямо на столе: запишите минуту данных, закройте файл, прочитайте его обратно скетчем из урока про режимы File и сверите size(). Затем уберите close — и увидите, как файл останется старого размера при полностью работающем скетче. Чтобы не таскать карту к компьютеру, выведите содержимое прямо в Serial Monitor тем же FILE_READ — там же, где мы выводили таблицы в уроке про таблицу данных в Serial: файл распечатается строка за строкой, и сразу видно, где дырки в интервале меток. Формат карты, на которой всё это живёт, мы разобрали в уроке про FAT16, FAT32 и размеры. Домашнее задание: посчитайте для своей строки CSV, сколько строк влезает в сектор 512 байт, и ответьте, сколько строк потеряет ваш скетч при выдёргивании питания без close — для схем с открытым файлом ответ редко бывает меньше сорока.
- 1. Куда попадает строка после println в открытый файл?
- 2. Вы записали сотню строк, не вызвали close и вставили карту в компьютер. Что в файле?
- 3. Чем flush() отличается от close()?
- 4. Сколько байт положит в файл println(7) при целочисленном значении 7?
- 5. Логгер пишет одну строку раз в 10 секунд. Какая схема записи разумнее?
- println копирует строку в буфер 512 байт; карта трогается, только когда сектор заполнен
- close() делает два дела: дожимает буфер и фиксирует размер файла в каталоге FAT
- flush() — сброс буфера для долго открытого файла; каталог он не обновляет и close не заменяет
- Перевод строки от println — два байта \r\n: считайте объём лога с этим запасом
- До одной записи в секунду — схема «открыл-записал-закрыл»; быстрее — открытый файл и flush каждые 2-5 секунд
- Светодиод на модуле показывает питание, а не запись; проверяйте запись чтением size() обратно
Частые вопросы по теме урока
- Куда попадает строка после println в открытый файл?
- Печать в File копирует байты в буфер размером с сектор. Физическая запись случается при заполнении буфера или по close/flush. Копируется в буфер 512 байт в оперативной памяти
- Вы записали сотню строк, не вызвали close и вставили карту в компьютер. Что в файле?
- Каталог обновляется при close. Без него компьютер видит файл старого размера, чаще всего пустого, даже если сектора с данными физически на карте. Скорее всего нулевая длина — размер в каталоге FAT не обновлён
- Чем flush() отличается от close()?
- flush — сброс буфера для долго открытого файла. Повторный open после flush вернёт ложь: дескриптор всё ещё занят, нужен close. flush сбрасывает буфер, но файл остаётся открытым и каталог не обновляет
- Сколько байт положит в файл println(7) при целочисленном значении 7?
- Символ 7 плюс перевод строки из двух байт \r\n — итого три. Эти два байта надо приплюсовывать к любому подсчёту объёма лога. Восемь: как long
- Логгер пишет одну строку раз в 10 секунд. Какая схема записи разумнее?
- При редкой записи цена open+close в десятки мс незаметна, зато сбой питания стоит одной строки. Открытый файл оставьте для потоков от 10 записей в секунду. Открыл — записал — закрыл на каждую строку