Два логгера — в подвале и на чердаке — писали температуру всю зиму. В феврале я решил свести оба файла в одну таблицу и сравнить ночи между собой, и тут же упёрся в стену: в первой колонке у обоих были секунды с момента старта. sec=3600 есть и у подвала, и у чердака — и это два разных часа двух разных суток, у которых нет ни дня, ни месяца. Сшить их нечем: millis() не знает ни даты, ни времени суток, он просто считает с момента включения. Файлов было по девять-десять тысяч строк, руками их не разнесёшь: после каждого отключения счётчик обнулялся, и в одном файле соседствовали полдюжины sec=0. Зиму данных я слил в один график с подписью «примерно январь-февраль».
Лечится штампом из часов реального времени, и базовый приём я уже показывал в уроке про логгер со штампами: строка вида YYYY-MM-DD HH:MM:SS, собранная с ведущими нулями. Здесь тот же штамп встанет в CSV-лог SD-карты, но разговор пойдёт о том, о чём тот урок молчал: как часы DS3231 и карта живут в одном скетче, хотя сидят на разных шинах; как синхронизировать часы перед стартом лога; и когда вместо человекочитаемой даты писать unixtime. Штамп встанет в первую колонку, и потому она всегда заполнена: запись без времени в этом скетче просто не случится.
Ко второй половине задания меня подвёл собственный скетч. Я вызывал rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) при каждом включении — «чтобы точно не сбилось». Через месяц в логе обнаружились три недели одного и того же дня: каждый сброс питания откатывал часы к моменту компиляции, и записи гордо ставили в файл одну дату. Синхронизация — процедура с условиями, а не строка, которую пишут и забывают.
Две шины в одном приборе: I2C и SPI
Карта разговаривает по SPI: MOSI на D11, MISO на D12, такты на D13, выбор кристалла на D10 — разводка разобрана в уроке про SPI-распиновку. DS3231 сидит на I2C: SDA на A4, SCL на A5, адрес 0x68 — урок про DS3231 подключал его к голой плате. Шины разные, протоколы разные, библиотеки друг о друге не знают — конфликтовать нечему, и это не компромисс, а нормальная архитектура: в моём метеологгере на той же I2C жили ещё и дисплей с адресом 0x27.
Питание у обоих модулей общее — вот где нужна осторожность. Часы тянут доли миллиампера, а карта в момент записи хватает импульсы до 150 мА, и просадка в этот момент бьёт по всем, кто делит линию: часы на провале могут сбиться или начать врать. Конденсатор у модуля карты и короткие провода — тема урока про устройство модуля. В скетче оба модуля будятся в setup(): Wire.begin() происходит внутри rtc.begin(), SPI — внутри SD.begin(), руками шины запускать не нужно. Порядок я держу такой: сначала часы и их синхронизация, потом карта — логгер не должен начать писать, пока не уверен во времени.
Попутный бонус DS3231 — собственный термометр: rtc.getTemperature() возвращает температуру кристалла с точностью порядка трёх градусов. Для климата это не прибор, зато бесплатная колонка «сколько было внутри корпуса» — по ней видно, не перегрелась ли плата. Подробно про этот термометр я писал в уроке про термометр DS3231; в скетче ниже он стоит на месте датчика только для примера — в вашем логгере там будет DHT22, DS18B20 или любой другой сенсор, а чтение семи байт даты по шине на 100 кГц не блокирует SPI: между rtc.now() и open файла нет ни ожиданий, ни конфликтов.
| Линия | Шина | Пин Uno | Что важно |
|---|---|---|---|
| SDA | I2C | A4 | часы DS3231, адрес 0x68; на одной шине живут и другие устройства |
| SCL | I2C | A5 | подтяжки уже стоят на модуле часов |
| MOSI, MISO, SCK | SPI | D11, D12, D13 | общие линии для всех SPI-устройств платы |
| CS карты | SPI | D10 | у каждого SPI-устройства свой выбор кристалла |
Перед первой записью часы обязаны знать правильное время, и проверка тут двухступенчатая. Первая ступень — rtc.lostPower(): микросхема сама ведёт флаг «питание пропадало», и пока он поднят, показаниям верить нельзя. Вторая — rtc.adjust(DateTime(F(__DATE__), F(__TIME__))): ставим время компиляции скетча, единственное «сейчас», которое плате известно. После установки строка adjust из рабочего кода уходит — иначе каждый запуск будет откатывать часы к моменту сборки, и мой месяц одинаковых дат вырос именно отсюда. Если батарейка CR2032 живая, часы после первого adjust идут годами сами — ресурс элемента и его ловушки разобраны в уроке про CR2032. Чтение времени само по себе копеечное: по шине на 100 кГц передача семи байт даты занимает доли миллисекунды, поэтому штамп можно брать перед каждой записью — на фоне 10-50 мс на открытие файла он незаметен.
#include <SPI.h>#include <SD.h>#include <Wire.h>#include <RTClib.h> const int chipSelect = 10;RTC_DS3231 rtc; const unsigned long INTERVAL = 60000UL;unsigned long lastLog = 0; void print2(File *f, int v) { // ведущий ноль для месяца и секунд if (v < 10) f->print(0); f->print(v);} void setup() { Serial.begin(9600); if (!rtc.begin()) { Serial.println("RTC не отвечает: SDA A4, SCL A5"); while (true) { } } if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); Serial.println("RTC потерял время - поставил время сборки"); } if (!SD.begin(chipSelect)) { Serial.println("SD begin failed"); while (true) { } } if (!SD.exists("log.csv")) { File f = SD.open("log.csv", FILE_WRITE); if (f) { f.println("stamp,temp"); f.close(); } } lastLog = millis();} void loop() { if (millis() - lastLog >= INTERVAL) { lastLog = millis(); DateTime now = rtc.now(); float t = rtc.getTemperature(); // замените на свой датчик File f = SD.open("log.csv", FILE_WRITE); if (f) { f.print(now.year()); f.print("-"); print2(&f, now.month()); f.print("-"); print2(&f, now.day()); f.print(" "); print2(&f, now.hour()); f.print(":"); print2(&f, now.minute()); f.print(":"); print2(&f, now.second()); f.print(","); f.println(t, 1); f.close(); Serial.print("unix="); Serial.print(now.unixtime()); Serial.print(" - строка записана"); Serial.println(); } else { Serial.println("open failed"); } }}В Serial скетч печатает unix= — и это вторая форма штампа, которую полезно держать в голове. now.unixtime() возвращает секунды с 1970 года: один long вместо девятнадцати символов, из которого даты вычитаются, сортируются и сравниваются обычной арифметикой — сколько секунд прошло между записями, какой день недели, что случилось раньше. Саму эпоху и арифметику дат я разобрал в уроке про unixtime. Человекочитаемый штамп стоит дороже — девятнадцать символов на строку — зато открывается блокнотом и Excel без единого преобразования, а сортируется как текст, потому что поля идут от крупного к мелкому. Против пары дата-время unixtime выигрывает ещё и в хранении: четыре байта против девятнадцати, а переполнение тридцатидвухбитного счётчика в 2038 году — для дачного логгера не аргумент, для чужих парсеров — аргумент.
Моё мнение с условием: файлы, которые я потом кормлю скрипту, получают unixtime — один столбец, ноль парсинга. Файлы, которые смотрю глазами, — ISO-штамп. Когда лог уходит и заказчику, и мне, пишу оба столбца: тридцать лишних байт на строку при минутном интервале — это 43 КБ в месяц, дешевле, чем объяснять по телефону, что за число 1762242423.
Пара слов про CSV и пробел внутри штампа. Разделитель формата — запятая, а пробел в значении разрешён, поэтому 2025-11-04 09:07:03 не требует кавычек и не ломает столбцы; в кавычки значение прячут только когда в нём встречается запятая. Считаем объём: девятнадцать символов штампа, запятая, температура с одним знаком — около 27 байт с переводом строки. Минутный интервал — 525 600 строк в год и порядка 14 МБ: карту на 8 ГБ такой файл не выберет и за пятьсот лет, зато первой строкой идёт шапка stamp,temp, и Excel раскладывает столбцы без приглашения в мастер импорта.
Симулятор: часы живые, карта — нет
SD в движке не эмулируется — вместо записи в файл скетч печатает строки лога в Serial с пометкой [SD]: на настоящей плате каждая из них ушла бы в log.csv как есть. А вот часы DS3231 эмулируются честно: rtc.now() возвращает живое время, и штамп в строке тикает по-настоящему. Смотрите на пару рядом: ISO-штамп для глаз и unixtime для машины — это одно и то же мгновение в двух форматах, ровно та пара, которую я пишу в файлы, уходящие и людям, и скриптам. В setup симуляторные часы проходят ту же процедуру, что и настоящие: проверка lostPower и, если флаг поднят, adjust временем сборки. Сравните два запуска подряд: штамп продолжается с того места, где остановился в прошлый раз, — часы не сбрасываются вместе со скетчем, в этом и есть весь смысл. Поднимите скорость кнопкой x30 и посмотрите, как за минуту реального времени пролетает полчаса лога. Печать getTemperature в конце строки — попутный термометр из текста выше: посмотрите, как он стоит в колонке данных после запятой.
- 1. На каких пинах живут SD-карта и DS3231?
- 2. Зачем в setup проверять rtc.lostPower()?
- 3. Что вернёт now.unixtime() для момента 00:00:00 1 января 1970?
- 4. Строка rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) осталась в рабочем скетче. Что будет в логе?
- 5. Какой штамп отсортируется хронологически обычной строковой сортировкой?
- SD живёт на SPI (D10-D13), DS3231 — на I2C (A4/A5, адрес 0x68): шины не конфликтуют, питание общее — конденсатор у карты обязателен
- Синхронизация перед логом: lostPower() — флаг пропажи питания, только по нему вызывается rtc.adjust(DateTime(F(__DATE__), F(__TIME__)))
- adjust при каждом старте откатывает часы к моменту компиляции — месяц одинаковых дат собирается незаметно
- unixtime — один long для скриптов и арифметики; ISO-штамп — девятнадцать символов для глаз и Excel
- CR2032 держит часы годами, но lostPower не всевидящ: при минутных расхождениях проверяйте батарейку
Частые вопросы по теме урока
- На каких пинах живут SD-карта и DS3231?
- SD — SPI-устройство, часы — I2C. Шины независимы, библиотеки друг о друге не знают, конфликтов между ними нет; скорости тоже разные — I2C на Uno ходит на 100 кГц, SPI карты быстрее, но трафика у обоих копейки. Карта на SPI: D11-D13 плюс CS на D10, часы на I2C: A4/A5
- Зачем в setup проверять rtc.lostPower()?
- DS3231 ведёт внутренний флаг пропажи питания. По нему и только по нему ставят время через rtc.adjust; флаг хранится в регистре микросхемы и переживает отключение основного питания — ради этого и стоит батарейка. Флаг показывает, что питание пропадало: без adjust показаниям верить нельзя
- Что вернёт now.unixtime() для момента 00:00:00 1 января 1970?
- unixtime — число секунд, прошедших с начала эпохи; в её нулевой точке оно равно нулю и растёт каждую секунду на единицу. Все производные — минуты, дни, недели — тоже обычная арифметика от 1970 года. 0
- Строка rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) осталась в рабочем скетче. Что будет в логе?
- adjust при каждом старте ставит момент сборки скетча. Мой месяц одинаковых дат в логе вырос ровно из этой строки. Все записи несут дату компиляции — каждый Reset откатывает часы
- Какой штамп отсортируется хронологически обычной строковой сортировкой?
- ISO-подобный формат: поля от крупного к мелкому и ведущие нули. unixtime тоже сортируется, но как число — другой механизм. 2025-11-04 09:07:03