Мне принесли три файла с одного устройства: log_1.csv, log_2.csv, log_3.csv — по одному на каждый перезапуск платы. В первом шапка sec,value и шестьсот строк. Во втором та же шапка стоит в середине файла, в третьем — две шапки с интервалом в тридцать строк. Метки времени нет ни в одном: только сквозной номер строки, по которому не ответишь, когда именно датчик вышел за порог. Всё это пишет один и тот же скетч — шапка при каждом запуске, номер вместо времени и файл, открытый от setup до close, которого у бесконечного loop не бывает.
Разберу договор CSV-лога, при котором таких файлов не заводится. Шапка пишется один раз и по условию, а не по настроению запуска. Каждое измерение — ровно одна строка: поля через запятую, перевод строки в конце. Метка времени — колонка от millis, честная и монотонная, хотя и без календаря: цену этой честности посчитаем отдельно. И главный практический приём — короткая сессия с картой на каждый замер: открыли файл, дописали строку, закрыли. При минутном интервале это ничего не стоит, а оборванный кабель питания превращает из катастрофы в потерю одной строки.
Первый скетч логгера и проводка модуля уже разобраны в уроке про первый CSV на карте, состав колонок проектировал в уроке про шапку и колонки. Таймер замера здесь собран на millis без единого delay — если этот скелет незнаком, сначала урок про millis вместо delay, а потом возвращайтесь: без него интервал плывёт при каждом замере.
Договор CSV: шапка один раз, строка на каждый замер
Шапка нужна файлу как оглавление: без неё через месяц никто, включая автора, не вспомнит, что значит вторая колонка. Пишется она не при каждом запуске, а при пустом файле: открыли файл в режиме FILE_WRITE, спросили size() — ноль значит файл свежий, пишем шапку; не ноль — журнал живёт, добавляем только строки. Режим FILE_WRITE ставит курсор записи в конец, это его сила и его коварство: на карте со старым журналом безусловная шапка допишется прямо в середину записей. Отсюда и второй шапки из моей истории, и третья.
Строка собирается цепочкой print с одним завершающим println: поля через запятую, без пробелов после запятых — пробел Excel начнёт считать частью значения. Если в поле может прилететь запятая (текстовый статус, комментарий), поле берётся в кавычки или разделитель меняется на точку с запятой — правило то же, что и для журнала в Serial. Ту же строку печатайте и в монитор: дубль стоит ноль миллисекунд, а показывает и содержимое, и сам факт записи. Половина полевых загадок закрывается этим двойным листом: файл молчит, а Serial виден.
Колонка метки — millis()/1000: секунды от включения платы. Число монотонное, никогда не прыгает назад, арифметика «сколько прошло» делается вычитанием — этого у календарных штампов с часовыми поясами нет. Цена известна заранее: после перезапуска отсчёт пойдёт с нуля, и дата в журнале не появится вовсе. Для тестов, счётчиков и станков с постоянным питанием этого достаточно; для архива с календарём нужен RTC, и это следующий урок.
Сессия раз в минуту: открыл, дописал, закрыл
У CSV-лога два стиля жизни. Первый — файл открыт от начала и навсегда: быстрее на миллисекунды, но при обрыве питания последний сектор со строками и обновление длины в каталоге не совпадают по времени, и хвост журнала числится призраком. Второй — короткая сессия на каждый замер: open, строка, close. Close делает два дела: дожимает буфер и обновляет длину файла в каталоге, поэтому после него компьютеру показывается ровно то, что записано, — обрыв питания стоит максимум одной недописанной строки, а не половины журнала.
Цена вопроса измеряется миллисекундами: открытие файла на Uno укладывается в единицы миллисекунд, и при минутном интервале это шум на фоне 60 секунд между замерами. Механику разлома — секторы против каталога и потерянные кластеры — разобрал в уроке про обрыв питания и кластеры, поведение буфера и flush — в уроке про flush и питание; здесь беру только практику: минутный интервал делает короткие сессии бесплатными. Светодиод окна записи — третья рука скетча: вспышка на время сессии, и любой желающий знает, когда карту трогать нельзя.
Отдельная мелочь, которую замечают через месяц работы: метки в журнале идут не ровно через 60 секунд, а через 60 с хвостиком. Причина — строка lastRow = ms: отсчёт начинается не от планового момента, а от фактического, и каждая сессия добавляет пару миллисекунд к следующему интервалу. Для минутного лога это копейки — за сутки набегает меньше секунды. Если нужна метрономная сетка, отсчёт ведут накоплением lastRow += INTERVAL, но тогда один сбойный тик сдвигает весь ряд навсегда. Для датчика с минутной сеткой я осознанно беру первый вариант: устойчивость к единичному сбою дороже идеальной сетки.
У millis-метки есть горизонт, о котором вспоминают через два месяца: счётчик тиков переполняется на 49,7 сутки, и колонка секунд прыгает с пяти миллионов обратно в ноль. Журнал не ломается — строки лягут после старых, — но сортировка по метке развалится: новый ноль встанет раньше старых миллионов. Лечится перезапуском логгера раз в месяц — у меня так и живёт дачный датчик, — либо unix-штампом из следующего урока. Считается и объём без калькулятора: минутная сетка — 1440 строк за сутки, десятки килобайт за месяц — даже скромная карта пустеет дольше, чем живёт проект.
Политика отказов вписывается в тот же скетч без новых механизмов: если SD.open вернул false, замер пропускается, lastRow уже обновлён, и попытка повторится через минуту. Журнал с минутной дырой лучше платы, зависшей в бесконечном цикле: дыру видно по меткам и её размер известен точно, а зависание прячет и дыру, и всё, что после неё. Счётчик пропусков в Serial закрывает вопрос честности: если за неделю пропущено две минуты — журнал можно использовать как есть, если двести — впереди разбор питания, а не графики.
Колонки заполняются сырыми числами или преобразованными — вопрос, который всплывает на второй неделе. Я кладу в строку оба: отсчёт АЦП и рядом градусы, пересчитанные той же формулой, что живёт в скетче. Плюс всплывёт позже сам: пересчёт не теряется вместе с версией прошивки, а подозрение на кривую калибровку проверяется по сырой колонке прямо в таблице. Цена — пятнадцать байт на строку. Совет писать только готовые градусы работает ровно до первого сомнения в формуле: когда в журнале одни градусы, спорить с ними нечем, и пересчёт с нуля стоит перепрошивки на объекте.
Граница метода честная: когда интервал падает до десятков миллисекунд, open и close на каждый замер начинают съедать заметную долю бюджета, и логгер переходит на долгий файл с редким flush и пакетной записью — её механику с ценой по строкам разбирал в уроке про буфер и пакетную запись. Для датчика температуры, освещённости, уровня — всего, что меняется за секунды и минуты, — короткие сессии остаются самым надёжным дефолтом.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10;const int LED_WR = 9; // окно записи; 13 занят SPIconst unsigned long INTERVAL = 60000UL; // замер раз в минуту File logFile;unsigned long lastRow = 0; void setup() { Serial.begin(9600); pinMode(LED_WR, OUTPUT); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); }} void loop() { unsigned long ms = millis(); if (ms - lastRow < INTERVAL) return; lastRow = ms; logFile = SD.open("log.csv", FILE_WRITE); if (!logFile) { Serial.println("открытие не удалось, замер пропущен"); return; } digitalWrite(LED_WR, HIGH); if (logFile.size() == 0) { logFile.println("sec,lux"); // шапка только пустому файлу } logFile.print(ms / 1000); logFile.print(","); logFile.println(analogRead(A1)); logFile.close(); // close обновил длину в каталоге digitalWrite(LED_WR, LOW); Serial.print("строка: "); Serial.print(ms / 1000); Serial.print(","); Serial.println(analogRead(A1)); // тот же лист - в Serial}В модели нажмите кнопку и посмотрите на первые две строки: шапка не пишется, потому что буфер байтов уже не пуст — это и есть проверка size() на плате. Каждая следующая строка закрывается собственной сессией, счётчик байтов растёт ровно на длину строки с переводом: 1203,47 — восемь знаков плюс запятая-разделитель, ничего лишнего. Остановите лог кнопкой и запустите снова — журнал продолжится с того же места без второй шапки, в отличие от трёх файлов из начала урока.
Чего модель не показывает — так это цены промаха в обратную сторону. Если держать файл открытым и забыть close, счётчик байтов на плате растёт, а каталог карты стоит: при обрыве питания неделя чисел превращается в призрака. Правило простое до занудства: одна строка — одна сессия: вспышка горит — карту в этот момент не трогать вовсе. А колонку метки для архива с календарём ставит следующий урок — там в журнал въедет настоящая дата.
| Вариант метки | Что даёт | Цена | Когда брать |
|---|---|---|---|
| millis()/1000 | секунды от включения, монотонные | отсчёт с нуля после перезапуска | тесты, счётчики, станки с постоянным питанием |
| номер строки | порядок измерений | ни длительности, ни момента | черновой лог на отладке |
| unixtime | 10 цифр, арифметика вычитанием | человеку нечитаем | графики и обработка программой |
| 2025-07-24 21:05:33 | читаемый календарь | 19 символов на строку | архив с RTC — тема следующего урока |
- 1. Когда лог CSV-лога должен писать шапку?
- 2. Режим FILE_WRITE открывает файл...
- 3. Что даёт close после каждой строки лога?
- 4. После перезапуска платы колонка millis()/1000 в журнале...
- 5. В поле значения может прилететь запятая. Что делать со строкой CSV?
- Шапка пишется один раз по условию size() == 0: безусловная шапка при каждом запуске влезает в середину журнала.
- Одна строка на измерение: поля через запятую без пробелов, запятая внутри поля — кавычки или другой разделитель.
- Миллис-метка millis()/1000 монотонна и удобна для арифметики; после перезапуска отсчёт с нуля — календарь даёт только RTC.
- Сессия открыт-дописал-закрыл на каждый замер стоит миллисекунды и ограничивает сбой питания одной строкой.
- Светодиод окна записи и дубль строки в Serial — бесплатный полевой контроль того, что и когда уехало на карту.
Частые вопросы по теме урока
- Когда лог CSV-лога должен писать шапку?
- Шапка привязана к пустому файлу: size() равен нулю — файл свежий, пишем заголовок. Безусловная шапка при каждом запуске дописывается в середину журнала, потому что FILE_WRITE ставит курсор в конец. Когда size() открытого файла равен нулю
- Режим FILE_WRITE открывает файл...
- FILE_WRITE ставит курсор записи в конец существующего файла. Это удобно для журнала и коварно для безусловной шапки — она окажется в середине записей. С курсором в конце: дозапись, а не перезапись
- Что даёт close после каждой строки лога?
- Close дожимает буфер и обновляет длину в каталоге. После него компьютеру видна ровно записанная часть, а сбой питания ограничен одной недописанной строкой. Обновляет длину файла в каталоге: обрыв питания стоит одной строки
- После перезапуска платы колонка millis()/1000 в журнале...
- millis считает от включения, перезапуск обнуляет счёт. Для календаря нужен RTC с батарейкой — без него метка честна, но дата в журнале не появится. Начнётся с нуля
- В поле значения может прилететь запятая. Что делать со строкой CSV?
- Формат, который режется по запятой, обязан уметь кавычки вокруг полей — или не пускать запятые в поля вовсе. Иначе разбора строки сломается на первом же значении с запятой. Поле в кавычки или сменить разделитель на точку с запятой