Лог с колонкой sec вёл себя безупречно, пока плата не перезагрузилась от просадки питания в грозу. Секунды пошли с нуля, и в файле образовался второй «первый день» — числа правильные, а хронология слиплась: понять, что было до грозы и после, можно только догадываясь по характеру кривой. Спросить у такого журнала «что было в четверг в восемь вечера» нельзя в принципе: у него нет календаря. Миллис-метка честна, но она считает от включения, а для архива нужна дата, которая переживёт и перезагрузку, и полную пропажу питания.
Здесь в журнал въезжает часы реального времени. Штамп из DS3231 делает каждую строку самодостаточной: дата, время, значение — и строку можно переслать, напечатать и понять без контекста. Имя файла собирается из даты — один день равен одному файлу, и «четверг» перестаёт быть позицией в общем журнале, превращаясь в имя на карте. Ротация по суткам — переключение файла в полночь — собирается из тех же элементов и занимает в скетче дюжину строк.
Время в модуль ставится один раз при прошивке — порядок действий расписан в уроке про первую установку времени. Скелет строки со штампом DS3231 и именем по дате уже разбирался в уроке про DS3231-штампы — здесь добираю ротацию по суткам и практику архива. Точность и дрейф обоих распространённых чипов сравнивал в отдельном уроке, ниже только паспортные цифры, на которые опирается расчёт.
Штамп из DS3231: три формата одной строки
rtc.now() возвращает DateTime с полями year, month, day, hour, minute, second — из них собирается штамп вида 2025-07-24 21:05:33. Ключевая деталь — ведущие нули: месяц 7 без нуля портит и сортировку, и вид; сборка через print с проверкой «меньше десяти — добавь ноль» занимает три строки. Формат с датой в порядке от крупного к мелкому сортируется как обычный текст, и Excel без всякого мастера импорта понимает его как дату со временем — это тот же принцип, что и у шапки латиницей из урока про экспорт: формат, который выживает в чужих руках.
Паспортная точность DS3231 — плюс-минус 2 ppm по температурно-компенсированному кварцу, это около минуты в год; батарейка CR2032 держит часы годами без основной платы, и после отключения питания штамп продолжается с той же секунды. Для сравнения: дешёвый DS1307 без компенсации уводится на минуты за месяц — на неделю лога это терпимо, на год уже нет. Часам нужен один бонус — правильное время при первой прошивке, дальше они молча ведут счёт сами.
Про батарейку — пара цифр для спокойствия. В хранении DS3231 тянет из CR2032 два-три микроампера, и паспортных двухсот двадцати миллиампер-часов хватает на десяток лет счёта без основной платы; моя старейшая связка встречает седьмую зиму на заводской банке. Проверка без выпайки: мультиметр на держателе при снятой плате — свежая CR2032 даёт около 3,0 вольта, а всё, что ниже 2,7, я меняю не дожидаясь штампов из 2000 года.
Второй формат — unixtime: одно число, секунды с 1970 года. Человеку он нечитаем, машине удобен: арифметика «сколько прошло» — одно вычитание, сортировка — по одному числу, а плоттер ест колонку без преобразований. Практика, к которой я пришёл: в строке живут оба штампа, текстовый — человеку и Excel, unix — программе обработки. Строка из двух штампов и значения выходит около 35 байт; при минутном интервале это 56 КБ в сутки — бюджет, который посчитан в уроке про неделю погоды.
Файл с датой в имени и полночь
Имя из даты собирается по тем же полям: LOG0724.CSV — месяц и день двумя парами цифр, год живёт в штампе каждой строки, поэтому в имени он лишний. Такой формат — классический 8.3: три буквы расширения, восемь знаков имени, и он проходит через любой каталог, любую старую карту и любой картридер. Первая грабля — запрещённые символы FAT: слэш и двоеточие в имени появятся сами собой, если скопировать в него текстовый штамп, и open молча вернёт false без всякого сообщения. В имени файла — только цифры, буквы и дефис, точка одна.
В имени без года прячется бомба отсрочки: файл LOG0724 следующего года откроется не новым, а старым — FILE_WRITE встанет в его конец, size() окажется не нулём, шапка не напишется, и июльский журнал ляжет поверх июльского же годичной давности. Я поймал это на 367-е сутки непрерывной работы дачного логгера: два года кривых лежали в одном файле, и Excel рисовал в июле скачок на полгода. Лечение из одной строки: две цифры года в имя — L250724.CSV, семь знаков, всё ещё честный 8.3, — либо перенос архива с карты при смене года.
Ротация по суткам — это слежение за днём месяца: перед каждой записью сравниваем now.day() с днём открытого файла; сменился — значит полночь, закрываем старый файл, собираем новое имя, открываем новый, пишем в него шапку. Один день равен одному файлу: сбой тянет максимум сутки, поиск «четверга» — открытие одного файла вместо прокрутки недели. Ротацию по размеру разбирал в уроке про ротацию логов — скелет тот же самый, меняется только условие переключения, поэтому оба вида ротации живут в одном скетче без конфликта.
Порядок проверки важнее самой ротации: день сравнивается до записи строки, а не после. Полночь, попавшая на замер, дописывает строку в старый файл, и новый открывается на следующей минуте — потеря нулевая. При обратном порядке строка полночи уезжает в файл уже вчерашнего дня, и граница суток плывёт на один замер. Механику переоткрытия — FILE_WRITE сам встанет в конец после return — описывал в уроке про сессии файла; здесь то же свойство честно работает на ротацию.
В шапку суточного файла я пишу вторую строку — маркер причины открытия: start, midnight или swap. Стоит тридцать байт и один if, а при разборе архива объясняет половину странностей: дыра на стыке двух файлов — это полночь, файл с маркером start посреди дня — перезагрузка или подача питания, и виноватого в журнале искать не нужно. Формат маркера держите латиницей и без запятых — тот же договор, что и у шапки: Excel покажет его текстом и не перемешает с числовой колонкой.
#include <SPI.h>#include <SD.h>#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int CS_PIN = 10; File logFile;int logDay = 0;unsigned long lastRow = 0; String makeName(DateTime t) { String m = String(t.month()); if (t.month() < 10) m = "0" + m; String d = String(t.day()); if (t.day() < 10) d = "0" + d; return "LOG" + m + d + ".CSV"; // 8.3: цифры, буквы, одна точка} void openDayFile(DateTime t) { if (logFile) logFile.close(); logFile = SD.open(makeName(t), FILE_WRITE); if (logFile && logFile.size() == 0) { logFile.println("stamp,unix,value"); // шапка каждому суточному файлу } logDay = t.day(); Serial.print("пишу в "); Serial.println(makeName(t));} void setup() { Serial.begin(9600); if (!rtc.begin()) { Serial.println("часы не поднялись"); while (true) delay(500); } if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } openDayFile(rtc.now());} void loop() { DateTime now = rtc.now(); if (now.day() != logDay) openDayFile(now); // полночь: смена ДО записи unsigned long ms = millis(); if (ms - lastRow < 60000UL) return; lastRow = ms; if (!logFile) return; logFile.print(now.year()); logFile.print("-"); if (now.month() < 10) logFile.print("0"); logFile.print(now.month()); logFile.print("-"); if (now.day() < 10) logFile.print("0"); logFile.print(now.day()); logFile.print(" "); if (now.hour() < 10) logFile.print("0"); logFile.print(now.hour()); logFile.print(":"); if (now.minute() < 10) logFile.print("0"); logFile.print(now.minute()); logFile.print(","); logFile.print(now.unixtime()); logFile.print(","); logFile.println(analogRead(A1)); logFile.flush();}В модели держите кнопку нажатой: минуты улетают, и на 00:00 светодиод вспыхивает, старый файл закрывается, новый открывается с шапкой — три строки монитора на одно событие, ровно как в скетче на плате. Обратите внимание, что строка полночи уехала в старый файл до переключения: порядок «сначала сравнить день, потом писать» виден в мониторе напрямую. Отпустите кнопку — журнал спокойно пишется в один файл до следующих суток.
Год на карте из суточных файлов превращается в триста шестьдесят пять имён в корне — уже не журнал, а свалка, в которой при длинах длинных имён кончается каталог раньше места. Лечение — папки по месяцам: 2025/07/LOG0724.CSV собирается из тех же полей DateTime, каталог создаётся mkdir-ом при смене месяца, и корень держит дюжину папок вместо сотен файлов. Механику создания папок разбирал в уроке про папки и структуру — здесь добавлю только порядок: месяц проверяется вместе с днём, одной проверкой до записи, и папка успевает создаться до первого файла месяца.
Граница метода: связка DS3231 и карты решает календарь на одной плате без сети. Если логгер живёт на ESP32 и Wi-Fi в доме есть, время можно подтягивать по NTP и обходиться без чипа часов — синхронизацию разбирал в уроке про NTP и RTC. Но для дачи, гаража и любых мест с пропадающей сетью чип за копейки и батарейка на годы остаются самым предсказуемым вариантом: штамп продолжается с той же секунды, что и был до обрыва.
Без часов на плате ротация по суткам тоже возможна, только без календаря: счётчик переполнившихся суток от millis — 86 400 секунд на день, имя файла LOGD03.CSV с номером дня от запуска. После перезагрузки счёт пойдёт заново, и файлы придётся различать номером включения — честная плата за отсутствие чипа. Журнал со днями без дат отвечает на вопрос «сколько продержался», но не на вопрос «когда это было» — вес аргумента в пользу чипа за копейки и батарейки на годы.
| Штамп в строке CSV | Длина | Кому удобен | Подводный камень |
|---|---|---|---|
| millis()/1000 | 5-7 цифр | плоттеру, коротким тестам | после перезапуска отсчёт с нуля |
| unixtime | 10 цифр | обработке программой | человек не читает без конвертера |
| 2025-07-24 21:05:33 | 19 символов | человеку и Excel | требует RTC с батарейкой |
| unix + текст рядом | около 30 символов | архиву на годы | строка длиннее, зато полная |
- 1. Паспортная точность DS3231 — плюс-минус 2 ppm. Это отклонение примерно...
- 2. Имя файла на 24 июля в формате 8.3 — это...
- 3. open с именем, где затесалось двоеточие, вернёт...
- 4. Полночь попала на минуту записи. Правильная ротация сделает что?
- 5. Зачем в одной строке два штампа — текстовый и unixtime?
- Штамп 2025-07-24 21:05:33 собирается из DateTime с ведущими нулями и сортируется как текст без мастера импорта.
- DS3231 уводится на минуту в год и живёт на CR2032 годами: обрыв питания штамп не рвёт.
- Имя файла LOG0724.CSV в формате 8.3: только цифры, буквы и дефис — двоеточие и слэш дают молчаливый false от open.
- Ротация по суткам: сравнение now.day() до записи, смена файла в полночь стоит дюжину строк скетча.
- Двойной штамп — текст для человека, unixtime для программы: строка около 35 байт, минутный интервал съедает 56 КБ в сутки.
Частые вопросы по теме урока
- Паспортная точность DS3231 — плюс-минус 2 ppm. Это отклонение примерно...
- Два промилле на миллион — это около 63 секунд в год: температурно-компенсированный кварц DS3231 уводится примерно на минуту за год, в отличие от некомпенсированного DS1307 с минутами за месяц. минута за год
- Имя файла на 24 июля в формате 8.3 — это...
- Восемь знаков имени, точка, три знака расширения: LOG0724.CSV. Год живёт в штампе каждой строки, слэшей и точек сверх одной формат не терпит. LOG0724.CSV
- open с именем, где затесалось двоеточие, вернёт...
- Двоеточие и другие запрещённые символы FAT дают тихий отказ: open возвращает false, и если результат не проверять, скетч продолжит писать в никуда. Имя собирайте только из цифр, букв и дефиса. false молча, без сообщения в мониторе
- Полночь попала на минуту записи. Правильная ротация сделает что?
- День сравнивается до записи: строка полночи дописывается в файл вчерашнего дня, новый открывается на следующей минуте. Потеря нулевая, граница суток чистая. Допишет строку в старый файл, новый откроет на следующем замере
- Зачем в одной строке два штампа — текстовый и unixtime?
- Текстовый штамп читается глазами и Excel без мастера, unixtime — одно число для арифметики и сортировки программой. Строка толще на десяток байтов, зато архив удобен обеим сторонам. Человеку читается текст, машине удобен unix