news 2026/9/20 9:22:06

ESP32 SD卡读写全攻略:SPI原理、Arduino与MicroPython实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32 SD卡读写全攻略:SPI原理、Arduino与MicroPython实战

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默认引脚说明
SCKGPIO 18时钟线
MISOGPIO 19数据输入
MOSIGPIO 23数据输出
CSGPIO 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”,意思是一样的。接线时如果模块上没有明确标注,可以看芯片手册或者用万用表量一下哪个引脚连着卡座的片选脚。这个确认工作花不了两分钟,但能省掉后面半小时的排查时间。

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

C++随机数生成指南:从rand到random库的实战避坑

1. 随机数到底在解决什么问题写C的人迟早会撞上随机数这个坎。做小游戏要随机掉落装备&#xff0c;写测试要造模拟数据&#xff0c;搞算法要随机初始化参数&#xff0c;甚至做个抽奖程序都离不开它。但很多人第一次用rand()的时候都会懵——为什么每次运行结果都一样&#xff1…

作者头像 李华
网站建设 2026/9/20 9:18:22

APKPure手机App详解:第三方安卓应用市场下载、安装与问题排查指南

这几年身边总有朋友问我&#xff0c;手机上明明自带应用商店&#xff0c;为什么还要折腾一个 apkpure 安装包来做备用。每次我都用同一个例子回答&#xff1a;你主力机是国内品牌的安卓&#xff0c;突然想装一个只在海外上架的效率工具&#xff1b;或者你出差带了一台国际版系统…

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

FreeRTOS入门实战:STM32环境搭建与第一个任务

/* 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:12:17

C++单元测试实践指南:框架选型与高级技巧

1. 为什么C项目需要单元测试在C开发领域&#xff0c;单元测试常常被忽视&#xff0c;但它的价值远超大多数开发者的想象。我经历过一个典型的场景&#xff1a;一个运行了3年的金融交易系统突然在凌晨崩溃&#xff0c;排查后发现是一个简单的边界条件处理函数在特定输入组合下产…

作者头像 李华