SD卡读写测试程序
**SD卡介绍**
SD 卡(Seecure Digital Memory Cardl)是一种基于 Flash 的新一代存储器,具有体积小、容量大、数据传输快、移动灵活、安全性能好等优点,是许多便携式电子仪器理想的外部存储介质。
SD 卡支持两种总线方式:SD 方式与 SPI 方式。其中 SD 方式采用 6 线制,使用 CLK、 CMD、DAT0~DAT3 进行数据通信。而 SPI 方式采用 4 线制,使用 CS、CLK、DataIn、DataOut 进行数据通信。SD 方式时的数据传输速度与 SPI 方式要快,采用单片机对 SD 卡进行读写时一般都采用 SPI 模式。采用不同的初始化方式可以使 SD 卡工作于 SD 方式或 SPI 方式。
这里只对其 SPI 方式进行介绍。
SPI 方式驱动 SD 卡的方法
SD 卡的 SPI 通信接口使其可以通过 SPI 通道进行数据读写。从应用的角度来看,采用 SPI 接口的好处在于,很多单片机内部自带 SPI 控制器,不光给开发上带来方便,同时也见降低了开发成本。然而,它也有不好的地方,如失去了 SD 卡的性能优势,要解决这一问题,就要用 SD 方式,因为它提供更大的总线数据带宽。SPI 接口的选用是在上电初始时向其写入第一个命令时进行的。以下介绍 SD 卡的驱动方法,只实现简单的扇区读写。
为了使SD卡初始化进入SPI模式,我们需要使用的命令有3个CMD0,CMD55,ACMD41(使用ACMD类的指令前应先发CMD55,CMD55起到一个切换到ACMD类命令的作用
为什么在使用CMD0以后不使用CMD1?CMD1是MMC卡使用的指令。我们上电或者发送CMD0后,应该首先发送CMD55+ACMD41确认是否有回应,如果有回应则为SD卡,如果等回应超时,则可能是MMC卡,再发CMD1确认。
SD卡调试步骤:
1.上电时要延时足够长的时间给SD卡一个准备的过程,SD卡初始化第一步发送CMD命令之前,在片选有效的情况下首先要发送至少74个时钟,否则可能会出现SD卡不能初始化的问题。那么为什么要74个CLK呢?因为在上电初期,电压的上升过程据SD卡组织的计算约合64个CLK周期才能到达SD卡的正常工作电压他们管这个叫做Supply ramp up time,其后的10个CLK是为了与SD卡同步,之后开始CMD0的操作,严格按照此项操作,一定没有问题。
2.SD卡发送CMD0命令,返回状态为0x01,则复位成功。
3.SD卡发送复位命令CMD0后,要发送版本查询命令CMD8,返回状态一般分为两种,若返回0x01,表示此SD卡接收CMD8,也就是说SD卡支持版本2;若返回0x05则表示SD卡支持版本1.
4.理论上要求发送CMD58获得SD卡电压参数,但实际上都实现知道SD卡的工作电压是3.3v,所以可以省略这一步。
5.发送CMD55命令,返回0x01,起到转换的作用。
6.再发送ACMD41,等待返回0x00,则表示完成初始化。这里要说的是如果最后的回应内容还是0x01的话,可以循环发送CMD55+ACMD41,直到回应的内容0x00
SD卡操作命令作步骤:
1.读步骤:
发送CMD17(单块)或CMD18(多块)读命令,返回0x00
接收数据开始令牌0xfe(或0xfc)+正式数据512B+CRC校验2B
2.写步骤:
发送CMD24(单块)或CMD25(多块)写命令,返回0x00.
发送数据开始令牌0xfe(或0xfc)+正式数据512B+CRC校验2B
3.擦除步骤:
发送CMD32,跟一个参数来指定首个要擦除的起始地址
发送CMD33,指定最后的地址
发送CMD38,擦除指定区间的内容。
向SD卡写入一个CMD或者ACMD指令的过程是这样的:
1.首先使CS为低电平,SD卡使能;其次在SD卡的Din写入指令;写入指令后还要附加8个填充时钟,是SD卡完成内部操作;之后在SD卡的Dout上接受回应;回应接受完毕使CS为低电平,再附加8个填充时钟。
2.在SD卡的Din没有数据写入时,应使Din保持高电平。
一些常用的指令格式:
CMD0:0x40,0x00,0x00,0x00,0x00,0x95
CMD8:0x48,0x00,0x00,0x01,0xaa,0x87
CMD55:0x77,0x00,0x00,0x00,0x00,0xff 0x77=0x40+0x37(55的16进制表示) ACMD41:0x69,0x40,0x00,0x00,0x00,0xff
CMD58:0x7a,0x00,0x00,0x00,0x00,0xff
ACMD41属于附加命令,发送起来要麻烦一些,必须提前通知SD卡下一条要发送的命令为ACMD,这个通知就是CMD55,它的4字节参数都为0即可
SD卡的命令格式:
每一个命令的长度都是固定的6个字节,前1个字节的值=命令号+0x40;中间4个字节为参数,不同的命令参数格式都不相同,但参数最多为4个字节;最后1个字节是CRC校验码和1位固定结束位‘1’。这里需要说明一下0x40的意思,任何命令都有一个固定的起始格式,即先0后1,这是固定的命令起始标志,前两个字节的二进制码就是:01xx xxxx
SD 卡所有的命令都是由 48 个数据位组成的,其结构如表 2 所示。
需要特殊说明的是CRC的问题,这是一种检验错误的方法,具体问题度娘说的还算明白,在SPI模式中,CRC校验默认是关闭的,也就是说这7位必须要发,但是SD卡会在读到CRC以后自动忽略它,所以全部发1就可以。例外的是,CMD0,CMD8这两个命令发送的时候SD卡还没有进入SPI模式,也就是说CRC校验在这个时候还是启用状态,因此这两个命令的CRC效验码必须要写正确,SD卡才会执行命令,否在在返回值R1中就会有相应的错误标志位提示开发人员CRC校验码错误。
SD卡的命令代码:
(1)复位
/*********************************
* @函数名:SD_SPI_Rest
* @描 述:SD卡复位
* @参 数:无
* @返回值:成功 0 失败 0x01
*********************************/
uchar SD_SPI_Rest()//SD卡复位,进入SPI模式,使用CMD0(命令0)
{
uchar time,temp,i;
uchar pcmd[] = {0x40,0x00,0x00,0x00,0x00,0x95}; //命令0的字节序列
SPI_Speed_Slow=1; //将SPI_Speed_Slow置为1
SD_CS=1; //关闭片选
for(i=0;i<0x0f;i++) //复位时,首先要发送最少74个时钟信号,这是必须的!!!
{
SD_SPI_Write_Byte(0xff); //120个时钟 0x0f=15*8=120个时钟信号
}
SD_CS=0; //打开片选
time=0;
do
{
temp=SD_SPI_Write_CMD(pcmd);//连续写入CMD0
time++;
if(time==TRY_TIME)
{
SD_CS=1; //关闭片选
Send_String("初始化失败"); //提示初始化失败
return(0x01);//CMD0写入失败
}
}while(temp!=0x01); //返回0x01是复位成功
SD_CS=1; //关闭片选
SD_SPI_Write_Byte(0xff); //按照SD卡的操作时序在这里补8个时钟
return 0;//返回0,说明复位操作成功
}
(2)查版本号
/*********************************
* @函数名:SD_SPI_Version
* @描 述:读SD卡版本号
* @参 数:无
* @返回值:成功 0 失败 0x01
*********************************/
uchar SD_SPI_Version()
{
uchar time,temp;
uchar pcmd[] = {0x48,0x00,0x00,0x01,0xaa,0x87}; // //CMD8
SD_CS=0; //打开片选
time=0;
do
{
temp=SD_SPI_Write_CMD(pcmd);
time++;
if(time==TRY_TIME)
{
SD_CS=1; //关闭片选
Send_String("获取版本号失败");//获取版本号失败
return(0x01);
}
}while(temp!=0x01); //返回0x01是2.0版本
SD_CS=1; //关闭片选
SD_SPI_Write_Byte(0xff); //按照SD卡的操作时序在这里补8个时钟
return 0;//返回0,说明复位操作成功
}
(3)初始化
/*********************************
* @函数名:SD_SPI_Init
* @描 述:SD卡初始化
* @参 数:无
* @返回值:成功 0 失败 0x01
*********************************/
uchar SD_SPI_Init() //初始化,使用ACMD41
{
uchar time,temp;
uchar pcmd[] = {0x69,0x40,0x00,0x00,0x00,0xff}; //ACMD41的字节序列
uchar pcmd1[]={0x77,0x00,0x00,0x00,0x00,0xff};//CMD55
/*调用这两个函数*/
SD_SPI_Rest(); //复位SD卡
SD_SPI_Version();//版本号
SD_CS=0; //打开片选
time=0;
do
{
SD_SPI_Read_Byte(); //这4个读函数,必须有,没有将不会初始化成功
SD_SPI_Read_Byte();
SD_SPI_Read_Byte();
SD_SPI_Read_Byte();
temp=SD_SPI_Write_CMD(pcmd1); // 发送CMD55
if(temp!=0x01)//上一次的返回值,这次没有返回值
{
Send_String("CMD55错误");
return 0xfb;
}
temp=SD_SPI_Write_CMD(pcmd);//发送ACMD41
time++;
if(time==TRY_TIME)
{
SD_CS=1; //关闭片选
Send_String("初始化失败");
return(0x01);//ACMD41写入失败
}
}while(temp!=0x00); //当返回数据时0x00的时候,说明初始化成功
SPI_Speed_Slow=0; //初始化完毕,将SPI_Speed_Slow设置为0,为了提高以后的数据传输速度
SD_CS=1; //关闭片选
SD_SPI_Write_Byte(0xff); //按照SD卡的操作时序在这里补8个时钟
return(0); //返回0,说明初始化操作成功
}
(4)写一个扇区
/*********************************
* @函数名:SD_SPI_Write_Sector
* @描 述:写一扇区的数据
* @参 数:addr:扇区地址 *buffer:指向数据缓冲区的指针
* @返回值:成功 0x00 失败 0x01
*********************************/
uchar SD_SPI_Write_Sector(uchar *buffer,ulong addr) //向SD卡中的指定地址的扇区写入512个字节,使用CMD24(命令24)
{
uchar temp,time;
uint i;
uchar pcmd[] = {0x58,0x00,0x00,0x00,0x00,0xff}; //向SD卡中单个块(512字节,一个扇区)写入数据,用CMD24
addr<<=9; //addr = addr * 512 将块地址(扇区地址)转为字节地址 [这里就限制了SD卡的最大容量为4G]
pcmd[1]=((addr&0xff000000)>>24); //将字节地址写入到CMD24字节序列中
pcmd[2]=((addr&0x00ff0000)>>16);
pcmd[3]=((addr&0x0000ff00)>>8);
SD_CS=0;//打开SD卡片选
time=0;
do
{
temp=SD_SPI_Write_CMD(pcmd);//发送ACMD41
time++;
if(time==TRY_TIME)
{
SD_CS=1; //关闭片选
return temp ; //命令写入失败
}
}while(temp!=0); //写入地址,返回值是0x00正确
for(i=0;i<100;i++) //这里要插入若干时钟信号
{
SD_SPI_Write_Byte(0xff);
}
SD_SPI_Write_Byte(0xfe);//写入开始字节 0xfe,后面就是要写入的512个字节的数据
for(i=0;i<512;i++) //将缓冲区中要写入的512个字节写入SD卡
{
SD_SPI_Write_Byte(buffer[i]); //写入512个字节的数据
}
SD_SPI_Write_Byte(0xff);
SD_SPI_Write_Byte(0xff); //两个字节的CRC校验码,不用关心
temp=SD_SPI_Read_Byte(); //读取返回值
if((temp&0x1F)!=0x05) //如果返回值是 XXX00101说明数据已经被SD卡接受了
{
SD_CS=1;
return(0x01); //写块数据失败
}
while(SD_SPI_Read_Byte()!=0xff);//等到SD卡不忙(数据被接受以后,
// SD卡要将这些数据写入到自身的FLASH中,需要一个时间)
//忙时,读回来的值为0x00,不忙时,为0xff
SD_CS=1; //关闭片选
SD_SPI_Write_Byte(0xff);//按照SD卡的操作时序在这里补8个时钟
return(0); //返回0,说明写扇区操作成功
}
(5)读一个扇区
/*********************************
* @函数名:SD_SPI_Read_Sector
* @描 述:读一扇区的数据
* @参 数:addr:扇区地址 *buffer:指向数据缓冲区的指针
* @返回值:成功 0x00 失败 0x01
*********************************/
uchar SD_SPI_Read_Sector(uchar *buffer,ulong addr)//从SD卡的指定扇区中读出512个字节,使用CMD17(17号命令)
{
uint j;
uchar time,temp;
uchar pcmd[]={0x51,0x00,0x00,0x00,0x00,0xff}; //CMD17的字节序列
addr<<=9; //addr=addr*512 将块地址(扇区地址)转为字节地址
pcmd[1]=((addr&0xff000000)>>24);//将字节地址写入到CMD17字节序列中
pcmd[2]=((addr&0x00FF0000)>>16);
pcmd[3]=((addr&0x0000FF00)>>8);
SD_CS=0;//打开片选
time=0;
do
{
temp=SD_SPI_Write_CMD(pcmd); //写入CMD17
time++;
if(time==TRY_TIME)
{
return(0x01); //读块失败
}
}while(temp!=0);
while (SD_SPI_Read_Byte()!= 0xfe); //一直读,当读到0xfe时,说明后面的是512字节的数据了
for(j=0;j<512;j++) //将数据写入到数据缓冲区中
{
buffer[j]=SD_SPI_Read_Byte(); //把读出的数据存到buffer数组里
}
SD_SPI_Read_Byte();
SD_SPI_Read_Byte();//读取两个字节的CRC校验码,不用关心它们
SD_CS=1; //SD卡关闭片选
SD_SPI_Write_Byte(0xff);//按照SD卡的操作时序在这里补8个时钟
return 0;
}
注意:
1.2GB以内的SD卡(标准卡)和2GB以上的SD卡(大容量卡在地址访问形式上不一样。
2.对某一块要进行写操作时最好先执行擦除命令,这样写入的速度就能大大的提高。
3.对标准卡进行字节操作时,起始和终止必须在一个物理扇区内。不管是标准卡还是大容量卡一个读写读写命令只能对一个快进行操作,不允许跨物理层地址操作。