简介:基于51单片机的SD卡读卡器仿真设计资料,面向单片机初学者、电子类课程设计与毕业设计人群,用来解决SD卡SPI通信协议理解与仿真验证问题。压缩包共19个文件,大小约203KB,主要包含Proteus仿真原理图(DSN/DBK)、Keil工程源码(C语言源文件、STARTUP启动代码、UV2工程文件)、编译生成的hex/obj/lst中间文件以及MMC卡镜像文件,可直接打开仿真环境查看运行效果,并对应源码逐行学习。内容覆盖SD卡初始化、GO_IDLE_STATE与SEND_IF_COND识别配置、CMD16设置块长度、CMD24写入与CMD17读取数据块、应答码错误处理及CRC校验等完整流程,能够帮助读者从硬件连接和软件驱动两个层面掌握51单片机与SD卡的交互机制,并了解仿真调试和程序烧录的整体思路。目前已有215人浏览学习,适合需要快速搭建读卡器仿真项目、开展课程设计或二次开发的人员参考使用。
1. 51单片机SD卡读卡器,为什么仿真先行
做SD卡读卡器最大的坑不在C语言,而在时序和电气连接。这个基于51单片机SD卡读卡器仿真设计,把整条链路放进Proteus:51单片机用SPI驱动SD卡模型,读CID、按扇区读写,LCD1602显示,附带的源程序直接编译进仿真。
仿真先行能把主控逻辑和硬件问题分开排查。51单片机课程设计里,SD卡出错大多在初始化顺序和响应判断,而不是C语言本身。Proteus里波形、电平、响应字节都能直接观测,这是实物调试给不了的可视性。
适合两类人:做单片机课程设计的学生,要能答辩、能现场演示的完整方案;想吃透SPI外设驱动的开发者,把SD卡当标准SPI从设备拆解,后续换STM32或移植FatFs时,只需要替换底层读写两个函数。
2. Proteus仿真51单片机SD卡读卡器硬件连接与引脚分配
2.1 SD卡SPI模式引脚定义与51单片机接线
先说选型。常见做法是用AT89C51配合Proteus元件库里的SD/MMC卡模型。51单片机没有硬件SPI外设,SD卡通信全部用软件模拟,四个引脚可以任选IO口,这里我习惯用P1口的低四位,原因是P1口内部带上拉,复位后默认高电平,贴近SPI模式下SD卡空闲电平的要求。
Proteus仿真51单片机搭建读卡器时,连线按下面这张表:
| SD卡引脚 | SPI功能 | 51单片机引脚 | 说明 |
|---|---|---|---|
| CS/DAT3 | 片选 | P1.0 | 低电平选中,空闲拉高 |
| CLK/SCLK | 时钟 | P1.1 | 空闲低电平,上升沿采样 |
| DI/CMD | MOSI | P1.2 | 主到从数据 |
| DO/DAT0 | MISO | P1.3 | 从到主数据 |
| VCC | 供电 | 3.3V网络 | 与单片机电源分开 |
| GND | 地 | GND | 与51共地 |
P1.0片选线上接一颗10kΩ上拉电阻到3.3V,防止单片机复位期间电平不确定误选中SD卡;P1.3的MISO线上同样建议接上拉。仿真时SD卡模型对悬空输入比较敏感,少一颗上拉最常见的表现是CMD0之后MISO一直返回0xFF,后面排查初始化问题会反复遇到。
引脚定义固定下来后,源程序开头写成这样,接线和代码一一对应:
#include <reg52.h> sbit SD_CS = P1^0; // 片选,低有效 sbit SD_SCK = P1^1; // SPI 时钟 sbit SD_MOSI = P1^2; // 主出从入 sbit SD_MISO = P1^3; // 主入从出这四行用了sbit位寻址,对应AT89C51的P1端口。换引脚时只需要改这一处,后面所有函数都引用SD_CS、SD_SCK这些符号,不用动逻辑。这个习惯在51单片机硬件设计里值得保持,外设在代码里和原理图用同一套命名,答辩时讲起来也顺。
2.2 5V单片机与3.3V SD卡的电气匹配
仿真阶段这个点容易被跳过,但它恰恰是读卡器原理图里最需要提前说明的地方。SD卡供电是3.3V,数据线电平也是3.3V;AT89C51供电5V,IO口输出高电平接近5V。把5V直接送到SD卡的DI、CLK、CS,Proteus的卡模型一般不会报错,实物上长期这样用会损伤卡。
我的处理办法是:单片机VCC保持5V,SD卡模型接独立的3.3V网络,MOSI、SCK、CS三条输出线上各串一颗1kΩ限流电阻。仿真时模型只认逻辑电平,串阻不影响通信,但这么画出来的原理图可以直接迁移到实物——实物上把这组电阻替换成电阻分压或电平转换芯片即可。MISO方向不用处理,SD卡输出的3.3V高于AT89C51高电平阈值2.0V,能被正确识别。
另外在SD卡VCC和GND之间放一颗100nF去耦电容,贴紧卡电源脚。仿真时这颗电容不是必需品,但原理图里带入它,后面画PCB时不会漏,这是我从SD卡原理图类资料里总结的通用习惯。
2.3 Proteus元件清单与连接检查顺序
搭建仿真图用到的都是Proteus自带库元件,不需要额外下载模型,清单如下:
| 元件 | 型号/数值 | 数量 | 用途 |
|---|---|---|---|
| 单片机 | AT89C51 | 1 | 主控 |
| 晶振 | 11.0592MHz | 1 | 主时钟 |
| 电容 | 33pF | 2 | 晶振负载 |
| 电容 | 10µF、100nF | 各1 | 复位、去耦 |
| 电阻 | 10kΩ | 4 | 复位上拉、SD卡上拉 |
| 电阻 | 1kΩ | 3 | 数据线串阻 |
| SD卡 | SD/MMC Card | 1 | 从设备 |
| 液晶 | LCD1602 | 1 | 状态显示 |
| 按键 | BUTTON | 2 | 复位、触发读卡 |
布线检查固定按三个顺序:先看电源和地是否共地,再看四根SPI线有没有和LED、按键复用引脚,最后双击SD卡模型检查属性页里的容量设置。Proteus的卡模型容量如果偏小,后面读到高扇区时数据全FF,先回来改这里。LCD1602接8位数据接口,和源程序保持一致。
3. SD卡SPI模式协议与C51源程序初始化实现
3.1 SD卡命令格式与SPI时序
SPI模式下SD卡命令包固定6字节:第一字节是0x40或运算命令号,第二到第五字节是32位参数,第六字节是CRC7校验值。SPI模式下除CMD0和CMD8外,命令CRC不校验,填0xFF即可。响应方面,初始化阶段只用R1响应,一个字节,最高位为0表示命令被接受,bit0为1表示当前处于Idle状态。
表:初始化与读写涉及的关键命令
| 命令 | 参数 | 作用 | 期望R1 |
|---|---|---|---|
| CMD0 | 0x00000000 | 切入SPI模式 | 0x01 |
| CMD8 | 0x000001AA | 探测V2.0卡 | 0x01 |
| CMD10 | 0x00000000 | 读CID寄存器 | 0x00 |
| CMD55 | 0x00000000 | 标志下条为应用命令 | 0x01 |
| ACMD41 | 0x40000000 | 初始化卡并等待就绪 | 0x00 |
| CMD1 | 0x00000000 | V1.0老卡初始化 | 0x00 |
| CMD17 | 扇区号 | 读单扇区 | 0x00 |
| CMD24 | 扇区号 | 写单扇区 | 0x00 |
SPI时钟配置为模式0:空闲低电平、上升沿发数据、下降沿采样。软件模拟时的动作顺序就是:拉高SCK之前放好MOSI,拉低SCK之前读走MISO,下面的代码严格按这个顺序,不要颠倒。
3.2 最小可运行的SPI读写与初始化代码
底层字节收发用一个函数同时完成读和写,整个源程序都依赖它:
// SPI模式0字节收发,传入要发的字节,返回收到的字节 unsigned char SPI_Transfer(unsigned char dat) { unsigned char i, rx = 0; for (i = 0; i < 8; i++) { SD_MOSI = (dat & 0x80) ? 1 : 0; // 上升沿前放好输出位 dat <<= 1; SD_SCK = 1; // 上升沿:SD卡采样MOSI rx <<= 1; if (SD_MISO) rx |= 0x01; // 高电平期间读MISO SD_SCK = 0; // 下降沿:SD卡切换下一位 } return rx; }SPI_Transfer的8次循环就是8个时钟周期,MOSI先送位再拉高SCK,保证SD卡在上升沿采到稳定电平;MISO的读取放在SCK为高期间,51单片机单周期IO操作足够快,不需要额外延时。读数据时传0xFF即可,因为这个函数同时充当SPI读函数。
命令发送和初始化流程:
// 发送命令并等待R1响应,返回R1字节 unsigned char SD_Cmd(unsigned char cmd, unsigned long arg, unsigned char crc) { unsigned char i, r1; SD_CS = 0; // 低电平选中 SPI_Transfer(0xFF); // 命令前补8个时钟 SPI_Transfer(0x40 | cmd); // 命令号0~63 SPI_Transfer((arg >> 24) & 0xFF); // 参数高字节 SPI_Transfer((arg >> 16) & 0xFF); SPI_Transfer((arg >> 8) & 0xFF); SPI_Transfer(arg & 0xFF); // 参数低字节 SPI_Transfer(crc); // CRC,无关命令填0xFF for (i = 0; i < 8; i++) { // 最多等8个字节周期 r1 = SPI_Transfer(0xFF); if ((r1 & 0x80) == 0) break; // 高位为0才算有效R1 } SD_CS = 1; SPI_Transfer(0xFF); // 收尾补时钟 return r1; } unsigned char SD_Init(void) { unsigned char i, r1; for (i = 0; i < 10; i++) SPI_Transfer(0xFF); // 上电至少74个时钟 SD_CS = 1; for (i = 0; i < 10; i++) { // 反复CMD0直到Idle r1 = SD_Cmd(0, 0x00000000, 0x95); if (r1 == 0x01) break; } if (r1 != 0x01) return 1; // 错误1:未进入SPI模式 r1 = SD_Cmd(8, 0x000001AA, 0x87); // 探测V2.0支持 if (r1 == 0x01) { for (i = 0; i < 4; i++) SPI_Transfer(0xFF); // 丢弃4字节返回参数 for (i = 0; i < 100; i++) { SD_Cmd(55, 0x00000000, 0xFF); r1 = SD_Cmd(41, 0x40000000, 0xFF); // ACMD41 if (r1 == 0x00) break; // 0x00表示就绪 } } else { for (i = 0; i < 100; i++) { // 老卡走CMD1路径 r1 = SD_Cmd(1, 0x00000000, 0xFF); if (r1 == 0x00) break; } } if (r1 != 0x00) return 2; // 错误2:初始化未完成 return 0; }几个参数必须说明。CMD0的CRC是固化的0x95,CMD8的CRC是固化的0x87,这两个值改一个就切不进SPI模式。ACMD41参数0x40000000把HCS位置1,告诉卡主控支持高容量卡;SD卡模型容量小于2GB时HCS填0更稳妥。CMD55的作用是把下一条命令标记为应用命令,ACMD41只有在CMD55之后发送才有效。SD_Cmd在每条命令前后各自拉低再拉高CS,这个间隙在SD卡规范允许范围内。
提示:12MHz晶振下软件SPI位周期约1µs,接近SD卡初始化阶段400kHz的时钟上限。个别Proteus模型会初始化超时,稳妥做法是在SD_Init开头的SPI_Transfer里临时插入两个_nop_()降速,初始化完成后恢复正常。
3.3 初始化失败时看哪几个字节
仿真里最常见的三类失败可以对号入座。第一种SD_Cmd返回0xFF:读到的MISO一直是高电平,查P1.3接线、查上拉电阻是否误接在P1.0上、确认SD卡模型确实放进了原理图。第二种CMD0永远等不到0x01:检查74个时钟是否在上电后第一时间发出,单片机复位期间IO为高电平,复位时间过长会让卡错失切入窗口。第三种CMD8返回0x05,bit2置位表示命令非法,说明这是V1.0老卡,代码自动走CMD1分支,属于正常路径。
4. 读卡器核心功能:扇区读写、CID读取与FAT16文件解析
4.1 单扇区读写与CID读取代码
初始化完成后,第一件验证SPI链路的事是读CID。CID能读到说明SPI物理层和命令层都通了,再做扇区级功能才有意义:
// 读CID寄存器,结果存入16字节数组 unsigned char SD_Read_CID(unsigned char *cid) { unsigned char i, r1; SD_CS = 0; SPI_Transfer(0xFF); SPI_Transfer(0x40 | 10); // CMD10,读取CID SPI_Transfer(0x00); SPI_Transfer(0x00); SPI_Transfer(0x00); SPI_Transfer(0x00); SPI_Transfer(0xFF); for (i = 0; i < 8; i++) { // 等R1响应 r1 = SPI_Transfer(0xFF); if ((r1 & 0x80) == 0) break; } if (r1 != 0x00) { SD_CS = 1; return 1; } for (i = 0; i < 16; i++) cid[i] = SPI_Transfer(0xFF); SPI_Transfer(0xFF); SPI_Transfer(0xFF); // 2字节CRC,丢弃 SD_CS = 1; return 0; }这里读CID命令保持CS拉低期间读完16个字节,和SD_Cmd不同,因为SD_Cmd返回R1后会把CS拉高,后续数据阶段就断了。这也是很多源程序读CID失败的直接原因。
扇区读写用CMD17和CMD24,参数是扇区号左移9位,因为SD卡按字节寻址,一个扇区512字节:
unsigned char SD_Read_Sector(unsigned long sector, unsigned char *buf) { unsigned char i, r1; r1 = SD_Cmd(17, sector << 9, 0xFF); if (r1 != 0x00) return 1; // 命令被拒,地址超容量或卡忙 for (i = 0; i < 200; i++) { // 等待0xFE数据起始令牌 if (SPI_Transfer(0xFF) == 0xFE) break; } if (i >= 200) return 2; // 数据超时 for (i = 0; i < 512; i++) buf[i] = SPI_Transfer(0xFF); SPI_Transfer(0xFF); // 2字节CRC,SPI模式不校验 SPI_Transfer(0xFF); return 0; } unsigned char SD_Write_Sector(unsigned long sector, unsigned char *buf) { unsigned char i, r1; r1 = SD_Cmd(24, sector << 9, 0xFF); if (r1 != 0x00) return 1; SPI_Transfer(0xFE); // 数据起始令牌 for (i = 0; i < 512; i++) SPI_Transfer(buf[i]); SPI_Transfer(0xFF); // CRC可任意填 SPI_Transfer(0xFF); r1 = SPI_Transfer(0xFF); // 写应答 if ((r1 & 0x05) != 0x05) return 2; // 应答格式无效 for (i = 0; i < 65535; i++) { // 等忙信号结束 if (SPI_Transfer(0xFF) == 0xFF) break; } if (i >= 65535) return 3; return 0; }写应答的低三位格式是xxx0,0x05表示数据被卡接受;随后反复发送0xFF,读到0x00表示卡还在写内部Flash,读到0xFF表示写完成。仿真里常见的诡异现象是写后读回全FF,优先查0xFE令牌是否在CS拉低期间发出、写之前是否执行过SD_Init。
4.2 FAT16文件读取:从BPB到簇链
只做扇区读写还不算完整读卡器,我一般会在源程序里加一段FAT16文件读取,演示时直接把卡里的文本文件内容显示到LCD。FAT16布局是引导扇区、FAT表、根目录区、数据区四段,先读第0扇区解析BPB:
| BPB偏移 | 长度 | 含义 | 用途 |
|---|---|---|---|
| 0x0B | 2 | 每扇区字节数 | 固定512 |
| 0x0D | 1 | 每簇扇区数 | 换算数据位置 |
| 0x0E | 2 | 保留扇区数 | 定位FAT起始 |
| 0x10 | 1 | FAT表数量 | 通常为2 |
| 0x11 | 2 | 根目录条目数 | 计算根目录扇区数 |
| 0x16 | 2 | FAT表大小 | 定位根目录 |
根目录起始扇区=保留扇区数+FAT表数量×FAT表大小;数据区起始扇区=根目录起始扇区+根目录条目数×32/512。目录项32字节,文件名在偏移0,首簇号低16位在偏移26,文件大小在偏移28。找到文件名后按FAT簇链往下读:
// 读取文件下一簇号,fat_start是FAT表起始扇区 unsigned int FAT16_Next_Cluster(unsigned int cluster, unsigned long fat_start, unsigned char *fat_buf) { unsigned long fat_sector = fat_start + (cluster * 2) / 512; unsigned int offset = (cluster * 2) % 512; SD_Read_Sector(fat_sector, fat_buf); return fat_buf[offset] | ((unsigned int)fat_buf[offset + 1] << 8); }FAT16的表项宽度是2字节,簇号乘2得到表内偏移,表项值就是下一簇号,读到0xFFFF表示文件结束。每簇对应的数据扇区=数据区起始扇区+(簇号-2)×每簇扇区数,用SD_Read_Sector搬数据循环即可。
注意:FAT16的簇号从2开始计,0和1是保留值,换算数据扇区时一定要做簇号-2的偏移,漏掉这个会整体错一个簇,读出的文件开头是乱的。
4.3 LCD1602显示与读卡流程整合
显示层用LCD1602的8位接口,初始化写0x38、0x0C、0x06三条命令即可。我把读卡演示做成三步按键触发:第一步显示SD Init结果,第二步显示CID前8字节的十六进制,第三步读指定扇区前16字节或读FAT16文件内容。每一步都有屏幕反馈,答辩时能顺着状态讲清楚当前执行到哪一层。
51单片机没有格式化输出的硬件支持,不引入sprintf,字节转十六进制用查表:
unsigned char hex2char(unsigned char x) { return (x < 10) ? ('0' + x) : ('A' + x - 10); }16字节一行、每字节两个字符正好排满LCD的第一行,第二行放文件名或扇区号。整个源程序的顶层就是按键扫描加状态机,初始化失败时把错误码显示出来,对应第3章3.3节的判断表排查。
5. 仿真调试必查点与实物移植的3个关键改动
5.1 用虚拟示波器确认SPI时序
Proteus的虚拟示波器直接拖到P1.1和P1.3两个引脚,触发选下降沿。正常波形是SCK空闲低电平、每8个脉冲一组,组间有明显间隔;MISO在CMD0阶段能看到0x01,ACMD41完成后能看到0x00。如果SCK波形不完整,或MOSI与SCK边沿对不齐,回第3章检查SPI_Transfer的置位顺序。
5.2 仿真现象与处理对照
| 现象 | 判断 | 处理 |
|---|---|---|
| 初始化返回1 | 未进入SPI模式 | 查CMD0的CRC、上电74时钟 |
| 初始化返回2 | 初始化超时 | 查CMD55、HCS位、时钟速度 |
| 读扇区返回1 | 命令被拒 | 查扇区号是否超出模型容量 |
| 写后读回全FF | 写入未生效 | 查0xFE令牌和CS时序 |
5.3 从仿真到实物的3个改动
仿真跑通后转实物,三处必改。第一,电平转换:把原理图里的1kΩ串阻换成真正的电平匹配方案,MOSI、SCK、CS三条输出线用电阻分压或74LVC245,MISO方向保持直连,SD卡输出的3.3V高于51单片机高电平阈值2.0V,能被直接识别。第二,卡座供电:实物卡座VCC脚必须就近放100nF和10µF两颗电容,仿真里没有的电源噪声在实物上表现为初始化时好时坏。第三,初始化降速:实物SD卡对初始化阶段400kHz时钟上限更敏感,在SD_Init开头的SPI_Transfer里加nop延时,初始化完成后再恢复正常速度,只影响几十个时钟。
收尾验证用一个动作:把SD卡格式化成FAT16,放入固定内容的短文本文件,源程序锁定目标文件名,LCD能显示文件内容和长度,说明SPI物理层、命令层、文件系统层整条链路全部打通。这时候再往FatFs迁移或改STM32版本,都只需要替换底层读写两个函数。
本文还有配套的精品资源,点击获取