很多单片机系统都需要大容量存储设备,以存储数据。目前常用的有 U 盘,FLASH 芯片,
SD 卡等。他们各有优点,综合比较,最适合单片机系统的莫过于 SD 卡了,它不仅容量可以做
到很大(32Gb 以上),而且支持 SPI 接口,方便移动,并且有几种体积的尺寸可供选择(标准
的 SD 卡尺寸,以及 TF 卡尺寸等),能满足不同应用的要求。
只需要 4 个 IO 口即可外扩一个最大达 32GB 以上的外部存储器,容量从几十 M 到几十 G选择尺度很大,更换也很方便,编程也简单,是单片机大容量外部存储器的首选。
SD 卡接口(在背面),可使用 STM32 自带的SPI 接口驱动,本章我们使用 SPI 驱动,最高通信速度可达 18Mbps,每秒可传输数据 2M 字节以上,对于一般应用足够了。
SD 卡
SD 卡(Secure Digital Memory Card)中文翻译为安全数码卡,它是在 MMC 的基础上发
展而来,是一种基于半导体快闪记忆器的新一代记忆设备,它被广泛地于便携式装置上使用,
例如数码相机、个人数码助理(PDA)和多媒体播放器等。SD 卡由日本松下、东芝及美国 SanDisk公司于 1999 年 8 月共同开发研制。大小犹如一张邮票的 SD 记忆卡,重量只有 2 克,但却拥有高记忆容量、快速数据传输率、极大的移动灵活性以及很好的安全性。按容量分类,可以将SD 卡分为 3 类:SD 卡、SDHC 卡、SDXC 卡。
SD 卡和 SDHC 卡协议基本兼容,但是 SDXC 卡,同这两者区别就比较大了,本章我们讨
论的主要是 SD/SDHC 卡(简称 SD 卡)。
SD 卡一般支持 2 种操作模式:
1,SD 卡模式(通过 SDIO 通信);
2,SPI 模式;
主机可以选择以上任意一种模式同 SD 卡通信,SD 卡模式允许 4 线的高速数据传输。SPI
模式允许简单的通过 SPI 接口来和 SD 卡通信,这种模式同 SD 卡模式相比就是丧失了速度。
SD 卡的引脚排序如下图 所示:
SD 卡引脚功能描述:
SD 卡只能使用 3.3V 的 IO 电平,所以,MCU 一定要能够支持 3.3V 的 IO 端口输出。注意:
在 SPI 模式下,CS/MOSI/MISO/CLK 都需要加 10~100K 左右的上拉电阻。
以下是根据您提供的信息生成的表格:
名称 | 宽度(位) | 描述 |
---|---|---|
CID | 128 | 卡标识寄存器:存储卡的唯一标识信息,包括制造商信息、产品名称、序列号等。 |
RCA | 16 | 相对卡地址寄存器:用于在本地系统中标识卡的地址。该地址是动态变化的,在主机初始化时确定。 注意:在SPI模式下,RCA可能不被使用或以不同方式处理。 |
CSD | 128 | 卡描述数据寄存器:包含与卡的操作条件相关的信息数据,如数据速率、容量、支持的命令集等。 |
SCR | 64 | SD配置寄存器:存储SD卡特定的配置信息,如支持的SD协议版本、支持的命令集等。 |
OCR | 32 | 操作条件寄存器:包含卡的当前操作条件信息,如电源状态、时钟频率、总线宽度等。 |
SD 卡的命令格式
SD 卡的指令由 6 个字节组成,字节 1 的最高 2 位固定为 01,低 6 位为命令号(比如 CMD16,为 10000B 即 16 进制的 0X10,完整的 CMD16,第一个字节为 01010000,即 0X10+0X40)。
字节 2~5 为命令参数,有些命令是没有参数的。
字节 6 的高七位为 CRC 值,最低位恒定为 1。
SD 卡的命令总共有 12 类,分为 Class0~Class11,本章,我们仅介绍几个比较重要的命令
大部分的命令是初始化的时候用的。表中的 R1、R3 和 R7 等是 SD 卡的回应,SD
卡和单片机的通信采用发送应答机制,如图
每发送一个命令,SD 卡都会给出一个应答,以告知主机该命令的执行情况,或者返回主
机需要获取的数据。SPI 模式下,SD 卡针对不同的命令,应答可以是 R1~R7,R1 的应答,各位描述如表
R2~R7 的响应,我们就不介绍了,请的大家参考 SD 卡 2.0 协议。接下来,我们看看 SD 卡
初始化过程。因为我们使用的是 SPI 模式,所以先得让 SD 卡进入 SPI 模式。方法如下:在 SD卡收到复位命令(CMD0)时,CS 为有效电平(低电平)则 SPI 模式被启用。不过在发送 CMD0之前,要发送>74 个时钟,这是因为 SD 卡内部有个供电电压上升时间,大概为 64 个 CLK,剩下的 10 个 CLK 用于 SD 卡同步,之后才能开始 CMD0 的操作,在卡初始化的时候,CLK 时钟最大不能超过 400Khz!。
接着我们看看 SD 卡的初始化,SD 卡的典型初始化过程如下:
1、初始化与 SD 卡连接的硬件条件(MCU 的 SPI 配置,IO 口配置);
2、上电延时(>74 个 CLK);
3、复位卡(CMD0),进入 IDLE 状态;
4、发送 CMD8,检查是否支持 2.0 协议;
5、根据不同协议检查 SD 卡(命令包括:CMD55、CMD41、CMD58 和 CMD1 等);
6、取消片选,发多 8 个 CLK,结束初始化
这样我们就完成了对 SD 卡的初始化,注意末尾发送的 8 个 CLK 是提供 SD 卡额外的时钟,
完成某些操作。通过 SD 卡初始化,我们可以知道 SD 卡的类型(V1、V2、V2HC 或者 MMC),
在完成了初始化之后,就可以开始读写数据了。
SD 卡读取数据,这里通过 CMD17 来实现,具体过程如下:
1、发送 CMD17;
2、接收卡响应 R1;
3、接收数据起始令牌 0XFE;
4、接收数据;
5、接收 2 个字节的 CRC,如果不使用 CRC,这两个字节在读取后可以丢掉。
6、禁止片选之后,发多 8 个 CLK;
以上就是一个典型的读取 SD 卡数据过程,SD 卡的写于读数据差不多,写数据通过 CMD24
来实现,具体过程如下:
1、发送 CMD24;
2、接收卡响应 R1;
3、发送写数据起始令牌 0XFE;
4、发送数据;
5、发送 2 字节的伪 CRC;
6、禁止片选之后,发多 8 个 CLK;
软件设计
打开 SD 卡实验工程,可以看到我们新建了 MMC_SD.C 文件和 MMC_SD.h,所有 SD 卡相
关的驱动代码和定义都在这两个文件中。因为 SD 卡是通过 spi 操作的,所以对应的我们会加入stm32f10x_spi.c 源文件和对应的头文件 stm32f10x_spi.h。
打开 MMC_SD.C 文件,在该文件里面,我们输入与 SD 卡相关的操作代码,这里由于篇
幅限制,我们不贴出所有代码,仅介绍两个最重要的函数,第一个是 SD_Initialize 函数,该函
数源码如下:
//初始化 SD 卡
u8 SD_Initialize(void)
{
u8 r1; // 存放 SD 卡的返回值
u16 retry; // 用来进行超时计数
u8 buf[4]; u16 i;
SD_SPI_Init(); //初始化 IO
SD_SPI_SpeedLow(); //设置到低速模式
for(i=0;i<10;i++)SD_SPI_ReadWriteByte(0XFF);//发送最少 74 个脉冲
retry=20;
do
{
r1=SD_SendCmd(CMD0,0,0x95);//进入 IDLE 状态
}while((r1!=0X01) && retry--);
SD_Type=0;//默认无卡
if(r1==0X01)
{
if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0
{
for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);
if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持 2.7~3.6V
{
retry=0XFFFE;
do
{
SD_SendCmd(CMD55,0,0X01); //发送 CMD55
r1=SD_SendCmd(CMD41,0x40000000,0X01);//发送 CMD41
}while(r1&&retry--);
if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//鉴别 SD2.0 卡版本开始
{
for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);//得到 OCR 值
if(buf[0]&0x40)SD_Type=SD_TYPE_V2HC; //检查 CCS
else SD_Type=SD_TYPE_V2;
}
}
}else//SD V1.x/ MMC V3
{
SD_SendCmd(CMD55,0,0X01); //发送 CMD55
r1=SD_SendCmd(CMD41,0,0X01); //发送 CMD41
if(r1<=1)
{
SD_Type=SD_TYPE_V1;
retry=0XFFFE;
do //等待退出 IDLE 模式
{
SD_SendCmd(CMD55,0,0X01); //发送 CMD55
r1=SD_SendCmd(CMD41,0,0X01);//发送 CMD41
}while(r1&&retry--);
}else//MMC 卡不支持 CMD55+CMD41 识别
{
SD_Type=SD_TYPE_MMC;//MMC V3
retry=0XFFFE;
do //等待退出 IDLE 模式
{
r1=SD_SendCmd(CMD1,0,0X01);//发送 CMD1
}while(r1&&retry--);
}
if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)SD_Type=SD_TYPE_ERR;
}
}
SD_DisSelect(); //取消片选
SD_SPI_SpeedHigh(); //高速
if(SD_Type)return 0;
else if(r1)return r1;
return 0xaa;//其他错误
}
该函数先设置与 SD 相关的 IO 口及 SPI 初始化,然后发送 CMD0,进入 IDLE 状态,并设置
SD 卡为 SPI 模式通信,然后判断 SD 卡类型,完成 SD 卡的初始化,注意该函数调用的 SD_SPI_Init等函数,实际是对 SPI1 的相关函数进行了一层封装,方便移植。另外一个要介绍的函数是SD_ReadDisk,该函数用于从 SD 卡读取一个扇区的数据(这里一般为 512 字节),该函数代码如下:
//读 SD 卡
//buf:数据缓存区
//sector:扇区
//cnt:扇区数
//返回值:0,ok;其他,失败.
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt)
{
u8 r1;
if(SD_Type!=SD_TYPE_V2HC)sector <<= 9;//转换为字节地址
if(cnt==1)
{
r1=SD_SendCmd(CMD17,sector,0X01);//读命令
if(r1==0) r1=SD_RecvData(buf,512);//指令发送成功,接收 512 个字节
}else
{
r1=SD_SendCmd(CMD18,sector,0X01);//连续读命令
do
{
r1=SD_RecvData(buf,512); //接收 512 个字节
buf+=512;
}while(--cnt && r1==0);
SD_SendCmd(CMD12,0,0X01); //发送停止命令
}
SD_DisSelect();//取消片选
return r1;
}
此函数根据要读取扇区的多少,发送 CMD17/CMD18 命令,然后读取一个/多个扇区的数据,
详细见代码,这里我们就不多介绍了。接下来我们打开 MMC_SD.H, 内容如下:
#ifndef _MMC_SD_H_
#define _MMC_SD_H_
#include "sys.h"
#include <stm32f10x.h>
// SD 卡类型定义
#define SD_TYPE_ERR 0X00
#define SD_TYPE_MMC 0X01
#define SD_TYPE_V1 0X02
#define SD_TYPE_V2 0X04
#define SD_TYPE_V2HC 0X06
// SD 卡指令表
#define CMD0 0 //卡复位
#define CMD1 1
#define CMD8 8 //命令 8 ,SEND_IF_COND
#define CMD9 9 //命令 9 ,读 CSD 数据
#define CMD10 10 //命令 10,读 CID 数据
#define CMD12 12 //命令 12,停止数据传输
#define CMD16 16 //命令 16,设置 SectorSize 应返回 0x00
#define CMD17 17 //命令 17,读 sector
#define CMD18 18 //命令 18,读 Multi sector
#define CMD23 23 //命令 23,设置多 sector 写入前预先擦除 N 个 block
#define CMD24 24 //命令 24,写 sector
#define CMD25 25 //命令 25,写 Multi sector
#define CMD41 41 //命令 41,应返回 0x00
#define CMD55 55 //命令 55,应返回 0x01
#define CMD58 58 //命令 58,读 OCR 信息
#define CMD59 59 //命令 59,使能/禁止 CRC,应返回 0x00
//数据写入回应字意义
#define MSD_DATA_OK 0x05
#define MSD_DATA_CRC_ERROR 0x0B
#define MSD_DATA_WRITE_ERROR 0x0D
#define MSD_DATA_OTHER_ERROR 0xFF
//SD 卡回应标记字
#define MSD_RESPONSE_NO_ERROR 0x00
#define MSD_IN_IDLE_STATE 0x01
#define MSD_ERASE_RESET 0x02
#define MSD_ILLEGAL_COMMAND 0x04
#define MSD_COM_CRC_ERROR 0x08
#define MSD_ERASE_SEQUENCE_ERROR 0x10
#define MSD_ADDRESS_ERROR 0x20
#define MSD_PARAMETER_ERROR 0x40
#define MSD_RESPONSE_FAILURE 0xFF
//这部分应根据具体的连线来修改!
//MiniSTM32 开发板使用的是 PA3 作为 SD 卡的 CS 脚.
#define SD_CS PAout(3) //SD 卡片选引脚
u8 SD_SPI_ReadWriteByte(u8 data);
void SD_SPI_SpeedLow(void);
void SD_SPI_SpeedHigh(void);
u8 SD_WaitReady(void); //等待 SD 卡准备
u8 SD_GetResponse(u8 Response); //获得相应
u8 SD_Initialize(void); //初始化
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt); //读块
u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt); //写块
u32 SD_GetSectorCount(void); //读扇区数
u8 SD_GetCID(u8 *cid_data); //读 SD 卡 CID
u8 SD_GetCSD(u8 *csd_data); //读 SD 卡 CSD
#endif
该部分代码主要是一些命令的宏定义以及函数声明,在这里我们设定了 SD 卡的 CS 管脚为
PA3。最后我们来看看 main.c 的内容:
//读取 SD 卡的指定扇区的内容,并通过串口 1 输出
//sec:扇区物理地址编号
void SD_Read_Sectorx(u32 sec)
{
u8 *buf;
u16 i;
buf=mymalloc(512); //申请内存
if(SD_ReadDisk(buf,sec,1)==0) //读取 0 扇区的内容
{
LCD_ShowString(60,190,200,16,16,"USART1 Sending Data...");
printf("SECTOR 0 DATA:\r\n");
for(i=0;i<512;i++)printf("%x ",buf[i]);//打印 sec 扇区数据
printf("\r\nDATA ENDED\r\n");
LCD_ShowString(60,190,200,16,16,"USART1 Send Data Over!");
}
myfree(buf);//释放内存
}
int main(void)
{
u8 key;
u32 sd_size;
u8 t=0;
delay_init(); //延时函数初始化
uart_init(9600); //串口初始化为 9600
LED_Init(); //初始化与 LED 连接的硬件接口
LCD_Init(); //初始化 LCD
KEY_Init(); //按键初始化
mem_init(); //初始化内存池
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(60,50,200,16,16,"Mini STM32");
LCD_ShowString(60,70,200,16,16,"SD CARD TEST");
LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(60,110,200,16,16,"2014/3/13");
LCD_ShowString(60,130,200,16,16,"KEY0:Read Sector 0");
while(SD_Initialize())//检测不到 SD 卡
{
LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(500);
LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500);
LED0=!LED0;//DS0 闪烁
}
POINT_COLOR=BLUE;//设置字体为蓝色
//检测 SD 卡成功
LCD_ShowString(60,150,200,16,16,"SD Card OK ");
LCD_ShowString(60,170,200,16,16,"SD Card Size: MB");
sd_size=SD_GetSectorCount();//得到扇区数
LCD_ShowNum(164,170,sd_size>>11,5,16);//显示 SD 卡容量
while(1)
{
key=KEY_Scan(0);
if(key==KEY0_PRES)SD_Read_Sectorx(0);//KEY0 按,读取 SD 卡扇区 0 的内容
delay_ms(10);t++;
if(t==20) { LED0=!LED0; t=0; }
}
}
这里总共 2 个函数,其中 SD_Read_Sectorx 用于读取 SD 卡指定扇区的数据,并将读到的
数据通过串口 1 输出。然后 main 函数则通过 SD_GetSectorCount 函数来得到 SD 卡的扇区数,间接得到 SD 卡容量,然后在液晶上显示出来,接着我们通过按键 KEY0 控制读取 SD 卡的扇区 0,然后把读到的数据通过串口打印出来。