news 2026/9/20 9:28:45

ESP32外接SD卡读写实战:从硬件接线到数据可靠存储

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32外接SD卡读写实战:从硬件接线到数据可靠存储

刚玩ESP32那会儿,我用的是最早一批WROOM-32开发板,Flash总共4MB。程序、字库、网页资源一塞,剩余空间基本见底。当时想做一个带网页配置和简单日志上报的小设备,数据量虽然不大,但架不住Flash不够用。后来才想明白一件事:ESP32本身就不是为了“存放大量数据”设计的,想拥有海量存储空间,最省事、成本最低、通用性最强的方案,就是外挂一张SD卡。

这篇文章把读写SD卡这件事从头到尾捋一遍。硬件怎么接、代码怎么写、初始化为什么失败、数据为什么会丢,这些零基础容易卡住的地方都会讲到。文章基于ESP32 + Arduino开发环境,适合刚把ESP32跑起来、想让它保存更多数据的玩家。看完你不仅能写出“创建文件、写日志、读配置”这类基础操作,还能避开不少我踩过的坑。

1. 为什么要给ESP32外接SD卡:先算一笔存储账

1.1 板载Flash到底能装多少东西

ESP32开发板上的Flash大小常见有4MB、8MB、16MB,听起来好像不小,但真正能用的是打折后的。一个Arduino工程编译出来,带WiFi、BLE、HTTP Server的固件动辄1~2MB,如果再开OTA升级,又要额外留一个同等大小的分区。也就是说,你买了一块16MB Flash的开发板,实际留给文件存储的可能也就几MB,甚至在某些默认分区表下只有几百KB。

SPIFFS和LittleFS虽然能挂载到内部Flash,但它们给用户用的空间本来就是从整个Flash里抠出来的。你在IDE里选择分区方案时,会看到“Huge APP”“Minimal SPIFFS”这类选项,选择恐惧症直接犯了。而且Flash是有擦写寿命的,频繁写入日志对Flash并不友好,数据量一大,整个方案就站不住脚。

1.2 SD卡在存储扩展方案里的定位

给ESP32扩存储,市面上其实有几条路:内部Flash的LittleFS、外部SPI Flash芯片、SD卡。三者各有各的用途,但SD卡的优势非常明显:容量可以做到几十GB级别,拔下来插电脑就能直接读取,格式化也方便,成本只有几块钱到十几块钱。对于数据采集、日志记录、离线素材存放、Web服务器静态文件这类场景,SD卡是最合适的“仓库盘”。

SD卡不是用来跑程序的,而是用来“装东西”的。理解这一点很重要。你不会想把代码里频繁查找的配置数据库放SD卡上,但你会想把几十MB的传感器历史数据、图片素材、音频文件放上去。搞清楚这个定位,后面的技术选型就顺了。

2. 硬件准备与SPI接线:第一道门槛

2.1 模块选型:自带电平转换的模块最省心

SD卡本身是3.3V逻辑,工作电压也是3.3V。但很多零基础玩家手里可能只有5V输出的模块,或者直接把裸卡座往面包板上一插,结果SD卡要么不识别,要么频繁出错。这里建议直接买“Micro SD卡模块”,尽量选带3.3V稳压器和电平转换电路的版本。这种模块通常有4到6个引脚:VCC、GND、CS、SCK、MOSI、MISO,有的还引出CD(卡检测)引脚,后面会细说。

模块上有没有电平转换芯片,可以看板子上的丝印,常见的有“74LVC245”“TXS0108”等字样的芯片。如果模块只是几个电阻直接连出来,那你就得把它当成裸卡座对待,老老实实只接3.3V,并自己处理电平兼容。零基础阶段别给自己加戏,直接买“带电平转换”的模块,能少踩一半坑。卡的种类也建议用正规品牌的TF卡,容量在2GB到32GB之间,初始阶段别拿那种“路边送的杂牌卡”来测试,否则你会以为是代码问题,其实是卡本身就有问题。

2.2 接线表与引脚选择逻辑

ESP32的SPI接口有两组,VSPI和HSPI。Arduino环境下默认用的是VSPI,引脚分布为:SCK=GPIO18、MISO=GPIO19、MOSI=GPIO23、SS=GPIO5。这个组合也是最常见的接法,零基础直接照着接就行。接线表如下:

SD卡模块引脚ESP32开发板引脚
VCC3.3V(或者模块规格书注明的5V输入)
GNDGND
CSGPIO5
SCKGPIO18
MOSIGPIO23
MISOGPIO19

有人会问,为什么不用ESP32自带的SDMMC接口?那玩意儿速度快,4-bit模式能跑到几十MB/s,但它有几个引脚和启动配置位有关,比如GPIO12在上电时的电平会影响Flash工作电压,接错之后可能直接导致开发板上电失败或Flash异常。零基础阶段用SPI模式,稳定、代码简单、网上资料最多,速度对日志和配置文件来说绰绰有余。

CS引脚其实可以换,只要避开启动关键引脚就行。比如GPIO15(TDO)默认上电时需要保持低电平,你硬把CS接上去可能没大问题,但如果卡上电时把线拉高就不太好了。建议就用GPIO5,这个引脚被SD卡示例默认绑定,省心。

2.3 供电细节与上拉电阻

SD卡在读写时的瞬时电流可以达到100mA以上,虽然ESP32开发板上的3.3V LDO通常能提供几百mA,但如果你同时接了OLED屏幕、传感器、舵机等一堆外设,3.3V总线上电压可能被拉低,出现“卡挂载成功但读写出错”的诡异问题。我的建议是:模块的VCC单独接3.3V,GND共地,如果外设太多,考虑用外部3.3V稳压模块给SD卡单独供电,或者至少别把所有大电流设备都堆在同一路LDO上。

关于上拉电阻,很多模块没有给MISO加上拉。SD卡在SPI模式下,MISO(D0)信号线建议通过10kΩ电阻上拉到3.3V,避免卡未选中时信号悬空产生干扰。如果你的模块没有明确标注内置上拉,自己在MISO与3.3V之间焊一个10kΩ电阻。这不是玄学,而是SD卡规范的要求。你可能觉得不加上拉也能跑,但偶尔读取出错的“偶发性问题”往往就出在这种细节上。

3. 初始化SD卡:从格式化到代码跑通

3.1 先把卡格式化成FAT32

SD卡不是插上就能用,文件系统得对上号。ESP32的SD库默认支持FAT32,这是最稳妥的选择。新买的卡或者是旧手机上拆的卡,可能格式是exFAT,或者是NTFS,ESP32不一定认,所以第一步就是在电脑上把卡格式化成FAT32。

Windows资源管理器自带的格式化工具最大只支持到32GB,容量超过32GB的卡,界面里根本看不到FAT32选项。这种情况可以用SD协会官方的SD Card Formatter工具,或者命令行执行format /FS:FAT32 X:(X是卡符),但大容量卡全盘格式化会有点久。macOS用户用“磁盘工具”,格式选“MS-DOS (FAT)”即可。Linux下用mkfs.vfat -F 32 /dev/sdX1。格式化会清空卡上所有数据,操作前注意备份。

3.2 最小初始化代码

代码方面,Arduino IDE需要先装好ESP32开发板支持包,然后在工程里包含FS.hSD.hSPI.h三个头文件。我整理了一份最精简的初始化代码:

#include "FS.h" #include "SD.h" #include "SPI.h" #define SD_CS 5 #define SPI_SCK 18 #define SPI_MISO 19 #define SPI_MOSI 23 void setup() { Serial.begin(115200); SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, SD_CS); if (!SD.begin(SD_CS)) { Serial.println("SD Card Mount Failed"); return; } uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { Serial.println("No SD card attached"); return; } Serial.println("SD Card Mounted"); Serial.printf("Total space: %llu MB\n", SD.totalBytes() / (1024 * 1024)); Serial.printf("Used space : %llu MB\n", SD.usedBytes() / (1024 * 1024)); } void loop() {}

上电后打开串口监视器,波特率115200,如果能看到“SD Card Mounted”以及容量信息,说明硬件和初始化已经没问题。如果你的开发板引脚被占用,需要自定义SPI引脚,SPI.begin第一个参数是SCK,第二个是MISO,第三个是MOSI,第四个是CS,顺序别搞反了,很多新手在这一步把MISO和MOSI接反,导致初始化失败。

SD.begin(SD_CS)内部的默认SPI时钟频率不算太高,对于普通杜邦线接线是够用的。如果线路比较长或模块质量一般,也可以手动压低SPI频率来换取稳定,写成SD.begin(SD_CS, SPI, 10000000)就是10MHz,再不行就4MHz。稳定压倒一切,别急着追求速度。

3.3 初始化失败时的排查顺序

遇到“Card Mount Failed”,不要慌,这是一步一步排查的事。下面是我自己反复用过很多遍的排查链路,建议按顺序来,不要跳步:

  • 先确认CS引脚在代码里和实际接线一致。GPIO5就是GPIO5,别看着丝印是D5就真的接5号引脚,不同开发板的丝印规则乱七八糟。
  • 把SD卡重新插拔一次,确保卡座金属弹片接触良好。插卡时要听到“咔哒”一声,插到底。
  • 检查卡是否已经格式化为FAT32。在电脑上重新格式化,别用exFAT。
  • 换一张已知正常的SD卡再试。这一步能快速排除“卡本身是坏的”这种最让人绝望的情况。
  • 用万用表测模块VCC对GND电压,确保在3.3V左右。如果电压低到3.0V以下,检查供电。
  • 在MISO线上加上10kΩ上拉电阻,如果模块没内置,这一步经常能救回来。
  • 把SPI时钟降到4MHz再试,通过SD.begin(SD_CS, SPI, 4000000)
  • 检查杜邦线长度。十几厘米的线已经是极限,线越长信号越差,波形越丑。
  • 换一块开发板或者换一个SD卡模块,排除硬件个体差异。

这套流程走一遍,90%以上的初始化问题都能暴露出来。最怕的是“偶尔能挂载,偶尔不能挂载”,这种问题多半是接触不良或供电不稳,跟代码关系不大。

4. 文件读写实战:从建文件到日志记录

4.1 打开文件的几种模式

SD库提供了一套类似C语言FILE的操作接口,最常用的打开模式有四种:FILE_READFILE_WRITEFILE_APPEND,以及通过字符串方式指定"r""w""a"。它们之间的区别直接决定了文件内容会不会被覆盖,这一点必须搞清楚。

打开模式行为常用场景
FILE_READ以只读方式打开文件,文件不存在则失败读取配置、读取日志
FILE_WRITE以写方式打开,从文件头开始覆盖写创建/重写配置文件
FILE_APPEND以写方式打开,从文件尾部追加追加日志、累积数据点
"r"同FILE_READ兼容写法
"w"同FILE_WRITE兼容写法
"a"同FILE_APPEND兼容写法

有个零基础很容易踩的坑:在ESP32的SD库中,FILE_WRITE并不像某些其他平台那样是“追加写”,而是覆盖写。如果你打开一个已有文件,写入比原文件短的内容,部分固件版本不会自动把旧文件截断,文件尾部会出现“原来的残留数据”,看起来就像文件损坏了。所以我的习惯是:如果是彻底重写一个文件,先执行SD.remove(path)再写;如果想不清空原文件,只往后面加内容,一定用FILE_APPEND

4.2 写入数据:print、println、write、flush

写入文本数据最常用的就是file.print()file.println(),用法和Serial.print几乎一样。写入二进制数据则用file.write(buf, len)。一段完整的写入操作长这样:

File file = SD.open("/hello.txt", FILE_WRITE); if (!file) { Serial.println("Failed to open file for writing"); return; } file.println("Hello, SD Card!"); file.print("Temperature: "); file.println(26.5); file.write((uint8_t*)"\x01\x02\x03", 3); file.flush(); file.close();

这里要特别强调flush的作用。SD卡的数据不是写一个字节就立即落盘一字节,文件系统会先把数据放进缓冲区,缓冲区满了或者收到flush/close命令才真正写入物理介质。这不只是速度优化,更是数据安全的一部分。如果你写完不调用close就突然断电,最后写入的那部分数据大概率丢失,甚至可能导致文件系统元数据损坏。关于这一点,后面单独开一节细说。

4.3 读取数据:逐字节、按行、分块

读取文件的套路也很固定。逐字节读取适用于小文件,按行读取适用于CSV这类文本配置,分块读取适用于大文件。先看一个按行读取CSV日志的例子:

File dataFile = SD.open("/log.csv", FILE_READ); if (!dataFile) { Serial.println("Failed to open file for reading"); return; } while (dataFile.available()) { String line = dataFile.readStringUntil('\n'); Serial.println(line); } dataFile.close();

readStringUntil适合读文本行,但如果文件很大,或者每行非常长,用String去接会占用不少RAM,ESP32虽然内存比普通ATmega大不少,但也不是无底洞。处理大文件时,更好的做法是用固定缓冲区分块读:

File f = SD.open("/big.bin", FILE_READ); if (!f) return; uint8_t buf[512]; int n; while ((n = f.read(buf, sizeof(buf))) > 0) { // 每次处理512字节 } f.close();

这种方式无论文件多大,内存占用始终不超过512字节,是处理音频、图片等大文件时的标准姿势。零基础阶段可以先把逐字节和按行读法练熟,分块读法是接下来进阶的必经之路。

4.4 做一个真实可用的日志系统

下面放到实际场景里。假设你要做一个环境数据记录器,每隔一段时间把温湿度写到SD卡上的CSV文件里,要求不能覆盖历史数据。代码可以这样写:

float readTemp() { // 接DHT11/DHT22时替换成真实读取代码 return 26.5 + random(-30, 30) / 10.0; } float readHumi() { // 接DHT11/DHT22时替换成真实读取代码 return 60.0 + random(-50, 50) / 10.0; } void logData() { // 检查文件是否存在,不存在则先写表头 if (!SD.exists("/log.csv")) { File header = SD.open("/log.csv", FILE_WRITE); if (header) { header.println("time,temp,humi"); header.close(); } } File logFile = SD.open("/log.csv", FILE_APPEND); if (!logFile) { Serial.println("Open log.csv failed"); return; } logFile.print(millis()); logFile.print(","); logFile.print(readTemp()); logFile.print(","); logFile.println(readHumi()); logFile.close(); } void setup() { Serial.begin(115200); SPI.begin(18, 19, 23, 5); if (!SD.begin(5)) { Serial.println("SD Card Mount Failed"); return; } } void loop() { logData(); delay(5000); }

这套代码里有一个小细节值得注意:写表头之前先调用SD.exists("/log.csv")判断文件是否存在。因为文件打开用FILE_WRITE会覆盖,所以“建表头”和“写数据”必须分开处理,不能让每次跑日志都先创建一个新文件覆盖旧数据。实际跑起来后,拔卡插到电脑上,用Excel或Notepad打开log.csv,就能看到一行行的数据记录。这个操作流程跑通,说明你已经掌握了SD卡读写最核心的链路。

5. 数据可靠性经验:掉电、长文件名、大文件这些细节决定成败

5.1 flush和close:别让最后一条数据悄悄消失

我在做室外采集设备时,遇到过一件特别恼火的事:设备断电重启后,日志文件最后几条数据经常消失,有时候甚至整个文件变成0字节。排查到最后,绝大多数原因都是没有正确调用closeflush

SD库的写入有缓冲区,printwrite只是把数据放进了内存缓冲,真正写进卡里需要等到缓冲区满、显式调用flushcloseflush负责把缓冲区的数据送到卡上,closeflush的基础上还会关闭文件、更新文件目录项,比如文件长度、修改时间这些信息。

长期记录数据的设备,写完一条就close一次比较稳妥,代价是频繁开关文件会让写入速度变慢,对卡的磨损也略大。我的折中方案是:重要数据每条都flush,每隔一段时间或者批量写完再close;低频率的日志系统,直接每条写完后close,简单可靠。原则就一个:任何情况下,别在没调用close之前直接断电。

5.2 追加模式和长期记录的正确姿势

日志系统要长期运行,必然涉及“只往文件尾部添加内容”的需求。FILE_APPEND就是为此设计的。但追加模式也有讲究:每次打开文件都是以追加方式打开,写完关闭,下次再打开继续追加。这个模式适合低频写入,每次需要打开、定位到尾部、写入、关闭,有一点点开销,但对绝大多数日志场景完全够用。

如果是高频记录,比如每秒写几十条甚至上百条,频繁开关文件的开销会变大。这时候可以打开文件后保持不关闭,持续往里面写,但一定别忘了周期性调用flush。这里要注意,如果设备意外断电,保持打开状态的文件可能丢失最后一段未flush的数据,甚至目录项来不及更新。对于高频记录场景,建议在内存里再缓存一批数据,凑够一定条数或者每隔几秒批量写一次,既减少写入次数,也降低数据丢失的风险。

5.3 文件名与目录规范:避开中文和特殊符号

SD卡上的文件名不是随便起的。FAT32文件系统支持长文件名,但ESP32的FATFS底层在处理某些中文编码时容易出问题。你在电脑上建了一个“测试数据.txt”,拿到ESP32上可能能读出来,但ESP32自己创建中文文件名,再插回电脑,大概率出现乱码。这不是某块板子的毛病,而是字符编码转换的老问题。稳妥的做法是统一使用英文字母、数字、下划线、中划线,例如log_20240101.csvconfig.inidata.bin,不要带空格和中文。

另一个容易忽略的是目录。SD.open("/data/log.txt", FILE_APPEND)需要/data目录已经存在,否则打开文件会失败。目录得先创建:

if (!SD.exists("/data")) { SD.mkdir("/data"); }

养成好习惯:文件路径里的每一级目录,要么已经存在,要么先显式创建,别指望库函数自动帮你补一个目录出来。

5.4 大文件分块读写与空间判断

前面提到了分块读取,写入也一样。当你要把一块大的数据写入SD卡时,不要写一个几百KB甚至几MB的数组,然后一次性write出去。ESP32虽然内存有几百KB可用,但也不能这么挥霍。正确做法是分块写入,比如每次写512字节或1024字节,写完后立即继续下一块。

写入前最好判断一下卡上的剩余空间。SD.totalBytes()SD.usedBytes()返回总容量和已用容量,相减就是剩余空间。这个函数不是免费的,计算量不小,别在循环里频繁调用。比较好的做法是每次写入前调用一次,剩余空间低于某个阈值就开始清理旧文件或者停止记录,避免写满后出现错误。

5.5 山寨卡与磨损均衡的隐患

很多人喜欢买“白菜价”SD卡,结果就是读写速度忽快忽慢,偶尔出现坏块,甚至格式化完没几天又出问题。SD卡内部其实有主控和Flash,正品卡的主控有磨损均衡、坏块管理逻辑,山寨卡可能什么都没有,或者做得很差。SD卡作为可插拔介质,本来就不适合长期高频写入,但这不代表它不靠谱,关键是选卡。买卡的时候尽量选择主流品牌,别贪便宜。如果项目对可靠性要求很高,建议在硬件设计上做掉电保护,比如在SD卡供电端加一个容量稍大的电容,给文件系统留出完成写入的时间。这个细节在工业现场能救命。

6. 高频踩坑速查与进阶方向

6.1 高频踩坑速查表

把零基础玩家最常见的SD卡问题整理成了一张表,照着排查能省不少时间:

现象常见原因处理方法
串口输出Card Mount Failed卡未格式化、接触不良、CS接错重新格式化FAT32、插紧卡、核对CS引脚
挂载成功但打不开文件目录不存在、文件名非法先SD.mkdir建目录,文件名改英文短名
文件打开成功但写入内容丢失没调close/flush、供电不足写完必须flush/close,检查3.3V电压
数据写了一半卡死SPI线太长、时钟太快降低SPI频率,缩短杜邦线
卡在电脑上正常但ESP32不认文件系统是exFAT/NTFS重新格式化为FAT32
偶尔能挂载偶尔失败供电不稳、MISO缺上拉补10kΩ上拉电阻,外设分开供电
日志尾部有旧数据FILE_WRITE覆盖写不截断先SD.remove再写,或用FILE_APPEND
中文文件名乱码编码不兼容全部改用英文数字下划线

这张表是我自己在项目里反复踩坑总结出来的,不一定覆盖所有问题,但覆盖了绝大多数零基础玩家会遇到的状况。

6.2 再往前走:SD卡还能这么玩

读写SD卡只是第一步。把基础稳定之后,可以做的事情很多,每条都值得单独开一篇来讲。比如把ESP32配置成一个小型Web服务器,网页文件放在SD卡上,通过浏览器访问ESP32的IP,就能直接浏览文件列表、下载日志、甚至上传新文件。再比如配合音频解码模块,从SD卡读取MP3或WAV文件做语音播放,做一个能定时播报的闹钟或语音提示器。数据采集方向可以做得更深:把传感器数据按天存到不同CSV文件,定期清理过期数据,写一个简单的FATFS巡检逻辑,保证SD卡长时间运行不出错。

我个人在实际项目中的体会是:SD卡读写初看简单,但真正决定一个设备是“能跑”还是“能稳定跑”的,往往是细节。比如代码里有没有养成“文件不使用了就close”的习惯,硬件上有没有把供电和上拉做扎实,选卡时有没有多花几块钱买一张正经卡。这些经验不是看文档能看出来的,是踩坑踩出来的。

最后再分享一个小技巧:调试SD卡问题的时候,别老盯着代码看。用串口把SD.cardType()SD.totalBytes()SD.usedBytes()打出来,先确认物理层和文件系统层是通的,再去分析文件操作逻辑。很多莫名其妙的“代码问题”,最后都回到了“卡没插紧”或者“供电不够”这种最朴素的根因上。SD卡这个外设,只要能稳住电源、稳住信号、稳住文件关闭时机,基本就能稳稳陪你很久。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 9:26:58

调 Claude Code 时 API 请求失败?TaoToken 这边先查 Base URL 填没填对

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 9:26:45

Atlas 300V 24G部署YOLOv5s:AI推理加速卡全流程指南

前阵子朋友扔过来一块Atlas 300V 24G,让我赶紧把YOLOv5s的检测服务跑起来,结果他第一句话就是问我:“这卡是运算加速卡吗?”这个问题其实问到根子上了:它确实是加速卡,但不是大多数人熟悉的GPU,…

作者头像 李华
网站建设 2026/9/20 9:26:39

FX Configurator-EN-L:三菱PLC以太网通信参数配置与IP在线修改实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 9:25:24

Claude Mods实战指南:从安装到定制你的AI编程助手

1. Claude Mods到底是什么?——社区为什么集体盯上「魔改」最近在AI编程工具圈子里,"Claude Mods"这个词的出现频率突然高了起来。如果你跟我一样长期关注Claude Code的更新动态,会发现自从官方放开了扩展能力之后,GitH…

作者头像 李华
网站建设 2026/9/20 9:23:52

梯度下降算法:原理、变体与实践指南

1. 梯度下降算法概述在机器学习的世界里,优化算法就像是一位经验丰富的向导,带领模型穿越复杂的参数空间,寻找最佳解决方案。梯度下降(Gradient Descent)无疑是这个领域最基础也最重要的算法之一。我第一次接触这个概念…

作者头像 李华