ESP32-S3 驅動 Micro SD 卡模組完整教學(SPI 模式 + Arduino 程式碼)

ESP32-S3 透過 SPI 模式驅動 Micro SD 卡模組,實現檔案列表讀取、讀寫刪除等完整操作。附接線圖、完整程式碼和避坑指南。

一句話摘要:ESP32-S3 透過 SPI 模式讀寫 Micro SD 卡,30 分鐘從接線到串口輸出檔案列表。

ESP32-S3 驅動 Micro SD 卡模組完整教學(SPI 模式 + Arduino 程式碼)

難度:⭐⭐☆☆☆(有一點基礎就能上手) 預計時間:30 分鐘 測試環境:Arduino IDE 2.3.x + ESP32 Arduino Core 3.x


TL;DR(直接上手版):

  1. 接線:GPIO5 → CMD(MOSI),GPIO13 → D0(MISO),GPIO14 → CLK,GPIO4 → D3(CS)
  2. 電源接 3.3V,不要接 5V
  3. SD 卡格式化成 FAT32(32G 卡尤其注意)
  4. 用內建 SD.h 函式庫,無需額外安裝
  5. 燒錄程式碼,打開串口監視器(115200),看到檔案列表就成功了

前言

做 ESP32 專案做到一半,你有沒有遇過這個問題:

想播一段音訊、存一堆感測器資料、或者塞幾張圖片進去…… 結果發現 ESP32 的內建 Flash 根本不夠用。

這時候最簡單的解法就是外掛一張 SD 卡。儲存空間從幾 MB 直接升級到幾十 GB,讀寫速度也夠用。本文就帶你把 ESP32-S3 + Micro SD 卡模組 這套組合從零跑通,用 SPI 模式讀取 32G 的 SD 卡裡的檔案列表。

接好線、燒錄程式碼,30 分鐘內你應該能在串口監視器裡看到自己 SD 卡裡的檔案名稱。


演示效果

串口輸出大概長這樣:

=== ESP32-S3 SD SPI Test ===
MOSI=5, MISO=13, SCK=14, CS=4
SD card mounted successfully.
SD Card Type: SDHC
SD Card Size: 30436MB
Total space: 30436MB
Used space : 512MB
Listing directory: /
  DIR : music
  FILE: readme.txt  SIZE: 128
  FILE: config.json  SIZE: 256


模組介紹

SD 卡模組就像是給 ESP32 裝了一個「讀卡機」。ESP32 本身沒有 SD 卡槽,這塊小模組充當中間人,把 ESP32 的 SPI 訊號翻譯成 SD 卡能聽懂的協定,讓你的 SD 卡變成可以隨意讀寫的外部儲存空間。

參數規格說明
介面協定SPI 模式 / SDIO 模式(本文用 SPI)
支援卡類型Micro SD(SDSC / SDHC,最高支援 32GB)
工作電壓3.3V(不要接 5V,會燒毀模組或燒卡
腳位數量CMD / CLK / D0 / D1 / D2 / D3 / 3.3V / GND
SPI 模式用到的腳位CMD(MOSI)/ D0(MISO)/ CLK / D3(CS)

選這款模組的原因:體積小、接線少(SPI 模式只用 4 條訊號線),是 ESP32 外擴儲存最常見的方案,網路上資料也多,踩坑有人陪。


BOM 清單

元件數量備註
ESP32-S3 開發板1任意帶 GPIO 的 S3 板均可
Micro SD 卡模組1支援 SPI 模式(背面有標註)
Micro SD 卡1建議 32G 以內,格式化 FAT32
跳線(杜邦線)若干公對母,盡量短

完整接線方法

ESP32-S3 腳位SD 模組腳位說明
3.3V3.3V只接 3.3V,不要接 5V
GNDGND共地,必須接
GPIO13D0SPI MISO:SD 卡回傳資料給 ESP32
GPIO5CMDSPI MOSI:ESP32 發資料給 SD 卡
GPIO14CLKSPI 時脈,ESP32 主控
GPIO4D3SPI 片選(CS),低電位時選中 SD 卡
不接D1 / D2 / CDSPI 模式下用不到,空著即可

⚠️ 接好線之後,建議逐條對照上表過一遍,能省你 80% 的排錯時間。 另外,杜邦線別太長(30cm 以內最穩),線一長訊號容易抖,32G 大卡對時序更挑剔。


需要安裝的函式庫

不需要額外安裝!

本文用到的 SPI.hSD.h 都已經內建在 ESP32 Arduino Core 裡了。只要你的 Arduino IDE 已經安裝了 ESP32 的開發板支援包,直接編譯就行。

如果還沒裝開發板套件,在 Arduino IDE → 工具 → 開發板管理員 裡搜尋 esp32,安裝 Espressif Systems 出的那個套件(本文測試版本:ESP32 Arduino Core 3.0.x)。


完整程式碼

#include <SPI.h>
#include <SD.h>

// 第一步:定義 SPI 腳位
static const int SD_MOSI = 5;   // 對應 SD 模組 CMD
static const int SD_MISO = 13;  // 對應 SD 模組 D0
static const int SD_SCK  = 14;  // 對應 SD 模組 CLK
static const int SD_CS   = 4;   // 對應 SD 模組 D3(片選)

SPIClass spi = SPIClass(FSPI);  // ESP32-S3 上用 FSPI 匯流排

// 遞迴列出目錄裡的所有檔案和子資料夾
void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
  Serial.printf("正在列出目錄:%s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println("開啟目錄失敗,檢查接線或 SD 卡格式");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("這不是一個目錄");
    return;
  }

  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print("  [資料夾] ");
      Serial.println(file.name());
      if (levels) {
        listDir(fs, file.path(), levels - 1);  // 遞迴進子目錄
      }
    } else {
      Serial.print("  [檔案]   ");
      Serial.print(file.name());
      Serial.print("    大小:");
      Serial.print(file.size());
      Serial.println(" bytes");
    }
    file = root.openNextFile();
  }
}

// 列印 SD 卡基本資訊
void printCardInfo() {
  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("沒有偵測到 SD 卡,檢查接線和供電");
    return;
  }

  Serial.print("SD 卡類型:");
  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("未知類型");

  uint64_t cardSize   = SD.cardSize()   / (1024 * 1024);
  uint64_t totalBytes = SD.totalBytes() / (1024 * 1024);
  uint64_t usedBytes  = SD.usedBytes()  / (1024 * 1024);

  Serial.printf("SD 卡容量:%llu MB\n", cardSize);
  Serial.printf("可用空間:%llu MB\n",  totalBytes);
  Serial.printf("已用空間:%llu MB\n",  usedBytes);
}

void setup() {
  Serial.begin(115200);
  delay(1500);  // 等串口穩定

  Serial.println();
  Serial.println("=== ESP32-S3 SD SPI Test ===");
  Serial.printf("MOSI=%d, MISO=%d, SCK=%d, CS=%d\n",
                SD_MOSI, SD_MISO, SD_SCK, SD_CS);

  // 第二步:初始化 SPI 匯流排,指定腳位順序:SCK, MISO, MOSI, CS
  spi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);

  // 第三步:先把 CS 拉高,避免初始化時誤選中 SD 卡
  pinMode(SD_CS, OUTPUT);
  digitalWrite(SD_CS, HIGH);

  // 第四步:掛載 SD 卡,初始時脈 10MHz(不穩定可降到 4MHz)
  if (!SD.begin(SD_CS, spi, 10000000)) {
    Serial.println("SD 卡掛載失敗!按以下順序排查:");
    Serial.println("1. 接線 GPIO5→CMD / GPIO13→D0 / GPIO14→CLK / GPIO4→D3");
    Serial.println("2. 供電確認是 3.3V,不是 5V");
    Serial.println("3. SD 卡格式化成 FAT32");
    Serial.println("4. 把 10000000 改成 4000000,降低 SPI 頻率再試");
    return;
  }

  Serial.println("SD 卡掛載成功!");
  printCardInfo();

  // 第五步:列出根目錄下 5 層檔案結構
  listDir(SD, "/", 5);
}

void loop() {
  // 檔案讀取只在 setup 裡做一次,loop 暫時留空
  // 如需定時輪詢,可在這裡加 delay + listDir
}

檔案操作擴展示例

跑通主程式之後,光列檔案還不夠用。下面這些函式不修改主程式, 只需要把它們貼在主程式的 listDir() 函式旁邊, 然後在 setup() 末尾按需呼叫即可, 覆蓋讀 / 寫 / 附加以 / 建立 / 刪除 / 重新命名所有常用操作。

寫檔案 · 覆蓋與附加

FILE_WRITE 模式會清空原檔案後寫入,FILE_APPEND 模式會從檔案末尾附加。 做日誌記錄、感測器資料採集,幾乎都用 附加模式

// === 寫檔案(覆蓋寫入)===
// 如果檔案不存在則建立,如果已存在則清空原內容後寫入
void writeFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("寫入檔案:%s\n", path);

  File file = fs.open(path, FILE_WRITE);  // FILE_WRITE 模式:覆蓋
  if (!file) {
    Serial.println("開啟檔案失敗(寫入模式)");
    return;
  }

  if (file.print(message)) {
    Serial.println("✅ 寫入成功");
  } else {
    Serial.println("❌ 寫入失敗");
  }
  file.close();  // 一定要關閉,否則資料可能沒刷到卡上
}

// === 附加寫入(不覆蓋原內容)===
// 適合做日誌:每次往檔案尾部附加一行
void appendFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("附加內容到:%s\n", path);

  File file = fs.open(path, FILE_APPEND);  // FILE_APPEND 模式:附加
  if (!file) {
    Serial.println("開啟檔案失敗(附加模式)");
    return;
  }

  if (file.print(message)) {
    Serial.println("✅ 附加成功");
  } else {
    Serial.println("❌ 附加失敗");
  }
  file.close();
}

// 呼叫示例(寫在 setup() 裡 listDir 後面即可):
// writeFile(SD, "/hello.txt", "Hello ESP32-S3 SD!\n");
// appendFile(SD, "/hello.txt", "這是附加的第二行\n");

💡效能提醒每次 file.close() 都會觸發 SD 卡的實體寫入,頻繁開關檔案會很慢。 如果是高頻日誌,建議保持 File 實例開啟,每寫 N 行呼叫一次 file.flush() 把緩衝刷到卡裡。

讀檔案 · 整體讀 與 逐行讀

readFile() 適合小檔案一次讀完;readFileByLine() 適合處理 CSV、設定檔等結構化文字。

// === 讀檔案(一次讀完,按位元組列印)===
void readFile(fs::FS &fs, const char * path) {
  Serial.printf("讀取檔案:%s\n", path);

  File file = fs.open(path);  // 預設就是 FILE_READ 模式
  if (!file) {
    Serial.println("開啟檔案失敗,檔案可能不存在");
    return;
  }

  Serial.print("檔案內容:");
  while (file.available()) {
    Serial.write(file.read());  // 逐位元組讀取並列印
  }
  Serial.println();
  file.close();
}

// === 逐行讀取(適合設定檔、CSV 資料)===
void readFileByLine(fs::FS &fs, const char * path) {
  Serial.printf("逐行讀取:%s\n", path);

  File file = fs.open(path);
  if (!file) {
    Serial.println("開啟檔案失敗");
    return;
  }

  int lineNum = 1;
  while (file.available()) {
    String line = file.readStringUntil('\n');  // 讀到換行符為止
    Serial.printf("第 %d 行:%s\n", lineNum++, line.c_str());
  }
  file.close();
}

// 呼叫示例:
// readFile(SD, "/hello.txt");
// readFileByLine(SD, "/config.txt");

ℹ️說明file.available() 回傳剩餘位元組數;file.readStringUntil('\n') 會把換行符之前的內容一次讀成 String, 注意大檔案別用 String,會爆記憶體,改用固定大小的 char buf[128] + file.readBytesUntil() 更安全。

建立 / 刪除 / 重新命名

涵蓋資料夾的建立刪除、空檔案建立、檔案刪除、重新命名(也可用於「搬移」)。

// === 建立資料夾 ===
void createDir(fs::FS &fs, const char * path) {
  Serial.printf("建立資料夾:%s\n", path);
  if (fs.mkdir(path)) {
    Serial.println("✅ 資料夾建立成功");
  } else {
    Serial.println("❌ 建立失敗(可能已存在或父目錄不存在)");
  }
}

// === 建立空檔案 ===
// 直接用 FILE_WRITE 開啟再關閉就會建立一個空檔案
void createEmptyFile(fs::FS &fs, const char * path) {
  Serial.printf("建立空檔案:%s\n", path);
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("❌ 建立失敗");
    return;
  }
  file.close();
  Serial.println("✅ 空檔案建立成功");
}

// === 刪除檔案 ===
void deleteFile(fs::FS &fs, const char * path) {
  Serial.printf("刪除檔案:%s\n", path);
  if (fs.remove(path)) {
    Serial.println("✅ 刪除成功");
  } else {
    Serial.println("❌ 刪除失敗(檔案不存在或權限問題)");
  }
}

// === 刪除資料夾(必須是空資料夾)===
void removeDir(fs::FS &fs, const char * path) {
  Serial.printf("刪除資料夾:%s\n", path);
  if (fs.rmdir(path)) {
    Serial.println("✅ 資料夾刪除成功");
  } else {
    Serial.println("❌ 刪除失敗(資料夾不為空或不存在)");
  }
}

// === 重新命名 / 搬移檔案 ===
void renameFile(fs::FS &fs, const char * oldPath, const char * newPath) {
  Serial.printf("重新命名:%s → %s\n", oldPath, newPath);
  if (fs.rename(oldPath, newPath)) {
    Serial.println("✅ 重新命名成功");
  } else {
    Serial.println("❌ 重新命名失敗");
  }
}

// 呼叫示例(按順序執行可演示完整流程):
// createDir(SD, "/logs");
// createEmptyFile(SD, "/logs/empty.txt");
// renameFile(SD, "/logs/empty.txt", "/logs/data.txt");
// deleteFile(SD, "/logs/data.txt");
// removeDir(SD, "/logs");

⚠️注意SD.rmdir() 只能刪空資料夾。如果要遞迴刪除整個目錄,需要先遍歷刪掉裡面所有檔案, 再刪資料夾本身,SD.h 函式庫沒有內建 rm -rf,需要自己寫遞迴函式。


程式碼說明

為什麼 CMD 對應 MOSI? SD 卡在 SPI 模式下,ESP32 發給卡的資料走的就是 CMD 腳位,所以 CMD = MOSI。這是 SD 協定 SPI 模式的硬性規定,不是接錯了。

為什麼 D0 對應 MISO? SPI 模式下,SD 卡把資料回傳給主機走 D0 腳位,所以 D0 = MISO。

為什麼 D3 對應 CS? SD 卡進入 SPI 模式後,D3 承擔片選(Chip Select)功能,低電位時卡被啟用。

為什麼 D1、D2 不接? 它們是 4-bit SDIO 模式專用,SPI 模式用不到,空著就行。

SPIClass spi = SPIClass(FSPI) 是什麼意思? ESP32-S3 有多個 SPI 匯流排(FSPI / HSPI),這裡手動指定用 FSPI,避免和其他外設衝突。


常見問題排查

別慌,90% 的初始化失敗都出在這幾個地方,按順序查一遍基本能解決:

1. Serial.println 卡在「SD 卡掛載失敗」不動? 先確認接線:GPIO5→CMD、GPIO13→D0、GPIO14→CLK、GPIO4→D3,任何一條接錯都會失敗。

2. 接線沒問題,還是失敗? 把 SPI 頻率從 10MHz 降到 4MHz,改這一行:

if (!SD.begin(SD_CS, spi, 4000000)) {

32G 卡對時序更挑剔,低頻率更容易跑通,跑通後再慢慢往上調。

3. 串口根本沒有任何輸出? 檢查串口鮑率是不是 115200,以及 USB 線有沒有資料傳輸能力(純充電線不行)。

4. 偶發掛載失敗,時好時壞? 供電問題。導線太長、接觸不良都會導致 SD 卡初始化時電壓抖動,嘗試縮短杜邦線、換品質好一點的線。

5. 32G 卡掛載失敗,換 8G 就好了? 32G 卡通常是 SDHC 格式,需要格式化成 FAT32(Windows 預設給 32G 卡格式化成 exFAT,ESP32 的 SD.h 不支援 exFAT)。可以用 SD Card Formatter 格式化。

6. 掛載成功但 listDir 沒有輸出任何檔案? SD 卡可能是空的,或者根目錄檔案全在隱藏資料夾裡。往卡裡放一個 txt 檔案再測試。


FAQ

Q:我的 SD 卡模組是 5V 供電的,能接 ESP32-S3 嗎? A:不建議。ESP32-S3 的 GPIO 是 3.3V 邏輯,如果模組沒有做電平轉換,訊號線直接接 5V 模組會導致腳位損壞。確認模組支援 3.3V 工作電壓,或者買帶電平轉換晶片的模組。

Q:SPI 頻率設多少合適? A:從 4000000(4MHz)開始,能跑通再試 10000000(10MHz)。理論上 SD 卡 SPI 模式最高支援 25MHz,但受杜邦線長度和模組品質影響,實際跑不了那麼高。

Q:ESP32-S3 的哪些 GPIO 可以替換接 SD 卡? A:ESP32-S3 的 FSPI 支援自訂腳位,理論上大多數 GPIO 都可以使用,但建議避開 GPIO0(Boot 模式腳位)、GPIO45/GPIO46(有固定功能)。換腳位後記得同步修改程式碼裡的 SD_MOSI / SD_MISO / SD_SCK / SD_CS 常數。

Q:32G 的 SD 卡必須格式化 FAT32 嗎?不能用 exFAT? A:Arduino 的 SD.h 函式庫只支援 FAT16 和 FAT32,不支援 exFAT。32G 及以下的卡格式化成 FAT32 沒有問題,推薦用 SD Card Formatter 工具,不要用 Windows 自帶的格式化(它會給 32G 卡預設 exFAT)。

Q:SD 卡讀寫速度大概是多少? A:SPI 模式下實際吞吐量大約在 500KB/s~2MB/s 之間,取決於 SPI 時脈頻率和卡的速度等級。如果需要更高速度,可以考慮 SDIO 4-bit 模式(需要換接法,不在本文範圍內)。

Q:可以同時掛載多張 SD 卡嗎? A:可以。SPI 匯流排支援多裝置,每張卡用不同的 CS 腳位,分別初始化成不同的 SD 實例即可。不過 SD.h 只支援單實例,多卡需要換用 SD_MMC.h 或第三方函式庫 SdFat。

Q:ESP32-S3 執行這段程式碼,CPU 佔用高嗎? A:不高。檔案列表操作是一次性的 I/O,setup() 執行完就結束,loop() 是空的,CPU 幾乎不佔用。如果你在 loop() 裡持續讀寫檔案,才需要關注效能。


延伸玩法

跑通了基礎讀取之後,這些方向可以繼續探索:

  • 從 SD 卡播放 MP3:搭配 ESP32-audioI2S 函式庫,接上 I2S DAC,從 SD 卡讀音訊檔案,告別網路卡頓
  • 資料採集儲存:感測器資料按時間戳寫入 CSV,斷電不遺失,方便後續用 Python 分析
  • 接 TFT 螢幕:讀取 SD 卡裡的圖片(BMP / JPG),顯示在螢幕上,做一個簡易相框
  • 設定檔讀取:把 Wi-Fi 帳號密碼寫在 SD 卡的 config.json 裡,程式碼不用每次修改再燒錄

參考資料