Первый замер обнадёжил: скетч писал строку за строкой, Serial показывал ровный ритм, и казалось, что карта съест всё, что ей предложат. Через минуту ритм сломался: плата задумалась на полсекунды, две строки пропали, потом всё пошло как раньше. Средняя скорость в этом танце ничего не значит: карта пишет не потоком, а блоками, и изредка задумывается надолго. Логгер с интервалом в секунду такой паузы не заметит, логгер на 20 Гц — поймает на каждом шаге, и никакой Serial этого не покажет, пока не начать мерить millis() вокруг записи. Полсекунды тишины при записи в файл — это не зависание платы, это карта вела уборку внутри себя.
Ниже — механика блока в 512 байт, замер пауз на настоящей Uno и арифметика ритма: почему раз в секунду комфортно, раз в 200 мс — терпимо, а раз в 50 мс — уже дырки в журнале, и это не фигура речи: строка, которую скетч пропустил из-за занятой карты, не доехала ни в буфер, ни в файл, её не вернёт никакой разбор. Общая теория потерь при частой записи уже была в уроке про скорость и потери — я не пересказываю его, а спускаюсь под капот: что происходит между вызовом print() и байтами на флеш-памяти. И сразу договоримся о словах: скорость — это не только «сколько байт в секунду», но и «какая самая длинная пауза». Для логгера вторая часто важнее первой.
Что происходит между print() и картой
Карты SDHC пишут блоками ровно по 512 байт — размер задан протоколом, выбирать не приходится. Библиотека SD держит внутренний буфер на один блок: каждый print() лишь наполняет его байтами, а на карту они уходят, когда буфер полон или файл закрывают. Хвост дописывается при close() и flush() — отсюда привычка закрывать файл перед выдёргиванием карты. Сброс — это команда записи блока: карта программирует флеш-память обычно за единицы миллисекунд, изредка за 10-50 мс, когда внутри затеялась собственная уборка секторов. Именно эти редкие длинные паузы и ломают частые логи.
Отдельный вопрос — держать файл открытым или открывать на каждую запись. Открытый файл экономит вызовы и работает быстрее, но переживание выдернутого питания слабеет: хвост буфера не дописан, каталог не обновлён, файл числится короче, чем есть. Открытие на каждую строку медленнее, зато каждая запись завершена. Компромисс, к которому я пришёл на батареечных логгерах: держать открытым и звать flush() раз в минуту — скорость близка к максимальной, потеря при сбое ограничена последней минутой.
На максимуме и в типичном режиме цифры расходятся в сотню раз. SdFat при записи полными блоками выжимает с Uno сотни КБ/с, а средняя латентность — около одной миллисекунды; типовой скетч на штатной SD-библиотеке с построчной записью живёт в единицах КБ/с. Разница — в том, сколько байтов едет за один сброс и как часто дёргается флеш-память. Сколько едет у вас, покажет замер: код ниже считает millis() вокруг каждой записи и вываливает паузы в Serial. Строки с dt больше пары миллисекунд — это и есть сброс блока; строка с dt за полсотни — редкая уборка. За сутки таких событий единицы, и каждое — возможная дырка в частом логе.
#include <SPI.h>#include <SD.h>#include <RTClib.h> RTC_DS3231 rtc;File logFile;unsigned long lastLog = 0;unsigned long wroteTotal = 0;int rows = 0; void setup() { Serial.begin(9600); rtc.begin(); if (!SD.begin(10)) { Serial.println("SD не стартовала"); while (1); } Serial.println("замер: открытие файла, строка, закрытие - по millis");} void loop() { if (millis() - lastLog < 1000) return; lastLog = millis(); DateTime now = rtc.now(); char stamp[20]; snprintf(stamp, sizeof(stamp), "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); unsigned long t0 = millis(); // паузу ловим вокруг записи logFile = SD.open("SPEED.CSV", FILE_WRITE); logFile.print(stamp); logFile.print(",22.4,45.1"); logFile.close(); unsigned long dt = millis() - t0; rows++; wroteTotal += 30; // длина строки с переводом Serial.print("строка "); Serial.print(rows); Serial.print(": "); Serial.print(dt); Serial.print(" мс, всего "); Serial.print(wroteTotal); Serial.print(" байт, до сброса блока: "); Serial.println(512 - (wroteTotal % 512));}Ритм: раз в секунду против раз в 50 мс
Арифметика ритма для строки в 30 байт. Интервал одна секунда: 30 байт в секунду, буфер 512 копится почти 17 секунд, один сброс в 17 секунд занимает доли процента времени — карта живёт праздно, логгер ничего не замечает. Интервал 200 мс: 150 байт в секунду, сброс раз в 3,4 секунды — всё ещё ровно. Интервал 50 мс: 600 байт в секунду, сброс каждые 0,85 секунды, и редкая пауза в 50 мс оказывается длиннее самого интервала — строка уезжает, следующая запаздывает, в журнале дырка. За сутки это миллион с лишним строк и под пятьдесят мегабайт — по объёму карта справится, по ритму нет.
Замеры сообщества сходятся в одной точке: практический потолок простого скетча со штатной библиотекой — около 20-25 записей в секунду. Дальше начинается инженерия: буфер в RAM на десятки строк и пакетный сброс — приём из урока про пакетную запись; периодика строго на millis, потому что delay() на 50 мс, наложенный на паузу карты, удвоит пропуск — планировщик без delay разобран в уроке про millis. Второй потолок кроется в самом транспорте: SPI на Uno крутится на 8 МГц, и программные расходы в каждой транзакции съедают остаток — быстрее простого скетча не станет без переписывания библиотеки.
Почему я вообще заморачиваюсь с этим счётом. Один логгер писал вибрацию насоса на 25 Гц и терял строку-другую в час — тихо, без признаков и сообщений: Serial выглядел идеально, потому что в него шли те же строки, что и в файл, но без пауз. Диагноз занял вечер: редкие паузы карты длиннее интервала в 40 мс, и каждая такая съедает запись целиком. Лекарство заняло полчаса: буфер на пятнадцать строк в RAM и сброс раз в полминуты — паузы уехали внутрь резервного окна, потери кончились. Мораль не в цифрах, а в порядке действий: сначала измерьте паузы вокруг записи, потом выбирайте интервал, и только потом вините библиотеку. Замер из урока стоит ставить в каждый свой проект логгера — он дешевле любого осциллографа.
| Интервал | Нагрузка | Что происходит |
|---|---|---|
| 1 раз в секунду | 30 байт/с | блок копится 17 секунд; сброс занимает доли процента — комфортно годами |
| раз в 200 мс | 150 байт/с | сброс каждые 3,4 секунды; ещё ровно, редкие паузы никто не замечает |
| раз в 50 мс | 600 байт/с | сброс каждые 0,85 с; пауза 50 мс длиннее интервала — строки пропадают |
| раз в 20 мс | 1,5 КБ/с | сброс чаще паузы; простой скетч не успевает — нужен буфер в RAM и пакетный сброс, это вопрос выживания журнала |
Замер полезно вести постоянно: тот же код с millis() вокруг записи печатает паузы в Serial, а график неровностей рисует Serial Plotter — всплески в момент сброса блока видны как на ладони. За пределы буфера лучше не заходить: потерянный хвост при выдёргивании питания — тема урока про flush и питание. Батареечным логгерам вся эта экономия миллисекунд экономит микроамперы: как сложить ритм записи со сном, собрано в уроке про сон и батарейку. Паузы вида «50 мс раз в десять минут» — это карта переносит служебные таблицы; предсказать момент нельзя, ловится только замером. И проверяйте карту на подделки: надпись на корпусе и реальное поведение расходятся в разы, дешёвые карты проседают именно на длинных паузах.
- 1. Блок какого размера пишут карты SDHC?
- 2. Что делает print() при записи строки в файл?
- 3. Строка 30 байт, интервал одна секунда. Как часто буфер 512 уезжает на карту?
- 4. Почему при интервале 50 мс строки пропадают?
- 5. Как поднять скорость записи выше простого скетча?
- Карты SDHC пишут блоками ровно по 512 байт; print() лишь наполняет буфер библиотеки
- Пауза на сброс блока — обычно единицы мс, изредка 10-50 мс; в этот момент loop стоит целиком
- Строка 30 байт при интервале 1 с: блок уходит раз в 17 секунд — комфортно; при 50 мс — каждые 0,85 с и пауза уже пропуск
- Полные блоки дают максимум: SdFat выжимает сотни КБ/с, типовой построчный скетч — единицы КБ/с
- Дальше — буфер в RAM и пакетный сброс; периодику держите на millis, не на delay
Частые вопросы по теме урока
- Блок какого размера пишут карты SDHC?
- Протокол SDHC фиксирует блок в 512 байт; частичная запись по времени стоит как полная. Библиотека держит буфер ровно на этот размер. 512 байт — размер жёстко задан протоколом
- Что делает print() при записи строки в файл?
- print() кладёт байты в внутренний буфер на 512; карта получает их, когда буфер полон или файл закрыт. Хвост дописывается при close() и flush(). Наполняет буфер библиотеки; на карту байты уходят блоком
- Строка 30 байт, интервал одна секунда. Как часто буфер 512 уезжает на карту?
- 512 разделить на 30 — примерно 17 строк; при одной строке в секунду сброс случается раз в 17 секунд и занимает доли процента времени. Это и есть зона комфорта. Раз в ~17 секунд
- Почему при интервале 50 мс строки пропадают?
- Средняя скорость тут ни при чём: изредка карта программирует блок 10-50 мс, и пауза длиннее пятимиллисекундного ритма съедает строку целиком. Плечо — буфер в RAM и пакетный сброс; форматирование карты на это не влияет никак. Редкая пауза на запись блока длиннее самого интервала
- Как поднять скорость записи выше простого скетча?
- Полный блок за один write() — самый быстрый режим карты: SdFat на полных блоках выжимает сотни КБ/с. Построчная запись и переформатирование на это не влияют. Собрать строки в буфер в RAM и сбрасывать полными блоками 512