Мой тепличный логгер два месяца писал температуру и влажность в один-единственный log.csv, и в какой-то день Excel стал открывать его с заметной паузой. Арифметика была против меня с самого старта: строка «sec,24.3,55» тянет около двадцати байт, замер раз в минуту даёт 1440 строк в сутки, и к исходу месяца файл подбирался к мегабайту. Открыть его ещё можно, а вот ответить на вопрос «что было ночью со вторника на среду» — уже нет: прокручивать сотни тысяч строк не станет ни один человек. Файл-монстр неудобен не столько размером, сколько поиском.
Лечение — файл на сутки: сегодняшние замеры ложатся в dht01.csv, завтра — в dht02.csv, поиск одного дня превращается в открытие одного файла. Часов реального времени на плате нет, а городить их ради имени файла не хочется, поэтому номер суток я веду сам: счётчик, инкремент на старте, две цифры в имени. Со RTC схема сводится к тому же циклу, только номер выдаёт календарь — такую связку со штампами разбирал в уроке про DS3231 и файл на сутки. Здесь беру вариант без часов: дешевле по схеме и по коду.
Три вещи, из которых собирается такой логгер: шапка CSV, написанная один раз при создании файла; минутный ритм записи на millis без единого delay; close после каждой строки. Последний пункт звучит как педантизм, а решает судьбу данных: с открытым навсегда файлом сбой питания съедает хвост журнала. Первый скетч DHT в CSV уже был в уроке про логгер DHT, ритм сессий — в уроке про минутный CSV-лог; здесь собираю всё в прибор с ротацией.
Имя файла как переменная: счётчик вместо календаря
Первый шаг — перестать писать в константу. Имя файла собирается в буфер из номера суток: dht01.csv, dht02.csv, dht03.csv — и SD.open принимает его так же, как строку в кавычках. Две цифры номера — не эстетика, а порядок сортировки: без ведущего нуля dht10 встанет в каталоге раньше dht2, и список файлов на карте прочитается задом наперёд. Имя из семи знаков с точкой проходит через классический формат 8.3, поэтому такой файл виден и старыми картридерами, и любыми каталогами — ограничения коротких имён я раскладывал в уроке про имена 8.3 и лимит корня.
Откуда берётся сам номер суток — честный вопрос. Вариант без часов я уже назвал: счётчик в EEPROM, инкремент при каждом старте платы. Цена простоты видна сразу: «сутки» в этой схеме означают «сессию питания». Плата, которую выключают на ночь, честно заведёт новый файл утром; плата с постоянным питанием будет месяц писать в один файл, потому что перезапусков не было. Для логгера на зарядке от солнца или на даче со счетчиком включений это почти всегда то, что нужно, — но называть файл календарём нельзя: даты в имени нет, есть только номер.
Шапка привязывается к моменту создания файла, а не к запуску скетча: открыли файл в FILE_WRITE, спросили size() — ноль означает свежее имя, пишем строку sec,temp_c,hum_pct; не ноль — журнал уже живёт, дописываем только замеры. Именно эта проверка не даёт шапкам размножаться после каждой перезагрузки. Колонки держу латиницей и без запятых внутри — состав колонок проектировал в уроке про шапку и колонки, здесь тот же договор: файл должен открываться в Excel без мастера импорта.
Отдельная строка честности — замеры, которых не было. DHT22 иногда отвечает нечислами: проводок на клемме дрогнул, датчик задумался дольше паспортных двух секунд, и чтение возвращает NaN. Логгер в этом случае строку не пишет: подменять пропуск нулём или повторять последнее значение — способ испортить график самому себе, а дыра в минутной сетке виднее любой подмены. Пропуск стоит максимум одной строки из 1440, и счётчику суток он безразличен: номер инкрементируется на старте, а не по числу записей, так что день с десятью сбойными замерами всё равно закроется своим файлом.
Минутный ритм и цена close
Минуту климата держит таймер на millis: came ли срок — замер, нет — loop крутится дальше. DHT22 опрашивается не чаще половины герца, раз в две секунды по паспорту, DHT11 — раз в секунду; минутный интервал сидит в этих рамках с тридцатикратным запасом, так что датчик между замерами отдыхает, а скетч между замерами свободен. Если собрать ритм на delay, кнопки и остальная логика перестанут отвечать — приёмы неблокирующего цикла расписаны в уроке про millis вместо delay.
Объём считается на пальцах и зависит от состава строки. Строка секунды, температуры и влажности — «120,24.3,55» — укладывается в 10–12 байт; строка с RTC-штампом — в 30–40. При минутной сетке это от 17 до 58 килобайт в сутки: даже щедрый вариант — около 60 КБ, месяц — 1,8 МБ, и обычная карта на 2 ГБ пустеет дольше, чем живёт проект. Если колонок больше — давление, освещённость, заряд батареи, — умножайте свою длину строки на 1440 и получите свой бюджет; оперировать своими числами полезнее, чем верить чужим.
Close после каждой строки — тот пункт, на котором держится целостность журнала. Библиотека пишет не на карту напрямую, а в 512-байтный буфер; close дожимает его в сектор и обновляет длину файла в каталоге. Открыли файл, дописали строку, закрыли — и при пропадании питания максимум, что теряется, — одна недописанная строка. Если же файл держать открытым всегда, буфер копит данные, каталог стоит на месте, и обрыв превращает последние сотни байтов журнала в призрака: их нет ни в файле, ни в каталоге. Механику секторов и flush разбирал в уроке про sync и питание — здесь только практика: минутный интервал делает короткие сессии бесплатными, откладывать close некуда.
#include <SPI.h>#include <SD.h>#include <DHT.h>#include <EEPROM.h> const int CS_PIN = 10;const int LED_WR = 9; // окно записи; 13 занят SPIconst unsigned long INTERVAL = 60000UL; // замер раз в минутуconst int A_DAY = 0; // ячейка счётчика суток DHT dht(2, DHT22);File logFile;int dayN;unsigned long lastRow = 0; void makeName(int n, char* buf) { buf[0] = 'd'; buf[1] = 'h'; buf[2] = 't'; buf[3] = '0' + (n / 10) % 10; // ведущий ноль: dht07, а не dht7 buf[4] = '0' + n % 10; buf[5] = '.'; buf[6] = 'c'; buf[7] = 's'; buf[8] = 'v'; buf[9] = 0;} void openDayFile() { char name[10]; makeName(dayN, name); logFile = SD.open(name, FILE_WRITE); if (!logFile) { Serial.println("открытие не удалось"); return; } if (logFile.size() == 0) { logFile.println("sec,temp_c,hum_pct"); // шапка только свежему имени } logFile.close(); // даже шапку пишем короткой сессией} void setup() { Serial.begin(9600); pinMode(LED_WR, OUTPUT); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } dht.begin(); dayN = EEPROM.read(A_DAY) + 1; // каждый запуск = новые сутки EEPROM.update(A_DAY, dayN); openDayFile();} void loop() { unsigned long ms = millis(); if (ms - lastRow < INTERVAL) return; lastRow = ms; float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t) || isnan(h)) { Serial.println("DHT молчит, замер пропущен"); return; } char name[10]; makeName(dayN, name); logFile = SD.open(name, FILE_WRITE); if (!logFile) { Serial.println("открытие не удалось, замер пропущен"); return; } digitalWrite(LED_WR, HIGH); logFile.print(ms / 1000); logFile.print(","); logFile.print(t, 1); logFile.print(","); logFile.println(h, 0); logFile.close(); // close дожал буфер и каталог digitalWrite(LED_WR, LOW); Serial.print(name); Serial.print(": "); Serial.print(t, 1); Serial.print(" C, "); Serial.print(h, 0); Serial.println(" %"); // тот же лист - в Serial}В модели следите за двумя вещами. Первая — имя файла: пять строк живут в dht01.csv, шестая встречает «полночь» и открывает dht02.csv со свежей шапкой, и так по кругу, пока вы не выключите симуляцию. Вторая — светодиод: он горит долю секунды, ровно пока идёт строка, и гаснет сразу после close. На плате это окно длится единицы миллисекунд и означает одно: в это время карту нельзя вынимать, кнопка рестарта неуместна, питание рвать нельзя.
Чего модель не показывает — цену промаха: уберите из своего скетча close, и внешне ничего не изменится, строки будут печататься. Развалится только полевая проверка: выдёрните питание платы через минуту после пары замеров, вставьте карту в ноутбук — и последних строк там не окажется. Буфер 512 байт не дожат, запись каталога не сделана, и журнал кончается раньше, чем помнит Serial. Эта разница между тем, что печаталось, и тем, что лежит на карте, и есть цена пропущенного close.
Сверка объёма делается прямо на плате, без ноутбука: короткая сессия «открыл файл, посмотрел size(), закрыл» говорит, сколько байтов накопилось с полуночи, и стоит пары миллисекунд. Я печатаю это число раз в час: прирост порядка сорока байт в минуту — журнал дышит; Serial живой, а размер стоит на месте — разрыв между датчиком и картой. Тот же контрольный лист поймал у меня однажды отошедший провод CS: замеры печатались неделю, карта молчала, и заметил я это по нулевому приросту размера, а не по Serial.
| Схема имени | Что даёт | Цена | Когда брать |
|---|---|---|---|
| счётчик суток dht01.csv | работает без часов, файл на сессию питания | даты в имени нет | дача, автономки, перезапуски каждые сутки |
| дата из RTC | настоящие сутки и сортировка по именам | нужен модуль часов | архив, где важен календарь |
| имя по размеру | файлы ровные по объёму | дату смотрим в строках | непрерывная техника с постоянным питанием |
| один файл навсегда | минимум кода | файл-монстр, поиск страдает | тесты на столе, короткие проекты |
Когда ротация не нужна, тоже стоит сказать прямо: если логгер пишет отладку на столе и живёт не дольше недели, один файл проще и честнее — его потом не раскладывать по суткам. Ротация окупается на длинных проектах и там, где по журналу ищут конкретное время: авария, скачок, визит заказчика. Моя граница примерно такая: больше трёх суток записи или поиск по моментам — файл на сутки; всё короче — один файл и не думать.
Вторая граница — глубина архива на карте. Суточный файл живёт рядом с десятками таких же, и через полгода на карте начинается другая работа: старые имена переезжают в архив, битые файлы после обрывов питания удаляются, каталог чистится от мусора прерванных записей. Это отдельный набор операций — rename, remove, exists, — и его разбор занимает следующий урок: уборкой карты скетч занимается сам, без выдёргивания карты в ноутбук.
- 1. Зачем в имени dht01.csv ведущий ноль?
- 2. Датчик DHT22 по паспорту опрашивается...
- 3. Сколько занимает минутный лог из трёх коротких колонок за сутки?
- 4. Что делает close, кроме записи буфера в сектор?
- 5. Без RTC «сутки» в ротации по счётчику запусков — это...
- Имя файла — переменная из счётчика суток: dht01.csv с ведущим нулём сортируется правильно и остаётся в формате 8.3.
- Шапка CSV пишется один раз, по условию size() == 0 у свежесозданного файла — перезагрузки не размножают заголовки.
- DHT22 не опрашивают чаще раза в 2 секунды; минутный интервал на millis оставляет датчику запас в 30 раз.
- 1440 строк в сутки — это десятки килобайт: короткая строка около 17 КБ, со штампом — около 60 КБ.
- Close после каждой строки дожимает буфер и каталог: сбой питания стоит одной строки, а не хвоста журнала.
Частые вопросы по теме урока
- Зачем в имени dht01.csv ведущий ноль?
- Без ведущего нуля dht10 встанет в каталоге раньше dht2 — сортировка по имени развалится. Короткое имя с нулём остаётся в формате 8.3. Чтобы список файлов сортировался по номеру правильно
- Датчик DHT22 по паспорту опрашивается...
- Паспорт DHT22 — 0,5 Гц, одно чтение в две секунды; DHT11 — 1 Гц. Минутный интервал логгера сидит в этих рамках с запасом. Не чаще чем раз в 2 секунды
- Сколько занимает минутный лог из трёх коротких колонок за сутки?
- 1440 строк в сутки при 10–12 байтах на короткую строку — около 17 КБ, а строка с RTC-штампом тянет на 40 байт и даёт порядка 60 КБ. Обе цифры сходятся в десятки килобайт. Около 60 КБ
- Что делает close, кроме записи буфера в сектор?
- Close дожимает буфер и обновляет длину файла в каталоге — только после этого компьютеру виден настоящий размер. Без close сбой питания оставляет журнал короче, чем печатал Serial. Обновляет длину файла в каталоге
- Без RTC «сутки» в ротации по счётчику запусков — это...
- Счётчик инкрементируется на старте, поэтому границы «суток» совпадают с подачей питания. Постоянно включённая плата будет писать в один файл месяц — для календаря нужен RTC. Сессия питания: перезапуск платы открывает новый файл