ESP32-S3 SDカード書き込み速度比較

# はじめに

ESP32 WROOM 32EでSdFat.hを使用して1kHzのデータロギングを試みた際に性能不足の問題に直面しました。
そこで、次世代機としてESP32 S3への移行を検討するにあたり、以下の3つのSDカードライブラリの性能を比較評価しました。

- SD.h - Arduino標準ライブラリ
- SdFat.h - 高性能を謳うサードパーティライブラリ
- SD_MMC.h - ESP32のMMCモード専用ライブラリ

:::note alert
本記事は概略的な性能比較を目的としており、厳密な測定ではありません。
:::

# 環境

- macOS Sonoma
- PlatformIO
- ESP32-S3- 開発ボード
- マイクロSDカードスロットDIP化キット
- KIOXIAマイクロSDカード16GB

https://akizukidenshi.com/catalog/g/g105488/

https://www.amazon.co.jp/%E3%82%AD%E3%82%AA%E3%82%AF%E3%82%B7%E3%82%A2-KIOXIA-microSDHC%E3%82%AB%E3%83%BC%E3%83%89-Amazon-co-jp%E3%83%A2%E3%83%87%E3%83%AB-KTHN-MW016G/dp/B088KJFWMD?source=ps-sl-shoppingads-lpcontext&ref_=fplfs&smid=AN1VRQENFRJN5&th=1

# 評価方法

次の3つを評価しました。

1. 書き込み速度（MB/s）
    - 一定サイズのデータを書き込む時間を計測
2. レイテンシ（ms）
    - 書き込み開始から完了までの遅延時間
    - 最大/平均/最小を計測
3. IOPS (Input/Output Operations Per Second)
    - 1秒あたりの書き込み操作回数

（本当はSDカードも数種類準備した方が良いですが、お金がないため、家にあった1種類のみで評価しました。）

ファイルサイズは、64KB/256KB/1MBの3種類で、それぞれのファイルサイズ、ライブラリで10回ずつ測定し、平均を求めました。
# コード

<details><summary>SD_MMC.h</summary>

```c++:main.cpp
#include <Arduino.h>
#include <SD_MMC.h>

class SDMMCBenchmark {
private:
  const size_t BUFFER_SIZE = 512;
  uint8_t *buffer;

public:
  SDMMCBenchmark() {
    buffer = new uint8_t[BUFFER_SIZE];
    for (size_t i = 0; i < BUFFER_SIZE; i++) {
      buffer[i] = random(0, 255);
    }
  }

  ~SDMMCBenchmark() { delete[] buffer; }

  void printCSVHeader() {
    Serial.println(
        "Iteration,FileSize(KB),BlockSize(B),WriteSpeed(MB/s),Latency(ms),MinLatency(ms),MaxLatency(ms),IOPS");
  }

  void runBenchmark(const char *filename, size_t totalBytes, int iterations = 10) {
    for (int i = 0; i < iterations; i++) {
      File file = SD_MMC.open(filename, FILE_WRITE);
      if (!file) {
        Serial.println("Error: Failed to open file for writing");
        return;
      }

      unsigned long startTime = millis();
      size_t bytesWritten = 0;
      int operations = 0;

      float minLatency = 999999;
      float maxLatency = 0;
      float totalLatency = 0;

      while (bytesWritten < totalBytes) {
        unsigned long writeStart = micros();
        size_t written = file.write(buffer, BUFFER_SIZE);
        unsigned long writeEnd = micros();

        float latency = (writeEnd - writeStart) / 1000.0;
        minLatency = min(minLatency, latency);
        maxLatency = max(maxLatency, latency);
        totalLatency += latency;

        bytesWritten += written;
        operations++;
      }

      unsigned long endTime = millis();
      float duration = (endTime - startTime) / 1000.0;

      float speed = (bytesWritten / 1024.0 / 1024.0) / duration;
      float avgLatency = totalLatency / operations;
      float iops = operations / duration;

      Serial.print(i + 1);
      Serial.print(",");
      Serial.print(totalBytes / 1024.0, 2);
      Serial.print(",");
      Serial.print(BUFFER_SIZE);
      Serial.print(",");
      Serial.print(speed, 2);
      Serial.print(",");
      Serial.print(avgLatency, 2);
      Serial.print(",");
      Serial.print(minLatency, 2);
      Serial.print(",");
      Serial.print(maxLatency, 2);
      Serial.print(",");
      Serial.println(iops, 0);

      file.close();
      delay(3000);
    }
  }
};

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

  if (!SD_MMC.setPins(14, 15, 2, 4, 12, 13)) { // これらのピン番号は例です
    Serial.println("Pin configuration failed!");
    return;
  }

  if (!SD_MMC.begin()) {
    Serial.println("SD_MMC initialization failed!");
    return;
  }

  SDMMCBenchmark benchmark;

  Serial.println("SD_MMC initialization succeeded!");
  benchmark.printCSVHeader();

  const size_t sizes[] = {64 * 1024, 256 * 1024, 1024 * 1024}; // 64KB, 256KB, 1MB

  for (size_t size : sizes) {
    String filename = "/test_" + String(size / 1024) + "kb.txt";
    benchmark.runBenchmark(filename.c_str(), size);
    delay(5000);
  }
}

void loop() {
  // Empty
}
```
</details>

<details><summary>SD.h</summary>

```c++:main.cpp
#include <Arduino.h>
#include <SD.h>

#define CUSTOM_CS 4
#define CUSTOM_MOSI 5
#define CUSTOM_MISO 6
#define CUSTOM_SCK 7

class SDBenchmark {
private:
  const size_t BUFFER_SIZE = 512;
  uint8_t *buffer;
  const int chipSelect = CUSTOM_CS;

public:
  SDBenchmark() {
    buffer = new uint8_t[BUFFER_SIZE];
    for (size_t i = 0; i < BUFFER_SIZE; i++) {
      buffer[i] = random(0, 255);
    }
  }

  ~SDBenchmark() { delete[] buffer; }

  void printCSVHeader() {
    Serial.println(
        "Iteration,FileSize(KB),BlockSize(B),WriteSpeed(MB/s),Latency(ms),MinLatency(ms),MaxLatency(ms),IOPS");
  }

  void runBenchmark(const char *filename, size_t totalBytes, int iterations = 10) {
    for (int i = 0; i < iterations; i++) {
      File file = SD.open(filename, FILE_WRITE);
      if (!file) {
        Serial.println("Error: Failed to open file for writing");
        return;
      }

      unsigned long startTime = millis();
      size_t bytesWritten = 0;
      int operations = 0;

      float minLatency = 999999;
      float maxLatency = 0;
      float totalLatency = 0;

      while (bytesWritten < totalBytes) {
        unsigned long writeStart = micros();
        size_t written = file.write(buffer, BUFFER_SIZE);
        unsigned long writeEnd = micros();

        float latency = (writeEnd - writeStart) / 1000.0;
        minLatency = min(minLatency, latency);
        maxLatency = max(maxLatency, latency);
        totalLatency += latency;

        bytesWritten += written;
        operations++;
      }

      unsigned long endTime = millis();
      float duration = (endTime - startTime) / 1000.0;

      float speed = (bytesWritten / 1024.0 / 1024.0) / duration;
      float avgLatency = totalLatency / operations;
      float iops = operations / duration;

      Serial.print(i + 1);
      Serial.print(",");
      Serial.print(totalBytes / 1024.0, 2);
      Serial.print(",");
      Serial.print(BUFFER_SIZE);
      Serial.print(",");
      Serial.print(speed, 2);
      Serial.print(",");
      Serial.print(avgLatency, 2);
      Serial.print(",");
      Serial.print(minLatency, 2);
      Serial.print(",");
      Serial.print(maxLatency, 2);
      Serial.print(",");
      Serial.println(iops, 0);

      file.close();
      delay(3000);
    }
  }
};

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

  SPI.begin(CUSTOM_SCK, CUSTOM_MISO, CUSTOM_MOSI, CUSTOM_CS);
  SPI.setDataMode(SPI_MODE0);

  if (!SD.begin(CUSTOM_CS)) {
    Serial.println("SD initialization failed!");
    return;
  }

  SDBenchmark benchmark;

  Serial.println("SD initialization succeeded!");
  benchmark.printCSVHeader();

  const size_t sizes[] = {64 * 1024, 256 * 1024, 1024 * 1024}; // 64KB, 256KB, 1MB

  for (size_t size : sizes) {
    String filename = "/test_" + String(size / 1024) + "kb.txt";
    benchmark.runBenchmark(filename.c_str(), size);
    delay(5000);
  }
}

void loop() {
  // Empty
}
```
</details>

<details><summary>SdFat.h</summary>

```c++:main.cpp
#include <Arduino.h>
#include <SPI.h>
#include <SdFat.h>

#define CUSTOM_CS 4
#define CUSTOM_MOSI 5
#define CUSTOM_MISO 6
#define CUSTOM_SCK 7

SdFat SDFAT;

class SDBenchmark {
private:
  const size_t BUFFER_SIZE = 512;
  uint8_t *buffer;
  const int chipSelect = CUSTOM_CS;

public:
  SDBenchmark() {
    buffer = new uint8_t[BUFFER_SIZE];
    for (size_t i = 0; i < BUFFER_SIZE; i++) {
      buffer[i] = random(0, 255);
    }
  }

  ~SDBenchmark() { delete[] buffer; }

  void printCSVHeader() {
    Serial.println(
        "Iteration,FileSize(KB),BlockSize(B),WriteSpeed(MB/s),Latency(ms),MinLatency(ms),MaxLatency(ms),IOPS");
  }

  void runBenchmark(const char *filename, size_t totalBytes, int iterations = 10) {
    for (int i = 0; i < iterations; i++) {
      File32 file;
      file.open(filename, O_RDWR | O_CREAT | O_AT_END);
      if (!file) {
        Serial.println("Error: Failed to open file for writing");
        return;
      }

      unsigned long startTime = millis();
      size_t bytesWritten = 0;
      int operations = 0;

      float minLatency = 999999;
      float maxLatency = 0;
      float totalLatency = 0;

      while (bytesWritten < totalBytes) {
        unsigned long writeStart = micros();
        size_t written = file.write(buffer, BUFFER_SIZE);
        unsigned long writeEnd = micros();

        float latency = (writeEnd - writeStart) / 1000.0;
        minLatency = min(minLatency, latency);
        maxLatency = max(maxLatency, latency);
        totalLatency += latency;

        bytesWritten += written;
        operations++;
      }

      unsigned long endTime = millis();
      float duration = (endTime - startTime) / 1000.0;

      float speed = (bytesWritten / 1024.0 / 1024.0) / duration;
      float avgLatency = totalLatency / operations;
      float iops = operations / duration;

      Serial.print(i + 1);
      Serial.print(",");
      Serial.print(totalBytes / 1024.0, 2);
      Serial.print(",");
      Serial.print(BUFFER_SIZE);
      Serial.print(",");
      Serial.print(speed, 2);
      Serial.print(",");
      Serial.print(avgLatency, 2);
      Serial.print(",");
      Serial.print(minLatency, 2);
      Serial.print(",");
      Serial.print(maxLatency, 2);
      Serial.print(",");
      Serial.println(iops, 0);

      file.close();
      delay(3000);
    }
  }
};

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

  SPI.begin(CUSTOM_SCK, CUSTOM_MISO, CUSTOM_MOSI, CUSTOM_CS);
  SPI.setDataMode(SPI_MODE0);

  if (!SDFAT.begin(CUSTOM_CS, SD_SCK_MHZ(10))) {
    Serial.println("SD initialization failed!");
    return;
  }

  SDBenchmark benchmark;

  Serial.println("SD initialization succeeded!");
  benchmark.printCSVHeader();

  const size_t sizes[] = {64 * 1024, 256 * 1024, 1024 * 1024}; // 64KB, 256KB, 1MB

  for (size_t size : sizes) {
    String filename = "/test_" + String(size / 1024) + "kb.txt";
    benchmark.runBenchmark(filename.c_str(), size);
    delay(5000);
  }
}

void loop() {
  // Empty
}
```
</details>



# ピン接続

<details><summary>SD.h&SdFat.h</summary>

| ESP32 pin | SPI pin name | SD card pin | Notes |
|------------|--------|---------------| --- |
| 4 | CS | CS | - |
| 5 | MOSI | MOSI/DI | 10k pullup |
| 6 | MISO | MISO/DO | 10k pullup |
| 7 | SCK | SCK/CLK | 10k pullup |
</details>

<details><summary>SD_MMC.h</summary>

| ESP32 pin | SD card pin | Notes      |
| --------- | ----------- | ---------- |
| IO14      | CLK         | 10k pullup |
| IO15      | CMD         | 10k pullup |
| IO2       | D0          | 10k pullup |
| IO4       | D1          | 10k pullup |
| IO12      | D2          | 10k pullup |
| IO13      | D3          | 10k pullup |
</details>

どちらともVSSとVDDの間にそこそこ容量の大きいパスコンを配置してください。

# 結果
長いので平均のみ載せます。

**取得した値の平均**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/26485a87-c028-0c7e-71a5-20972a962115.png)

**グラフ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/8f6a8406-3f5d-636d-d479-7cc08a2397f0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/ac0e55c5-3d05-caf4-5fd8-7136720afad0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/198ea339-0fac-73ff-8bb8-e37349253dc5.png)

見ずらいのでSDMMCを除いたグラフも作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/add96f85-6637-da7b-e5d9-15f7da1d09db.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/b23f82bb-692b-84dc-c9b5-a6ab6600916d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871960/d5accfec-e614-79cd-4f08-97b0d0be5ad3.png)

# 測定結果から

## 書き込み速度について
- 全てのファイルサイズで約3.9MB/sを維持
- SD.h、SdFat.hと比較して約8-9倍の速度差
- ファイルサイズによる性能変動が少ない
- SD.hとSdFat.hはほぼ同じだが、SdFat.hがやや速い
-> SD.h、SdFat.hがデータ線2本のSPIに対してSD_MMC.hは4本だから？

## レイテンシについて
- SD_MMC.hは最小レイテンシで0.05ms以下を実現
- SD.hとSdFat.hは似た傾向を示すが、SdFat.hがやや短い

## IOPSについて
- SD_MMC.hが最も高い
- ファイルサイズが大きくなるにつれてIOPSは低下傾向

# 終わりに
思ったよりSD_MMCが圧倒的な速さでした。

Qiita投稿2回目&メモしながら書いた記事なので、間違い等ありましたらコメントで教えていただけるとありがたいです。

# 参考にしたサイト
https://prototype09.com/archives/637#toc8