0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 SD 卡讀寫完整教學:SPI 模式、SDMMC 4-bit 初始化序列、FAT32 檔案系統與 Data Logging

2026 年 6 月 22 日 7點熱度 0人點贊 0條評論

ESP32 SD 卡讀寫:SPI 與 SDMMC 模式完整教學

SD 卡是 IoT 設備最常用的資料儲存方案,可用於感測器資料記錄(Data Logging)、韌體更新檔案儲存、多媒體檔案存放等。ESP32 支援 兩種 SD 卡通訊模式:SPI 模式與 SDMMC 模式,各有優缺點。

本文將深入探討:

  • SD 卡 SPI 初始化序列與通訊協定
  • 區塊讀取(CMD17)與區塊寫入(CMD24)時序
  • ESP32 Arduino SD.h 與 SD_MMC.h 程式設計
  • FAT32 檔案系統操作(建立、讀寫、刪除)
  • CSV 資料記錄(Data Logging)實戰
  • SPI vs SDMMC 效能比較

SD 卡通訊模式

SD 卡 SPI / SDMMC 1-bit / SDMMC 4-bit 接線

SPI 初始化序列

SD 卡在 SPI 模式下的初始化流程是固定的,必須按照以下順序操作:

  1. 延遲 ≥ 74 CLK:上電後先給 74 個空時脈讓 SD 卡內部電壓穩定
  2. CMD0 (GO_IDLE):重置信卡,進入 SPI 模式
  3. CMD8 (SEND_IF_COND):檢查 SD 卡版本與電壓範圍(2.7~3.6V)
  4. CMD58 (READ_OCR):讀取 OCR 暫存器確認電壓支援
  5. ACMD41 (SD_SEND_OP_COND):啟動初始化並等待卡就緒(HCS=1 表示支援 SDHC)
  6. CMD2 (ALL_SEND_CID):取得卡片識別碼
  7. CMD3 (SEND_REL_ADDR):取得相對位址(RCA)
  8. CMD7 (SELECT/DESELECT_CARD):選擇卡片進入傳輸狀態
  9. 初始化後可將時脈提高到 20~40 MHz

SD 卡 SPI 初始化序列

區塊讀取時序

SD 卡 SPI 單區塊讀取 CMD17

區塊寫入時序

SD 卡 SPI 單區塊寫入 CMD24

Arduino 程式設計

SPI 模式:基本讀寫

// ESP32 SD 卡 SPI 模式 — 基本讀寫
#include <SD.h>
#include <SPI.h>

#define SD_CS  5   // Chip Select (GPIO 5)

void setup() {
    Serial.begin(115200);

    // 初始化 SD 卡 (SPI 模式)
    if (!SD.begin(SD_CS)) {
        Serial.println("SD 卡初始化失敗!");
        Serial.println("請檢查:");
        Serial.println("  1. 接線是否正確");
        Serial.println("  2. SD 卡是否格式化為 FAT32");
        Serial.println("  3. CS 腳位是否正確");
        return;
    }
    Serial.println("SD 卡初始化成功!");
}

void loop() {
    // === 寫入檔案 ===
    File dataFile = SD.open("/test.txt", FILE_WRITE);
    if (dataFile) {
        dataFile.println("Hello from ESP32!");
        dataFile.printf("Timestamp: %lu
", millis() / 1000);
        dataFile.close();
        Serial.println("寫入成功!");
    } else {
        Serial.println("開啟檔案失敗(寫入)");
    }

    // === 讀取檔案 ===
    File readFile = SD.open("/test.txt");
    if (readFile) {
        Serial.println("=== 檔案內容 ===");
        while (readFile.available()) {
            Serial.write(readFile.read());
        }
        readFile.close();
        Serial.println("=== 結束 ===");
    }

    delay(5000);
}

CSV 感測器資料記錄(Data Logging)

// ESP32 SD 卡 CSV 資料記錄器
#include <SD.h>
#include <SPI.h>
#include "RTClib.h"  // 選用:DS3231 RTC 模組

#define SD_CS 5

RTC_DS3231 rtc;
File logFile;

void setup() {
    Serial.begin(115200);

    // 初始化 RTC(選用)
    if (!rtc.begin()) {
        Serial.println("RTC 未找到,將使用 millis() 時間戳");
    }

    if (!SD.begin(SD_CS)) {
        Serial.println("SD 卡初始化失敗!");
        return;
    }

    // 檢查檔案是否存在,若不存在則寫入標題行
    if (!SD.exists("/sensor.csv")) {
        logFile = SD.open("/sensor.csv", FILE_WRITE);
        if (logFile) {
            logFile.println("timestamp,temp,hum,pressure,light");
            logFile.close();
        }
    }

    Serial.println("SD 卡資料記錄器已啟動");
}

void logData(float temp, float hum, float press, int light) {
    logFile = SD.open("/sensor.csv", FILE_APPEND);  // 附加模式
    if (!logFile) {
        Serial.println("開啟記錄檔失敗!");
        return;
    }

    // 時間戳
    unsigned long ts;
    if (rtc.begin()) {
        DateTime now = rtc.now();
        logFile.printf("%04d-%02d-%02d %02d:%02d:%02d,",
            now.year(), now.month(), now.day(),
            now.hour(), now.minute(), now.second());
    } else {
        logFile.printf("%lu,", millis() / 1000);
    }

    // 感測器資料
    logFile.printf("%.1f,%.1f,%.1f,%d
", temp, hum, press, light);
    logFile.close();
}

void loop() {
    // 模擬感測器資料
    float t = 25.0 + random(-20, 20) / 10.0;
    float h = 60.0 + random(-50, 50) / 10.0;
    float p = 1013.0 + random(-30, 30) / 10.0;
    int   l = analogRead(34);

    logData(t, h, p, l);
    Serial.printf("Logged: %.1f %.1f %.1f %d
", t, h, p, l);

    delay(60000);  // 每分鐘記錄一筆
}

檔案管理:列出所有檔案

// 列出 SD 卡根目錄所有檔案與大小
void printDirectory(File dir, int numTabs) {
    while (true) {
        File entry = dir.openNextFile();
        if (!entry) break;

        for (uint8_t i = 0; i < numTabs; i++) Serial.print("	");
        Serial.print(entry.name());

        if (entry.isDirectory()) {
            Serial.println("/");
            printDirectory(entry, numTabs + 1);
        } else {
            Serial.printf("	%lu bytes
", entry.size());
        }
        entry.close();
    }
}

void listRoot() {
    File root = SD.open("/");
    if (root) {
        printDirectory(root, 0);
        root.close();
    }
}

SDMMC 4-bit 模式(最快)

// ESP32 SDMMC 4-bit 模式
// 注意!SDMMC 模式使用固定腳位,不可更改!
#include "SD_MMC.h"

void setup() {
    Serial.begin(115200);

    // SDMMC 4-bit 模式初始化
    if (!SD_MMC.begin()) {
        Serial.println("SDMMC 初始化失敗!");
        Serial.println("請檢查:");
        Serial.println("  CMD  → GPIO 15");
        Serial.println("  CLK  → GPIO 14");
        Serial.println("  DATA0 → GPIO 2");
        Serial.println("  DATA1 → GPIO 4");
        Serial.println("  DATA2 → GPIO 12");
        Serial.println("  DATA3 → GPIO 13");
        return;
    }

    Serial.println("SDMMC 模式初始化成功!");

    // 讀寫測試
    File f = SD_MMC.open("/speed.txt", FILE_WRITE);
    if (f) {
        // 寫入 1MB 測試資料
        uint8_t buf[512];
        memset(buf, 0xAA, 512);
        unsigned long start = micros();
        for (int i = 0; i < 2048; i++) {
            f.write(buf, 512);
        }
        f.close();
        unsigned long elapsed = micros() - start;
        float speed = (1024.0 * 1024.0) / elapsed;
        Serial.printf("1MB 寫入耗時: %.2f s  (%.2f MB/s)
",
            elapsed / 1e6, speed);
    }
}

二進位資料記錄(高效)

// ESP32 二進位資料記錄 — 更小更快
struct SensorRecord {
    uint32_t timestamp;
    float    temperature;
    float    humidity;
    float    pressure;
    uint16_t light;
    uint16_t crc;  // 簡單校驗
};

void logBinaryRecord(File &file, const SensorRecord &rec) {
    // 直接寫入結構體(二進位)
    file.write((uint8_t*)&rec, sizeof(SensorRecord));
}

void readBinaryRecords(File &file) {
    SensorRecord rec;
    while (file.read((uint8_t*)&rec, sizeof(SensorRecord)) == sizeof(SensorRecord)) {
        // 處理每筆記錄
        Serial.printf("%u: %.1f C, %.1f %%
",
            rec.timestamp, rec.temperature, rec.humidity);
    }
}

效能測試

SD 卡讀寫速度比較

測試結果總結:

  • SPI 40 MHz:讀取 5.2 MB/s,寫入 3.5 MB/s(足夠一般日誌記錄)
  • SDMMC 4-bit:讀取 16 MB/s,寫入 10 MB/s(適合高速資料串流)
  • SDMMC 4-bit + DMA:讀取 24 MB/s,寫入 18 MB/s(極速!)
  • 寫入比讀取慢約 30~40%,因為寫入後需要等待 Flash 燒錄

常見問題與排查

問題 原因 解法
SD.begin() 失敗 CS 腳位錯誤或初始化時脈太快 確認 CS 腳位,在 begin() 前降低 SPI 時脈
卡片無法掛載 未格式化為 FAT32 使用 SDFormatter 工具格式化
SPI 模式 CS 須專用 其他 SPI 裝置共用 CS 會衝突 設定 SPI.setCSActiveLevel() 或使用獨立 CS
SDMMC 不穩定 GPIO 12 上拉電阻衝突 GPIO 12 為 MTDI 腳,需注意外部上拉
寫入速度慢 單筆寫入無緩衝 使用 file.write(buf, size) 批次寫入
檔案損毀 寫入中斷電或未 close() 確保 close(),使用 UPS 或大電容
2GB 以上無法使用 僅支援 FAT32 SDHC/SDXC 需 FAT32 格式化(不能 exFAT)
SPI 模式讀不到 SDHC ACMD41 未設 HCS=1 Arduino SD.h 已處理,但自訂實作需注意

SDMMC 腳位衝突

SDMMC 腳位 GPIO 衝突周邊 解決方案
CMD 15 RTC 時脈輸出 上拉電阻 10kΩ
CLK 14 TMS (JTAG) SDMMC 模式下不可使用 JTAG
DATA0 2 內建 LED 可使用,但 LED 會閃爍
DATA1 4 觸摸感測器 T0 不可同時使用 T0 觸摸
DATA2 12 MTDI(決定 Flash 電壓) 注意:GPIO 12 外部不可接上拉!否則會使 Flash 進入 1.8V 模式
DATA3 13 一般 GPIO 無衝突

SPI 模式也支援 SDHC

// ESP32 SD 卡 SPI 模式 — 手動設定 SPI 時脈
#include <SD.h>
#include <SPI.h>

#define SD_CS 5

void setup() {
    Serial.begin(115200);

    // 手動設定 SPI 參數
    SPI.begin(18, 19, 23, 5);  // SCK, MISO, MOSI, CS
    SPIClass spi = SPI;
    spi.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));

    if (!SD.begin(SD_CS, spi, 20000000)) {
        Serial.println("初始化失敗,降低時脈重試...");
        if (!SD.begin(SD_CS, spi, 1000000)) {
            Serial.println("仍然失敗!");
            return;
        }
    }

    Serial.println("SD 卡初始化成功!");
    Serial.printf("卡片容量: %llu MB
",
        SD.cardSize() / (1024 * 1024));
    Serial.printf("總空間: %llu MB
",
        SD.totalBytes() / (1024 * 1024));
    Serial.printf("已用空間: %llu MB
",
        SD.usedBytes() / (1024 * 1024));
}

ESP-IDF 原生 fatfs

// ESP-IDF SD 卡範例 (SPI 模式)
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdspi_host.h"

void app_main(void) {
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };

    sdmmc_card_t *card;
    const char mount_point[] = "/sdcard";

    esp_err_t ret = esp_vfs_fat_sdmmc_mount(
        mount_point, &mount_config, sdmmc_host, &slot_config, &card);

    if (ret != ESP_OK) {
        ESP_LOGE("SD", "Mount failed: %s", esp_err_to_name(ret));
        return;
    }

    sdmmc_card_print_info(stdout, card);

    // 讀寫檔案
    FILE *f = fopen("/sdcard/hello.txt", "w");
    fprintf(f, "Hello from ESP-IDF!");
    fclose(f);

    // 卸載
    esp_vfs_fat_sdmmc_unmount(mount_point, card);
}

總結

ESP32 的 SD 卡支援涵蓋了從低腳位 SPI 模式到高效能 SDMMC 4-bit DMA 模式的完整光譜,無論是簡易資料記錄還是高速多媒體串流都有對應方案。

選型參考:

  • 簡單日誌記錄 (1 KB/s):SPI 模式,僅需 6 條線,可用任何 GPIO
  • 高速感測器資料流 (100 KB/s):SPI 40 MHz + 512 bytes 批次寫入
  • 音頻/影像記錄 (1 MB/s):SDMMC 1-bit 模式,最少 5 條線
  • 高速串流 (10 MB/s+):SDMMC 4-bit + DMA,最佳效能但需注意腳位衝突
  • 跨平台相容:使用 FAT32 格式化,Windows/Mac/Linux 可直接讀取
標籤: 教學
最後更新:2026 年 6 月 22 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang