带FATFS文件系统的SD卡读写(SPI模式)
在一块小小的开发板上,如何让单片机像电脑一样“打开文件”“保存数据”?这不是魔法,而是嵌入式开发者每天都在解决的问题。想象一下:你设计的环境监测设备连续采集了三天的温湿度数据,客户却问:“能不能直接插到电脑上看?”——这时候,如果你还在用原始扇区操作或自定义二进制格式,恐怕只能苦笑。
真正实用的解决方案,是让MCU也能“认文件”。而实现这一点的关键组合就是:
SPI接口的SD卡 + FATFS文件系统
。这套方案不依赖操作系统、无需专用硬件,甚至能在只有几KB RAM的8位单片机上跑起来。更重要的是,它生成的
.txt
、
.csv
文件,Windows双击就能打开。
这背后的技术其实并不神秘。我们只需要搞清楚两件事:一是如何通过最普通的SPI通信让SD卡听话;二是怎样借助FATFS把一堆0和1变成结构化的文件。一旦打通这两个环节,你的嵌入式项目就从“能存数据”跃升为“会管理数据”。
几乎所有现代微控制器都配有SPI外设,这让SD卡成为最容易接入的外部存储介质之一。虽然它的理论速度远不如并行SDIO或eMMC,但在大多数实际场景中——比如记录传感器日志、存储配置参数、播放WAV音频——2~5 Mbps的传输速率已经绰绰有余。关键是,这种方案几乎不需要额外成本:你只需要四个GPIO引脚,加上一点代码逻辑。
但别小看这四个引脚。SD卡刚上电时,默认处于高速的SDIO模式,必须通过一套精确的初始化流程才能强制切换到SPI模式。这个过程有点像“唤醒沉睡的巨人”:先供电等待至少1ms,然后拉高MOSI线并发送不少于74个时钟脉冲,确保SD卡完成内部电源稳定。接着才是重头戏——发送CMD0命令(即
0x40 | 0 = 0x40
),期望收到返回值0x01,表示进入空闲状态。
接下来是一连串试探性的交互。我们会尝试发送CMD8查询电压支持情况,如果响应成功,说明这是一张较新的SDHC或SDXC卡;否则可能是老式的标准容量卡。无论哪种,最终都要通过循环发送CMD55 + ACMD41来驱动其进入SPI模式。整个过程中,每条命令都遵循固定格式:起始字节
0x40 + cmd_index
,后跟5字节参数,最后是CRC校验(初期可设为0x01)。主控通过MISO接收响应,通常是一个字节的R1类型状态码。
一旦初始化完成,就可以设置块大小(CMD16,默认512字节)并开始读写。单块读用CMD17,多块读用CMD18;对应地,CMD24和CMD25用于写操作。所有数据传输都以0xFE这样的令牌开始,随后是实际数据流。值得注意的是,SPI模式下的时钟频率是有严格限制的:初始化阶段不得超过400kHz,等一切就绪后再提升至10MHz甚至25MHz(视PCB布线质量而定)。
有人可能会问:为什么不用更高效的SDIO?答案很简单——通用性。STM32有SDIO,ESP32也有,但AVR、PIC、某些Cortex-M0芯片呢?它们可能根本没有专用控制器。而SPI几乎是标配,哪怕没有硬件模块,也能靠软件模拟实现。这就让“SPI+SD”成为一个极具移植性的方案,几乎可以在任何平台上复用同一套驱动框架。
当然,直接操作扇区的日子并不好过。你需要手动计算簇地址、维护FAT表项、处理目录条目……稍有不慎就会导致文件系统损坏。这时候,FATFS的价值就凸显出来了。
FATFS是由日本开发者ChaN编写的一个轻量级FAT文件系统模块,完全用C语言实现,不开源许可证限制,也不依赖任何RTOS。它最大的设计哲学是“抽象分层”:将底层存储设备封装成一个统一的磁盘I/O接口,只暴露两个核心函数——
disk_read()
和
disk_write()
。只要实现了这两个函数,上层的FATFS引擎就能自动处理文件创建、路径解析、缓存管理等一系列复杂逻辑。
举个例子,当你调用
f_open(&file, "data.csv", FA_WRITE | FA_CREATE_ALWAYS)
时,FATFS会自动检查是否存在同名文件,查找可用目录项,分配初始簇,并更新FAT链表。而这一切对开发者来说都是透明的。更妙的是,它的API风格几乎照搬ANSI C标准库,
f_read()
、
f_write()
、
f_lseek()
、
f_close()
……熟悉感扑面而来。
#include "ff.h"
#include "diskio.h"
FATFS fs;
FIL file;
FRESULT res;
UINT bytes_written;
res = f_mount(&fs, "", 1); // 挂载驱动器0
if (res != FR_OK) {
printf("Mount failed!\n");
return -1;
}
res = f_open(&file, "test.txt", FA_WRITE | FA_CREATE_ALWAYS);
if (res == FR_OK) {
const char *data = "Hello, FATFS over SPI SD Card!\r\n";
res = f_write(&file, data, strlen(data), &bytes_written);
if (res == FR_OK && bytes_written == strlen(data)) {
printf("Write success: %d bytes\n", bytes_written);
}
f_close(&file); // 自动刷新缓冲区
} else {
printf("Open/write failed: %d\n", res);
}
这段代码看似简单,背后却藏着不少细节。比如
f_mount()
第二个参数为空字符串,是因为我们在
ffconf.h
中已定义默认卷号;第三个参数为1表示强制立即挂载。又如
f_write()
并不会立刻写入物理介质,而是先缓存在RAM中,直到调用
f_close()
或
f_sync()
才会真正落盘。这意味着如果突然断电,未同步的数据将丢失——这是使用FATFS必须牢记的风险点。
为了适配不同的硬件平台,FATFS提供了高度可裁剪的配置机制,全部集中在
ffconf.h
中。你可以关闭长文件名支持以节省内存,禁用写功能做成只读卡,或者启用多卷管理同时挂载多个存储设备。对于资源紧张的系统,还可以选择静态分配LFN(长文件名)缓冲区而非动态申请,避免堆碎片问题。
在真实项目中,我还见过一些巧妙的设计。比如有人在SPI驱动层加入重试机制:当某次读取失败时,自动重新初始化SD卡并重试三次,极大提升了野外部署设备的稳定性。还有人在每次写操作前检测写保护开关状态,防止用户误操作导致数据覆盖。这些经验不是文档里写的,而是在一次次掉坑之后总结出来的。
说到稳定性,电源往往是被忽视的一环。SD卡在写入瞬间电流可能飙升至100mA以上,如果共用LDO且滤波不足,很可能造成电压跌落,引发卡死或初始化失败。我的建议是:至少并联一个10μF电解电容加一个0.1μF陶瓷电容,条件允许的话单独供电。另外,SPI信号线尽量短,远离高频干扰源,特别是时钟线不要绕太长,否则高速下容易出错。
还有一点值得强调:不要频繁调用
f_mount()
。虽然它看起来像是“连接U盘”的必要步骤,但实际上每次调用都会重新解析BPB(BIOS参数块),涉及多次扇区读取。对于需要频繁开关文件的应用,完全可以挂载一次后长期保持,仅在系统启动或异常恢复时重新挂载。
那么,这套技术到底适合哪些场景?
如果你要做一个数据记录仪,每隔一秒存一次传感器读数,生成CSV文件供后期分析,那简直是量身定制。如果是智能家居网关,需要保存Wi-Fi配置、用户偏好、固件升级包,同样适用。哪怕是做音乐盒,想从SD卡播放MP3,也可以先用FATFS打开文件,再把数据交给解码芯片。反过来,若应用要求持续高速写入(如视频录制),或者追求极致低功耗待机,那或许该考虑eMMC或串行Flash配合专用控制器。
展望未来,“SPI + SD + FATFS”这套组合拳不会很快退出舞台。尽管QSPI Flash容量越来越大,eMMC性能越来越强,但对于广大的中低端市场而言,SD卡依然是性价比最高的可移动存储方案。更重要的是,它的生态极其成熟:量产工具丰富、PC兼容性好、用户教育成本低。哪怕将来新型非易失性存储器普及,FAT文件系统的地位短期内也难以撼动。
归根结底,这项技术的魅力在于“化繁为简”。它没有复杂的协议栈,不需要庞大的内存支持,却能让最基础的单片机具备完整的文件管理能力。当你第一次看到自己写的嵌入式程序生成的
log_20250405.txt
在电脑上顺利打开时,那种成就感,正是每一个工程师热爱硬件的理由。

1万+


被折叠的 条评论
为什么被折叠?



