Расчёт перед запуском был простой: строка лога примерно на 30 байт, восемь тысяч записей в сутки — четверть мегабайта, карта на 2 ГБ уйдёт в архив лет через десять. По факту суточный файл весил вчетверо больше расчётного, и через год на карте закончилось место. CSV платит за свою читаемость каждым символом: дата, запятая, число — всё это символы, по байту за штуку, плюс перевод строки на каждой записи. Отсюда правило, к которому я пришёл не сразу: байты записи считают до запуска логгера, а не по факту, когда карта уже полная. Расчёт тот был честный — просто я считал байты полезной нагрузки, забыв, что в CSV штамп весит вдвое больше самого значения.
Разберём одни и те же показания двумя способами. Запись: штамп DS3231 и температура с датчика. Первый вариант — привычный CSV со штампом строкой. Второй — байтовые поля: время четырьмя байтами unixtime, температура двумя байтами с фиксированной точкой. Оба варианта пишут на одну и ту же карту через одну и ту же библиотеку, обе ветки умещаются в один скетч — и разница между ними нагляднее любой таблицы. Симулятор ниже прогоняет обе ветки параллельно: счётчики растут рядом, и перевес бинарника накапливается у вас на глазах.
Текст: по байту за каждый символ
Текстовая строка из прошлого урока выглядит так: 2025-01-15 14:07:32,22.4. Посчитаем: штамп — 19, запятая — 1, температура 22.4 — 4 символа, перевод строки — 2. Итого 26 байт. Число в тексте — всегда символы: минус двенадцать с точностью до десятой (-12.3) — уже 5 знаков, а температура с двумя знаками после точки добавит ещё. Всё это человек читает глазами, Excel открывает двойным щелчком, а правка блокнотом не требует вообще ничего. Шапка с именами колонок из урока про CSV-колонки делает файл самодокументируемым — за эту роскошь и платим байтами.
Суточный объём при интервале в секунду: 86 400 строк по 26 байт — около 2,2 МБ. Неделя — пятнадцать мегабайт, месяц — под шестьдесят. Для карты на 2 ГБ это не приговор, но добавьте второй датчик, влажность и длинный штамп — и годовой архив перевалит за сотню мегабайт. Ничего криминального, пока вы не захотели хранить год на одной карте вместе с WAV-файлами и конфигами.
У текста есть и мелкие зубастые грабли. Десятичный разделитель: Arduino печатает точку, а русский Excel по умолчанию ждёт запятую — числа прилетают текстом и не строятся в график без замены разделителя. Лечится двумя кликами при импорте или раз и навсегда — настройкой точки в параметрах таблицы. Вторая — запятая внутри значений: встретится текст с запятой, парсер разрежет строку заново, и колонки сдвинутся; значения с запятыми берут в кавычки. Третья — число колонок: добавите поле — старая шапка врёт, и разбор тихо съезжает на соседний столбец. Ни одна не страшна, пока вы помните о них до экспорта, а не после.
Бинарник: шесть байт на запись
Бинарный вариант кладёт те же поля без единого символа. Время — четыре байта: unixtime из DateTime, младший байт первым, little-endian. Температура — два байта: умножаем на десять и пишем целое, 22.4 превращается в 224, минус и точка исчезают, точность до одной десятой градуса остаётся. Итого 6 байт против 26 — запись тоньше в 4,3 раза, суточный объём падает до половины мегабайта. Пишется это методом write() из массива байт: никакого форматирования, готовые байты уходят в файл как есть.
Главная роскошь фиксированной длины — прямой доступ. N-я запись лежит по смещению 6 x N: seek — и она ваша, без разбора строк и без чтения лишнего. Для поиска максимума за месяц, ресемплинга и чтения записи номер сто тысяч это решает всё. Оборотная сторона: символы в файле больше не живут, глазами его не открыть — читается только тот, кто знает формат. Про десятибайтовый unixtime как человекочитаемый компромисс и про то, откуда взялась эпоха 1970 года, — в уроке про unixtime.
Бинарный формат стоит проектировать как договор с будущим собой: номер версии в первой записи, поля фиксированной ширины, множители зафиксированы в описании. Захотите влажность — не переписывайте старый формат, откройте версию 2 в новом файле: ридер читает обе версии, старые архивы остаются живыми. И множитель выбирайте с запасом: температура с точностью до сотой влезет в int16 со множителем сто — диапазон до плюс 327,67 градуса перекрывает любой бытовой термометр втрое. Пол-размерности перепланировать сложнее, чем не жалеть байт: uint32 на штамп, а не uint16, который переполнится за восемнадцать часов.
Бинарник ещё и быстрее. Каждый print() форматирует число в символы — на AVR это подпрограмма перевода float в ASCII, небыстрая и прожорливая по тактам. write() пихает готовые байты: меньше работы процессору, меньше байтов едет по SPI. Штатная SD-библиотека на Uno пишет текстовые строки со скоростью в единицы КБ/с — бинарный поток упирается в этот потолок реже, потому что байтов вдвое меньше на ту же информацию. Строку с float библиотека собирает через dtostrf — десятки тактов на символ; умножьте на пару полей и на частоту записи — и разница в тактах станет видна на батарейке.
Но даром не бывает. Поменяли состав полей — старые файлы читаются только старым скетчем, формат придётся нумеровать и хранить рядом с записями. Мой расклад после нескольких полевых сезонов: журнал, который смотрю руками и таскаю в Excel, — всегда CSV; плотный поток от батареечного узла, где каждый байт стоит микроамперов, — бинарник. Про то, что происходит с файлами у предела 4 ГБ, — в уроке про предел размера, а если готовый CSV нужно выгрузить без вынимания карты — выгрузка лога через Serial.
Три сценария из практики, чтобы расклад был не абстрактным. Погодная станция с записью раз в десять секунд пишет восемь тысяч строк в сутки: 225 КБ текстом против 52 КБ бинарником, за год — 80 МБ против 19 МБ, разница между «карта полна» и «карта живёт третий год». Лог вибрации станка на 50 Гц считает байтами без вариантов: текст родит под сорок мегабайт за смену, бинарник — под десять. А журнал доступа, который коллеги будут открывать руками, — только CSV: экономия байтов тут ничего не стоит, а удобство чтения стоит всего. Правило простое: сначала ответьте, кто и как будет читать этот файл, потом выбирайте формат.
#include <SPI.h>#include <SD.h>#include <RTClib.h> RTC_DS3231 rtc;File logFile;unsigned long lastLog = 0; void setup() { Serial.begin(9600); rtc.begin(); SD.begin(10); logFile = SD.open("TEXTLOG.CSV", FILE_WRITE); if (logFile) { logFile.println("stamp,temp"); // шапка для Excel logFile.close(); }} void loop() { if (millis() - lastLog < 5000) return; lastLog = millis(); DateTime now = rtc.now(); float temp = 22.4; char stamp[20]; snprintf(stamp, sizeof(stamp), "%04d-%02d-%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()); logFile = SD.open("TEXTLOG.CSV", FILE_WRITE); logFile.print(stamp); // 19 символов logFile.print(","); // 1 logFile.println(temp, 1); // 4-5 символов + перевод строки logFile.close(); // строка вышла на 26 байт}#include <SPI.h>#include <SD.h>#include <RTClib.h> RTC_DS3231 rtc;File logFile;unsigned long lastLog = 0; void setup() { Serial.begin(9600); rtc.begin(); SD.begin(10); // формат 1: uint32 unixtime (4 байта) + int16 temp*10 (2 байта) // описание формата храню рядом, в HEADER.TXT - не в самом файле} void loop() { if (millis() - lastLog < 5000) return; lastLog = millis(); DateTime now = rtc.now(); long t10 = round(22.4 * 10.0); // фикс-точка: 22.4 -> 224 byte rec[6]; rec[0] = now.unixtime() & 0xFF; // little-endian: младший байт первым rec[1] = (now.unixtime() >> 8) & 0xFF; rec[2] = (now.unixtime() >> 16) & 0xFF; rec[3] = (now.unixtime() >> 24) & 0xFF; rec[4] = t10 & 0xFF; rec[5] = (t10 >> 8) & 0xFF; logFile = SD.open("BINLOG.DAT", FILE_WRITE); logFile.write(rec, 6); // ровно 6 байт, без форматирования logFile.close();}| Критерий | Текст (CSV) | Бинарник |
|---|---|---|
| Запись со штампом и температурой | 26 байт | 6 байт: 4 на время, 2 на температуру |
| Сутки при 86 400 записях | около 2,2 МБ | около 0,5 МБ |
| Чем открывается | Excel, блокнот, любой парсер | скетч-ридер, Python struct, hex-редактор |
| Доступ к N-й записи | нет: строки разной длины | да: смещение 6 x N, seek и готово |
| Смена состава полей | свободно: колонки сами разберутся | новый формат — новые файлы, старые читает старый скетч |
- 1. Сколько байт занимает бинарная запись «время + температура» из урока?
- 2. Где лежит N-я запись бинарного лога?
- 3. Как температуру 22.4 уместить в два байта?
- 4. Чем открыть бинарный лог?
- 5. Во сколько раз бинарный суточный архив тоньше текстового?
- Текстовая запись со штампом и температурой — 26 байт, бинарная из тех же полей — 6: разница в 4,3 раза
- Число в бинарник кладут с множителем: температура 22.4 хранится как int16 со значением 224
- Фиксированная длина даёт прямой доступ: N-я запись лежит по смещению 6 x N
- CSV открывают Excel и блокнот; бинарник — скетч-ридер, Python struct или hex-редактор
- Мой расклад: журнал для глаз — CSV, плотный поток от батарейки — бинарник; смена формата внутри файла запрещена
Частые вопросы по теме урока
- Сколько байт занимает бинарная запись «время + температура» из урока?
- unixtime — это uint32, ровно четыре байта; температура с множителем на десять — int16, два байта. Итого шесть против двадцати шести в CSV — разница в 4,3 раза. 6: четыре на время, два на температуру
- Где лежит N-я запись бинарного лога?
- Фиксированная длина записи — главный подарок бинарника: seek на смещение 6 x N читает нужную строку без разбора всего файла. У CSV такой роскоши нет — строки разной длины. По смещению 6 x N: записи фиксированной длины
- Как температуру 22.4 уместить в два байта?
- Умножение на десять переводит число в целое с фиксированной точкой: 224 в int16 занимает два байта, точность до десятой градуса сохраняется. float на AVR весит четыре байта. int16 со множителем на десять: 224
- Чем открыть бинарный лог?
- В бинарнике нет символов — есть байты, и читает их только тот, кто знает формат. Excel и блокнот показывают кашу; для глаз рядом держат маяки или отдельный CSV. Скетчем-ридером, скриптом с struct или hex-редактором
- Во сколько раз бинарный суточный архив тоньше текстового?
- 26 разделить на 6 — примерно 4,3. За сутки это 2,2 МБ против 0,5 МБ при записи каждую секунду, за год — разница в сотни мегабайт. В 4,3 раза: 26 байт против 6