
简介面向单片机开发者和嵌入式初学者的51单片机SD卡读写源码工程以振南电子znFAT范例为基础完整演示8051读写SD卡并访问FAT32/FAT16文件系统的思路适合快速入门与二次开发。工程基于Keil开发亲测编译通过运行后通过串口输出SD卡中的文件系统信息内容涵盖底层扇区读取、目录项解析、FAT表操作等关键流程便于理解文件系统在MCU上的实际运作。压缩包共42个文件以C源码、头文件、汇编源文件为主同时包含Keil工程配置、hex烧录文件、例程说明文档以及运行效果截图源码中主控逻辑、SD卡底层驱动、文件系统解析和串口打印分层较为清晰整体仅175KB结构精简适合快速导入与移植。已有1761人学习下载可直接作为51系列单片机读写SD卡、文件系统移植、嵌入式存储开发的参考模板也可用于课程设计、毕业设计或项目预研。1. 项目概述与方案思路做了这么多年单片机我始终觉得“数据存储”是一个绕不开的坎。早年间做温湿度记录仪数据只能存在EEPROM里容量小不说导出的时候还得靠上位机一点点读出来稍微复杂点的应用就非常吃力。后来接触到SD卡方案再配合FAT32文件系统写出来的数据直接插读卡器就能在电脑里打开体验完全不一样。这个项目就是一套完整的、精简到可以在51单片机上跑的SD卡读写FAT32源码适合做数据记录仪、简易U盘、离线采集器这类产品。项目核心要解决三件事第一51单片机怎么通过SPI接口读写SD卡第二FAT32文件系统的关键结构在8位单片机上如何解析第三怎么在KB级内存的约束下把文件读写跑起来。整套源码拆成了驱动层、文件系统层、应用层三个部分驱动层完全依赖IO口模拟SPI不挑单片机型号文件系统层只保留FAT32最核心的读文件、写文件、创建文件、删除文件功能等于把PC上庞大复杂的文件系统裁剪成一棵只结必要果子的树。为什么非要用FAT32而不是裸读写这件事我踩过很深的坑。早期项目图省事直接把采集数据按自定义格式往SD卡扇区里扔结果数据导出必须配套专用的上位机解析软件用户拿SD卡插电脑打开一看全是乱码根本没法用。换用FAT32之后生成的就是标准CSV或者TXT文件电脑上直接打开、直接用Excel处理对用户零门槛。至于FAT16虽然结构更简单但单个分区最大只能到2GB现在市面上很多新卡出厂格式化要么是FAT32要么是exFATWince时代那种小容量FAT16卡反而不容易買到了所以直接上FAT32更稳。很多人一听到51单片机跑FAT32就觉得不可思议觉得这东西不是应该跑在ARM上吗。实际上FAT32比FAT16只复杂在目录项的簇号是32位多了一些字段解析算法核心并不复杂。真正的问题是内存所以整个文件系统层在设计时严格控制全局变量和缓冲区占用配合512字节扇区缓冲复用实际运行只占用约1KB的RAMSTC15系列、STC8系列2K以上RAM的单片机都能跑得动。2. 硬件连接与底层驱动设计2.1 硬件接线与电平匹配SD卡在SPI模式下只需要6根线就能工作VCC、GND、CLK、MOSI、MISO、CS。51单片机是5V逻辑SD卡是3.3V逻辑直接连接会有电平不匹配的风险虽然很多老玩家直接串联一个电阻也能用但正规做法是加电平转换或分压电阻。我常用的方案是MOSI和CLK各串联一个100欧到200欧的电阻CS同样串联MISO线因为是卡输出到单片机输入可以直连因为很多51单片机的IO在输入模式下是高阻态但是如果你用的是准双向IO的老传统51还是建议加一个10K上拉到3.3V保险一点。供电问题值得单独说。SD卡正常工作电流在几十毫安到上百毫安启动瞬间电流更大。如果用AMS1117-3.3从5V转出来注意输入电容和输出电容都要加而且尽量靠近卡的电源引脚避免电机、继电器这类大功率器件跟卡共用电源。曾经有过惨痛教训继电器吸合瞬间把SD卡供电拉低导致写入数据错乱后来把卡供电和继电器驱动分开供电才解决。2.2 模拟SPI时序要点STC系列单片机的硬件SPI虽然可以用但考虑到代码复用性我还是选择了IO口模拟SPI这样换任何一款51单片机都不需要改驱动。SPI模式0是SD卡SPI通信的标准配置CPOL为0CPHA为0也就是空闲时CLK低电平数据在CLK上升沿采样下降沿输出。模拟SPI的代码核心就是一个字节的收发函数unsigned char SPI_RW_Byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { if (dat 0x80) MOSI 1; else MOSI 0; dat 1; CLK 1; // 上升沿主机发送数据从机采样 if (MISO) dat | 0x01; CLK 0; // 下降沿准备下一bit } return dat; }这里最关键的一点是速度控制。51单片机跑12MHz晶振时一条IO翻转指令大约1个微秒模拟出来的SPI时钟大约在几百KHz级别这个速度对SD卡来说非常安全。很多初学者上来就喜欢超频把SPI时钟拉到几MHz结果SD卡初始化老失败其实SD卡SPI模式有一个明确要求初始化阶段时钟不能超过400KHz初始化完成之后才能提高速度。这也是为什么很多成熟代码里初始化时故意延时读写时才能全速跑的原因。2.3 SD卡初始化流程拆解SD卡上电后的初始化是整个项目中最容易翻车的地方流程严格且必须按顺序来第一步先把CS拉高然后给CLK发送至少74个空脉冲0xFF让SD卡内部完成上电稳定过程。 第二步CS拉低发送CMD00x40, 0x00, 0x00, 0x00, 0x00, 0x95带正确CRC校验等待SD卡返回0x01进入SPI模式。 第三步发送CMD80x48, 0x00, 0x00, 0x01, 0xAA, 0x87区分SD V1.x和SD V2.0以上卡如果返回0x01且后面4个字节是0x00 0x00 0x01 0xAA说明是2.0以上卡。 第四步反复发送CMD55ACMD41组合0x77, 0x69直到返回0x00表示卡初始化完成。 第五步发送CMD58读OCR寄存器确认卡的电压范围。这套流程我一开始也是从各种资料里拼凑出来的后来发现关键点是超时处理。如果ACMD41发了200次还没返回0不能死等要重新复位SD卡再来一遍。在实际调试时用示波器观察CLK和CS的时序关系能够快速定位是时序问题还是电压问题。我曾经遇到过一种情况卡在其他板子上跑得好好的换到新做的板子上死活初始化不了最后发现是PCB走线太长导致信号畸变把SPI时钟频率降低一半就好了。3. FAT32文件系统核心结构解析3.1 DBR扇区与BPB参数FAT32的起点是DBR扇区也就是整个分区的第0扇区。SD卡出厂时不同类型的卡分区情况不一样很多卡本身自带了MBR主引导记录真正的FAT32分区从第2048个扇区开始。所以代码里做了一个判断先读0号扇区如果偏移510处的标志是0x55AA且偏移0x1BE处第一个分区表项的类型字段是0x0B或0x0C就把分区起始扇区读出来加上这个偏移再做FAT32解析。如果是直接用FAT32格式化的卡0号扇区本身就是DBR不需要偏移代码里做了兼容处理。从DBR里需要提取的关键信息包括偏移地址长度含义示例值0x0B2字节每扇区字节数5120x0D1字节每簇扇区数8即4KB一簇0x0E2字节保留扇区数320x101字节FAT表数量20x244字节每FAT表占用扇区数40960x2C4字节根目录首簇号2这些参数全部要解析出来保存到全局结构体里后续所有文件操作都依赖这些基础值。簇号转扇区号的计算公式是数据区起始扇区 保留扇区数 FAT表数量乘每个FAT表扇区数 根目录簇占用的扇区数通常是0因为根目录也按簇管理了然后簇对应的扇区 (簇号 - 2)乘每簇扇区数 数据区起始扇区。这个公式是整个文件系统层最核心的计算逻辑。3.2 目录项与FAT表的工作机制FAT32的目录项每个占32字节短文件名格式是8.3规则。文件名11个字节前8个是主名后3个是扩展名全部大写。比如data.txt在目录项里存储为“DATA TXT”中间用空格补齐。属性字节在偏移0x0B处0x01表示只读0x02表示隐藏0x10表示子目录0x20表示归档文件。文件首簇号的高16位在偏移0x14处低16位在0x1A处合起来组成32位簇号这是FAT32和FAT16最大的区别。FAT表本质上是一张簇号映射表每个表项占4字节存储的是下一个簇的簇号。如果表项值是0x0FFFFFFF说明这是文件的最后一个簇0x00000000表示空簇0x0FFFFFF7表示坏簇。读取文件时先从目录项拿到首簇号然后根据这个簇号去FAT表对应位置查找下一个簇号一直循环到结束标志。写入文件时反向操作找到空簇后把数据写进去再更新前一个簇对应的FAT表项指向新簇号。这里有一个关键细节需要特别提醒FAT表是文件系统最核心的元数据所以FAT32会有两份FAT表主表和备份表。写文件时修改FAT表项保险做法是主表和备份表都更新一遍否则一旦主表损坏会出现数据能读出来但文件系统报错的情况。在51这种存储可靠性要求高的场景多写一份备份绝对值得。3.3 51内存约束下的裁剪策略标准FAT32文件系统库代码量通常几千行占用的RAM动不动几十KB这在51上是不可能直接跑的。我采取的策略简单粗暴砍掉长文件名支持、砍掉子目录遍历、砍掉磁盘格式化功能、砍掉多文件同时打开只保留单文件打开做到极致精简。长文件名支持为什么砍掉因为它依赖Unicode转换和大量的临时缓冲区还要在目录项里处理0x0F类型的附属目录项对8位单片机来说负担太重。实际项目里不管是记录日志还是存储采集数据8.3短文件名完全够用你自己命名文件时注意一下规则就好。缓冲区复用也是一个关键技巧。整个源码只定义一个512字节的全局数组作为扇区缓冲区这个缓冲区既是读扇区的暂存区也是写数据的源缓冲区还是解析目录项的工作区。文件打开后需要用到的文件信息文件首簇号、当前簇号、当前扇区内偏移等单独用一个结构体存储不跟缓冲区混用这样既节省了RAM也避免了缓冲区被意外覆盖导致的各种诡异bug。4. 源码实现与核心流程4.1 源码模块划分与结构体定义整套源码我拆成了三个文件sd_card.c/h负责底层SD卡命令收发fat32.c/h负责文件系统解析main.c放用户调用示例。SD卡驱动不需要多讲重点看FAT32层的数据结构typedef struct { unsigned char SectorsPerCluster; unsigned short BytesPerSector; unsigned short ReservedSectorCount; unsigned char NumFATs; unsigned long FatSize; // 每个FAT表占用的扇区数 unsigned long RootDirFirstCluster; // 根目录首簇 unsigned long FirstDataSector; // 数据区起始扇区 } FAT32_Info; typedef struct { unsigned long FileFirstCluster; unsigned long FileCurrentCluster; unsigned long FileCurrentSector; unsigned long FileSize; unsigned long FileOffset; // 当前读写位置距文件头偏移 unsigned char FileOpenFlag; } FILE_HANDLE;FAT32_Info在初始化SD卡时加载一次所有扇区偏移计算都基于这个结构体的数据。FILE_HANDLE是文件句柄每次打开文件时填充信息读写时基于当前簇和偏移做操作。由于51的RAM有限这个结构体设计得尽量紧凑每个成员都是4字节对齐的long或者unsigned char避免用结构体嵌套导致的额外内存开销。4.2 读取文件的完整实现读取一个文件比如根目录下的“DATA.TXT”整个流程分为三步第一步从根目录区开始搜索文件名。根目录首簇号从DBR的0x2C处读取通常是2。根据簇号转扇区公式找到根目录所在扇区逐扇区读取32字节的目录项比较名字字段是否跟目标文件名匹配。如果匹配且属性位不是0x0F说明找到了目标文件的目录项提取文件大小、首簇号填充FILE_HANDLE结构体。第二步根据FAT表找到文件数据所在的簇链。核心函数是GetNextClusterunsigned long GetNextCluster(unsigned long cluster) { unsigned long fatOffset cluster * 4; // 每个FAT表项占4字节 unsigned long fatSector BPB_ResvdSecCnt fatOffset / 512; unsigned long fatEntryOffset fatOffset % 512; SD_ReadSector(fatSector, sectorBuffer); return *(unsigned long *)sectorBuffer[fatEntryOffset] 0x0FFFFFFF; }注意这里有个陷阱读到的FAT表项值是4字节但高4位是保留位必须用0x0FFFFFFF做掩码去掉。很多移植代码在这里疏忽导致簇号计算错误文件读到一半数据就不对了。第三步读取数据。每读取一个簇的数据就用GetNextCluster找到下一个簇号直到遇到0x0FFFFFFF结束标志。整个循环就是顺着FAT链依次读取逻辑非常简单。4.3 新建文件与写入数据流程写文件比读文件复杂在需要管理空闲簇和更新FAT表。unsigned char FAT32_WriteFile(FILE_HANDLE *file, unsigned char *data, unsigned int len) { unsigned long needCluster len / (BPB_BytsPerSec * BPB_SecPerClus) 1; unsigned long newCluster FindFreeCluster(); // 在FAT表里找空闲簇 // 把前一个簇的FAT表项指向新簇 SetFATEntry(file-FileCurrentCluster, newCluster); // 新簇的FAT表项写结束标志 SetFATEntry(newCluster, 0x0FFFFFFF); // 写入数据到新簇对应的扇区 WriteClusterData(newCluster, data); // 更新目录项中的文件大小和首簇号 UpdateDirEntry(file); }这里有几个细节值得展开。找空闲簇是从FAT表第2个表项开始扫描遇到值为0的表项就是空簇。找到之后要把主FAT表和备份FAT表同时更新做好双写保护。写完数据后文件的目录项一定要用新数据覆盖写回否则文件大小还是0。在51上写一个簇的数据有一个内存问题一个簇可能是4KB甚至32KB而缓冲区只有512字节。解决办法是分多次写入先写第一扇区并把剩余扇区内的数据填充为0再写后续扇区保证文件系统不会因为读到旧数据而错乱。4.4 写扇区与擦写均衡的注意点SD卡写扇区时速度明显比读慢而且频繁写同一个簇会导致Flash磨损不均。虽然SD卡内部有磨损均衡算法但应用层稍微注意一下能显著延长卡的寿命。我的做法是如果数据是周期性采集就按小时生成一个新文件避免反复覆写同一个文件同时每次写入前后都检查返回值写失败就做一次卡重新初始化再重试最大程度避免花屏文件的情况。写单个扇区的代码需要注意启动令牌和忙等待void SD_WriteSector(unsigned long sector, unsigned char *buf) { unsigned char resp; SD_SendCmd(0x58, sector, 0xFF); // CMD24 SD_ReadByte(0xFF); // 丢弃CRC SD_ReadByte(0xFF); SPI_RW_Byte(0xFE); // 写启动令牌 for (i 0; i 512; i) SPI_RW_Byte(buf[i]); SPI_RW_Byte(0xFF); SPI_RW_Byte(0xFF); // 2字节CRCSPI模式下可任意 resp SPI_RW_Byte(0xFF); // 等待数据响应低5位为0x05表示成功 while (SPI_RW_Byte(0xFF) ! 0xFF){} // 等待卡内部擦写完成 }等待忙的这个while循环在实际调试中经常出问题因为不同卡的擦写时间差别很大老卡可能要好几十毫秒。这里建议加超时退出避免极端情况下卡死在循环里。我一般设置100ms超时超过就返回错误码让上层决定是重试还是放弃。5. 常见问题与排查技巧5.1 SD卡初始化失败这是大家反馈最多的问题大概占所有问题的六成。常见原因有几种上电时序不对没有先给74个时钟周期就发CMD0SPI速率过高初始化阶段超过400KHz供电电压不稳尤其用了劣质LDOSD卡和单片机之间的电平转换电路设计不当。我的排查建议是按照这个顺序来先确认VCC电压在3.3V左右且纹波小于100mV再用示波器看CLK波形是否干净、有没有过冲最后检查CMD0发出去之后有没有响应字节返回。没有响应的话很大概率是卡还在SD模式看看是不是CMD0的CRC校验字节写错了。SPI模式下CMD0必须带正确的CRC而进入SPI模式之后大部分命令的CRC可以随意填充这是很多人忽略的细节。5.2 能读扇区但是读不到文件这类问题通常是MBR和DBR混淆造成的。我用代码做了兼容先判断0号扇区是不是DBR如果是就往里找“FAT32”字符串找不到就当作MBR处理读取偏移0x1BE处的第一个分区表项来定位FAT32分区的起始扇区。另外有一些卡出厂带特殊分区格式第一个分区不是FAT32这时候需要遍历四个分区表项找到类型是0x0B或0x0C的分区。还有一种比较容易忽略的情况SD卡容量显示正常但读取DBR里的BPB参数完全不合理比如每扇区字节数不是512。可以用WinHex在电脑上打开SD卡镜像检查DBR是否正常这也是快速定位问题的有效方法。5.3 文件写入后电脑打不开大概率是FAT表更新不完整或者目录项的大小字段没更新。我做过调试实验单独写数据但不更新目录项大小文件管理软件能看到文件但打开提示数据损坏。解决办法就是前面强调的写完数据后必须回读目录项所在扇区修改大小、首簇号和时间戳再写回去。另外分区里如果有大量文件碎片FAT表项连接散乱51单片机这种小内存设备计算簇链很容易出错。我自己的经验是记录类应用定期格式化SD卡保证FAT表干净比在代码层面做复杂的碎片整理要省事得多尤其在生产环境下格外重要。5.4 内存不够用编译时报data空间溢出这是51移植FAT32最让人头疼的问题。有几个实用招数把main函数里的大数组改成static避免在栈上分配51编译器默认变量放data区如果单片机支持xdata把缓冲区定义成xdata类型可以把512字节大数组挪到外部RAM优化编译选项把code区优化级别调高减少函数导致的栈开销。像STC15W408AS这种带1KB SRAM的单片机data区够放常规变量缓冲区放xdata整体跑下来还有余量。做一个数据记录仪的时候我只用了STC15W204S这颗只有512字节RAM的芯片。为了把FAT32跑进去我甚至把每簇扇区数改成1也就是一簇512字节这样查找FAT表项时避免了跨扇区读取的复杂逻辑缓冲区也只用了256字节。虽然文件碎片化会严重一些但小文件写入场景完全够用。6. 实测效果与后续扩展方向用这块代码做了一个温湿度记录仪硬件是用STC15W408AS外接一个2.4英寸TFT屏和一张8GB的TF卡。实测效果是每秒采集一次温度数据每5分钟写一行记录到CSV文件连续跑了三天文件大小约8MB期间没有出现写失败和死机的情况。最让我满意的是部署现场维护变得异常简单直接把卡拔下来插到电脑上复制数据同事不需要学任何工具软件。如果想在这个基础上有更多玩法有几个扩展方向我觉得非常实用。第一就是往源码里加一个文件索引表通过内存里的映射表快速定位文件偏移大文件读取性能提升明显。第二是加一个掉电保护逻辑在写FAT表之前先做一个标记上电后发现标记没完成就回滚到上一个有效状态防止写一半断电导致整个文件系统损坏。第三是把这套FAT32源码移植到STM8或者新唐M051上只需要改SPI驱动和延时函数文件系统层几乎不用动因为接口都是标准的扇区读写函数。做这套源码最大的感受是文件系统这东西在PC上习以为常在单片机上却需要非常务实的设计取舍。割舍长文件名和子目录支持时我也犹豫过但看到产品在客户手里稳定运行了几个月才明白嵌入式开发的本质就是在资源和需求之间找到最好的平衡点。如果你想在自己项目里跑文件系统建议先通读SD卡规范和FAT32白皮书然后在开发板上把初始化流程跑通最后再逐步移植代码这样出问题时不至于一头雾水。希望这套源码能帮你少走一些弯路。本文还有配套的精品资源点击获取