Конфиг теплицы жил на карте файлом settings.txt, и однажды мне понадобилось дописать туда строку с отладочным флагом. Я открыл файл FILE_WRITE, дописал — и файл стал длиннее, а порог вентилятора остался прежним. Вечером я сделал вторую ошибку, похуже: решил «затереть» первую строку через seek(0) и вписать новый порог поверх. В файле лежало threshold=2640 — кто-то вписал четыре цифры, я записал поверх threshold=26, и хвост из двух цифр никуда не делся: в настройках честно осталось 2640. Вентилятор молчал при любой жаре, потому что порог в 2640 градусов не наступит никогда.
Обе ошибки — из одного незнания: FILE_WRITE — это дозапись, а не «открыть и менять», а seek двигает курсор, но ничего не стирает. Режим перезаписи в библиотеке SD вообще отсутствует, и это надо знать до того, как устройство уедет на объект.
Разберёмся, что такое объект File и почему это не сам файл, а ручка от него; что делают FILE_READ и FILE_WRITE при каждом запуске; как честно перезаписать файл и как читать конфиг построчно. По ходу увидим таблицу режимов и скетч, который проживает все три сценария на одной карте.
File — это не файл, а ручка от него
SD.open() возвращает объект типа File — дескриптор, через который вы работаете с файлом на карте. Сам файл лежит в кластерах, а File — это запись о нём: где курсор, какой режим, куда ведёт цепочка кластеров. Пока дескриптор существует, файл считается занятым: правило одного открытого файла из первого урока раздела действует всегда, и повторный SD.open() того же имени при незакрытом первом вернёт «ложь» — второй ручки не дадут. Закрыли close() — карта свободна, можно открывать следующий.
Чтение устроено ровно как в Serial, и это не совпадение: File наследует тот же интерфейс потока. f.available() говорит, есть ли ещё байты, f.read() отдаёт следующий, f.peek() подглядывает без чтения — если вы проходили урок про available и read, тело цикла чтения узнаете с первого взгляда. Работает и f.readStringUntil('\n') — та же функция, которой мы ловили команды в уроке про команды из Serial. Служебные вопросы отвечают f.position() (где курсор), f.size() (размер файла в байтах) и f.seek(n) (переставить курсор на байт номер n).
Проверку if (f) я ставлю всегда: open возвращает «ложный» объект, если файла нет (при FILE_READ), карта не смонтирована или имя уже занято. Без проверки скетч молча пишет в никуда, и вы узнаете об этом по пустому файлу через неделю наблюдений.
Пара слов об именах. Файловая система FAT хранит короткие имена в формате восемь-точка-три: log.csv — идеально, а отчёт_за_январь уже требует длинного имени, которое ест несколько записей каталога. Мне хватает коротких: run.log, cfg.txt, d2512.csv (день и месяц прямо в имени). Вложенные папки SD.h умеет — SD.mkdir() и путь вида dir/file.txt — но я держу всё в корне: на карте в несколько гигабайтов навигация по папкам не окупается, зато меньше шансов ошибиться в пути.
| Режим | Файла нет | Файл есть | Курсор после open | Типичное применение |
|---|---|---|---|---|
| FILE_READ | open вернёт ложь | открыто, только чтение | начало (pos=0) | конфиг, чтение лога |
| FILE_WRITE | создаст | открыто, дозапись | конец файла | логи, журнал событий |
| FILE_WRITE + seek(0) | создаст | открыто | переставите сами | правка начала — хвост старого остаётся |
| SD.remove + FILE_WRITE | создаст | создаст с нуля | pos=0 | полный перезапуск файла |
Как на самом деле перезаписать файл
FILE_WRITE — это «создай, если нет, и встань в конец». Хвостовая позиция курсора хранится в каталоге FAT, поэтому файл можно открывать и закрывать хоть сто раз за день — каждая дозапись продолжится ровно оттуда, где закончилась предыдущая. Так лог растёт между перезагрузками платы, и это то поведение, которое нужно в девяти случаях из десяти. Плате даже не важно, сколько времени прошло между дозаписями: час или трое суток — курсор ляжет на то же место, потому что позицию помнит каталог, а не переменная в RAM.
А в десятом случае — когда надо поменять содержимое — начинаются грабли, на которые я наступил с порогом 2640. seek(0) переставляет курсор записи в начало, но файл не обрезается: байты поверх, которые вы не переписали, остаются. Порог 26 поверх порога 2640 дал threshold=260 — ноль суровее нужного в десять раз, вентилятор включался бы при 260. Единственный честный способ перезаписи в SD.h: SD.remove(name) удалить файл и открыть его FILE_WRITE заново — создастся с чистого листа. Два обращения к карте вместо одного, зато ноль сюрпризов. Моё правило: seek я применяю только при чтении середины больших файлов, для перезаписи — никогда.
Почему хвост так коварен: числа в файле имеют переменную длину. Порог 26 и порог 2640 отличаются на два байта, и если новый короче старого, разница остаётся в хвосте и прилипает к концу числа. Если длиннее — вы затрёте начало следующей строки. Именно поэтому конфиги я переписываю целиком: один remove, один open, один блок println — и файл всегда представляет собой то, что вы только что в него положили. Для чтения seek наоборот удобен: например, перескочить шапку CSV, зная её длину в байтах.
#include <SPI.h>#include <SD.h> const int chipSelect = 10; void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } // 1. Полная перезапись: удалить и создать заново if (SD.exists("run.log")) { SD.remove("run.log"); } File w = SD.open("run.log", FILE_WRITE); w.println("zapusk sketcha"); w.close(); // 2. Дозапись: FILE_WRITE ставит курсор в конец File a = SD.open("run.log", FILE_WRITE); a.println("vtoraya stroka"); a.close(); // 3. Чтение: FILE_READ, курсор в начале File r = SD.open("run.log", FILE_READ); while (r.available()) { Serial.write(r.read()); // печатаем содержимое в Serial } r.close();} void loop() { }Обратите внимание на структуру: каждый файл открылся и закрылся до того, как открылся следующий. Порядок «открыл — поработал — закрыл» я держу даже внутри setup, где соблазн оставить дескриптор открытым до конца скетча велик. В симуляторе ниже те же три запуска показаны как журнал: смотрите, куда встаёт курсор после каждого open и как растёт размер файла — это и есть поведение FILE_WRITE в чистом виде.
- Дописать строку в лог: SD.open(name, FILE_WRITE), print-вызовы, close
- Прочитать конфиг целиком: SD.open(name, FILE_READ), while (f.available()), close
- Заменить содержимое: SD.remove(name), затем SD.open(name, FILE_WRITE) и новые данные
- Проверить наличие файла до открытия: SD.exists(name)
- Спросить у дескриптора, где курсор и какой размер: f.position() и f.size()
- 1. FILE_WRITE при открытии существующего файла ставит курсор:
- 2. Что произойдёт при print() в файл, открытый режимом FILE_READ?
- 3. В файле строка threshold=2640. Открыли FILE_WRITE, сделали seek(0), записали threshold=26. Что в файле?
- 4. Как в SD.h честно перезаписать файл целиком?
- 5. Повторный SD.open() того же файла при незакрытом первом дескрипторе:
- File — дескриптор-ручка, а не сам файл; открыл — поработал — закрыл, один открытый файл за раз
- FILE_WRITE — создать при отсутствии и курсор в конец: дозапись, а не перезапись
- FILE_READ — только чтение; печать в него молча возвращает 0 байт
- seek(0) двигает курсор, но не стирает хвост: короткая строка поверх длинной оставляет мусор
- Полная перезапись = SD.remove + SD.open(name, FILE_WRITE)
- Чтение — while (f.available()) с read() или readStringUntil, тем же кодом, что и для Serial
Теперь вы умеете открывать файл в любую сторону и не оставлять в нём хвостов. Отдельная развилка — где вообще хранить настройки: журнал измерений просится на карту, а вот пара чисел вроде порога и счётчика пусков честно живёт в EEPROM без всякой файловой системы — так это устроено в уроке про счётчик включений. Моё разделение: EEPROM — для флагов и чисел, файл — для всего, что хочется открыть глазами в блокноте. Осталось главное: понять, что происходит с байтами между println и картой — почему файл без close() выглядит пустым даже после сотни строк и как строить запись так, чтобы сбой питания отнимал у вас одну строку, а не весь ночной лог. Этим займётся следующий урок про запись и close. А если ваш конфиг уже читается построчно, взгляните, как тот же приём выглядит для CSV-лога с шапкой и столбцами — он разобран в уроке про CSV и чтение конфига.
Частые вопросы по теме урока
- FILE_WRITE при открытии существующего файла ставит курсор:
- FILE_WRITE — «создай, если нет, и встань в конец». Поэтому лог растёт между запусками скетча, а не затирается. В конец — дальнейшая запись дозапишется
- Что произойдёт при print() в файл, открытый режимом FILE_READ?
- Функции записи возвращают 0, логика продолжает работать. Ловушка тихая: узнаёте по пустому файлу, а не по сообщению об ошибке. Молча не запишется ни один байт
- В файле строка threshold=2640. Открыли FILE_WRITE, сделали seek(0), записали threshold=26. Что в файле?
- seek двигает курсор, но не стирает байты за пределами новой записи. Порог станет 260 — в десять раз суровее задуманного. threshold=260 — хвост старой строки остался
- Как в SD.h честно перезаписать файл целиком?
- Режима «затереть и писать» в библиотеке нет. Удалить и создать заново — два обращения к карте, зато файл начинается с чистого листа. SD.remove(name), затем SD.open(name, FILE_WRITE)
- Повторный SD.open() того же файла при незакрытом первом дескрипторе:
- Библиотека держит один дескриптор на имя. Порядок «открыл — закрыл — открыл следующий» обязателен. Вернёт «ложь»: один файл — один открытый дескриптор