Погодный логгер на даче писал температуру на карту каждый час и делал это три месяца исправно. А потом мне понадобилось узнать, когда именно в оранжерее случился тот самый перегрев, — и выяснилось, что в журнале времени нет. Вместо него была колонка с номером строки: сорок вторая запись, сорок третья… Восемьдесят шесть строк в сутки, две с половиной сотни файлов за три месяца — и ни одного ответа на вопрос «когда». Второй мой логгер был умнее: ставил в строку millis() в секундах. До первой грозы. Плата перезапустилась, счётчик обнулился, и в файле рядом оказались запись «двенадцать миллионов секунд» и запись «четыре секунды» — обе про один и тот же вечер.
Оба тупика лечатся одним и тем же: штамп для журнала должен приходить из часов с батарейкой. DS3231 после любого отключения питания продолжает тикать, а millis() — счётчик с последнего запуска, у которого нет ни календаря, ни памяти; эту разницу я развёл по полочкам в уроке про millis и RTC. Сегодня собираем строку, которая соединяет всё сразу: штамп в формате YYYY-MM-DD HH:MM:SS, запятая, показание датчика. Такую строку открывают и Excel, и любой текстовый редактор, и через полгода она всё ещё отвечает на вопрос «когда».
Журнал — это таблица, и столбец времени в ней главный: без него нет ни сравнения записей, ни графика по осям, ни ответа на вопрос «что произошло раньше». Формат CSV выбран за то, что его понимают все подряд: Excel и LibreOffice импортируют его без настроек, скрипт на ноутбуке читает построчно, а блокнот на телефоне — просто показывает. Первый столбец такой таблицы — штамп, остальные — столбцы данных, и сколько бы вы ни добавили датчиков, первые девятнадцать символов каждой строки остаются неизменными.
Штамп, который сортируется сам
Формат 2025-11-04 09:07:03 выбран не для красоты. В нём поля идут от крупного к мелкому и каждое дополнено нулями до фиксированной ширины, поэтому обычная строковая сортировка файла расставляет строки в хронологическом порядке — никакой обработки дат не нужно: сортировка по тексту уже сортировка по времени. Это стандарт ISO 8601, и как только внутри формата появляется односимвольный месяц «9» вместо «09», сортировка ломается: «9:7» встанет позже «10:6». Та же беда с человеческим глазом — журнал из строк вида 2025-9-4 9:7:3 читается мучительно. Паддинг делает крошечная функция pad2: меньше десяти — приклей ноль спереди.
Штамп собирается из шести чисел объекта DateTime: год четырьмя цифрами без паддинга, остальным pad2. Объект снимается из rtc.now() один раз на строку — если дёрнуть now() в середине сборки, в одном штампе могут встретиться разные секунды. Запятая после штампа — разделитель CSV: первая колонка время, дальше столбцы данных. С числовой альтернативой — unixtime, секундами от 1970 года, — я работал в уроке про разбор DateTime: для арифметики интервалов она удобнее, но в человекочитаемый журнал идёт именно строка ISO.
#include <Wire.h>#include <RTClib.h>#include <SPI.h>#include <SD.h> RTC_DS3231 rtc;const int CS_PIN = 10; String pad2(int v) { if (v < 10) return "0" + String(v); return String(v);} String stamp() { DateTime now = rtc.now(); // один снимок на строку String s = ""; s += now.year(); s += "-"; s += pad2(now.month()); s += "-"; s += pad2(now.day()); s += " "; s += pad2(now.hour()); s += ":"; s += pad2(now.minute()); s += ":"; s += pad2(now.second()); return s;} void setup() { Serial.begin(9600); rtc.begin(); if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } SD.begin(CS_PIN); // CS на 10-м пине, шина SPI} void loop() { float t = 21.3; // сюда читайте свой датчик File f = SD.open("log.csv", FILE_WRITE); if (f) { f.print(stamp()); f.print(","); f.println(t); f.close(); // буфер дошёл до карты } else { Serial.println("kartata ne otkrylas"); } delay(60000UL); // строка раз в минуту}Сборка строки — самое дешёвое место для граблины с памятью. Класс String на Uno живёт в куче: каждая конкатенация создаёт новую строку, и длинные цепочки сложений фрагментируют два килобайта ОЗУ быстрее, чем кажется. У меня два рабочих варианта. Первый — тот, что в скетче: одна функция stamp() собирает строку целиком и отдаёт её, и при строке раз в минуту куча не задыхается. Второй — вообще без String: печатать по частям прямо в файл, f.print(now.year()) и pad2-помощник между ними, — байты в файле получаются те же, а куча не трогается вовсе. В симуляторе я собираю строку в переменную, чтобы показать её в мониторе одной строкой. Экономная классика snprintf с форматом %02d на плате тоже живёт, но в движок симулятора printf не завезли — поэтому в скетче его нет.
Считаем грузопоток. Штамп — девятнадцать символов, запятая и показание — итого строка порядка двадцати пяти байт. Раз в минуту — тридцать шесть килобайт в сутки; двухгигабайтная карта при таком темпе не заполнится и за век, так что узкое место не объём, а дисциплина записи. Распиновку SPI-модуля microSD (CS на десятом пине, питание, MISO/MOSI/SCK) я не тяну сюда по полной — она разобрана в уроке про подключение SD-модуля, а механику первого открытия файла и библиотеку SD — в уроке про логирование на SD. Здесь моё — штамп и ритм.
Имя файла оставляйте постоянным: при открытии с FILE_WRITE библиотека дописывает в конец существующего файла, и журнал живёт неделями в одном log.csv — все режимы открытия разобраны в уроке про FILE_WRITE. Если хочется ротации по месяцам, собирайте имя из года и месяца перед открытием. И не питайте модуль карты от пина 3,3 В платы: у Uno там всего пятьдесят миллиампер, а запись — пик потребления; от слабого питания у меня выходили битые строки раз в несколько дней, пока модуль не переехал на пять вольт со штатного стабилизатора.
Тему close() я вынес в отдельную заметку ниже, потому что именно на ней обычно и горят. Скольким своим ранним журналам я обязан этой граблиной: файл открыт в setup, строки пишутся в буфер, карта получает их порциями, а после случайного сброса последняя порция пропадает вместе с плавающим указателем.
| Штамп | Сортируется | Переживает сброс | Размер |
|---|---|---|---|
| 2025-11-04 09:07:03 | лексикографически, то есть хронологически | да — от RTC с батарейкой | 19 символов |
| unixtime 1741429805 | как число | да | 10 символов |
| millis() = 123456 | по числу, но только внутри одного запуска | нет — обнуляется | до 10 символов |
| номер строки 42 | как число | да, но даты в нём нет | переменный |
Симулятор: строка в Serial вместо карты
SD-карту движок симулятора не поддерживает, и я не стал прятать логику за пустым экраном: скетч собирает ровно ту же строку со штампом, которая на плате ушла бы в log.csv, и печатает её в Serial-монитор. Отправьте число вроде 23.7 — в ответ придёт готовая строка журнала: штамп, запятая, значение. Вторая честная оговорка: в симуляторе время после запуска стоит на месте — один запуск равен одному кадру, поэтому все строки одного прогона получат одинаковый штамп. На настоящей плате между строками проходят настоящие минуты, и журнальная колонка времени живёт своей жизнью. Протокол и формат в симе те же, что в коде для карты, — меняется только назначение печати.
Значение читается функцией readStringUntil до перевода строки и превращается в число через toFloat — такой протокол с монитором порта я подробно разбирал в уроке про команды через Serial. Попробуйте послать нечисловой мусор: toFloat молча вернёт ноль, и в журнал уедет строка с нулём вместо показания. На плате это повод проверять ввод, а не верить ему — хотя бы диапазоном, как я делал с температурными датчиками в журнальных проектах.
- Отправьте 23.7 и сверьте строку с форматом: девятнадцать символов штампа, запятая, два знака значения
- Поменяйте месяц и день в rtc.adjust и запустите заново: штамп обновится целиком — так вытащить журнал можно любой датой
- Уберите pad2 из сборки минуты и пришлите значение — строка станет на символ короче, и подумайте, как такая колонка отсортируется в Excel
- Добавьте второй колонкой rtc.getTemperature() — в соседнем уроке объясняю, что это за термометр и зачем он в журнале
Полгода спустя журнал должен отдавать ответы за секунды. ISO-штамп позволяет находить весь день одной строкой фильтра: записи 4 ноября начинаются с «2025-11-04», и поиск по подстроке выхватывает их подряд, без разбора дат. В Excel колонку времени импортируют как текст и сортируют без настроек формата — порядок уже правильный. Я держу в конце каждого журнала строку с числом записей и версией скетча: она пишется при каждом старте, и по плотности таких строк видно, сколько раз плата перезагружалась — гроза, зависание, моё вмешательство. Тот же приём годится для календарной логики из урока про праздники: каждое срабатывание по дате — ещё одна строка со штампом, и за месяц видно, как часто реально стреляет ваша логика, а не как часто вам кажется.
- Штамп YYYY-MM-DD HH:MM:SS сортируется строковой сортировкой как хронология: нули в паддинге — это не украшение, а условие сортируемости
- pad2 — единственный помощник, которого не хватает RTClib для готовой строки: год без паддинга, остальным — ведущий ноль
- now() снимайте один раз на строку журнала: два вызова — и в одном штампе разные секунды
- close() после каждой записи сбрасывает 512-байтовый буфер на карту; открытый навсегда файл теряет хвост при сбросе
- millis-метка обнуляется при перезагрузке, RTC-штамп переживает любую грозу: в журнал идёт только второй
- 1. Почему метка из millis() не годится для журнала на SD-карте?
- 2. Что даёт формат 2025-11-04 09:07:03 при сортировке файла?
- 3. Сколько символов в штампе 2025-11-04 09:07:03?
- 4. Зачем закрывать файл после каждой строки журнала?
- 5. Что симулятор печатает вместо записи на SD-карту?
Частые вопросы по теме урока
- Почему метка из millis() не годится для журнала на SD-карте?
- millis() — счётчик с последнего включения. Один сброс питания — и после строки «двенадцать миллионов секунд» идёт строка «четыре секунды». Календарного вопроса он не решает вообще. millis слишком медленная
- Что даёт формат 2025-11-04 09:07:03 при сортировке файла?
- Поля идут от крупного к мелкому с ведущими нулями, поэтому лексикографический порядок совпадает с хронологическим — Excel и текстовые редакторы сортируют журнал без разбора дат. Строки расставляются хронологически обычной текстовой сортировкой
- Сколько символов в штампе 2025-11-04 09:07:03?
- Четыре цифры года, три раза по две с дефисами, пробел и три пары с двоеточиями — ровно девятнадцать символов фиксированной ширины. 19
- Зачем закрывать файл после каждой строки журнала?
- Запись буферизуется блоками по 512 байт. close() сбрасывает буфер; открытый навсегда файл при перезагрузке теряет недописанный хвост и часть журнала. Чтобы буфер записи гарантированно дошёл до карты до сброса питания
- Что симулятор печатает вместо записи на SD-карту?
- Движок SD не поддерживает, поэтому строка того же формата уходит в монитор порта: протокол и формат штампа совпадают с кодом для карты. Ту же строку со штампом в Serial-монитор