マイコンでSDカードを使う方法
マイコンでSDカードを使おうとする時、通常はSPI通信で行うのが一般的です。SPI通信はクロックと出力・入力の2線式ですが、SDカードはDATA0~3のピンがあり、4線式で高速に通信することができるはずです。はずというのカードの仕様は公にはされておらず、SD本来の機能を使うのはSDアソシエーションという団体に会費(安くない)を払う必要があり、個人で使うのは現実的ではありません。そのため、個人がアクセスできる範囲でどの方法が一番高速にSDカードとの通信ができるかを調べました。
マイコン
マイコンはESP32とTeensy4.1を使用しています。Teensy4.1はCortex-M7を搭載した高性能マイコンでSDカードスロットが予めついています。SDカードスロットが無い代わりに大きさが約半分のTeensy4.0もあります(こちらも頑張ればSDカードを付けられます)。
↓爆速なTeensy4.1についてはこちらもご参照ください。
ライブラリ
SDライブラリ
Arduino標準のライブラリです。ただ、このライブラリの欠点として滅茶苦茶遅いのとFAT32にしか対応していないことがあります。FAT32とはSDカードをフォーマット(初期化)する時のファイルシステムのひとつで、一般的なファイルシステムではあるのですが、古いシステムのため32GBよりも大きい(64GB以上)SDカードではFAT32ではフォーマットできません。32GBよりも大きいSDカードを使用するためにはexFATでフォーマットする必要があります。
SDFATライブラリ
上記のSDライブラリを改良したもので、こちらはexFATでフォーマットしたSDカードにも読み書きをすることができます。またSDライブラリよりも高速であることが知られています。(原理はわかりません)
SDMMCライブラリ
ESP32マイコンには3つのSPI通信があり、その中でHSPIと呼ばれるものにはSD_が付く気になる表記が見られます。DATAの送受信にそのHSPIの4ピンを使うライブラリとして「SDMMCライブラリ」というものがあり、今回それを試しました(そのため配線は他の2つとは異なります。)。おそらくESP32でSDカード本来の速度を出す方法があるのだと思われるのですが、それはブラックボックスなので…
Name | No. | Type | Function |
IO14 | 13 | I/O | GPIO14, ADC2_CH6, TOUCH6, RTC_GPIO16, MTMS, HSPICLK, HS2_CLK, SD_CLK, EMAC_TXD2 |
IO12 | 14 | I/O | GPIO12, ADC2_CH5, TOUCH5, RTC_GPIO15, MTDI, HSPIQ, HS2_DATA2, SD_DATA2, EMAC_TXD3 |
IO13 | 16 | I/O | GPIO13, ADC2_CH4, TOUCH4, RTC_GPIO14, MTCK, HSPID, HS2_DATA3, SD_DATA3, EMAC_RX_ER |
IO15 | 23 | I/O | GPIO15, ADC2_CH3, TOUCH3, MTDO, HSPICS0, RTC_GPIO13, HS2_CMD, SD_CMD, EMAC_RXD3 |
IO2 | 24 | I/O | GPIO2, ADC2_CH2, TOUCH2, RTC_GPIO12, HSPIWP, HS2_DATA0, SD_DATA0 |
IO4 | 26 | I/O | GPIO4, ADC2_CH0, TOUCH0, RTC_GPIO10, HSPIHD, HS2_DATA1, SD_DATA1, EMAC_TX_ER |
結果
ここを見る方は結果をまず知りたいと思いますので、先に結果を貼ります。
試験方法
・各ライブラリで書き込み速度と読み込み速度を各5回ずつ計測しています
・512Byteの書き込み・読み込みを2048回繰り返した(約1MB)時間を測定。
・5種類のSDカードで測定を行っています。
- Transcend 1GB
- Transcend 8GB SDHC スピードクラス4
- Transcend Premium 400x 32GB SDHC スピードクラス10 UHSスピードクラス1
- SanDisk Ultra 32GB SDHC スピードクラス10 UHSスピードクラス1
- Samsung EVO Plus 64GB SDXC UHSスピードクラス3
・Teensy4.1はクロックスピードを変更できるため、600MHzと960MHzで比較しています(が、特に変化はありませんでした)
・ESP32+SDFATライブラリではSPIの周波数を設定できるため、デフォルトの16MHzと通信できる限界の24MHzで比較しています
結果(数値)
※平均・速度の数値が0になっているものは読み書きができなかったものです
(クリックで拡大)
結果(グラフ)
(クリックで拡大)
Teensy4.1+SDFAT速すぎ
Teensy4.1の場合、FIFOという方式でSDカードと通信しているようなのですが、なんでこんなに速いのか不明です。書き込み速度はSDカードによって差がありますが、最高で読み書きともに22MB/sのスピードが出ています、桁違い。通常のSDHCカードでも書き込み15MB/s、読み込み22MB/sでできています。
なんでこんなに速いのかよくわからないけど、とにかく動くのでヨシ!
Teensy4.1+SDFATが速すぎて他のものが比較できないので、これだけ省いたグラフが以下です。(クリックで拡大)
ESP32でもSDFATライブラリは速いですね、Teensy4.1の10分の1ではありますが…
Teensy4.1のSDライブラリも速い方ですが、書き込みがESP32+SDFATよりも遅いのと読み込み速度はSDカードによってバラつきがあります。
SDMMCライブラリは情報収集に苦労した割には思ったよりもスピードが出ていません。SDライブラリよりは速いですが、これなら通常のSPI接続でSDFATライブラリを使った方が良さそうです。
配線
Teensy4.1はSDカードスロット内蔵なので配線は不要です(便利ですね)。ESP32の場合、通常のSPI通信(SD/SDFATライブラリ)であれば情報はたくさん出てきますが、SDMMCの場合、情報が少ない&バラついています。そのため、自分の環境ではこれで動いたよ。というものになります。
SDカードスロットには秋月電子のマイクロSDカードスロットDIP化キットを使用しています。
ESP32 SPI通信
左下はESP32にプログラムを書き込むためのダウンローダのピンを書いているだけなので気にしないでください。
SPI通信のためのピン(IO5/23/18/19)は念のためすべて10kBの抵抗でプルアップしています。ENの所についているスイッチはリセット用です。
ESP32 SDMMC通信
こちらのサイトを参考にしています(https://ht-deko.com/arduino/esp-wroom-32.html#13_02)。
本当はIO2とIO12もプルアップする必要があるのですが、これらのピンを初めからプルアップしているとプログラムが書き込めなかったり、デモモードで動作してしまったりするので、プログラム上でプルアップしています。
プログラム
プログラムはスケッチ例を元に切り貼りしているのであまり綺麗には書いていません。あくまで参考程度に。
ESP32 SDライブラリ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
/* * Connect the SD card to the following pins: * * SD Card(MMC) | ESP32 * D2 NC * D3 5 /pullup 10k * CMD 23 /pullup 10k * VSS GND * VDD 3.3V * CLK 18 /pullup 10k * D0 19 /pullup 10k * D1 NC */ #include "FS.h" #include "SD.h" #include "SPI.h" 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){ static uint8_t buf[512]; File file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } size_t i; uint32_t start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } uint32_t end = millis() - start; Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end); file.close(); file = fs.open(path); size_t len = 0; start = millis(); 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"); } } void setup(){ Serial.begin(115200); if(!SD.begin()){ Serial.println("Card Mount Failed"); return; } uint8_t cardType = SD.cardType(); if(cardType == CARD_NONE){ Serial.println("No SD_MMC card attached"); return; } Serial.print("SD_MMC 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_MMC Card Size: %lluMB\n", cardSize); for(int i=0;i<5;i++){ if(SD.exists("/test.txt")){ deleteFile(SD, "/test.txt"); } testFileIO(SD, "/test.txt"); } Serial.println("done"); } void loop(){ } |
ESP32 SDFATライブラリ
「#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(24))」の「SD_SCK_MHZ(24)」は24MHzで動作する設定です。「SD_SCK_MHZ(16)」で16MHzにできます。24MHz以上ではSDカードを認識しませんでした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
//SDFAT+FIFOで速度の計測 #include "SdFat.h" const uint8_t SD_CS_PIN = SS; #define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(24)) SdFs sd; FsFile file; void deleteFile(const char * path){ Serial.printf("Deleting file: %s\n", path); if(sd.remove(path)){ Serial.println("File deleted"); } else { Serial.println("Delete failed"); } } void testFileIO(const char * path){ static uint8_t buf[512]; file = sd.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } size_t i; uint32_t start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } uint32_t end = millis() - start; Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end); file.close(); file = sd.open(path); size_t len = 0; start = millis(); 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.close(); } void setup() { Serial.begin(115200); while (!Serial) { ; // wait for serial port to connect. } Serial.print("Initializing SD card..."); if(!sd.begin(SD_CONFIG)){ Serial.println("Card Mount Failed"); return; } for(int i=0;i<5;i++){ deleteFile("/test.txt"); testFileIO("/test.txt"); } Serial.println("done"); } void loop() { } |
ESP32 SDMMCライブラリ
苦労した割には成果が少なかったコ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
//参照:https://ht-deko.com/arduino/esp-wroom-32.html#13_02 /* * Connect the SD card to the following pins: * * SD Card(MMC) | ESP32 * D2 12 * D3 13 /pullup 10k * CMD 15 * VSS GND * VDD 3.3V * CLK 14 * D0 2 * D1 4 /pullup 10k */ #include "FS.h" #include "SD_MMC.h" 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){ static uint8_t buf[512]; File file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } size_t i; uint32_t start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } uint32_t end = millis() - start; Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end); file.close(); file = fs.open(path); size_t len = 0; start = millis(); 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"); } } void setup(){ pinMode( 2, PULLUP); pinMode(12, PULLUP); Serial.begin(115200); if(!SD_MMC.begin()){ Serial.println("Card Mount Failed"); return; } uint8_t cardType = SD_MMC.cardType(); if(cardType == CARD_NONE){ Serial.println("No SD_MMC card attached"); return; } Serial.print("SD_MMC 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_MMC.cardSize() / (1024 * 1024); Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize); for(int i=0;i<5;i++){ deleteFile(SD_MMC, "/test.txt"); testFileIO(SD_MMC, "/test.txt"); } } void loop(){ } |
Teensy4.1 SDライブラリ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#include "SD.h" #include "SPI.h" // change this to match your SD shield or module; // Arduino Ethernet shield: pin 4 // Adafruit SD shields and modules: pin 10 // Sparkfun SD shield: pin 8 // Teensy audio board: pin 10 // Teensy 3.5 & 3.6 & 4.1 on-board: BUILTIN_SDCARD // Wiz820+SD board: pin 4 // Teensy 2.0: pin 0 // Teensy++ 2.0: pin 20 //const int chipSelect = SS;//2線(SPI):うまくいかない const int chipSelect = BUILTIN_SDCARD;//4線 void deleteFile(const char * path){ Serial.printf("Deleting file: %s\n", path); if(SD.remove(path)){ Serial.println("File deleted"); } else { Serial.println("Delete failed"); } } void testFileIO(const char * path){ static uint8_t buf[512]; File file = SD.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } size_t i; uint32_t start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } uint32_t end = millis() - start; Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end); file.close(); file = SD.open(path); size_t len = 0; start = millis(); 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.close(); } void setup(){ Serial.begin(115200); while (!Serial) { ; // wait for serial port to connect. } Serial.print("Initializing SD card..."); if(!SD.begin(chipSelect)){ Serial.println("Card Mount Failed"); return; } for(int i=0;i<5;i++){ deleteFile("/test.txt"); testFileIO("/test.txt"); } } void loop(){ } |
Teensy4.1 SDFATライブラリ
どういう原理で動くかわからないけど、最速で動く不思議なコ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
//SDFAT+FIFOで速度の計測 #include "SdFat.h" // Use Teensy SDIO #define SD_CONFIG SdioConfig(FIFO_SDIO) SdFs sd; FsFile file; void deleteFile(const char * path){ Serial.printf("Deleting file: %s\n", path); if(sd.remove(path)){ Serial.println("File deleted"); } else { Serial.println("Delete failed"); } } void testFileIO(const char * path){ static uint8_t buf[512]; File file = sd.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } size_t i; uint32_t start = millis(); for(i=0; i<2048; i++){ file.write(buf, 512); } uint32_t end = millis() - start; Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end); file.close(); file = sd.open(path); size_t len = 0; start = millis(); 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.close(); } void setup() { Serial.begin(115200); while (!Serial) { ; // wait for serial port to connect. } Serial.print("Initializing SD card..."); if(!sd.begin(SD_CONFIG)){ Serial.println("Card Mount Failed"); return; } for(int i=0;i<5;i++){ deleteFile("/test.txt"); testFileIO("/test.txt"); } } void loop() { } |
終わりに
ESP32でもSDFATを使えば実用的なスピードが出ることが分かって良かったです。より高速な通信が必要な場合はTeensy4.1を使うのが良いですね。他にこうすると速くなるよという情報があればコメント欄で教えてください。
コメント
XIAO ESP32C6等にSDIOが出ている様になりました。
ただESP32のSDIOで速度の比較データが見つかりません。
Teensy4.x同等の速度が出るのかご存知でしょうか?
ご質問いただきありがとうございます!
ESP32C6のモジュールはまだ手に入れていないため、検証できないですが、確かにSDIO_DATA0~3まであるので適切なライブラリがあれば高速化する可能性がありますね。
ESP32C6を手に入れたら検証してみたいと思います。