Как использовать microSD-карту с ESP32
Вы когда-нибудь начинали проект на ESP32 и обнаруживали, что вам не хватает места для данных? Будь то метеостанция, которой нужно записывать данные за годы наблюдений, умное домашнее устройство, требующее огромного конфигурационного файла, или даже крошечный музыкальный плеер, которому нужно хранить ваши любимые мелодии — встроенная память ESP32 может быстро стать узким местом.
Вот тут-то на помощь приходит скромная microSD-карта! Это идеальное решение для расширения ёмкости хранения вашего проекта, не требующее больших затрат.
В этом руководстве вы узнаете, как подключить модуль microSD-карты к ESP32, а также как читать и записывать файлы на карте. Вы также узнаете, как работать с папками, создавать и удалять файлы, дописывать данные и даже проверять размер и скорость карты.
Давайте начнём!
Модуль microSD-карты
Существует несколько типов модулей microSD-карт, но в этом руководстве мы используем тот, что показан на изображении ниже.
Этот конкретный модуль microSD-карты является хорошим выбором, поскольку он работает при напряжении 3,3 вольта. Это то же напряжение, которое используется как самой microSD-картой, так и платой ESP32, что означает, что всё работает вместе без необходимости каких-либо дополнительных компонентов для преобразования уровней напряжения.
Модуль использует протокол связи SPI для обмена данными с ESP32. На этом модуле линии данных SPI имеют подтягивающие резисторы 10 кОм, подключённые к 3,3 В, что помогает обеспечить надёжную передачу данных.
Важно понимать, что потребляемый ток microSD-карты может меняться в зависимости от выполняемой операции.
Когда карта не используется, она обычно потребляет около 500 мкА.
При чтении данных с карты потребление может составлять от 15 до 30 мА.
Запись данных требует больше энергии — некоторые microSD-карты могут потреблять до 100 мА при записи.
Из-за этого важно убедиться, что ваш источник питания может обеспечить достаточный ток. Выход 3,3 В на ESP32 может обеспечить до 500 мА, чего обычно достаточно как для чтения, так и для записи на карту. Однако если у вас возникают проблемы при попытке чтения или записи данных, проблемы с питанием — это одно из первых, что следует проверить.
Распиновка
Модуль microSD-карты имеет шесть выводов, и вот что делает каждый из них:
3V3 — это вход питания. Подключите его к выводу 3,3 В ESP32.
CS (Chip Select) — это управляющий вывод, используемый для активации модуля на шине SPI, позволяющий ему обмениваться данными при необходимости.
MOSI (Master Out Slave In) — это входной вывод SPI модуля microSD-карты, который принимает данные от ESP32.
CLK (Serial Clock) — принимает тактовые импульсы от ведущего устройства (вашего ESP32) для синхронизации передачи данных.
MISO (Master In Slave Out) — это выходной вывод SPI модуля microSD-карты, который отправляет данные на ESP32.
GND — это вывод заземления.
Подключение модуля microSD-карты к ESP32
Теперь давайте подключим модуль microSD-карты к вашему ESP32.
Начните с подключения вывода 3V3 на модуле microSD-карты к выводу питания 3,3 В на ESP32. Затем подключите вывод GND на модуле к одному из выводов GND на ESP32.
Далее мы настроим выводы, используемые для связи по SPI. Поскольку microSD-картам нужна быстрая передача данных, они лучше всего работают при подключении к аппаратным выводам SPI ESP32. На ESP32 выводы SPI по умолчанию: GPIO 18 (CLK), GPIO 19 (MISO), GPIO 23 (MOSI) и GPIO 5 (CS).
Вот краткая таблица соединений выводов:
Модуль microSD-карты |
ESP32 |
|---|---|
3V3 |
3.3V |
CS |
5 |
MOSI |
23 |
CLK |
18 |
MISO |
19 |
GND |
GND |
На схеме ниже показано, как именно всё подключить:
Подготовка microSD-карты
Перед использованием microSD-карты в проекте важно убедиться, что она правильно отформатирована с нужной файловой системой — FAT16 или FAT32. Это помогает ESP32 читать и записывать файлы без каких-либо проблем.
Если вы используете совершенно новую SD-карту, она, вероятно, уже отформатирована с файловой системой FAT. Однако заводское форматирование может быть не идеальным, и вы можете столкнуться с проблемами. Если вы используете старую карту, которая уже использовалась ранее, её определённо нужно будет переформатировать. В любом случае, рекомендуется отформатировать карту перед использованием в вашем проекте.
Существует два способа форматирования microSD-карты:
Способ 1
Сначала вставьте microSD-карту в компьютер. Затем найдите диск вашей SD-карты и щёлкните по нему правой кнопкой мыши. Выберите «Форматировать» из меню. Появится окно — выберите FAT32 в качестве файловой системы, затем нажмите «Начать», чтобы начать форматирование. Следуйте инструкциям на экране до завершения.
Способ 2
Для лучших результатов и меньшего количества ошибок настоятельно рекомендуется использовать официальную утилиту форматирования SD-карт от SD Association. Этот инструмент надёжнее, чем базовая программа форматирования, поставляемая с вашим компьютером. Вы можете скачать его с сайта SD Association. После установки запустите программу, выберите свою SD-карту из списка дисков и нажмите кнопку «Format». Этот специальный инструмент помогает избежать распространённых проблем, вызванных неправильным или неполным форматированием, что может сэкономить вам много времени на устранение неполадок в дальнейшем.
Настройка Arduino IDE
Мы будем использовать Arduino IDE для программирования ESP32, поэтому, пожалуйста, убедитесь, что у вас установлено дополнение ESP32, прежде чем продолжить:
Микроконтроллер ESP32 быстро стал одной из самых популярных плат среди любителей, инженеров и людей, интересующихся Интернетом вещей (IoT)…
Пример кода
Arduino IDE включает несколько встроенных примеров, которые поставляются с ядром Arduino для ESP32, и показывают, как работать с файлами на microSD-карте с помощью ESP32.
Чтобы получить доступ к этим примерам, откройте Arduino IDE и перейдите в File > Examples > SD. Вы увидите два примера скетчей. Вы можете выбрать любой из них, чтобы загрузить скетч в вашу IDE. Давайте загрузим SD_Test.
Этот пример скетча охватывает практически все основные задачи, которые вы можете захотеть выполнить с microSD-картой. Он показывает, как вывести содержимое папки (также называемой каталогом), создать новый каталог и удалить его. Он также показывает, как прочитать содержимое файла, записать новые данные в файл и дописать дополнительное содержимое к существующему файлу. Кроме того, пример включает переименование файла и удаление файла с карты.
Помимо работы с файлами, скетч также показывает, как инициализировать microSD-карту, проверить, какой тип карты подключён, и узнать размер карты.
Попробуйте этот скетч, прежде чем мы перейдём ко всем подробностям.
#include "FS.h"
#include "SD.h"
#include "SPI.h"
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
} else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}
void setup(){
Serial.begin(115200);
if(!SD.begin(5)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
listDir(SD, "/", 0);
createDir(SD, "/mydir");
listDir(SD, "/", 0);
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello ");
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
deleteFile(SD, "/foo.txt");
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
testFileIO(SD, "/test.txt");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}
void loop(){
}
После загрузки скетча откройте монитор порта и убедитесь, что скорость передачи данных установлена на 115200. Затем нажмите кнопку EN (сброс) на вашем ESP32. Если всё работает правильно, вы должны увидеть похожие сообщения в мониторе порта.
Объяснение кода
Код начинается с подключения трёх основных библиотек: FS.h, SD.h и SPI.h. Эти библиотеки позволяют ESP32 использовать протокол SPI для связи с microSD-картой и выполнять операции файловой системы, такие как чтение, запись и управление файлами и папками.
#include "FS.h"
#include "SD.h"
#include "SPI.h"
Также есть раздел кода (в настоящее время закомментированный), который позволяет переназначить выводы SPI. Если вы не хотите использовать выводы SPI ESP32 по умолчанию, вы можете раскомментировать этот раздел и задать свои собственные номера выводов для SPI-подключения.
/*
Uncomment and set up if you want to use custom pins for the SPI communication
#define REASSIGN_PINS
int sck = -1;
int miso = -1;
int mosi = -1;
int cs = -1;
*/
Функция setup()
Функция setup() — это место, где происходит всё основное. Сначала мы инициализируем монитор порта, чтобы видеть вывод нашей программы.
Serial.begin(115200);
Далее мы пытаемся инициализировать SD-карту. Код сначала проверяет, определили ли вы REASSIGN_PINS для использования пользовательских выводов; в противном случае он использует стандартные выводы SPI ESP32 для SD-карты. Если карта не инициализируется, будет выведено сообщение об ошибке и работа прекратится.
#ifdef REASSIGN_PINS
SPI.begin(sck, miso, mosi, cs);
if (!SD.begin(cs)) {
#else
if (!SD.begin()) {
#endif
Serial.println("Card Mount Failed");
return;
}
После успешного монтирования карты мы проверяем её тип (MMC, SDSC или SDHC) и выводим эту информацию вместе с общим размером карты в мегабайтах. Эта начальная настройка даёт нам хорошее представление о карте, с которой мы работаем.
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
После начальных проверок функция setup() переходит к демонстрации различных операций файловой системы на microSD-карте. Мы начинаем с вывода содержимого корневого каталога («/»).
listDir(SD, "/", 0);
Затем мы создаём новый каталог с именем mydir и снова выводим содержимое корневого каталога, чтобы убедиться, что он был создан.
createDir(SD, "/mydir");
listDir(SD, "/", 0);
Сразу после этого мы удаляем этот же каталог и ещё раз выводим содержимое. Это показывает нам, как создавать и удалять каталоги.
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
Затем код переходит к операциям с файлами. Мы создаём файл с именем hello.txt и записываем в него текст Hello.
writeFile(SD, "/hello.txt", "Hello ");
Далее мы дописываем текст World!\n в тот же файл, не перезаписывая исходное содержимое. Затем мы читаем полный файл и выводим его содержимое в монитор порта.
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
Чтобы показать, как обрабатывать файлы, код пытается удалить файл с именем foo.txt, который не существует, демонстрируя случай неудачи.
deleteFile(SD, "/foo.txt");
Затем он переименовывает наш файл hello.txt в foo.txt и читает переименованный файл для подтверждения изменения.
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
Затем программа выполняет тест ввода-вывода файлов с помощью testFileIO(). Эта функция читает и записывает большой объём данных в файл с именем test.txt и сообщает, сколько времени заняла каждая операция. Это помогает понять скорость чтения и записи вашей SD-карты.
testFileIO(SD, "/test.txt");
Наконец, программа выводит общий и использованный объём пространства на SD-карте.
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
Теперь давайте рассмотрим каждую из пользовательских функций, используемых в скетче:
Вывод содержимого каталога
Функция listDir() используется для вывода всех файлов и папок в определённом каталоге на SD-карте. Вы передаёте ей файловую систему SD, путь к папке, которую хотите просмотреть, и число, указывающее, насколько глубоко она должна заходить во вложенные папки. Функция открывает папку, проверяет её корректность, а затем перебирает каждый файл или папку внутри неё, выводя их имена и размеры. Если она находит другую папку и уровень больше 0, она заходит глубже в эту папку.
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("Failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
Например, чтобы вывести содержимое корневого каталога, вы вызовете её так:
listDir(SD, "/", 0);
Создание каталога
Функция createDir() создаёт новую папку на SD-карте. Вы передаёте ей файловую систему SD и имя папки (или путь). Она пытается создать папку и сообщает, получилось это или нет.
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
Например, чтобы создать папку с именем mydir, вы вызовете:
createDir(SD, "/mydir");
Удаление каталога
Функция removeDir() удаляет папку с SD-карты. Так же, как и функция createDir(), вы передаёте ей файловую систему SD и путь к папке. Она попытается удалить её и выведет сообщение, чтобы сообщить вам, была ли операция успешной.
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
Например, чтобы удалить mydir, вы вызовете:
removeDir(SD, "/mydir");
Чтение файла
Функция readFile() открывает файл, читает его посимвольно и выводит в монитор порта. Как и в предыдущих функциях, вы передаёте ей файловую систему SD и путь к файлу.
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
Например, следующая строка читает содержимое файла hello.txt.
readFile(SD, "/hello.txt")
Запись в файл
Функция writeFile() открывает файл в режиме записи и записывает указанное сообщение, перезаписывая любое существующее содержимое. Вам нужно передать ей файловую систему SD, путь к файлу и сообщение, которое вы хотите записать.
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
Следующая строка записывает Hello в файл hello.txt.
writeFile(SD, "/hello.txt", "Hello ");
Дописывание содержимого в файл
Функция appendFile() также записывает сообщение в файл, но вместо перезаписи содержимого она добавляет сообщение в конец. Это называется дописыванием (appending). Это полезно, когда вы хотите продолжать добавлять новые данные без удаления того, что уже было записано.
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
Следующая строка дописывает сообщение World!\n в файл hello.txt. \n означает, что в следующий раз, когда вы запишете что-то в файл, это будет на новой строке.
appendFile(SD, "/hello.txt", "World!\n");
Переименование файла
Функция renameFile() изменяет имя существующего файла. Вы передаёте ей файловую систему SD, текущее имя файла и новое имя, которое вы хотите задать. Если переименование прошло успешно, она выведет соответствующее сообщение.
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
Следующая строка переименовывает файл hello.txt в foo.txt.
renameFile(SD, "/hello.txt", "/foo.txt");
Удаление файла
Функция deleteFile() удаляет файл с SD-карты. Вы указываете файловую систему SD и путь к файлу, который хотите удалить.
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
Следующая строка удаляет файл foo.txt с microSD-карты.
deleteFile(SD, "/foo.txt");
Измерение скорости чтения и записи
Функция testFileIO() — это более продвинутый пример. Она тестирует, как быстро ваша microSD-карта может читать и записывать данные. Сначала она открывает файл, читает большое количество байт порциями для измерения скорости чтения, затем закрывает его. Затем она открывает тот же файл для записи и записывает большой объём данных порциями для измерения скорости записи. Эта функция полезна, если вы хотите понять производительность вашей SD-карты при работе с большими объёмами данных.
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
}
else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}