Термометр в теплице месяц писал в файл голые числа — «23.4», «24.1», «22.8» — и к концу месяца файл стал бесполезен: где здесь утро, где полдень, где тот день, когда полив заливал до луж, теперь не скажет никто. Показания без метки времени — просто числа, а журнал — это всегда момент плюс значение. Сама пара «SD-карта плюс RTC» и хранение меток разобраны в уроке про логгер с метками времени; здесь я вскрываю то, вокруг чего крутится любой такой логгер — саму строку «2025-03-08 14:05:00,23.4»: как она собирается из полей DateTime, почему именно в таком порядке и на чём она ломается.
Порядок год-месяц-день час-минуту-секунды выбран не для красоты. В такой записи поля идут от старшего к младшему, и строка сортируется как обычный текст: сортировка в LibreOffice, Excel или блокноте с плагином выстраивает хронологию без единой формулы. Переверните порядок на «08.03.2025» — и сортировка по-прежнему «работает», но ставит третье число любого месяца раньше первого, и журнал лжёт. Запятая после метки — стандартный разделитель CSV, 19 байт на метку плюс значение; та же строка в человеческом порядке нужна и для суточного архива по слотам, когда считается не по файлу, а по часам суток.
Склейка строки: ведущие нули, буфер, разделители
Готовой функции «отдай мне строку в этом формате» в моей сборке RTClib нет, и полагаться на особенности версий я не хочу — собираю из полей сам, это честно работает везде. Рецепт: год, дефис, месяц двумя цифрами, дефис, день двумя цифрами, пробел, время с двоеточиями, запятая, значение. Ведущие нули решает крошечная функция z2: число меньше десяти — сначала пишем символ ноль, потом само число. Без неё март превратится в «2025-3-8», и хотя такая строка всё ещё сортируется правильно, ширина поля прыгает, а разборщик столбцов у соседа по проекту этого не простит.
Про String скажу с числами, потому что вокруг него много страшилок. На Uno куча — 2 КБ всей памяти вместе с переменными, и каждая склейка строк при перераспределении переезжает по куче, оставляя позади дыры, которые никуда не деваются; при записи раз в 100 мс такие дыры складываются в каскад, и скетч через часы работы ловит каскадное падение. Но строка у меня занимает около 24 символов и рождается раз в минуту — за сутки это 34 КБ в файл, а нагрузка на кучу мизерная, и String здесь безобиден. Порог я провожу так: реже раза в секунду — String, чаще — переносите сборку в char-буфер со snprintf и форматами %02d, там склейка идёт в одном месте стека и кучу не трогает вовсе.
Десятичный разделитель — место, где CSV ломается чаще всего. По-русски число с дробью пишут через запятую, и «23,4» внутри CSV с запятой-разделителем превращает один столбец в два — «23» и «4», причём второй едет по соседним колонкам и ломает таблицу. Моё правило: в файле всегда точка, а если пользователь открывает журнал в русском Excel — либо разделителем назначают точку с запятой, либо импорт через мастер с указанием символа. Точка с запятой в заголовок урока не влезла, но в таблице ниже я свёл варианты, и вы решите сами, чей край вам ближе.
Экранирование в CSV — тема, до которой доходишь после первого чужого журнала. Если в поле затесалась запятая, кавычка или перевод строки, поле оборачивается в двойные кавычки, а кавычки внутри удваиваются — так строка перестаёт рваться на разборщике. Мой столбец sobytie этому научил: отметка «форточка, вторая створка» без кавычек разваливалась на два столбца, и график недели лгал. Но правило чище держать на месте: в своих полях я запятых не выпускаю вовсе, синоним подбирается на стороне записи, и экранирование остаётся делом чужих файлов.
#include <SPI.h>#include <SD.h>#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int CS_PIN = 10;unsigned long last = 0; String z2(int v) { // 5 -> "05" String s = ""; if (v < 10) s = "0"; s = s + v; return s;} String metka(DateTime t) { // 2025-03-08 14:05:00 String s = ""; s = s + t.year(); s = s + "-" + z2(t.month()) + "-" + z2(t.day()); s = s + " " + z2(t.hour()) + ":" + z2(t.minute()) + ":" + z2(t.second()); return s;} void setup() { Serial.begin(9600); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: SDA A4, SCL A5"); while (1) {} } if (SD.begin(CS_PIN) == false) { Serial.println("SD ne startovala: CS na D10"); while (1) {} } if (SD.exists("log.csv") == false) { // шапка пишется один раз File h = SD.open("log.csv", FILE_WRITE); h.println("datetime,temp"); h.close(); } Serial.println("Log na SD: log.csv");} void loop() { if (millis() - last >= 60000) { // строка раз в минуту last = millis(); DateTime now = rtc.now(); float t = rtc.getTemperature(); // датчик кристалла DS3231 String stroka = metka(now) + "," + String(t, 1); File f = SD.open("log.csv", FILE_WRITE); f.println(stroka); f.close(); // закрыли - строка переживает моргание питания Serial.println(stroka); // эхо для контроля на столе } delay(500);}Куда летит строка: файл на плате, Serial в симуляторе
На плате строка уходит в файл, и тут один развилочный вопрос: держать файл открытым всегда или открывать на каждую строку. Я открываю, пишу и закрываю: одна строка раз в минуту — цена операции копеечная, зато при моргании питания теряю максимум ту строку, которую писали в момент выключения. Постоянно открытый файл без закрытия — способ потерять весь хвост буфера, если запись не успела доехать до карты; компромиссный вариант с flush и его цены разобраны в уроке про flush и питание SD. Режимы открытия и поведение файла при повторном FILE_WRITE — отдельная тема, и для этого урока достаточно правила «открыл, написал, закрыл».
Симулятор этого сайта SD-карту не эмулирует вовсе — и я не буду притворяться, что эмулирует. Поэтому сим-скетч печатает те же самые строки в Serial: строитель метки в нём тот же, что уйдёт на плату, только вместо f.println(stroka) стоит Serial.println(stroka). Ползунок A0 изображает датчик: крутите — меняется второй столбец. Кнопка D2 пишет внеплановую строку с третьим столбцом sobytie — так в мой тепличный журнал попадали включения форточки, и через месяц по этим строкам восстановился весь режим дня. Родственный приём с журналом событий в мониторе разобран в уроке про штамп на каждой строке.
unixtime в этой истории — запасной формат, который стоит держать в голове. Если журнал пойдёт не в таблицу, а в базу или телеметрию, секунд с 1970 хватит и останется место для значений: десять байт, одна шкала, никаких часовых поясов. Я завёл в проекте правило двух столбцов: человеческая метка для глаз и unixtime для машины — сорок процентов лишнего объёма, зато любой скрипт на ноутбуке разбирает файл без единой функции перевода календаря. Для месяца тепличного лога это вышло около семнадцати килобайт сверху, и я не считаю это расточительством.
Что смотреть в потоке строк через неделю, когда новизна стёрлась. Первое — дыры в интервалах: строка с номером 412 через три минуты после 411 означает моргнувшее питание или зависший SD-запрос, и по плотности таких провалов видно, живёт ли прибор вообще. Второе — осмысленность значений: температура, которая скачет с 18 на 24 между соседними строками, чаще врёт проводом A0, чем погодой. Третье — события: строки со столбцом sobytie должны ложиться на то, что вы реально делали, иначе кнопка держится на дребезге. Такой журнал потом ложится в CSV-логгер DHT без переделки формата — уже с влажностью в третьем столбце.
И про связи с соседями по разделу, чтобы не изобретать вторую машину времени. На SD-стороне есть готовый разбор связки RTC плюс SD в одном логе — там про порядок инициализации и что бывает, когда карта стартует позже часов. А если журнал нужен не на карте, а как протокол отладки, кнопочный CSV из урока про лог по кнопке делается тем же строителем строки. Формат один — и вы переезжаете между носителями, не переписывая ни строки логики.
Почему в метке есть секунды, если плановая запись идёт раз в минуту? За внеплановые строки: событие с кнопки прилетает внутри той же минуты, что и плановая пара, и без секундной отметки такие соседние записи неотличимы — а с секундами порядок читается сразу. Секунды выкупают и вторую услугу: при разборе файла дубли меток означают двойное срабатывание кнопки или повторную запись после сбоя, и их видно сортировкой на месте. Дешёвые 19 байт против возможности расследовать инцидент по файлу — я плачу эту цену в каждом логгере.
Шапку файла я теперь считаю контрактом, а не украшением. Столбцы datetime,temp переехали в три проекта без правок, а однажды добавленный на живую четвёртый столбец сломал утренний скрипт супруги — она мне это припомнила цитатой из семейного чата. Правило вывода простое: шапка пишется один раз при создании файла, новые поля добавляются только справа и никогда — между старыми; тогда даже древний разборщик по первым двум столбцам продолжит жить. Проверка на столе та же, что у логгера: неделю пишем в Serial и только потом заводим карту — дешевле, чем переучивать формат на работающем приборе.
| Формат метки в первом столбце | Байт | Сортируется как текст |
|---|---|---|
| unixtime: 1741431900 | 10 | да: одна длина и одна шкала |
| 2025-03-08 14:05:00 | 19 | да: поля от старшего к младшему |
| 08.03.2025 14:05 | 16 | нет: числа дней перепутают порядок |
| секунды от старта платы | переменно | да, но сдвигается при каждой перезагрузке |
- 1. Почему строка «2025-03-08 14:05:00» сортируется правильно как обычный текст?
- 2. Сколько байт занимает такая метка времени в файле?
- 3. Что происходит со String-склейками на Uno при записи строки каждые 100 мс?
- 4. Почему температуру в CSV я пишу с точкой, а не с запятой?
- 5. Что честно умеет симулятор этого урока?
- Метка «2025-03-08 14:05:00» — поля от старшего к младшему: сортируется текстом, стоит 19 байт
- Строку собираем из полей DateTime сами: ведущие нули — z2, десятичный разделитель — всегда точка
- String на Uno безобиден при записи реже раза в секунду; чаще — char-буфер и snprintf
- Симулятор SD не эмулирует: те же строки идут в Serial, на плате их принимает log.csv
- open/println/close на каждую строку переживает моргание питания; альтернатива — открытый файл с flush
Частые вопросы по теме урока
- Почему строка «2025-03-08 14:05:00» сортируется правильно как обычный текст?
- От старшего к младшему с ведущими нулями: лексикографический порядок строк совпадает с хронологическим. Формат «08.03.2025» сортируется по числу месяца и лжёт через неделю. Поля идут от старшего к младшему: год, месяц, день, время
- Сколько байт занимает такая метка времени в файле?
- 19 символов: десять на дату с дефисами, пробел, восемь на время с двоеточиями. Unixtime короче — 10 байт, но без секундных полей и читаемости. 19
- Что происходит со String-склейками на Uno при записи строки каждые 100 мс?
- Каждая склейка переезжает по куче и оставляет дыру; при частых записях дыры каскадируются. Рецепт — char-буфер со snprintf, а String оставить для строк раз в минуту и реже. Куча фрагментируется, и через часы работы скетч падает
- Почему температуру в CSV я пишу с точкой, а не с запятой?
- «23,4» внутри CSV превращается в два столбца — 23 и 4, и таблица тихо расползается. Точка в числах или разделитель «точка с запятой» — оба варианта чинят журнал навсегда. В русском Excel запятая-десятичная конфликтует с запятой-разделителем
- Что честно умеет симулятор этого урока?
- Строитель метки в симе тот же, вывод идёт в Serial с ползунком в роли датчика и кнопкой-событием. На плате те же строки принимает файл log.csv через open/println/close. Строит ту же строку и печатает её в Serial — SD движок не эмулирует