Логгер в гараже писал температуру три недели. Открываю файл — а сопоставить записи с событиями не выходит: колонка sec считала секунды с включения, счётчик обнулялся при каждом моргании питания, и три куска журнала начинались с sec=0. Какой кусок раньше — загадка без календаря. Millis — секундомер, не календарь: он честно отвечает на вопрос «сколько уже работает», но на «когда это было» не отвечает вовсе. Двойной сбой за один вечер я поймал лично: пропало питание в 23:58, плата перезапустилась в полночь, и две записи от соседних дней легли в одну кучу с одинаковыми номерами секунд. Для разбора многодневного журнала нужна дата, и не где-нибудь на шапке файла, а в каждой строке: шапку ещё надо дотащить до строки, а после перезапуска она врёт.
Часы DS3231 закрывают дыру: модуль тикает от собственной батарейки CR2032, переживает выдернутый шнур питания и по I2C на адресе 0x68 отдаёт готовую дату с секундами. Базовую штамповку я раскладывал в уроке про штампы времени — там же про то, почему плата без часов после рестарта думает, что на дворе 1970 год. Здесь иду дальше, в два места, где время работает на весь журнал сразу: в каждую строку и в имя файла. Оба места устроены проще, чем принято думать, и оба ломаются об одни и те же грабли — про них честно ниже. И оба требуют ровно одного модуля за копейки: DS3231 стоит меньше обеда, а сэкономленные им часы разбора окупают его в первый же месяц.
Строка со временем: девятнадцать символов, которые окупаются
Штамп вида 2025-01-15 14:07:32 стоит ровно 19 символов: дата — десять, пробел, время — восемь. Собирается он одним вызовом: snprintf со строкой формата %04d-%02d-%02d %02d:%02d:%02d и полями DateTime — year, month, day, hour, minute, second. Нули в полях обязательны, и это не косметика: сортировка по алфавиту совпадает с сортировкой по времени только при одинаковой ширине полей. Без нулей «1:7:3» и «11:12:9» сортируются как строки, январь перепутывается с ноябрём, и хронология рассыпается ровно в тот момент, когда вы начнёте её читать.
Есть и компактный вариант: unixtime — секунды с 1970 года, десять цифр вместо девятнадцати символов, один шаг от DateTime через unixtime(). Для арифметики он удобнее на порядок: разность двух чисел даёт интервал без всякого парсинга, среднее за час считается в уме. Для человеческого взгляда — хуже: 1736941652 не скажет «в субботу вечером». Я обычно кладу человекочитаемый штамп, а unixtime держу про запас; что с ним можно посчитать и почему он такой короткий, разобрано в уроке про unixtime.
Компромисс для тесных строк — штамп без даты: 14:07:32 — восемь символов, а сутки несёт имя файла LOG0115. Экономия честная: день и так виден по имени, дублировать его в каждой строке незачем. Я всё же плачу за полный штамп: карты не вечны, файлы копируются поодиночке и теряются, и строка с полной датой остаётся опознаваемой даже в отрыве от имени файла. За сутки при интервале в секунду полный штамп добавляет чуть меньше мегабайта — цена терпимая.
Имя файла из даты: LOG0115.CSV
FAT хранит имена в формате 8.3: до восьми символов имени, точка, три на расширение. Из даты выжимается LOG0115: LOG плюс две цифры месяца плюс две цифры дня — семь знаков, влезает с запасом в один. Полная дата не влезает: LOG20250115 — это одиннадцать символов, карта такой файл либо не создаст, либо обрежет по-своему. Год при этом не теряется — он есть в штампе каждой строки, а если захочется порядка по годам, папку на год заведёт тот же приём, что и с именами: подробный разбор корня и правил — в уроке про 8.3.
Каждые сутки — новый файл, и имя назначает сам DateTime: сменится день — сменится имя, дописывать в чужой файл не понадобится. Бонусы идут в нагрузку: свежие записи не смешиваются со старыми, размер одного файла держится в разумных сотнях килобайт, а вынутая карта читается сверху вниз по именам — хронология видна ещё до открытия. Когда записи в один файл всё-таки нужны дольше суток, наступает черёд append и ротации — это разобрано в уроке про дописывание, здесь достаточно имени из даты.
Есть и другая выгода имени из даты — поиск нужного дня глазами. Двадцать файлов на карте выстраиваются списком по алфавиту, а алфавит здесь и есть календарь: LOG0108 лежит перед LOG0115. Чтобы найти ночь аварии, открывать каждый файл не нужно — список имён уже рассказывает историю. С папками на месяц это масштабируется: корень карты не превращается в свалку из сотен файлов, а целые месяцы переносятся на компьютер одним движением.
Почему время спасает разбор
Разбор журнала — это почти всегда сопоставление: температура скакнула в 14:07:32, а в 14:07:30 щёлкнуло реле двери. Без календарного штампа такую связь не восстановить даже с журналом в блокноте — секундомер с включения не имеет привязки к ночи, выходным и перезагрузкам. С штампом файл сортируется сам, столбец времени в Excel сразу строит ось графика, а логи двух устройств с двумя DS3231 склеиваются в одну хронологию без единого скрипта. Мне хватило одного случая: журнал обогрева теплицы и запись энергопотребления легли рядом по штампам, и причина ночного холода нашлась за минуту — насос перезапустился в 03:12 и до утра не включался. Штамп отбирает у такой работы всю магию: совпадения по времени находятся поиском по колонке, а не прокруткой двух окон со сверкой на глаз. Бонус для отладки: таймштамп в Serial и таймштамп в файле собираются одной и той же строкой формата, и событие ищется одним числом в обоих местах — расхождение между ними само становится симптомом.
Пара слов о практической обвязке. Интервал в одну секунду — золотая середина для погодных журналов: строка в 26 байт копится в буфере библиотеки и уезжает на карту раз в десятки секунд, флеш-память живёт спокойно. Цена частых записей — отдельный разговор, он ждёт в уроке про скорость записи, там же про паузы, которых не видно в Serial. Второй канал дублируйте: те же строки, что уходят в файл, печатайте в Serial — всегда видно, что именно пишет логгер прямо сейчас, а порт с логом в Excel становится второй копией архива, пока карта добирается до компьютера. И приучите скетч печатать имя текущего файла при старте: через полгода, вынимая карту, вы скажете себе спасибо. Интервал выбирайте по скорости процесса, а не по смелости: температура в теплице меняется за минуты — секунда избыточна, десяток секунд достаточен; вибрация и пульс требуют миллисекунды, и это уже разговор про скорость записи. Лишние строки стоят байтов, а байты — места на карте.
#include <SPI.h>#include <SD.h>#include <RTClib.h> RTC_DS3231 rtc;File logFile;unsigned long lastLog = 0;char fileName[13]; // LOG0115.CSV плюс нуль-терминатор void setup() { Serial.begin(9600); if (!rtc.begin()) { Serial.println("DS3231 не отвечает: проверьте SDA/SCL"); while (1); } if (rtc.lostPower()) { rtc.adjust(DateTime(__DATE__, __TIME__)); // грубая установка при первом пуске } if (!SD.begin(10)) { Serial.println("SD.begin не прошёл: карта, проводка или CS=10"); while (1); }} void loop() { if (millis() - lastLog < 1000) return; lastLog = millis(); DateTime now = rtc.now(); // имя из даты: LOG + месяц + день; сменились сутки - новое имя snprintf(fileName, sizeof(fileName), "LOG%02d%02d.CSV", now.month(), now.day()); if (!SD.exists(fileName)) { logFile = SD.open(fileName, FILE_WRITE); if (logFile) { logFile.println("date,time,temp_C"); // шапка нового файла logFile.close(); Serial.print("новый файл: "); Serial.println(fileName); } } char stamp[20]; snprintf(stamp, sizeof(stamp), "%04d-%02d-%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()); float temp = 22.4; // здесь показания вашего датчика logFile = SD.open(fileName, FILE_WRITE); if (logFile) { logFile.print(stamp); logFile.print(","); logFile.println(temp, 1); logFile.close(); } else { Serial.println("файл не открылся: карта вынута или переполнена"); }}| Вариант штампа | Цена в строке | Что даёт и где ломается |
|---|---|---|
| Строка 2025-01-15 14:07:32 | 19 символов | читается глазами и Excel-ом; самый толстый вариант — 19 байт против 10 |
| unixtime 1736941652 | 10 цифр | интервалы считаются вычитанием; человеку нужен переводчик |
| Секунды с включения | 5-6 символов | самый экономный; обнуляется при рестарте — многодневный разбор встаёт |
| Имя файла LOG0115.CSV | 0 в строке | сутки видны до открытия файла; внутри суток не заменяет штамп |
- 1. Сколько символов занимает человекочитаемый штамп 2025-01-15 14:07:32?
- 2. Почему имя файла собирают как LOG0115, а не LOG20250115?
- 3. Колонка sec обнулилась после перезапуска. Что даст сквозную хронологию?
- 4. Что выгоднее держать в строке, если планируете считать интервалы?
- 5. Какой вызов в setup предупредит, что часы сбросились?
- Штамп из 19 символов собирает snprintf из полей DateTime; нули в полях обязательны, иначе сортировка рассыпается
- Имя файла из даты: LOG + месяц + день = LOG0115.CSV — влезает в 8.3, каждые сутки новый файл без append
- Millis — секундомер: обнуляется при рестарте и переполняется за 49 дней; календарь даёт только RTC с батарейкой
- rtc.lostPower() и проверка года меньше 2024 в setup ловят часы, сброшенные на 2000-01-01
- Штамп в строке плюс имя из даты: журнал сортируется сам, склеивается с другими устройствами и читается глазами
Частые вопросы по теме урока
- Сколько символов занимает человекочитаемый штамп 2025-01-15 14:07:32?
- Дата — 10 символов, пробел — один, время — 8, итого 19; перевод строки в штамп не входит. Разница с unixtime — 9 символов на каждой строке, за сутки это под половину мегабайта. 19: дата, пробел и время
- Почему имя файла собирают как LOG0115, а не LOG20250115?
- FAT хранит до восьми символов имени и три на расширение; одиннадцать символов файл либо не создаст, либо обрежет. Месяц и день влезают, год живёт в штампе каждой строки. Формат 8.3: имя длиннее восьми символов FAT не сохранит
- Колонка sec обнулилась после перезапуска. Что даст сквозную хронологию?
- Millis — секундомер с включения: у него нет привязки к календарю, и любой рестарт его обнуляет. DS3231 тикает от CR2032 и отдаёт дату с секундами после любого перерыва питания. RTC-модуль с батарейкой: календарный штамп переживает питание
- Что выгоднее держать в строке, если планируете считать интервалы?
- Разность двух unixtime — это уже интервал в секундах, без парсинга. Строковый штамп сначала надо разобрать, а разбор на Arduino — лишние строки кода и лишние байты. unixtime — секунды одним числом
- Какой вызов в setup предупредит, что часы сбросились?
- lostPower() возвращает флаг сброса питания часов; страховкой служит проверка года меньше 2024. Без неё лог молча уйдёт в файлы с датой 2000-01-01. rtc.lostPower()