存儲(chǔ)設(shè)備是我們?cè)谧?a class="article-link" target="_blank" href="/baike/502952.html">嵌入式開發(fā)時(shí)經(jīng)常用到的,常用的如flash、eeprom、SD卡、U盤等。SD卡的好處是容量大,讀寫速度相對(duì)較快(可以使用SDIO或SPI接口通信)。之前我也有介紹過(guò)flash的使用,那這一講主要講解一下SD卡的使用。
注:因?yàn)槲疫@里只有ESP32和ESP8266,這兩個(gè)MCU都是沒有SDIO接口的,所以這里就以SPI接口來(lái)講解了。
1 硬件介紹
1.1 模塊簡(jiǎn)介
本文只測(cè)試了ESP32的SPI讀寫SD卡。
注:如果要用其他MCU,如ESP8266,實(shí)際上讀寫的流程是完全一樣的,只有SPI的接口引腳號(hào)以及SPI庫(kù)有略微區(qū)別,稍做修改即可。
硬件配置如下:
模塊 | 型號(hào) | 說(shuō)明 |
---|---|---|
ESP32 | ESP-WROOM-32 | MCU是樂鑫的一款芯片,開發(fā)板型號(hào)ESP32 DEVKITV1 |
MicroSD | 無(wú) | 這個(gè)SD模組只有2個(gè)IC,1個(gè)是電壓LDO,1個(gè)是MOS集成IC,所以實(shí)際通信是直接通過(guò)SPI訪問SD卡的 |
下圖是我在某寶買的一個(gè)SD卡通用模組,電路非常簡(jiǎn)單,就只有幾個(gè)器件,所以這里就不再具體介紹了。
1.2 硬件連接
ESP32 | MicroSD | 說(shuō)明 |
---|---|---|
VCC | VCC | 電源正,3.3V |
GND | GND | 電源負(fù) |
GPIO18SPI_CLK | CLK | SPI時(shí)鐘線 |
GPIO23SPI_MOSI | MOSI | SPI數(shù)據(jù)線,ESP32輸出,SD卡輸入 |
GPIO19SPI_MISO | MISO | SPI數(shù)據(jù)線,ESP32輸入,SD卡輸出 |
GPIO5SPI_CS | CS | SPI片選 |
提示:模塊上面的絲印是直接對(duì)應(yīng)MCU那邊的,也就是說(shuō)不需要MOSI接MISO這樣反一下
本文主要講解軟件部分開發(fā),硬件部分也比較簡(jiǎn)單,這里就不在這里具體介紹了。
2 軟件編程和測(cè)試
版本說(shuō)明:
本文測(cè)試時(shí)各軟件使用的版本如下:
軟件 | 版本 | 備注 |
---|---|---|
Arduino IDE | 1.8.16 | Arduino IDE |
ESP32 package | 2.0.1 | 建議使用2.0.0或以上版本的ESP32 package,2.0.0以下版本不兼容esp32-s2及esp32-c2 |
2.1 Arduino IDE環(huán)境搭建
Arduino開發(fā)板庫(kù)安裝
esp8266和esp32開發(fā)板環(huán)境搭建具體就不說(shuō)了,不懂的同學(xué)可以看下我之前發(fā)布的博客。
esp8266開發(fā)入門教程(基于Arduino)——環(huán)境安裝
使用VS code搭建Arduino IDE環(huán)境
2.2 SD庫(kù)API介紹
我這里只列舉常用的幾個(gè)函數(shù),具體的介紹可以看下官方的SD庫(kù)。
函數(shù) | 說(shuō)明 | 示例 | 函數(shù)原形 |
---|---|---|---|
begin() | 初始化函數(shù),可以根據(jù)實(shí)際使用入?yún)ⅲㄆx引腳,SPI引腳等),不入?yún)⒌脑挵茨J(rèn)配置 | SD.begin(); SD.begin(4); | begin(uint8_t ssPin, SPIClass &spi, uint32_t frequency, const char * mountpoint, uint8_t max_files, bool format_if_empty) |
cardType() | 讀取SD卡類型 | card_type = SD.cardType(); | sdcard_type_t SDFS::cardType() |
open() | 打開指定路徑或文件 | fs.open(“/test.txt”); | File FS::open(const String& path, const char* mode, const bool create) |
remove() | 刪除文件 | fs.remove(“/test.txt”); | bool FS::remove(const char* path) |
write() | 往文件里寫數(shù)據(jù) | file.write(“hello world”); | size_t File::write(const uint8_t *buf, size_t size) |
read() | 從文件里讀數(shù)據(jù) | size = file.read(buf, 10); | size_t File::read(uint8_t* buf, size_t size) |
mkdir() | 創(chuàng)建目錄 | fs.mkdir(“/”); | bool FS::mkdir(const String &path) |
2.3 測(cè)試示例
我這里先直接用官方庫(kù)提供的示例做一個(gè)測(cè)試,官方庫(kù)這里基于常用的幾個(gè)API做了一個(gè)封裝,添加了很多調(diào)試信息,對(duì)可能出現(xiàn)的一些錯(cuò)誤操作也做了提示(如:在讀寫之前會(huì)先判斷文件是否正確打開),使得整個(gè)文件操作的流程更加完善,所以這套demo里面有很多細(xì)節(jié)都是值得參考的。
示例代碼:
#include "FS.h"
#include "SD.h"
#include "SPI.h"
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %sn", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.path(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %sn", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %sn", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %sn", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %sn", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %sn", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %sn", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %sn", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
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 msn", flen, end);
file.close();
} else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u msn", 2048 * 512, end);
file.close();
}
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 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: %lluMBn", cardSize);
listDir(SD, "/", 0);
createDir(SD, "/mydir"); // 創(chuàng)建一個(gè)目錄/mydir
listDir(SD, "/", 0);
removeDir(SD, "/mydir"); // 刪除/mydir目錄
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello "); // 打開創(chuàng)建一個(gè)hello.txt文件,并在文件開頭寫入"Hello "
appendFile(SD, "/hello.txt", "World!n"); // 在hello.txt文件末尾繼續(xù)寫入"World!",然后換行
readFile(SD, "/hello.txt"); // 讀取hello.txt文件的內(nèi)容并打印出來(lái)
deleteFile(SD, "/foo.txt"); // 刪除foo.txt文件(提示:該目錄下沒有foo.txt文件,應(yīng)該會(huì)提示錯(cuò)誤)
renameFile(SD, "/hello.txt", "/foo.txt"); // 把hello.txt文件重命名為foo.txt
readFile(SD, "/foo.txt"); // 讀取foo.txt文件的內(nèi)容并打印出來(lái)
testFileIO(SD, "/test.txt"); // 測(cè)試文件讀寫速度
Serial.printf("Total space: %lluMBn", SD.totalBytes() / (1024 * 1024)); // 讀取并打印SD卡的總?cè)萘?/span>
Serial.printf("Used space: %lluMBn", SD.usedBytes() / (1024 * 1024)); // 讀取并打印SD卡的剩余容量
}
void loop(){
}
測(cè)試結(jié)果:
通過(guò)串口打印可以看到以下信息:
如果硬件異常或者接線不對(duì),會(huì)出現(xiàn)通訊異常,提示如下:
3 結(jié)束語(yǔ)
好了,關(guān)于如何基于Arduino使用SD卡就介紹到這里,內(nèi)容不多,講的也比較簡(jiǎn)單,如果還有什么問題,歡迎在評(píng)論區(qū)留言或者私信給我。
想了解更多Arduino的內(nèi)容,可以關(guān)注一下博主,后續(xù)我還會(huì)繼續(xù)分享更多的經(jīng)驗(yàn)給大家。
esp8266基于Arduino的開發(fā)教程匯總:
https://blog.csdn.net/ShenZhen_zixian/article/details/121659482
如果這篇文章能夠幫到你,就…你懂得。