Гаражный логгер писал строку раз в секунду схемой открыл — дописал — закрыл из урока про println и close, и весь день я не мог найти в корпусе место, где кнопка управления не слепла бы. Миллис-замер из урока про скорость записи назвал цену: 12-18 мс на каждую запись, а раз в несколько сотен — до сотни. Восемь записей в минуту — восемь пауз, и в каждой из них стоят кнопки, датчики и вся остальная жизнь скетча.
Лекарство — буфер в RAM и пакетная запись. Строки копятся в массиве String buf[8], а когда соберётся восьмая, весь пакет уезжает одной сессией: open, восемь println в цикле, close. Пауза одна вместо восьми, и скетч живёт между ними спокойно — заодно уходит главная нервотрёпка прямого режима: тайминги остальных дел становятся предсказуемыми. Плата платит за это окном потерь: при сбое питания умирает всё, что ещё сидело в буфере, — до восьми строк. Компромисс осознанный, и ниже я его посчитаю таблицей, чтобы решение принималось числами, а не интуицией.
Вторая половина урока — дисциплина выключения. Буфер умирает вместе с питанием, поэтому перед снятием провода скетч обязан выдать хвост на карту: в моей сборке это кнопка D2 — нажал, LED мигнул, питание можно резать. Правило «нажал — вспышка — выключай» висит у меня в гараже рядом с выключателем и уже дважды спасло хвост лога. Формат строки при этом прежний, с шапкой и колонками, которые мы выбрали в уроке про CSV-колонки: буфер меняет момент записи, а не вид лога.
Восемь строк и их цена в RAM
Устройство буфера простое: String buf[8] — восемь ячеек, в каждой готовая CSV-строка вида sec,v, и счётчик n, показывающий, сколько ячеек занято. Новая строка ложится в buf[n], n растёт; дошло до восьми — пакет уехал, n обнулилось. RAM на Uno — 2048 байта на всё: скетч, Serial-буферы, библиотека SD. Мой буфер весит порядка двухсот байт вместе с накладными расходами String — на фоне двух килобайт заметно, но не смертельно; открытый файл и внутренности библиотеки съедают больше.
Почему восемь, а не тридцать: окно потерь растёт вместе с буфером. При интервале десять секунд восемь строк — это восемьдесят секунд: столько потеряете при сбое посреди цикла. Тридцать строк — уже пять минут, и это я считаю некрасивым: «запишем потом» легко превращается в «потеряли всё». В обратную сторону буфер из двух-трёх строк почти ничего не даёт: паузы станут чаще, и выигрыш против записи на каждую строку растает. Таблица ниже — мои рабочие точки, посчитанные от строки в восемнадцать байт.
| Строк в буфере | RAM (примерно) | Окно потерь при 10 с на строку | Записей на карту в минуту |
|---|---|---|---|
| 1 — без буфера | 0 | 10 секунд | 60 |
| 4 | около 120 байт | 40 секунд | 15 |
| 8 | около 200 байт | 80 секунд | 7-8 |
| 24 | около 600 байт | 4 минуты | 2-3 |
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const int BTN = 2; // ручной сброс буфера перед выключениемconst int LED = 12; // вспышка = пакет на карте const int CAP = 8; // ёмкость буфера, строкString buf[CAP];int n = 0; const unsigned long INTERVAL = 10000UL;unsigned long lastLog = 0;unsigned long startedAt = 0; void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } startedAt = millis(); lastLog = millis(); Serial.println("buf=0/8");} void loop() { // строка данных раз в 10 секунд - в буфер, карту не трогаем if (millis() - lastLog >= INTERVAL) { lastLog = millis(); unsigned long sec = (millis() - startedAt) / 1000; buf[n] = String(sec) + "," + String(analogRead(A0)); n = n + 1; Serial.print("buf="); Serial.print(n); Serial.println("/8"); } // буфер полон - один пакет, одна пауза if (n >= CAP) { flushBuf(); } // перед снятием питания: выдавить хвост кнопкой if (digitalRead(BTN) == LOW && n > 0) { flushBuf(); }} void flushBuf() { File f = SD.open("log.csv", FILE_WRITE); if (f) { for (int i = 0; i < n; i = i + 1) { f.println(buf[i]); // пакет: строка за строкой } f.close(); digitalWrite(LED, HIGH); delay(60); digitalWrite(LED, LOW); Serial.print("[SD] paket: "); Serial.print(n); Serial.println(" strok na kartu"); } n = 0;}Разбор цикла записи. Одна сессия open-close на пакет, File.println в цикле — по строке на итерацию. Почему массив, а не одна растущая строка-конкатенация: глобальный буфер живёт всю жизнь скетча и кучу не перемешивает, а String, собранный из восьми кусков на лету, переезжает по куче при каждом росте — цены этой склейки мы считали в уроке про String-конкатенацию. На восьми строках обе схемы безопасны; на сотнях конкатенация начинает ловить дыры в куче, а массив — нет.
Пауза пакета. Восемь строк по восемнадцать байт — около 144 байта; они ложатся в 512-байтный буфер библиотеки, физическая запись одна — 12-18 мс на моих 8 ГБ Class 4, пики до сотни случаются, но в восемь раз реже, чем при записи на каждую строку. Именно поэтому пакет спасал GPS-поток в уроке-диагнозе: там лечили пропадающие строки, здесь — слепоту кнопок и общий ритм скетча. Если интервал у вас короче секунды, держите файл открытым и сбрасывайте буфер по таймеру — эта развилка с flush() разобрана в уроке про flush и питание.
Flush кнопкой — порядок выключения: нажали D2, LED мигнул — хвост на карте, теперь питание можно резать. Полагаться на close в момент снятия провода нельзя: его может не быть вовсе, скетч в этот момент копит, а не пишет, и размер файла в каталоге останется старым. Что происходит с таблицей FAT при обрыве посреди записи — потерянные кластеры и просьба форматирования — разобрано там же, в уроке про питание; конденсатор на 470-1000 мкФ у модуля лечит импульсы записи, но окно буфера не переживает — это разные болезни.
Время одной сессии open-close на пакет измеряется тем же миллис-ом, что и одиночная запись: у меня 10-30 мс на пакет из восьми строк против 12-18 мс на одиночную запись прошлой сборки. Курсор открывается в конце файла, цепочка FAT уже размечена с прошлой записи — дорого стоит только самая первая запись нового файла, когда растёт каталог. Отсюда практический вывод: пусть буфер пишет в один и тот же log.csv, а не заводит файл заново каждые восемь строк — новой записи каталога на каждый пакет нет, и цена пакета не растёт со временем.
Что теряется при сбое: границы компромисса
При провале питания между пакетами теряется хвост буфера — до n строк, при моём интервале до восьмидесяти секунд. Провал в момент физической записи пакета страшнее: на кону сектор и таблица FAT, те же потерянные кластеры, что в уроке про питание. Моя граница применимости: буфер не держит больше минуты данных. Инкубатор с интервалом десять секунд даёт окно восемьдесят секунд — приемлемо, цена пропуска мне известна и опыт можно повторить. Счётчик аварий, где каждая строка на вес золота, в буфер не влезает вовсе: там пишу каждую строку сразу и мирюсь с паузами. Спорный вариант «всё через буфер всегда» я видел в чужом проекте: буфер на час данных выиграл пару процентов процессора и потерял владельцу выходной наблюдений — сделка, которую я бы не подписал.
Вариант для педантов — буфер на char: char buf[8][24], восемь строк по двадцать четыре байта, и ни одной ячейки в куче. RAM уходит фиксированно, String-объектов нет, прогноз по памяти точен до байта: 192 байта под строки плюс счётчики. Цена — огород из itoa и ручных нулей при сборке строки, поэтому на учебных скетчах я остаюсь на String, а на char перехожу, когда буфер растёт до десятков строк или скетч уже ест память на грани. Промежуточный приём — String глобально с резервом под максимум: ёмкость выбрали, память занята один раз, и роста по ходу работы нет.
Наблюдаемость пакета стоит держать в Serial. Счётчик n/8 после каждой строки, число строк в очередном пакете и суммарный saved после выгрузки — три цифры, по которым видно, как живёт буфер. Мой случай: интервал в три секунды переливал буфер быстрее, чем я думал, — пакеты уезжали каждые двадцать четыре секунды, и слепых окон стало вдвое больше, чем в расчёте. Расчёт врёт тогда, когда забывает про собственные delay и медленные датчики; печатайте цифры — они честнее планов.
- Строки копятся в String buf[8] и уезжают одним пакетом: одна пауза записи на восемь строк вместо восьми
- Цена пакета — окно потерь: при сбое теряется хвост буфера, при 10 с на строку это до восьмидесяти секунд
- Глобальный массив живёт всю жизнь скетча и не фрагментирует кучу — в отличие от растущей String-конкатенации на лету
- Выключение логгера: кнопка flush, LED-вспышка, потом питание; close в момент снятия провода не гарантирован никем
- Моя граница — не держать в буфере больше минуты данных; счётчик аварий пишется построчно, без буфера
- 1. Сколько раз в минуту скетч открывает файл на карте при буфере 8 строк и интервале 10 секунд?
- 2. Что происходит с буфером при провале питания?
- 3. Почему массив строк, а не растущая String-конкатенация пакета?
- 4. Как правильно выключить логгер с буфером?
- 5. Кому из логгеров буфер я не советую?
Частые вопросы по теме урока
- Сколько раз в минуту скетч открывает файл на карте при буфере 8 строк и интервале 10 секунд?
- Восемь строк собираются 80 секунд, значит пакет уезжает раз в 80 секунд — в минуту это 7-8 сессий open-close вместо шестидесяти. 7-8 — один раз на пакет
- Что происходит с буфером при провале питания?
- Буфер живёт в RAM и умирает с питанием. На карте всё, что успело уехать пакетами; в буфере — ничего. При 10 с на строку потеря ограничена восемьюдесятью секундами. теряется хвост буфера — строки, которые не уехали пакетом
- Почему массив строк, а не растущая String-конкатенация пакета?
- Конкатенация на лету гоняет растущую копию по куче при каждом росте и на сотнях строк ловит дыры во фрагментированной памяти; ячейки массива стоят на месте. глобальный массив живёт всю жизнь скетча и не переезжает по куче, не фрагментируя её
- Как правильно выключить логгер с буфером?
- Хвост буфера выдавливается кнопкой: вспышка подтвердила запись — карту можно вынимать. Reset и выдёргивание провода убивают скетч вместе с содержимым RAM. подождать 10 секунд и выключить — буфер опустеет
- Кому из логгеров буфер я не советую?
- Чем дороже пропущенная строка, тем меньше допустимое окно потерь. Для аварийных журналов я пишу каждую строку сразу и плачу паузами; климату буфер в восемь строк подходит. счётчику аварий, где каждая строка на вес золота