Логгер в теплице я запустил с честной, но ленивой прикидкой: «минутная строка — ну и что, карта же четыре гигабайта». Через неделю посмотрел на файл, а там за семь суток всего два с половиной мегабайта, зато ночные строки шли по пять в час, хотя датчик работал ровно. Разгадка была в самом скетче: интервал я менял прямо внутри delay, а ночью плата успевала пропустить по две записи. Сутки на delay — это часы, которые никто не проверял: скетч не знал, сколько строк он обязан написать за день, и потому не мог заметить, что забыл половину.
Здесь я собираю суточный логгер по-взрослому: расчёт объёма до запуска, millis-таймер, у которого интервал можно менять на ходу, и контроль written-байтов, который ловит пропуски раньше, чем вы увидите дыры в графике. Схему одной сессии «открыл — дописал — закрыл» я брал из урока про минутный CSV-лог, а колонки и шапку продумывал по уроку про формат CSV; здесь добавляется этаж — сутки целиком как единица планирования.
Бюджет суток: посчитайте, прежде чем писать
Арифметика тупая, но её почему-то никто не делает до первого запуска. Строк в сутки — 86400 делить на интервал в секундах. Строка у меня выглядит как «милис,температура,влажность,статус» и весит с концом строки двадцать восемь байт. При интервале 60 секунд это 1440 строк и около сорока килобайт за сутки — полтора десятка мегабайт в год. При интервале 5 минут — 288 строк и восемь с половиной килобайт в сутки. При 10 секундах — уже 259 КБ за день, и это другой разговор: кластерные паузы, о которых писал урок про блоки по 512, прилетают каждые несколько минут.
Объём при этом ни разу не был проблемой: даже скромная карта на 4 ГБ при минутном ритме заполнится года через двести пятьдесят. Реальные ограничители другие: качество карты (дешёвые Class 4 в полевых условиях я менял как батарейки — об этом урок про полевые симптомы) и ресурс секторов каталога, который пинается каждым открытием файла. Мой вывод после двух сезонов логов: план на сутки нужен не для экономии гигабайт, а чтобы скетч умел заметить собственный пропуск — когда написано на сто строк меньше расчётного, что-то случилось, и об этом надо кричать в Serial.
День и ночь в логе честно разные по ценности: днём растение живёт и температура меняется, ночью хватит и пятиминутки. Переменный интервал режет сутки с 1440 строк примерно до тысячи без потери пользы — и заодно убирает половину кластерных всплесков.
Переменный интервал на millis: ритм без дрейфа
Таймер строится не как «millis минус lastLog больше интервала», а как расписание: nextAt — абсолютный момент следующей записи. Сравнили millis() с nextAt — записали, потом nextAt += interval. Разница проявляется, когда интервал меняется на ходу: при схеме с lastLog и перескоком на 60 → 300 секунд следующая строка уезжает на перемену интервала плюс остаток, а при nextAt расписание просто растягивается ровно. После каждой записи прибавляйте к nextAt именно интервал, а не присваивайте millis() — иначе каждая запись тащит в себе накопленную задержку обработки, и за сутки расписание уползает на минуту-другую.
Часы суток без RTC я беру тупо из millis: часы = millis() / 3600000UL, окно дня — остаток от деления на 24. Это честно работает до первого пропадания питания: после рестарта счёт часов начинается с нуля, и «день» для скетча наступит заново. Для настоящего расписания по часам циферблата нужен модуль часов — механика разбиралась в разделах про RTC, а здесь я сознательно держу скетч на одном millis, чтобы таймер видно было целиком. Если вам нужны имена файлов с датой — урок про суточные файлы с меткой как раз про это.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10;const unsigned long DAY_MS = 60000UL; // день: строка раз в 60 сconst unsigned long NIGHT_MS = 300000UL; // ночь: раз в 5 минутconst unsigned long DAY_START = 6UL * 3600000UL; // 06:00 от стартаconst unsigned long DAY_END = 22UL * 3600000UL; // 22:00 от старта unsigned long nextAt = 0;int rowsToday = 0;unsigned long bytesToday = 0;bool wasDay = true; unsigned long currentInterval() { unsigned long t = millis() % 86400000UL; // «часы» от старта, сутки 24 ч bool day = (t >= DAY_START && t < DAY_END); if (day != wasDay) { // смена дня/ночи - печать итога Serial.print("[итог] смена режима: "); Serial.print(rowsToday); Serial.print(" строк, "); Serial.print(bytesToday); Serial.println(" байт за прошедшие сутки-полусутки"); rowsToday = 0; bytesToday = 0; wasDay = day; } return day ? DAY_MS : NIGHT_MS;} void setup() { Serial.begin(9600); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } File f = SD.open("sutki.csv", FILE_WRITE); // шапка один раз if (f) { f.println("ms,a0,a1"); f.close(); } nextAt = millis() + 2000UL; // первая строка через 2 с Serial.println("сутки поехали: день 60 с, ночь 300 с");} void loop() { unsigned long interval = currentInterval(); if (millis() < nextAt) return; if (nextAt + interval < nextAt) nextAt = millis(); // защита от переполнения nextAt += interval; // ритм ведёт расписание, не millis() File f = SD.open("sutki.csv", FILE_WRITE); if (!f) { Serial.println("[сбой] open вернул false - строка пропущена"); return; } String line = String(millis() / 1000) + "," + String(analogRead(A0)) + "," + String(analogRead(A1)); f.println(line); bytesToday += line.length() + 2; // CRLF rowsToday += 1; f.close(); if (rowsToday % 12 == 0) { // контроль раз в 12 строк File chk = SD.open("sutki.csv"); if (chk) { Serial.print("[контроль] строк "); Serial.print(rowsToday); Serial.print(", размер файла "); Serial.print(chk.size()); Serial.println(" байт"); chk.close(); } }}В модели за минуту видно главное: LED вспыхивает ровно по расписанию, счёт байтов растёт на длину строки плюс два за конец строки, а на 1440-й сим-минуте итог сравнивается с расчётом — если вы пару раз передёрнули ползунок, расхождение честно лежит в цифрах. Кнопка и клавиша n включают ночь: интервал растёт в пять раз, плотность вспышек падает — та самая экономия кластерных походов, о которой шла речь в теории.
На плате переносится всё один в один, только массив-журнал заменяет файл, а сим-минуты — настоящие секунды. Контроль размеров из скетча для платы дополняет картину: f.size() после close показывает то, что реально доехало до каталога, и его же я печатаю раз в дюжину строк. Если ваши сутки обязаны быть ровными — добавьте сравнение расчётных строк с rowsToday и кричите в Serial при расхождении больше пары строк: пропуски ловятся часами раньше, чем в графике.
| Интервал | Строк в сутки | Байт в сутки | За год | Прикидка |
|---|---|---|---|---|
| 10 секунд | 8640 | ~242 КБ | ~88 МБ | кластерные паузы каждые минуты; файл открыт с flush |
| 60 секунд | 1440 | ~40 КБ | ~15 МБ | рабочий ритм; сессия open-close на каждую строку |
| 5 минут | 288 | ~8 КБ | ~3 МБ | ночь и медленные процессы; карта живёт годами |
| час | 24 | ~700 байт | ~250 КБ | сводка; даже EEPROM хватит, SD — с запасом в века |
Границы метода у меня такие. Ровный переменный интервал подходит всему, что меняется плавно: температура, влажность, уровень. События туда пихать не надо — импульс счётчика воды или нажатие кнопки пишутся по факту, вне расписания, как в уроке про метку событий кнопкой; иначе вы порежете историю на сутки вперёд. И не смешивайте ритмы в одном файле: минутные строки и событийные вразнобой ломают парсер, которым потом будете читать CSV обратно.
Про питание последних слов два. План «сутки» ничего не стоит, если обрыв случился посреди сессии: механику того, что доезжает до карты при провале, разбирал урок про flush и питание — при минутном ритме с close на каждую строку вы теряете максимум текущую строку, и это приемлемая цена. А если логгер уезжает на батарейку, то между записями плату надо укладывать спать — схема из урока про логгер на батарейке встраивается в этот же таймер без переделки: спим до nextAt, проснулись, дописали строку, ушли снова.
- 1. Сколько байт напишет сутки лога при интервале 60 с и строке 28 байт?
- 2. Как держать ритм записи, когда интервал меняется на ходу?
- 3. Что реально ограничивает срок службы суточного логгера на SD?
- Бюджет суток считайте до запуска: строки = 86400/интервал, байты = строки × длина строки + 2.
- Переменный интервал день/ночь режет половину записей без потери смысла и половину кластерных пауз.
- Ритм держит absolute nextAt и nextAt += interval; пересброс на millis() накапливает дрейф.
- Часы суток без RTC считаются от millis() и живут до первого сброса питания — для расписания по циферблату нужен модуль часов.
- Контроль written-байтов через line.length() и f.size() ловит пропуски раньше графика.
Частые вопросы по теме урока
- Сколько байт напишет сутки лога при интервале 60 с и строке 28 байт?
- 86400/60 = 1440 строк, 1440 × 28 = 40320 байт, около сорока килобайт. За год — пятнадцать мегабайт: объём даже маленькой карты хватает на века. Около 40 КБ
- Как держать ритм записи, когда интервал меняется на ходу?
- nextAt += interval даёт расписание без накопления задержек. Присвоение millis() после записи тащит в него весь простой скетча — за сутки набегает дрейф. Хранить абсолютный момент nextAt и прибавлять интервал к нему
- Что реально ограничивает срок службы суточного логгера на SD?
- Минутный ритм пишет ~40 КБ в сутки — гигабайты кончаются через столетия. Умирают раньше дешёвые карты и сектора каталога, которые пинаются каждым open/close. Качество карты и ресурс секторов каталога при каждом открытии