Датчик температуры в моей теплице отлично показывал значения в Serial Monitor: строка за строкой, каждые две секунды. А потом я закрыл ноутбук — и вся история исчезла. Заказчик попросил «показать, что было ночью», и показать было нечего: Serial — это не журнал, а бегущая строка. Переменная с последним измерением жила в оперативной памяти и умирала вместе с питанием. Мне нужен был носитель, который переживёт и выключение платы, и выдёргивание USB.
Решением стала microSD-карта за двести рублей и модуль к ней — платка размером с ноготь, которая втыкается прямо в макетку. И вот тут начался мой первый тупик: пример из библиотеки упрямо печатал «SD initialization failed». Карта была исправная — телефон её читал. Модуль, как выяснилось позже, был ни при чём... точнее, был: карта лежала в формате exFAT, а библиотека SD понимает только FAT16/FAT32. Но даже после переформатирования модуль молчал дальше: питание через длинную линейку макетки проседало, и один провод MISO, как я обнаружил на вторые сутки, коротил на соседний контакт — изоляция на дешёвой перемычке треснула. Стабильные 5 вольт, FAT32, короткие целые провода — три условия, без которых SD-модуль не стартует.
В этом уроке мы подключим модуль microSD к Arduino Uno по шине SPI, напишем первый логгер, который кладёт на карту файл log.csv с показаниями датчика, и разберём операцию, которую новички пропускают чаще всего, — myFile.close(). Из-за этой одной строчки люди теряют часы отладки и целые ночи наблюдений: без неё данные физически не попадают на карту.
Если модуля под рукой нет — не страшно. В конце урока запустим логгер в симуляторе: файловые операции там эмулируются печатью в Serial, зато вся логика — открытие, запись, закрытие — видна как на ладони.
Как SD-карта разговаривает с Arduino: шина SPI
SD-карта — не «флешка с контактами», а полноценное устройство со своим контроллером. Общается она по шине SPI: мастер (наша Arduino) тактирует обмен и говорит с одним или несколькими подчинёнными по четырём линиям. MOSI — данные от мастера к карте, MISO — от карты к мастеру, SCK — тактовые импульсы, CS (Chip Select) — сигнал «внимание, говорю с тобой»: пока на ней низкий уровень, карта слушает.
На Uno линии SPI выведены на фиксированные пины, и выбрать другие (не прибегая к программной эмуляции) нельзя: MOSI — 11, MISO — 12, SCK — 13. Свободным остаётся только CS — по умолчанию для SD-библиотеки берут пин 10. То есть четыре пина уходят карте безвозвратно. Планируйте макетку заранее: если на 11–13 уже сидит дисплей или светодиодная лента, начнутся загадочные конфликты, в которых виноваты оба устройства сразу.
Модуль microSD — это не просто разъём. На его плате стоит стабилизатор питания (обычно AMS1117, выдающий карте положенные 3,3 В) и микросхема преобразователя уровней. Смысл такой: сама карта питается тремя с половиной вольтами и её логика трёхвольтовая, а пины Uno выдают пять. Поэтому «голую» карту напрямую к Arduino подключать нельзя — сожжёте либо карту, либо пин. Модуль же спокойно ест 5 В и сам разбирается с уровнями.
Скорости SPI для логов более чем достаточны: шину гоняют на 8 МГц и выше, и даже мегабайт данных переправляется за считанные секунды. Узкое место — не провод, а файловая система: каждая операция с файлом — это поиск кластеров в таблице FAT, и времени она ест заметно больше, чем сам обмен байтами. Но об этом чуть ниже.
- CS модуля — к пину 10 (именно его по умолчанию ждёт библиотека SD)
- MOSI — к пину 11
- MISO — к пину 12
- SCK — к пину 13
- VCC — к 5V на плате (на модуле свой стабилизатор до 3,3 В)
- GND — к GND
- Провода держите короче 10–15 см: длинные ненадёжные соединения — вторая по частоте причина «SD initialization failed» после неправильно размеченной карты
Первый логгер: одна строка в log.csv
#include <SPI.h>#include <SD.h> const int chipSelect = 10; void setup() { Serial.begin(9600); Serial.print("Инициализация карты... "); if (!SD.begin(chipSelect)) { Serial.println("не удалась. Проверь карту, формат и провода."); while (true) { } // без карты дальше идти смысла нет } Serial.println("ок"); File myFile = SD.open("log.csv", FILE_WRITE); if (myFile) { myFile.println("sec,temp"); // шапка для примера myFile.println("0,24.7"); myFile.close(); Serial.println("Две строки записаны в log.csv"); } else { Serial.println("Не удалось открыть log.csv"); }} void loop() { // Периодический логгер - в следующем уроке}SD.begin(chipSelect) — точка входа. Функция настраивает SPI, находит карту и монтирует файловую систему. Она возвращает bool: true — карта жива, false — что-то не так. Никогда не игнорируйте этот результат: если продолжить без проверки, каждый последующий SD.open() будет молча возвращать «неудачу», и вы будете искать ошибку в коде, которого нет.
SD.open("log.csv", FILE_WRITE) возвращает объект типа File — дескриптор, «ручка», за которую вы держите открытый файл. Если файла нет, он будет создан; если есть — курсор записи встанет в конец: режим FILE_WRITE — это дозапись, а не перезапись. Именно поэтому лог за несколько запусков скетча не затирается, а растёт. Если открытие не удалось, myFile будет «ложным» — проверка if (myFile) отсеет этот случай.
myFile.println(...) работает как Serial.println, только пишет в файл: данные плюс перевод строки. Есть и myFile.print() без перевода, и myFile.write() для сырых байтов. Мне удобнее собирать одну строку цепочкой print, а завершать одним println — так меньше шансов запутаться, где заканчивается запись.
myFile.close() — герой следующего раздела. Пока запомните правило: открыли — поработали — закрыли. Всё, что вы напечатали в файл между open и close, существует сначала в буфере, и только close (или flush) гарантирует попадание на карту.
Почему лог пустой: close(), формат карты и питание
Классическая жалоба: «я записал сто строк, вынул карту, вставил в компьютер — файл пустой или его вообще нет». Разберём механику. Карта пишет данными блоками по 512 байт (сектор). Пока вы печатаете в открытый файл, Arduino копирует строки в свой RAM-буфер и физически не трогает карту — так быстрее. Когда буфер заполняется, он сбрасывается на карту целиком. А close() делает два дела: дожимает остаток буфера (даже если там три байта) и обновляет служебные таблицы FAT, чтобы компьютер «увидел» новые данные.
Прервали питание до close — буфер ушёл в никуда, а таблица FAT по-прежнему говорит компьютеру «файл пуст». Arduino не знает о вашей привычке дёргать шнур: она считает запись завершённой только после close. Поэтому в серьёзных логгерах файл открывают, дописывают одну порцию и сразу закрывают — потеря при сбое питания ограничивается одной записью. Если файл обязан оставаться открытым долго, для принудительного сброса буфера есть myFile.flush().
Второе правило из тех же недр: один открытый файл за раз. Библиотека SD рассчитана на одиночный дескриптор, и открытие второго файла при незакрытом первом — источник мистических багов. Открыли log.csv — записали — закрыли; понадобился конфиг — открыли его, прочитали, закрыли. Медленнее на десятки миллисекунд, зато предсказуемо.
Какую карту брать? Библиотека SD понимает FAT16 и FAT32 — это карты SD и SDHC объёмом примерно до 32 ГБ. Новые SDXC-карты по умолчанию размечены в exFAT, который Arduino не читает. Мой выбор — старые карты на 2–16 ГБ, оставшиеся от телефонов и фотоаппаратов: для текстового лога 2 ГБ — это миллионы строк, и старые карты ведут себя предсказуемее новых скоростных.
Форматируйте на компьютере в FAT32, а не «быстрым удалением» в телефоне. И заведите привычку подписывать карты маркером: «логгер-теплица», «логгер-гараж». Через полгода у вас будет три одинаковые карты и ноль воспоминаний, где чей лог — проверено на себе.
Теперь симулятор. В браузерной среде нет настоящей карты памяти, поэтому файловые операции там эмулируются печатью в Serial: каждое действие логгера — открытие, запись строки, закрытие — печатается строкой с префиксом [SD]. Для отладки логики это даже удобнее живой карты: видно, когда файл открыт, что именно ушло в запись и в какой момент происходит close(). Логика кода переносится на настоящий модуль один в один — поменяются только вызовы SD на реальные.
- Сократите интервал с 2000 до 500 мс: убедитесь, что [SD] open появляется один раз, а close — по счётчику строк. Затем перенесите закрытие внутрь каждой записи — так безопаснее на настоящей плате.
- Добавьте второй столбец: читайте освещённость с A1 через analogRead и дописывайте её в строку после запятой — CSV позволяет сколько угодно столбцов.
- Сломайте логгер специально: уберите строки [SD] close из кода и объясните вслух, что случится с последними строками при отключении питания. Если можете объяснить про буфер 512 байт — вы поняли главную часть урока.
- Заведите функцию logLine(unsigned long sec, int t), внутри которой живут все print-вызовы, и вызывайте её из loop() — код станет вдвое короче.
- SD-карта работает по SPI: CS → 10, MOSI → 11, MISO → 12, SCK → 13; берём модуль, потому что карте нужны 3,3 В и преобразователь уровней
- SD.begin(10) возвращает false при проблемах с картой, форматом или питанием — проверяйте всегда
- SD.open(name, FILE_WRITE) создаёт файл или открывает существующий с курсором в конце — это дозапись
- Записи копятся в буфере 512 байт; myFile.close() (или flush()) сбрасывает его на карту и обновляет таблицу FAT
- Файловая система — только FAT16/FAT32; exFAT и NTFS библиотека SD не понимает
- Одна карта — один открытый файл; схема «открыл — записал — закрыл» переживает пропадание питания с минимальной потерей
- 1. По каким пинам Uno подключается модуль microSD (без программного SPI)?
- 2. Вы записали в открытый файл три строки и вынули карту без close(). Что окажется на карте?
- 3. Какую файловую систему должна иметь карта для библиотеки SD?
- 4. Что делает myFile.close()?
- 5. FILE_WRITE при открытии существующего файла означает:
- 6. Зачем модулю microSD нужен свой стабилизатор и преобразователь уровней?
Теперь ваши данные переживают выключение платы: измерения складываются на карту строка за строкой, и файл log.csv можно открыть в Excel или переслать себе в мессенджере. В следующем уроке наведём порядок в этом файле: разберём формат CSV по-настоящему — с шапкой, разделителями и подводным камнем русского Excel, — научимся писать в лог раз в 60 секунд без delay() и прочитаем с карты конфиг, чтобы менять порог срабатывания без перепрошивки.
Домашнее задание на подумать: сколько строк влезет на карту 2 ГБ, если одна строка — 12 байт? Прикиньте в столбик, а в следующем уроке сверимся: получится порядка 170 миллионов — такого логгера хватит на человеческую жизнь.
Частые вопросы по теме урока
- По каким пинам Uno подключается модуль microSD (без программного SPI)?
- Аппаратный SPI на Uno жёстко выведен на 11–13, CS по умолчанию 10. Эти линии заняты карте безвозвратно. 11 (MOSI), 12 (MISO), 13 (SCK) плюс CS на 10
- Вы записали в открытый файл три строки и вынули карту без close(). Что окажется на карте?
- Печать в File копирует данные в RAM-буфер. На карту они попадают при заполнении буфера или по close()/flush(). Без них — потеря. Скорее всего ничего: данные сидели в буфере 512 байт и не дошли до карты
- Какую файловую систему должна иметь карта для библиотеки SD?
- SD.h понимает FAT16/FAT32 (карты до ~32 ГБ). Карта в exFAT — самая частая причина «SD initialization failed». FAT16 или FAT32
- Что делает myFile.close()?
- close() дожимает остаток буфера и фиксирует файл в файловой системе — только после него данные гарантированно на карте. Сбрасывает буфер на карту и обновляет таблицу FAT
- FILE_WRITE при открытии существующего файла означает:
- FILE_WRITE — дозапись в конец. Поэтому лог не затирается между запусками скетча. Курсор встанет в конец — новые строки допишутся
- Зачем модулю microSD нужен свой стабилизатор и преобразователь уровней?
- Голая карта — трёхвольтовое устройство; пять вольт от пинов Uno её убьют. Модуль понижает питание и согласует уровни. Карта питается 3,3 В с 3,3-вольтовой логикой, а Uno выдаёт 5 В