Даталоггер в овощехранилище отработал сезон и отдал лог с дырами по несколько строк подряд. Serial каждые десять секунд печатал свежее измерение, скетч жив, питание не мигало — а на карте отдельных записей просто не было. Карта к концу сезона сыпалась секторами: не умерла, а именно сыпалась — читается, пишется, но часть секторов после записи не живёт. Всё это время я узнавал о потерях постфактум, раз в месяц, когда приезжал с ноутбуком.
Лечение, которое прижилось: замкнутый цикл. При каждом старте скетч пишет в карту контрольную строку, тут же читает её обратно и сверяет посимвольно. Совпало — карта исправна, LED горит, запись можно вести. Не совпало — месяц на объекте не пройдёт зря: дефект вскрывается в момент включения, в Serial падает точная картина. Цена — пара десятков миллисекунд при старте и один файл в корне карты.
Для замкнутого цикла нужны оба режима открытия: FILE_WRITE кладёт строку, FILE_READ возвращает её с карты — и оба ведут себя по-разному на пустой карте и на карте с историей. Таблицу режимов я приводил в уроке про File и режимы; здесь смотрю на них с другой стороны — со стороны содержимого, которое доезжает и не доезжает, плюс print против write и полный скетч самопроверки.
print против write: текст и байты
print и write едут в один буфер, но кладут разное. print(65) переводит число в десятичный текст: в файл лягут два символа — 0x36 и 0x35. write(65) кладёт один байт со значением 65 — в hex-редакторе 0x41, в блокноте буква A. Число одно, содержимое файла противоположное, и выглядит это как «записалось не то», хотя каждая функция отработала ровно как обещано.
Считаем байты. Измерение 65 в тексте — «65» плюс перевод строки в два байта: четыре на println. Бинарно — два байта на uint16 и ноль накладных на разделители. Серия в сто тысяч измерений текстом займёт порядка 600 КБ, байтами — 200: разница втрое, и при чтении сырого журнала тоже скорость выше. Мой расклад: CSV для людей и таблиц — print, сырые журналы под машинный разбор — write, пачками write(buf, len).
Грабли с усечением: write принимает байт, и write(1023) молча положит 0xFF — младшие восемь бит, старшие откусаны. Значения выше 255 едут парами: lowByte(1023) и highByte(1023) дают 0xFF и 0x03, при чтении собираются обратно в том же порядке. Я однажды полчаса сверял журнал с показаниями, пока не заметил, что все значения обрезаны до 255, — с той поры пары байтов складываю сразу в обёртку из двух write.
| Вызов | Что окажется в файле | Байт | Как читается потом |
|---|---|---|---|
| print(65) | текст «65» — байты 0x36, 0x35 | 2 | в блокноте увидите 65 |
| println(65) | «65» плюс перевод строки CR и LF | 4 | строка CSV |
| write(65) | один байт 0x41 | 1 | буква A в тексте, 65 в hex-редакторе |
| write(lowByte(1023)), write(highByte(1023)) | пара 0xFF, 0x03 | 2 | обратная сборка в 1023 тем же порядком |
FILE_WRITE и FILE_READ: пустая карта против карты с историей
FILE_WRITE — «создай, если нет, и встань в конец». На чистой карте создаст файл и вернёт живой дескриптор; на карте с историей — молча допишет в хвост существующего. FILE_READ строже: файла нет — open вернёт «ложный» объект, и это не сбой, а честный ответ: читать нечего. Оба случая молчаливы, и различает их только проверка if (f) — я ставлю её на каждый open, включая чтение.
Из строгого режима растёт паттерн, который живёт в каждом моём логгере, — счётчик прогонов. Открываем runs.txt FILE_READ: файла нет — первый старт, счётчик ноль; есть — читаем число, close. Дальше плюс один и перезапись заново: SD.remove плюс FILE_WRITE, механика из урока про режимы. Три строки дают скетчу память о том, сколько раз он просыпался, — полезно и для диагностики, и для ротации логов из урока про циклическую запись.
Симметрия, на которой ловятся: одна и та же строка SD.open с FILE_READ на пустой карте вернёт ложь, на карте с файлом — дескриптор. Скетч без проверки в первом случае тихо пропустит чтение, во втором отработает как ни в чём не бывало. Единственное видимое различие двух сценариев — как раз if (f); всё остальное библиотека делает молча.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const int LED = 12; // эталон: строка, которую пишем и читаем обратноchar expect[] = "sd-test-12345";const int LEN = 13; void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); if (!SD.begin(chipSelect)) { Serial.println("SD begin failed"); while (true) { } } // 1. сессия записи: open - print - close File w = SD.open("check.txt", FILE_WRITE); if (!w) { Serial.println("open for write failed"); while (true) { } } w.println(expect); w.close(); // без close чтение увидит старый размер // 2. сессия чтения: FILE_READ требует готового файла File r = SD.open("check.txt", FILE_READ); if (!r) { Serial.println("open for read failed"); while (true) { } } // 3. сверка посимвольно; CR и LF пропускаем int i = 0; while (r.available()) { int c = r.read(); if (i < LEN && c == expect[i]) { i = i + 1; } else if (c != 13 && c != 10) { i = -999; // посторонний байт - сверка провалена } } r.close(); if (i == LEN) { Serial.println("proverka OK: zapis doehala do sektora"); digitalWrite(LED, HIGH); } else { Serial.print("proverka provalena, soshlos simvolov: "); Serial.println(i); }} void loop() { }Как работает сверка. Запись через print возвращает количество байт, ушедших в буфер, — и это не гарантия, что сектор доехал до флеш-памяти: путь лежит через буфер 512 байт, механику которого я разбирал в уроке про println и close. Живой ответ даёт только чтение с носителя: цикл available/read из урока про чтение файла тянет каждую букву полным путём записи и чтения. Расхождение на первом же символе — и скетч уже печатает, сколько символов сошлось.
Числа и границы. Полный цикл — open, print, close, open, читать пятнадцать байт, close — четыре обращения к карте; на моей связке Uno с картой 8 ГБ это 15-25 мс раз при включении. Ловит сверка сыплющиеся секторы, оторванный провод MISO, карту, которая пишется, но не читается. Не ловит она будущее: карта, здоровая сегодня, может лечь через месяц, — для этого существуют flush по таймеру и конденсатор на питании из урока про sync и сбои.
Проверить, что реально легло в файл, дешевле всего на ПК: вынули карту, открыли check.txt в hex-редакторе и читаете байты слева. После print(65) там 36 35, после write(65) — 41, после пары lowByte и highByte — FF 03. Три минуты на карточке закрывают вопросы, которые в Serial не видны: библиотека печатает то, что вы отправили, а редактор показывает то, что доехало. Тот же трюк применяю к чужим файлам: увидел в hex-редакторе переводы строк CRLF в CSV, собранном не моим скетчем, — и стало ясно, почему чужой парсер не съел колонки.
В боевом логгере самопроверка живёт так: при старте пишем эталон в check.txt, сверяем, SD.remove убирает файл — карта проверена, лог не тронут. Сверка провалена — запись не стартует: LED мигает кодом, Serial печатает, сколько символов сошлось, и карта меняется до выезда, а не после месяца молчаливой порчи лога. Полную методику доведения карты до боевого состояния собирает диагностический чек-лист.
Сессию файла удобно держать в голове как транзакцию: open объявил начало и получил дескриптор, print или write наполнили буфер, close зафиксировал итог на карте. Разорванная посередине транзакция — пустой файл вчерашнего размера, о чём уже рассказывал урок про println и close. Второе правило — дескриптор один: файл, уже открытый где-то в скетче, повторным open не откроется, и if (f) это тоже поймает. Поэтому я не держу файл открытым «на всякий случай»: открыл — сделал дело — закрыл, а счётчик прогонов со своим runs.txt живёт короткой сессией в setup.
И ещё одно место, где замкнутый цикл окупается, — карты из неизвестного конвоя. Те самые «32 ГБ» с рынка за копейки нередко рисуют объём, которого нет: пишешь — отвечают успехом, читаешь — хвост не тот. Сверка при каждом старте вскрывает подделку за минуты: эталон из пятнадцати байт читается криво уже на третьей позиции. После такого вскрытия карта уезжает в утиль, а не в логгер, где она молча гробила бы месяц наблюдений. А в симуляторе ниже сессию прогоняют руками: pot задаёт значение, кнопка запускает цикл записи и чтения, тумблер изображает дефектную карту.
- print кладёт текст («65» — 0x36, 0x35), write — сырой байт (65 — 0x41): в файле это разные вещи
- Значения выше 255 едут парами lowByte/highByte: write(1023) молча усечётся до 0xFF
- FILE_WRITE создаёт и дописывает; FILE_READ на отсутствующем файле возвращает ложный дескриптор
- if (f) на каждом open — единственное видимое различие пустой карты и карты с историей
- Сверка чтением при старте: 15-25 мс и четыре обращения к карте — дефект вскрывается до выезда
- 1. Что окажется в файле после write(65)?
- 2. Как положить в файл значение 1023 ровно двумя байтами?
- 3. На карте нет файла cfg.txt. Что вернёт SD.open("cfg.txt", FILE_READ)?
- 4. Что подтверждает возвращаемое значение f.print(...)?
- 5. Чем сверка чтением честнее проверки по числу байт от print?
Частые вопросы по теме урока
- Что окажется в файле после write(65)?
- write отправляет значение как сырой байт: 65 станет 0x41 — буквой A в текстовом виде. print(65) положил бы два текстовых символа 0x36 и 0x35. байт 0x41 — один байт
- Как положить в файл значение 1023 ровно двумя байтами?
- write усекает до младших восьми бит, поэтому 1023 одним вызовом превратится в 0xFF. Пара lowByte/highByte даёт 0xFF и 0x03 — собирается обратно в 1023. lowByte(1023) и highByte(1023) двумя write
- На карте нет файла cfg.txt. Что вернёт SD.open("cfg.txt", FILE_READ)?
- FILE_READ на отсутствующем файле возвращает «ложный» объект — читать нечего. Созданием занимается FILE_WRITE; различить сценарии помогает только if (f). ложный дескриптор, который ловит if (f)
- Что подтверждает возвращаемое значение f.print(...)?
- print и write возвращают число байт, ушедших в буфер 512. До носителя байты едут позже — при заполнении сектора, flush или close. байты легли в буфер — и только это
- Чем сверка чтением честнее проверки по числу байт от print?
- Чтение тянет каждую букву полным путём записи и чтения с флеш-памяти. Счёт буфера говорит только о том, что байты приняты оперативкой платы. read возвращает содержимое сектора с носителя, а не счёт буфера