Логгер тока на стройплощадке писал строку каждые сто миллисекунд: секунда, вольты, амперы. Сетка замеров на millis держалась идеально, приёмы её сборки я брал из урока про millis вместо delay, а вот в файле вылезали дыры: между соседними строками вдруг пропадало по сто двадцать миллисекунд, и график за час выглядел как расчёска с выломанными зубьями. Датчики ни при чём: замеры стояли ровно, терялись именно строки записи. Сначала грешил на питание от китайского адаптера: пересобрал цепь, поставил конденсатор — дыры остались ровно там же. Пришлось признавать, что между Serial, где всё печаталось гладко, и картой, где всё лежало криво, живёт кто-то третий.
Третий — буфер библиотеки. File.print в Arduino не тянет ручку привода, а складывает байты в 512-байтную ячейку оперативной памяти, и на карту она уезжает только когда заполнится до конца. Ячейка ровно одна на файл, и её размер — 512 байт, потому что столько занимает сектор на карте. А при переходе через границу кластера — файлы на FAT растут блоками по несколько секторов — библиотека ищет свободный кластер в таблице и помечает его своим, и вот тут карта задумывается на десятки миллисекунд, на дешёвых Class 4 — на сотню. Эти всплески неустранимы, но их можно увидеть, посчитать и развести подальше от чувствительных дел скетча.
Разбираю здесь три вещи: метки времени вокруг записи, буфер на восемь строк с пакетной отдачей и границу между flush и close. Ритм записи по 512-байтовым секторам уже разбирался в уроке про блоки по 512, а цена скорости с потерями — в уроке про скорость и потери; здесь практика замеров, которую я теперь ставлю в каждый логгер, пишущий чаще раза в секунду.
Буфер 512 байт: println не значит записано
Механика на пальцах. Вызов file.println копирует строку в буфер и возвращается за микросекунды — карта ещё даже не слышала о ваших данных. Заполнится ячейка до 512 байт — байт за байтом уедет по SPI, и вот это уже миллисекунды на десятки байтов. Всё, что сидит в буфере на момент пропадания питания, пропадает молча: карта о нём не знает, каталог не знает, ноутбук не покажет. Close, flush и заполнение буфера — три точки, где данные доехали до карты; всё до них живёт только в ОЗУ.
Кластер — следующий этаж. На FAT32 кластер занимает от четырёх до тридцати двух килобайт, и новый файл физически растёт кусками: пока запись не пересекла границу кластера, библиотеке легко, а на границе она лезет в таблицу FAT, ищет следующий свободный кластер, обновляет цепочку — и карта в этот момент делает внутренние стирания и записи в NAND. Отсюда картина, которую я вижу на каждом дешёвом носителе: девять записей по десять-пятнадцать миллисекунд и одна на девяносто — сто тридцать, без всякой видимой причины.
Скорость канала при этом скромная: стандартная SD-библиотека на Uno по SPI держит устойчиво 2-20 КБ/с — зависит от класса карты и частоты SPI. Верхняя граница достижима на SDHC Class 10, нижняя — на ноунейм Class 4, которые маркетплейс раздаёт пачками. Строка в двадцать байт раз в секунду — это 72 КБ в час, запас в десятки раз. Мой вывод после пары пропущенных потоков: узкое место логгера не скорость, а паузы — карта успевает всё, но не всегда вовремя. Если ваш поток упирается именно в канал, менять надо библиотеку или носитель, и это уже другая история.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10;const int LED_WR = 12;const int CAP = 8; // строк в пакетеconst unsigned long INTERVAL = 1000UL; // строка раз в секунду String buf[CAP];int n = 0;unsigned long lastLine = 0; void flushBuf() { unsigned long t0 = millis(); // метка ДО сессии File f = SD.open("log.csv", FILE_WRITE); if (!f) { Serial.println("open: файл не открылся"); return; } for (int i = 0; i < n; i = i + 1) { f.println(buf[i]); } f.close(); unsigned long dt = millis() - t0; // метка ПОСЛЕ close digitalWrite(LED_WR, HIGH); delay(20); digitalWrite(LED_WR, LOW); Serial.print("[SD] пакет: "); Serial.print(n); Serial.print(" строк, запись "); Serial.print(dt); Serial.println(" мс"); if (dt > 50) { Serial.println("[SD] дольше 50 мс - похоже, кластер"); } n = 0;} void setup() { Serial.begin(9600); pinMode(LED_WR, OUTPUT); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } Serial.println("строка раз в 1000 мс, пакет по 8 строк");} void loop() { if (millis() - lastLine < INTERVAL) return; lastLine = millis(); buf[n] = String(millis() / 1000) + "," + String(analogRead(A0)); n = n + 1; if (n >= CAP) flushBuf();}Паузы в сто миллисекунд: метки вокруг записи
Скетч выше печатает длительность каждого пакета, и на моих 8 ГБ Class 4 она сходится с теорией: семь пакетов в диапазоне 10-25 мс и один — под 120 мс, это он, кластер. Главный сдвиг в голове случился, когда я понял, куда ставить замер: не на входе в замер датчика, а вокруг записи. Метка до открытия файла и после close — единственное место, где пауза видна; millis-ритм замеров от неё не спасает и спасать не обязан.
Поэтому в каждом логгере у меня теперь две метки: длительность записи уходит в Serial отдельной строкой, и всё, что дольше пятидесяти миллисекунд, помечается как кластерная пауза. Приём копеечный, а график перестаёт врать: строка с шагом сто двадцать мс — не сбой датчика, а поход библиотеки в FAT. Пробовал я и искать магический интервал, при котором пауз не бывает, — его не существует: кластер придёт на любой частоте, вопрос лишь когда именно. Зато распределение устойчивое: зная число кластеров в файле, можно предсказать, сколько всплесков ждёт лог за сутки.
Между flush и close разница такая: flush дожимает текущий буфер в сектор и возвращает управление, файл остаётся открытым; close делает то же и записывает длину файла в каталог. Записал, закрыл — при пропадании питания теряется максимум текущая строка. Файл открыт всегда, flush раз в минуту — операции короче, но при обрыве длина в каталоге останется старой: данные физически в секторах есть, а размер файла компьютеру виден прежний. Механику секторов и питания раскладывал в уроке про flush и питание — здесь правило простое: либо короткие сессии с close, либо flush по таймеру и осознанный риск. Я закрываю файл на каждую строку при минутном ритме и перехожу на открытый файл с flush, когда строк становится больше пяти в секунду.
В модели за минуту видно всё: строки ложатся в ячейки по одной, счётчик печатается на каждую, восьмая запускает пакет — LED горит ровно столько, сколько длится печать, и у каждого третьего пакета к длительности прибавляется сотня за кластер. Это и есть те самые метки, которые на плате ставятся millis вокруг open-print-close: без них пауза растворяется в общем ритме, а с ними — лежит в логе числом. Порог в пятьдесят мс держу в скетче константой: смена носителя — правка одного числа.
Перенос на плату прямой: буфер у вас уже есть — массив строк из урока про буферную запись, добавьте к пакету две строки — метку до и длительность после. Кнопка D2 в модели работает как ручной flush: хвост выдавлен, питание резать можно. И следите за сеткой времени: после кластерной паузы следующая строка приходит на сто миллисекунд позже привычного места — на плате это значит, что сдвинулся сам замер, и это честно: ритм держит запись, а не наоборот. Это же правило закрывает споры с коллегами: показываешь колонку длительностей, и разговор о «медленной карте» заканчивается за минуту.
| Стратегия записи | Пауза на пакет | Потери при обрыве питания | Когда брать |
|---|---|---|---|
| строка + close на каждый замер | 12-18 мс на сессию | максимум одна строка | интервалы от секунды и реже |
| буфер на 8 строк, пакет | одна пауза на 8 строк | до восьми строк | интервалы 5-60 секунд |
| файл открыт, flush раз в минуту | 5-15 мс на дожатие | хвост после последнего flush | потоки от 2 Гц |
| flush каждые 100 мс | карта занята постоянно | страховки нет, ресурс тает | не делаю нигде |
Граница метода у меня такая. Пишете реже раза в секунду — весь этот антураж не нужен: короткая сессия с close на каждую строку, потеряете максимум строку, спите спокойно. Пишете от двух герц и плотнее — файл держите открытым, строки копите в буфере, flush по таймеру в десятки секунд, метки вокруг каждой порции. Между полюсами лежит зона выбора по цене потери: сколько строк вам не жалко при обрыве питания — тот буфер и ставьте.
И про потолок канала честно: 2-20 КБ/с — предел стандартной библиотеки на Uno, у которой каждый байт проходит через программные накладные расходы. Ускориться в разы можно библиотекой SdFat в режиме SPI_FULL_SPEED, а дальше упираешься в класс карты и в саму шину, устройство которой разобрано в уроке про SPI-шину. Для журналов этого хватает с запасом; для аудиопотоков — SdFat и носитель классом выше, но это уже не логгер, а плеер. Мои потоки до пяти герц жили годами на штатной библиотеке — упирайтесь в числа, а не в привычку.
- 1. Что происходит при file.println, пока буфер библиотеки не заполнен?
- 2. Почему раз в несколько пакетов запись длится 90-130 мс вместо обычных 10-25?
- 3. Чем close отличается от flush?
- 4. Где ставить метку длительности записи?
- 5. Сколько реально тянет стандартная SD-библиотека на Uno по SPI?
- Println кладёт строку в 512-байтный буфер ОЗУ; на карту байты уезжают при заполнении сектора, по flush или close.
- Файлы на FAT растут кластерами: на границе кластера библиотека правит таблицу — отсюда паузы 10-100 мс.
- Метки ставьте вокруг записи, до open и после close; millis-ритм замеров от пауз записи не спасает.
- Flush дожимает буфер в сектор, close ещё и обновляет длину файла в каталоге — выбирайте по цене потери.
- Устойчивая скорость штатной библиотеки на Uno — 2-20 КБ/с; узкое место лога не она, а паузы.
Частые вопросы по теме урока
- Что происходит при file.println, пока буфер библиотеки не заполнен?
- Print кладёт строку в 512-байтный буфер и возвращается за микросекунды. На карту байты уезжают при заполнении ячейки, по flush или close — карта о недожатом буфере не знает. Байты копятся в 512-байтной ячейке ОЗУ
- Почему раз в несколько пакетов запись длится 90-130 мс вместо обычных 10-25?
- На границе кластера библиотека ищет свободный кластер и правит цепочку FAT, а карта делает внутренние стирания — отсюда всплеск в десятки-сотню мс на дешёвых Class 4. Библиотека пересекла границу кластера и обновляет FAT
- Чем close отличается от flush?
- Flush дожимает буфер в сектор, файл остаётся открытым, длина в каталоге стоит на месте. Close делает всё то же и обновляет длину файла — после него размер виден компьютеру. Close дополнительно записывает длину файла в каталог
- Где ставить метку длительности записи?
- Пауза живёт внутри сессии open-print-close, поэтому метки ставятся вокруг неё. Замер на входе в опрос датчика покажет ритм замеров и промолчит о кластере. До открытия файла и сразу после close
- Сколько реально тянет стандартная SD-библиотека на Uno по SPI?
- Устойчивые 2-20 КБ/с — потолок штатной библиотеки с её байтовыми накладными расходами. Больше дают SdFat в SPI_FULL_SPEED и носитель классом выше. 2-20 КБ/с в зависимости от класса карты