Логгер данных на Arduino: SD-карта и DS3231 с меткой времени
Логгер — первый «взрослый» проект на Arduino: данные живут дольше, чем включён экран монитора. Состав всегда один: SD-карта хранит файл, часы реального времени ставят метку в каждой строке, датчик даёт значение. В железе этот проект особенно коварен: три модуля, SPI и I2C одновременно, а ошибка «ничего не пишет» всплывает дома, а не на объекте. Поэтому логгер собирают и обкатывают в браузере, а на карту выносят уже проверенный скетч.
В нашем симуляторе обе карточки — «Модуль SD-карты» (SPI: CS, MOSI, SCK, MISO) и «Часы DS3231» (I2C: A4/A5) — ставятся кликом из палитры «Макетка». Рантайм исполняет SD.begin, SD.open, print/println, close и rtc.now() по-настоящему: содержимое файла видно в карточке SD построчно, часы идут живые. Ниже — проверенный скетч CSV-логгера и ловушка FILE_WRITE, которую я поймал прогоном.
Состав и провода: два шины в одном скетче
- SD-модуль на аппаратном SPI Uno: CS на D10 (стандарт), MOSI D11, SCK D13, MISO D12 — плюс питание 5 В и общая земля.
- Часы DS3231 на I2C: SDA на A4, SCL на A5, батарейка CR2032 держит время без питания платы.
- Датчик для значения: DHT11 на любом цифровом пине, в скетче ниже — D2; подойдёт и фоторезистор на аналоговом входе.
- Имя файла — короткое, в формате 8.3: log.csv или log01.txt; длинные имена и папки — уже расширение FAT, у которого свои лимиты.
Скетч: CSV-лог с меткой времени
Файл открывается один раз в setup() и остаётся открытым — это осознанное решение, объяснение ниже. Каждую секунду скетч дописывает строку «часы:минуты:секунды;температура». rtc.now() возвращает объект DateTime, из него читаются hour(), minute(), second(); dht.readTemperature() даёт градусы с дрожью в одну десятую, как живой датчик.
#include <SD.h>#include <RTClib.h>#include <DHT.h> RTC_DS3231 rtc;DHT dht(2, DHT11);File logFile;unsigned long poslednayaZapis = 0; void setup() { Serial.begin(9600); rtc.begin(); // часы стартуют dht.begin(); SD.begin(10); // CS на D10 logFile = SD.open("log.csv", FILE_WRITE); if (logFile) logFile.println("time;tempC"); // шапка CSV Serial.println("logger start");} void loop() { if (millis() - poslednayaZapis >= 1000) { // раз в секунду poslednayaZapis = millis(); DateTime now = rtc.now(); if (logFile) { logFile.print(now.hour()); logFile.print(":"); logFile.print(now.minute()); logFile.print(":"); logFile.print(now.second()); logFile.print(";"); logFile.println(dht.readTemperature()); } Serial.println("logged"); }}Прогон в симуляторе: в карточке SD-модуля появился файл log.csv со строками вида «11:8:51;23.55» — метка от живых часов, температура с дрожью ±0,1 градуса. Часы после rtc.begin() идут сами: если подвести время в карточке DS3231, скетч увидит переведённые значения. Serial печатает logged раз в секунду — ритм записи виден и без открытия файла.
Ловушка FILE_WRITE: почему строки пропадают
Классический скетч-логгер из интернета переоткрывает файл на каждой записи: open — println — close. В настоящей SD-библиотеке FILE_WRITE дозаписывает (курсор ставится в конец), а вот в рантайме браузера каждый open с FILE_WRITE открывает файл заново — прежние строки стираются. Я прогнал оба варианта: с переоткрытием каждую секунду в файле осталась одна последняя строка; с файлом, открытым в setup() один раз, строки накапливаются как надо. Отсюда правило для браузера: держите файл открытым, а на живой плате, наоборот, без flush() и close() рискуете потерять хвост лога при пропадании питания — компромисс разбирает урок про цикл сессии файла.
Честная граница: что исполняет симулятор
| Проверка | Симулятор | Только железо |
|---|---|---|
| SD.begin / open / print / println / close | исполняется, файл виден в карточке | то же самое |
| rtc.now() с живыми часами | исполняется, время тикает | то же самое |
| FILE_WRITE как дозапись | нет: каждый open обнуляет файл | да — seek в конец |
| exists / mkdir / remove / папки | исполняется | то же самое |
| Износ карты и потеря при выдёргивании | нет: карта вечная | да — flush перед извлечением |
| Скорость записи 512-байтными блоками | мгновенно, без буферизации | да — паузы loop() |