1. 为什么给ESP32配一张SD卡是性价比最高的存储扩展方案
搞ESP32开发的朋友迟早都会撞上一堵墙:芯片自带的Flash存储实在太小了。以常见的ESP32-WROOM-32模组为例,片内SPI Flash通常只有4MB,扣掉Bootloader、分区表、应用程序本身占用的空间,真正能拿来存用户数据的可能连1MB都不到。你随便跑一个数据采集项目,传感器每秒写一条记录,几天下来就把空间撑爆了。更别提音频录制、图片存储、日志长期归档这类需求,片内Flash根本扛不住。
这时候SD卡就是最务实的解法。一张8GB的MicroSD卡几十块钱,容量是片内Flash的两千倍,而且随时可以拔下来插到电脑上直接读数据,不需要任何额外的上位机工具。对于零基础的朋友来说,ESP32加SD卡这个组合,硬件成本低、接线简单、代码量少,是入门嵌入式存储的最佳切入点。
这篇文章面向的是完全没有SD卡使用经验的ESP32初学者,当然有Arduino或MicroPython基础更好。我会从硬件接线讲起,把SPI通信的底层逻辑用大白话解释清楚,然后分别给出Arduino和MicroPython两套完整可跑的代码,最后把实际调试中踩过的坑一个一个摊开来讲。看完你就能让自己的ESP32拥有海量存储空间,想存什么存什么。
2. 硬件准备与SPI通信原理拆解
2.1 需要哪些硬件
先列一下清单,这些东西加起来不到五十块钱:
- ESP32开发板一块(ESP32-WROOM-32、ESP32-S3、ESP32-C3都行,本文以最常见的ESP32-WROOM-32为例)
- MicroSD卡模块一个(市面上常见的那种带电平转换芯片的模块,通常标着“MicroSD Card Adapter”)
- MicroSD卡一张(建议用Class 10以上速度等级的,容量4GB到32GB都可以,格式化成FAT32)
- 杜邦线若干(母对母或公对母,看你的开发板和模块的引脚形式)
- 面包板一块(可选,但强烈建议用,接线更稳)
这里要特别说一下SD卡模块的选择。市面上有两种:一种是纯SD卡座,没有电平转换和稳压电路,工作电压是3.3V,可以直接和ESP32连;另一种是带AMS1117稳压芯片和电平转换的模块,支持3.3V和5V输入。我建议买后者,虽然多花几块钱,但它的电平转换电路能保护ESP32的GPIO不被意外电压损坏,而且模块上通常自带去耦电容,信号质量更稳定。
注意:ESP32的GPIO工作电压是3.3V,绝对不能直接接5V信号。如果你手头只有5V的SD卡模块,一定要确认模块上有电平转换芯片再使用。
2.2 SPI通信到底是怎么回事
SPI全称是Serial Peripheral Interface,翻译过来叫串行外设接口。你可以把它想象成一条单向的高速公路,数据只能沿着一个方向流动,但速度非常快。SPI通信需要四根线:
- SCK(Serial Clock):时钟线,由主机(ESP32)产生,像节拍器一样控制数据传输的节奏
- MOSI(Master Out Slave In):主机输出、从机输入,ESP32通过这根线把数据发给SD卡
- MISO(Master In Slave Out):主机输入、从机输出,SD卡通过这根线把数据回传给ESP32
- CS(Chip Select):片选线,ESP32通过拉低这根线来“点名”SD卡,告诉它“现在轮到你了”
SPI的通信过程就像老师(ESP32)提问、学生(SD卡)回答。老师先通过SCK打拍子,然后通过MOSI把问题一个字一个字地说出来,学生通过MISO把答案一个字一个字地传回来。CS线就是老师点名的那根手指,指到谁谁才开口。
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合而成。SD卡在SPI模式下固定使用Mode 0,也就是CPOL=0、CPHA=0。这意味着时钟空闲时为低电平,数据在时钟的上升沿被采样。这个细节你不需要死记,但调试时如果通信失败,检查一下SPI模式是否正确是排查方向之一。
2.3 ESP32的SPI引脚怎么分配
ESP32有两个硬件SPI控制器:SPI0和SPI1是内部使用的,SPI2(也叫HSPI)和SPI3(也叫VSPI)可以给用户自由使用。我们通常用VSPI,它的默认引脚是:
| 信号 | ESP32默认引脚 | 说明 |
|---|---|---|
| SCK | GPIO 18 | 时钟线 |
| MISO | GPIO 19 | 数据输入 |
| MOSI | GPIO 23 | 数据输出 |
| CS | GPIO 5 | 片选(可自定义) |
当然你也可以自定义引脚,ESP32的GPIO矩阵允许你把SPI信号映射到几乎任意引脚上。但为了简单起见,零基础阶段直接用默认引脚最省事。
接线的时候,把SD卡模块的对应引脚和ESP32连起来就行:
- SD模块的SCK接ESP32的GPIO 18
- SD模块的MISO接ESP32的GPIO 19
- SD模块的MOSI接ESP32的GPIO 23
- SD模块的CS接ESP32的GPIO 5
- SD模块的VCC接ESP32的3.3V
- SD模块的GND接ESP32的GND
提示:有些SD卡模块的引脚标注是CLK、DO、DI、CS,分别对应SCK、MISO、MOSI、CS,不要接错了。
3. Arduino环境下读写SD卡的完整实操
3.1 环境搭建与库安装
Arduino IDE从1.6.5版本开始就内置了SD库,不需要额外安装。但前提是你得先装好ESP32的开发板支持。打开Arduino IDE,依次点击“文件”→“首选项”,在“附加开发板管理器网址”里填入ESP32的板管理器地址,然后在“工具”→“开发板”→“开发板管理器”里搜索“esp32”并安装。
装好之后,在“工具”→“开发板”里选择“ESP32 Dev Module”,端口选对,就可以开始写代码了。
3.2 最简SD卡初始化与文件写入
先来看一段最基础的代码,功能是初始化SD卡、创建一个文件、写入一行文字、然后读出来验证:
#include <SPI.h> #include <SD.h> #define SD_CS 5 void setup() { Serial.begin(115200); delay(1000); Serial.println("正在初始化SD卡..."); if (!SD.begin(SD_CS)) { Serial.println("SD卡初始化失败!请检查接线和卡是否插好。"); return; } Serial.println("SD卡初始化成功!"); Serial.print("卡容量: "); Serial.print(SD.cardSize() / (1024 * 1024)); Serial.println(" MB"); // 写入文件 File myFile = SD.open("/test.txt", FILE_WRITE); if (myFile) { Serial.println("正在写入test.txt..."); myFile.println("Hello ESP32 SD Card!"); myFile.println("这是第二行测试数据。"); myFile.close(); Serial.println("写入完成。"); } else { Serial.println("打开文件失败!"); } // 读取文件 myFile = SD.open("/test.txt"); if (myFile) { Serial.println("test.txt的内容如下:"); while (myFile.available()) { Serial.write(myFile.read()); } myFile.close(); } else { Serial.println("读取文件失败!"); } } void loop() { // 这里什么都不做 }这段代码的逻辑很直白:SD.begin()负责初始化,参数是CS引脚号;SD.open()用来打开文件,第二个参数FILE_WRITE表示以写入模式打开,如果文件不存在就创建;myFile.println()写入内容;myFile.close()关闭文件,这一步很关键,不关文件数据可能不会真正写入卡里。
3.3 数据追加与目录操作
实际项目中,我们通常需要往同一个文件里不断追加数据,而不是每次覆盖。这时候用FILE_APPEND模式:
File logFile = SD.open("/datalog.csv", FILE_APPEND); if (logFile) { logFile.print(millis()); logFile.print(","); logFile.print(temperature); logFile.print(","); logFile.println(humidity); logFile.close(); }这段代码模拟了一个数据记录场景,每采集一次温湿度就追加一行CSV格式的数据。用FILE_APPEND模式打开文件时,写入指针会自动移到文件末尾,不会覆盖已有内容。
SD库还支持目录操作,比如创建目录、列出目录内容:
// 创建目录 SD.mkdir("/logs"); // 列出根目录内容 File root = SD.open("/"); File entry = root.openNextFile(); while (entry) { Serial.print(entry.name()); Serial.print("\t"); Serial.println(entry.size()); entry = root.openNextFile(); }3.4 性能优化:调整SPI时钟频率
SD库默认的SPI时钟频率是较低的,为了兼容性考虑。如果你用的是质量好的Class 10卡,可以手动提高频率来加快读写速度:
if (!SD.begin(SD_CS, SPI, 40000000)) { // 40MHz Serial.println("高速初始化失败,尝试默认速度..."); if (!SD.begin(SD_CS)) { Serial.println("SD卡初始化失败!"); return; } }实测下来,把频率从默认的几MHz提到20MHz到40MHz,写入速度能有明显提升。但要注意,频率越高对布线质量要求越高,杜邦线太长或者接触不良的话,高速下容易出错。我一般先用默认频率调通,再逐步往上加,找到一个稳定的最高值。
4. MicroPython环境下读写SD卡的完整实操
4.1 固件准备与驱动加载
MicroPython的SD卡支持需要固件里包含sdcard模块。官方的ESP32固件从1.13版本开始就内置了这个模块,所以只要你刷的是比较新的固件,直接就能用。刷固件的方法这里不展开,用esptool或者Thonny一键烧录都行。
接线方式和Arduino完全一样,还是那四根SPI线加电源。
4.2 初始化与基本读写
MicroPython的代码更简洁:
import machine import sdcard import os # 配置SPI spi = machine.SPI(2, sck=machine.Pin(18), mosi=machine.Pin(23), miso=machine.Pin(19)) sd = sdcard.SDCard(spi, machine.Pin(5)) # 挂载文件系统 os.mount(sd, '/sd') print('SD卡挂载成功') print('目录内容:', os.listdir('/sd')) # 写入文件 with open('/sd/test.txt', 'w') as f: f.write('Hello from MicroPython!\n') f.write('第二行数据。\n') # 读取文件 with open('/sd/test.txt', 'r') as f: print(f.read())这里有几个关键点:machine.SPI(2, ...)里的2表示使用VSPI控制器;sdcard.SDCard()创建SD卡对象;os.mount()把SD卡挂载到虚拟文件系统的/sd路径下。挂载之后,你就可以像操作普通文件一样用open()、read()、write()了。
4.3 追加模式与文件管理
MicroPython里追加内容用'a'模式:
with open('/sd/datalog.csv', 'a') as f: f.write('{},{},{}\n'.format(time.ticks_ms(), temp, humi))文件管理操作也很直观:
# 查看文件大小 stat = os.stat('/sd/datalog.csv') print('文件大小:', stat[6], '字节') # 删除文件 os.remove('/sd/old.txt') # 创建目录 os.mkdir('/sd/backup') # 重命名 os.rename('/sd/a.txt', '/sd/b.txt')4.4 卸载与重新挂载
在MicroPython里,如果你要拔卡或者重新插卡,最好先卸载文件系统:
os.umount('/sd') # 此时可以安全拔卡 # 重新插卡后再次挂载 os.mount(sd, '/sd')不卸载直接拔卡的话,如果正好有数据在写入缓冲区里没落盘,数据就会丢失,严重的话还会导致文件系统损坏。
5. 常见问题与排查技巧实录
5.1 SD卡初始化失败
这是最常见的问题,现象是串口打印“初始化失败”或者MicroPython报错。排查顺序如下:
| 排查项 | 可能原因 | 解决方法 |
|---|---|---|
| 接线 | 杜邦线松动或接错 | 逐根检查,重点确认MISO和MOSI没接反 |
| 供电 | 3.3V供电不足 | 换一个3.3V引脚,或用外部稳压供电 |
| 卡格式 | 不是FAT32格式 | 在电脑上格式化为FAT32,分配单元大小选32KB |
| 卡容量 | 超过32GB | 换32GB及以下的卡,大容量卡可能需要exFAT |
| CS引脚 | 引脚号写错 | 确认代码里的CS引脚号和实际接线一致 |
| 模块 | 电平转换芯片损坏 | 换一个模块试试 |
我遇到过最坑的一次是杜邦线内部断了,外表看不出来,万用表量通断才发现。所以接线问题不要只看表面,该量就量。
5.2 写入数据但电脑上读不到
有时候ESP32这边显示写入成功,但把卡插到电脑上发现文件是空的或者内容不全。原因通常是文件没有正确关闭。SD卡的数据是先写到缓冲区,再批量写入卡的。如果你写完不调close(),缓冲区里的数据可能还没落盘。
注意:每次写完文件一定要调
close(),或者用with语句自动关闭。在Arduino里就是myFile.close(),在MicroPython里用with open(...)最省心。
另一个原因是热插拔。运行中拔卡再插卡,文件系统可能没有重新挂载,后续写入都写到了内存缓冲区里,根本没到卡上。
5.3 读写速度慢
如果你觉得SD卡读写速度不理想,可以从这几个方面优化:
- 提高SPI时钟频率(前面讲过了,从默认提到20MHz到40MHz)
- 减少小文件频繁打开关闭,批量写入后再统一关闭
- 用二进制模式读写,比文本模式快
- 避免在循环里频繁调用
SD.open()和close(),可以保持文件打开,定期flush
实测数据:一张Class 10的16GB卡,在ESP32上以20MHz SPI频率写入,速度大约在200KB/s到500KB/s之间。写入1MB的数据大概需要2到5秒。这个速度对于日志记录、传感器数据采集完全够用,但如果你想录高采样率的音频,可能就有点吃力了。
5.4 文件系统损坏的修复
如果卡在电脑上提示“需要格式化”或者“文件系统损坏”,先别急着格式化,试试用电脑自带的磁盘修复工具。Windows下可以右键盘符→属性→工具→检查,或者用命令行chkdsk /f X:。Mac下用“磁盘工具”的“急救”功能。很多时候只是文件分配表出了小问题,修复一下数据还能救回来。
预防文件系统损坏的最好办法就是:每次断电前确保文件已关闭,长时间运行的项目定期调用flush()把缓冲区数据落盘。
6. 进阶玩法与项目扩展思路
6.1 用SD卡做数据记录仪
把ESP32和一个温湿度传感器(比如DHT22或BME280)结合起来,每隔几分钟往SD卡里写一条记录,就是一个完整的数据记录仪。CSV格式的数据可以直接用Excel打开画图表。这个项目麻雀虽小五脏俱全,涵盖了传感器读取、定时任务、文件操作三个核心技能点。
6.2 用SD卡存网页文件做小型服务器
ESP32可以跑Web服务器,把HTML、CSS、JS文件放在SD卡里,ESP32只负责读取文件并通过HTTP返回给浏览器。这样你就能做一个不依赖外部网络的本地控制页面,手机连上ESP32的热点就能访问。SD卡在这里充当了静态资源仓库的角色,比把网页内容硬编码在代码里优雅得多。
6.3 用SD卡做OTA固件备份
在做OTA升级之前,先把当前固件备份到SD卡里。万一升级失败变砖,还能通过串口从SD卡里恢复。这个操作需要读取Flash内容并写入SD卡,涉及到底层分区操作,适合有一定基础之后再来折腾。
6.4 多文件系统共存
ESP32的Flash上可以同时存在SPIFFS/LittleFS和SD卡两个文件系统。SPIFFS存配置文件和关键数据,SD卡存大容量日志和媒体文件。两者通过不同的挂载点区分,代码里用不同的路径前缀访问就行。这种组合方式在实际产品中很常见,兼顾了可靠性和容量。
7. 几个让我印象深刻的踩坑经历
第一个坑是关于SD卡格式化的。我一开始用了一张从旧手机里拔出来的64GB卡,怎么都初始化不了。后来查资料才知道,SD库对exFAT格式支持不好,必须格成FAT32。而Windows自带的格式化工具对64GB以上的卡只给exFAT选项,得用第三方工具才能格成FAT32。换了一张32GB的卡,问题立刻解决。
第二个坑是供电。我用面包板给SD卡模块供电,同时面包板上还插了其他几个模块,结果SD卡时好时坏。用万用表一量,3.3V电压在SD卡工作时会跌到3.0V以下。后来单独给SD卡模块拉了一根供电线,问题消失。ESP32的3.3V输出能力有限,多个外设同时工作时一定要算一下总电流。
第三个坑是文件名的坑。在Arduino的SD库里,文件名如果以“/”开头表示绝对路径,不以“/”开头表示相对于当前目录。我一开始写SD.open("test.txt"),文件创建在了根目录下,但后来写SD.open("/test.txt")又找不到文件,折腾了半天才发现是路径写法不一致导致的。统一用绝对路径最省心。
第四个坑是MicroPython的SPI引脚映射。我一开始用machine.SPI(1, ...),怎么都不工作。后来查文档才知道,SPI(1)在ESP32上对应的是内部Flash使用的控制器,用户不能用。必须用SPI(2)也就是VSPI。这个坑在Arduino里不存在,因为Arduino的SPI库默认就用VSPI。
8. 关于SD卡选型的一点个人建议
如果你打算长期跑数据记录项目,我建议买工业级的MicroSD卡,虽然贵一点,但耐高温、耐频繁写入,寿命比消费级卡长很多。消费级卡用久了容易出现坏块,导致文件系统损坏。另外容量不要贪大,8GB到16GB对于大多数ESP32项目来说绰绰有余,太大的卡反而可能遇到格式兼容问题。
速度等级方面,Class 10是底线,U1或U3更好。但要注意,ESP32的SPI接口本身速度有限,再快的卡也发挥不出全部性能,所以没必要追求顶级速度卡,均衡考虑性价比就行。
最后说一个细节:SD卡模块上的CS引脚,有些模块标注的是“CS”有些是“SS”,意思是一样的。接线时如果模块上没有明确标注,可以看芯片手册或者用万用表量一下哪个引脚连着卡座的片选脚。这个确认工作花不了两分钟,但能省掉后面半小时的排查时间。