秋月電子で買ったESP-WROOM-32の開発ボード(DevKit-C)に、amazonで見つけた安いMicroSDカードモジュールをつけてみた。WROOM-32内のSPIフラッシュを使ったspiffsを利用するためのコードやツールが揃っていないから、手っ取り早くESP32モジュール内のプログラムからファイルシステムを使うために使ってみた。
CATALEX MicroSD Card Module

amazonで中国からの送料込みで105円で売っている。回路図が見つからなかったので等価回路を起こしてみたが、誤りもあるかもしれない。
Arduino用ということで、電源やロジックは5Vレベルが想定されており、ボード上に+3.3V出力の三端子レギュレータ(AMS1117-3.3) とレベル変換にも使えるゲート付バッファICの74LVC125が載っている。そして、J2端子に来ているSPI接続用の4本(CS, CSK, MOSI, MISO) がバッファを介してMicroSD用のスロットに接続されている。各信号線には直列に3.3KΩの抵抗が入っているが、これは短絡時の電流を制限するためだろう。
74125の4つのバッファは独立してイネーブル可能だが、イネーブル信号のOEはすべてGNDに直結されているようだった。ということは、スレーブ出力マスター入力用のMISO(Master In Slave Out) 信号がハイインピーダンスになることはないから、マスター(ESP32側)と1:1で接続することになる。
ESP32モジュールと使う場合、このボードのJ2端子のVCCに+3.3Vを与えてしまうと、レギュレータのドロップアウトによってボード内のVDDの電圧は2.4~2.5Vになってしまい、MicroSDカードの規定電源電圧(+2.7~3.6V)に達しない。AMS1117を外して+3.3Vを与えることも考えたが、小さくて面倒なので開発ボードの電源にもなっているUSBのVbusから+5Vを与えることにした。
ESP32開発ボードとの接続
データシートによれば、ESP32では特別な用途に使われているピンを除いたすべてのGPIOにSPI機能を割り当てることができる。ただし、入力専用のピン(IO34~IO39)にマスタ側が出力する信号(SS, CLK, MOSI)を割り当てることはできない。今回はブレッドーボード上の配線の都合で、以下のような結線とした。

開発ボードのピン番号は、WiFiアンテナを上にして部品面を見たとき、左側が1~19、右側が(上から) 38~20というふうにしている。ちょうど、IO16~IO19がIO5をはさんで並んでいるので、そこを使うことにした。MicroSDモジュール内に立派なバスバッファが入っているので、SPI接続の各線はプルアップしていない。
ブレッドボードに載せて結線したら以下のようになった。


ブレッドボードとしては、電源用のレールが片側にしかないぶん結線する余地が広いサンハヤトの製品(SAD-101 ニューブレッドボード)を使っている。
スケッチ
SD_Text.inoの変更
現在(July. 2017) のArduino core for the ESP32 に入っているSPIおよびSDライブラリを使った。最初はSD/examplesのSD_Test.ino を使って動作を確認したが、SPI接続に使うピンを変更しているので、void setup() の最初の方をすこし変更して使った。
void setup(){ Serial.begin(115200); if(!SD.begin()){ Serial.println("Card Mount Failed"); return; } uint8_t cardType = SD.cardType(); ....
オリジナルでは、上記のようにSPIについては特に何も考慮せずに開始している。このサンプルが想定しているSPI接続のピン配置はArduino のそれを前提にしておりSD_Test.inoの最初にコメントとして書かれている。
enum { sd_sck = 18, sd_miso = 19, sd_mosi = 17, sd_ss = 5 }; void setup() { SPI.end(); SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss); pinMode(16, OUTPUT); digitalWrite(16, 1); Serial.begin(115200); if(!SD.begin(sd_ss, SPI)){ Serial.println("Card Mount Failed"); return; } uint8_t cardType = SD.cardType(); ....
今回の結線でサンプルスケッチを動作させるため、void setup()の先頭部分を上記のようにして実行した。SPI.end(); は実行開始時に作成される SPIClass のインスタンスにオリジナルの設定内容を忘れてもらうために念のために呼んでいる。直後の SPI.begin(); に今回のピン配置を与え、GPIO16に接続したLEDの初期化を行っている。SD.begin() にも SPIのss(slave select)のGPIO番号が必要なので追加した。
上記のような変更により、SD_test.inoは無事に動作した。
SPIの動作周波数を変えてみる
SPIClassクラスやSDFSクラスには、SPIの動作周波数を引数としてとるメソッドが用意されているので試してみた。
当初は SPI.begin() のあとで SPIClass::setFrequency(uint32_t freq); を呼んでみたがMicroSDカードの読み書き速度に変化がなかった。よく見てみると、SDFSクラスの begin()メソッドの引数にも周波数の項目があり、実際にSPIを介してMicroSDとの読み書きを行う sd_diskio.cpp 内の関数はSDFSクラスが保持する方の値を使っていた。
そのあたりを踏まえて、SD.begin() 時に設定する周波数と、MicroSD内のファイルを読み書きする際のバッファサイズをそれぞれ変えて転送時間を測るスケッチを書いてみた。
#include "FS.h" #include "SD.h" #include "SPI.h" // // 1MBytes書いて、1MBytes読む。 // #define FILE_SIZE 1048576 void fill_buffer(byte* pBuffer, int size) { memset(pBuffer, 0x55, size / 2); memset(pBuffer + (size / 2), 0xaa, size / 2); } bool read_write_test(fs::FS &fs, const char* cpPath, const int buffer_size) { byte* pBuffer = (byte*)malloc(buffer_size); if (!pBuffer) return false; int read_msec = 0; int write_msec = 0; File f = fs.open(cpPath, FILE_WRITE); if (!f) { Serial.printf("fs.open(%s) FILE_WRITE failed.\n", cpPath); free(pBuffer); return false; } unsigned int start_time = millis(); int left = FILE_SIZE; while(left > 0) { fill_buffer(pBuffer, buffer_size); f.write(pBuffer, left < buffer_size ? left : buffer_size); left -= buffer_size; } f.close(); write_msec = millis() - start_time; f = fs.open(cpPath, FILE_READ); if (!f) { Serial.printf("fs.open(%s) FILE_READ failed.\n", cpPath); free(pBuffer); return false; } start_time = millis(); left = FILE_SIZE; while(left > 0) { f.read(pBuffer, left < buffer_size ? left : buffer_size); left -= buffer_size; } f.close(); read_msec = millis() - start_time; free(pBuffer); Serial.printf("write : %d / read : %d\n", write_msec, read_msec); return true; } void die(const char* p) { Serial.printf("\n%s\n", p); for(;;) { digitalWrite(16, 1); delay(500); digitalWrite(16, 0); delay(500); } } enum { sd_sck = 18, sd_miso = 19, sd_mosi = 17, sd_ss = 5 }; void setup() { SPI.end(); SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss); pinMode(16, OUTPUT); digitalWrite(16, 1); Serial.begin(115200); if(!SD.begin(sd_ss, SPI)){ 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); Serial.println("-----"); } typedef struct { int freq; int bufsize; } condition_t; condition_t cond[] = { { 2000000, 512 }, { 2000000, 1024 }, { 2000000, 2048 }, { 4000000, 512 }, { 4000000, 1024 }, { 4000000, 2048 }, { 8000000, 512 }, { 8000000, 1024 }, { 8000000, 2048 }, { 12000000, 512 }, { 12000000, 1024 }, { 12000000, 2048 }, { 16000000, 512 }, { 16000000, 1024 }, { 16000000, 2048 }, { 20000000, 512 }, { 20000000, 1024 }, { 20000000, 2048 }, {0, 0} }; static condition_t* pcond = cond; void loop(){ int freq = pcond->freq; int bufsize = pcond-> bufsize; if (!freq || !bufsize) { die("finished"); } pcond++; SD.end(); delay(1); Serial.printf("read_write_test() freq = %d, bufsize = %d\n", freq, bufsize); if (!SD.begin(sd_ss, SPI, freq)) die("SD.begin() failed"); if (!read_write_test(SD, "/read_write.bin", bufsize)) die("read_write_test() failed"); }
SDカードのタイプを判定して表示する部分は、SD_Test.inoから抜粋している。
void loop() では、cond[] という配列に書いてある周波数とバッファサイズを順に使いながらSD.begin() とread_write_test() を呼ぶ。
read_write_test() は、1024kBytesのファイルの書込みと読出しを行うが、そのとき File::write() と File::read() を呼び出す際のバッファサイズを引数としてもらっている。loop() でcond[]の最後まで読むか、途中でエラーが起きるとGPIO16に接続したLEDを点滅させて終了。
以下が実行時にシリアルモニタに出力された内容になる。
SD Card Type: SDHC SD Card Size: 14888MB ----- read_write_test() freq = 2000000, bufsize = 512 write : 9453 / read : 5323 read_write_test() freq = 2000000, bufsize = 1024 write : 12064 / read : 5449 read_write_test() freq = 2000000, bufsize = 2048 write : 8897 / read : 5373 read_write_test() freq = 4000000, bufsize = 512 write : 7171 / read : 3175 read_write_test() freq = 4000000, bufsize = 1024 write : 8767 / read : 3277 read_write_test() freq = 4000000, bufsize = 2048 write : 6129 / read : 3226 read_write_test() freq = 8000000, bufsize = 512 write : 6095 / read : 2082 read_write_test() freq = 8000000, bufsize = 1024 write : 7098 / read : 2209 read_write_test() freq = 8000000, bufsize = 2048 write : 4792 / read : 2137 read_write_test() freq = 12000000, bufsize = 512 write : 5797 / read : 1770 read_write_test() freq = 12000000, bufsize = 1024 write : 6582 / read : 1877 read_write_test() freq = 12000000, bufsize = 2048 write : 4400 / read : 1819 read_write_test() freq = 16000000, bufsize = 512 [E][sd_diskio.cpp:721] sdcard_mount(): f_mount failed 0x(1) SD.begin() failed
ボードをリセットしながら何度か試してみたが、周波数が16000000 (16MHz) になると毎度sdcard_mount() が失敗する。今回使ったMicroSDカード(SDHC 16G, class10) によるものか、あるいはMicroSD Cardモジュールによるものか。残念ながら、自由に使える手持ちのMicroSDカードが1枚しかないので試せない。
バッファサイズ512バイトと2048バイトのときの1秒あたりの転送量(kBytes単位)をグラフにしてみた。

読出しの方が周波数を上げた効果が顕著にでる。書込みがそうでもないのは、MicroSDカード自体の特性か? (追記: カードの話じゃなさそうでした。)
追記
もうちょっと速くならないものかな、と思ってすこし触ってみたが、SD.begin() に16MHzを与えるとf_mountが失敗するのは変わらない。では、読み書き時のバッファサイズをより大きくしたらどうなるのかと思って再度計測してみた。

SD.begin()時には12000000 を与え、4MBytesのファイルに対する書込みと読出しを、バッファサイズを変えながら行った。読出しについてはあまり効果はなかった(かえって遅くなる)ものの、書込みについては16KBytesのバッファを使うと2KBytesのときの倍程度の速さになった。それでも400KBytes/sec程度である。読み書きともに、もうちょっと速くしたいところ。他のMicroSDカードモジュールを利用する機会があれば、また試してみよう。
きょうのまとめ
- SDFS::begin() で周波数を省略したときには、4000000 (4MHz) が与えられる。今回の構成ではその3倍までは使えた。
- 周波数を高くすることでread() もwrite()も高速化する。その効果はread()の方が顕著に現れる。ただ、ある程度以上周波数を高くすることはできない。
- read / write 時のバッファサイズの変更は、read()ではあまり影響していないように見える。write() では、1kBytesのときいつも遅くなるのがちょっと不思議。オーバーヘッドが大きいということだろうか。
- ちょっと前に作ったwebサーバーのスケッチと組み合わせることで、データの格納とWiFi経由での読出しを行う準備ができたことになる。高性能なESP32を活かせるアプリケーションということで、CMOSカメラモジュールをつなげたいと思っている。いつになることやら。