Логгер с Uno переехал на ESP32 ради Wi-Fi, и на первом же запуске монитор напечатал знакомое «card init. failed». Карта рабочая — телефон её читает. Модуль исправный, провода перекинуты аккуратно. Виновата привычка: на Uno шина SPI прибита к пинам 11-13 гвоздями, а у ESP32 своя география, и библиотека SD по умолчанию стучится не туда, куда вы воткнули провод. Пока пины скетча не согласованы с проводкой, карта молчит, и никакое переформатирование не поможет.
Здесь раскладываю, где у классического ESP32 живут контроллеры VSPI и HSPI, какие пины они держат по умолчанию, как перебросить линии на другие GPIO и как посадить карту и дисплей на одну шину, чтобы они делили её без драки. Кому при этом мешает чужой CS, почему у дешёвых дисплеев карту душит MISO и какой порядок инициализации спасает — тоже здесь, с моделью шины в симуляторе в середине.
Если модуль microSD ещё не поднимали ни на одной плате, стартуйте с устройства модуля и распиновки SPI — там про стабилизатор AMS1117 и преобразователь уровней. Я же считаю, что карту на Uno вы уже гоняли, и говорю только про ESP32-специфику.
Четыре контроллера, из которых два ваших
Внутри классического ESP32 (WROOM-32 и родня) сидят четыре SPI-контроллера, но два из них вам недоступны: SPI0 и SPI1 обслуживают внутреннюю флеш-память, из которой загружается скетч. Их линии не выведены на гребёнку, и лезть туда нечем. Для периферии остаются SPI2 и SPI3, и в Arduino-каркасе они ходят под именами HSPI и VSPI. К названиям относитесь как к ярлыкам, а не к паспорту скорости: я видел обсуждения, где человек уверял, что «на HSPI стабильнее», — стабильность задаёт частота, длина проводов и питание, а не слово в константе.
Дефолтные пины VSPI: SCK — GPIO18, MISO — GPIO19, MOSI — GPIO23, CS — GPIO5. У HSPI: SCK — GPIO14, MISO — GPIO12, MOSI — GPIO13, CS — GPIO15. Библиотека SD в каркасе ESP32 поднимает карту именно на VSPI: обычный SD.begin(5) без лишних аргументов работает на 18/19/23. Собрали проводку на этих пинах — вы вообще не заметите, что где-то есть второй контроллер, и это честный вариант по умолчанию. Линию CS при этом дёргает сама библиотека: в скетче вы лишь называете её номер, руками поднимать-опускать сигнал не нужно ни в одном месте кода.
Если пины заняты, любую линию можно перебросить почти на любой выводной GPIO через матрицу коммутации: SPI.begin(sck, miso, mosi, cs) до SD.begin — и шина живёт на новых ногах. Условие честное: переброшенные линии держат высокие частоты хуже заводских, выше 40 МГц начинаются сюрпризы с целостностью сигнала. Для дата-логгера это неважно: карта при старте запрашивает консервативные 4 МГц и больше не просит, так что даже скрученные перемычки на 20 сантиметров живут спокойно.
#include <SPI.h>#include <SD.h> // Классическая проводка VSPI: SCK 18, MISO 19, MOSI 23, CS 5.const int CS_PIN = 5;File logFile;unsigned long lastRow = 0;int row = 0; void setup() { Serial.begin(115200); if (!SD.begin(CS_PIN)) { Serial.println("Карта не поднялась: FAT32, проводка, питание"); while (true) delay(500); } logFile = SD.open("/log.csv", FILE_WRITE); if (!logFile) { Serial.println("Файл не открылся"); while (true) delay(500); } logFile.println("n,temp");} void loop() { unsigned long ms = millis(); if (ms - lastRow >= 2000) { lastRow = ms; row = row + 1; logFile.print(row); logFile.print(","); logFile.println(22.4 + row * 0.1); logFile.flush(); // строка доезжает до карты сразу Serial.print("строк: "); Serial.println(row); }}Карта и дисплей на одной шине: кто кем командует
Экономия здесь настоящая: три линии — SCK, MOSI, MISO — общие, и по ним гоняют и картинку, и файлы. Делят шину не пинами, а временем, и разруливает это линия CS: пока CS карты висит в HIGH, карта глуха к тактам, и дисплей тратит шину сколько влезет. Активное состояние — низкий уровень, поэтому в паузах оба CS обязаны стоять в HIGH, и каждая библиотека поднимает свою линию сама перед begin. А вот два устройства с LOW на CS одновременно — уже не шина, а свалка: байты уйдут обоим, и по MISO ответит тот, чей выходной каскад сильнее. У карты и дисплея эти ответы схлестнутся, и вы получите оба симптома сразу: экран в клеточку и init failed. Я ловил такое на шилде, где CS дисплея оказался прижат к земле перемычкой на самой платке: экран работал, карта — нет, и до сути дошёл только мультиметр.
Частоты у соседей разные, и это нормально, а не конфликт: SD-библиотека на ESP32 стартует карту на консервативных 4 МГц и поднимает скорость сама, когда карта раскрылась, а дисплеи вроде ILI9341 крутят шину на 40 МГц и выше. Шина в каждый момент живёт на одной частоте: дисплейный драйвер перед своей транзакцией выставляет свою скорость, SD-библиотека перед своей — свою. Именно поэтому важен порядок begin: сначала медленная карта, потом шустрый дисплей перенастроит шину под себя, а при обращении к файлам всё вернётся обратно. Нарушите порядок — иногда работает, но вы отдаёте поведение на волю очередности версий библиотек.
#include <SPI.h>#include <SD.h> // Карта и дисплей делят шину: линии общие, CS у каждого свой.#define SD_CS 5 // CS карты на VSPI#define TFT_CS 2 // свой CS дисплея: любой свободный выходной GPIO#define TFT_DC 4 // у ILI9341 отдельная линия команда/память unsigned long lastRow = 0; void setup() { Serial.begin(115200); // VSPI на дефолтных пинах. Нужен HSPI — заведите SPIClass spi(HSPI) // и позовите spi.begin(14, 12, 13, SD_CS) вместо следующей строки. SPI.begin(18, 19, 23, SD_CS); if (!SD.begin(SD_CS, SPI)) { // карта стартует первой, на 4 МГц Serial.println("Карта не поднялась"); while (true) delay(500); } // Дальше обычный begin дисплея на той же шине: // его библиотека выставит свою частоту и будет дёргать TFT_CS.} void loop() { unsigned long ms = millis(); if (ms - lastRow >= 2000) { lastRow = ms; File logFile = SD.open("/log.csv", FILE_APPEND); if (logFile) { logFile.println(ms); logFile.close(); // короткая сессия: дисплей почти не ждёт } }}Кому в этой компании больнее всего — карте. Дисплей переживает битый байт одной клеточкой на экране, а у файла испорченный сектор — это обрыв содержимого посреди строки. Поэтому проводку делайте короткой, землю сводите в одну точку, и отдельно смотрите на MISO: у большинства TFT-модулей выход MISO уходит в высокоимпедансное состояние, когда TFT_CS поднят, но на дешёвых платках встречаются партии без этого ключа. Такой дисплей держит линию за собой, и ответы карты до контроллера не доезжают — карта жива, просто её не слышно.
Диагноз ставится отключением: поднимите карту с дисплеем, вынутым из макетки. Поднялась — шина в порядке, виноват сосед. Дальше ведите себя по чек-листу диагностики begin и большой сводной проверке — там же про exFAT, просадки питания и длину проводов. И про уровни здесь напомню добрым словом: карта трёхвольтовая, ESP32 тоже, так что модуль без преобразователя уровней на ESP32 уместнее, чем на Uno — разбор в уроке про уровни 3,3 и 5 В.
Разбор опыта и границы метода
В модели подёргайте кнопкой и тумблером и сверьте реплики с текстом выше: один активный CS — шина работает, два — конфликт, ноль — свободна. На железе то же самое происходит с миллионами тактов в секунду, но логика совпадает байт в байт, и именно эту логику полезно один раз увидеть руками. Общая шина с картой — не экзотика и не хак: на Uno её же раскладывали в уроке про CS и общую шину с шилдами; на ESP32 сверху добавляются только выбор контроллера и разъехавшиеся частоты.
Граница метода: если дисплей сидит не на SPI, а на параллельной шине 8080 или на I2C, конфликтов по шине нет по определению, зато заняты другие пины — экономия спорная. И если планов две SD-карты, вторую заводите на HSPI: две карты на одном контроллере тоже поднимаются, но отладка двух CS на одной шине отнимает вечер, который я предпочитаю тратить на что-то другое. Кстати, если разница между шинами для вас принципиальна — помните, что на клонах ESP32 с меньшим числом выводов вроде S2 и C3 имена и дефолтные пины другие: сверяйтесь с datasheet конкретной платы, а не с этой таблицей.
| Вариант подключения | Пины | Когда брать |
|---|---|---|
| VSPI по умолчанию | SCK 18, MISO 19, MOSI 23, CS 5 | карта одна: SD.begin(5) и ничего больше |
| HSPI | SCK 14, MISO 12, MOSI 13, CS 15 | VSPI занят дисплеем или нужна вторая карта |
| Свои пины через GPIO matrix | любые выводные GPIO по вашему разъёму | плата с неудачной разводкой; выше 40 МГц не разгоняйте |
| Общая шина с дисплеем | SCK/MOSI/MISO общие, CS у каждого свой | экономия трёх пинов; следите за MISO соседа и порядком begin |
- 1. Сколько SPI-контроллеров классического ESP32 достаётся вашей периферии?
- 2. SD.begin(5) без дополнительных аргументов поднимает карту на...
- 3. CS карты и CS дисплея оказались низкими одновременно. Что произойдёт?
- 4. Зачем писать spi.begin(sck, miso, mosi, cs) перед SD.begin(CS, spi)?
- 5. Дешёвый TFT-модуль держит линию MISO при поднятом CS. Чем это кончится?
- SPI0 и SPI1 ESP32 заняты флеш со скетчем: периферии остаются SPI2 (HSPI) и SPI3 (VSPI).
- Дефолт VSPI: SCK 18, MISO 19, MOSI 23, CS 5 — SD.begin(5) работает без единой настройки.
- Свои ноги шины назначаются в spi.begin(sck, miso, mosi, cs); выше 40 МГц переброс держит частоту хуже.
- На общей шине линии общие, CS у каждого свой; сначала begin карты, потом дисплея — порядок важен.
- Карта молчит с дисплеем в цепи — подозревайте его MISO: серийный резистор 1-10 кОм ставит соседа на место.
Частые вопросы по теме урока
- Сколько SPI-контроллеров классического ESP32 достаётся вашей периферии?
- SPI0 и SPI1 обслуживают флеш-память со скетчем и наружу не выведены. Периферия садится на SPI2 (HSPI) и SPI3 (VSPI); Wi-Fi при этом ни при чём — он живёт на своей радиочасти. Два: SPI2 (HSPI) и SPI3 (VSPI)
- SD.begin(5) без дополнительных аргументов поднимает карту на...
- В каркасе ESP32 SD-библиотека по умолчанию работает на VSPI с пинами 18/19/23 и CS 5. Угадывания нет: проводка и скетч должны совпадать пин в пин. VSPI: SCK 18, MISO 19, MOSI 23
- CS карты и CS дисплея оказались низкими одновременно. Что произойдёт?
- CS выбирает адресата тактов. Два низких уровня = два слушателя сразу: передаваемые байты получат оба устройства, а их встречные ответы по MISO наложатся друг на друга. Байты уйдут обоим, а ответы схлестнутся по MISO
- Зачем писать spi.begin(sck, miso, mosi, cs) перед SD.begin(CS, spi)?
- Первая форма назначает физические ноги шины — это штатный способ уйти от дефолтных 18/19/23. Ограничение одно: переброшенные линии выше 40 МГц держат частоту хуже заводских. Чтобы перебросить линии шины на другие GPIO через матрицу коммутации
- Дешёвый TFT-модуль держит линию MISO при поднятом CS. Чем это кончится?
- Модуль без высокоимпедансного состояния на MISO глушит ответы карты: SD.begin отчитается о провале инициализации при исправной карте. Лечится серийным резистором 1-10 кОм в линию от дисплея. Карта не сможет ответить, и begin упадёт