weiqi7777

stm32驱动SDHC卡 (SPI方式) 二

0
阅读(7681)

前面说了关于SDHC卡的一些基础知识。现在就开始进行驱动我们的SDHC卡了。有了前面一章的两个函数的基础,来构建其他的函数。

首先是复位。复位是很重要的一个操作,复位如果不成功的话,那SD卡是不能使用的。所以首先要进行复位操作。复位操作还是相对有点麻烦的。

先来个流程图。

clip_image002

clip_image003

这一看,吓尿了。。。。也太复杂了吧。。其实很简单的。。。听我一一道来。

因为我们用的是SDHC卡,标准是SD.0。所以左边的部分都不用管。因为那是SD1.0标准的卡。

1、首先上电后。我们需要发送至少74个时钟。这个时钟是用来唤醒SD卡的。也就是告诉SD卡,嘿,赶快起来给我工作了。

2、唤醒SD卡后,就要发送CMD0命令,对SD卡进行初始化,SD的使用是必须要进行初始化的。否则不能使用。CMD0的响应式0x01.如果接收到0x01响应后。就说明发送CMD0命令成功。

3、接收到CMD0的0x01响应后。然后发送CMD8。这个命令是SD2.0标准加的。用来鉴别SD卡能否在目前的电压下工作。

CMD8的参数是0x1aa,crc是0x87.

CMD8的响应也是0x01. 0x01响应后,可以读取4个字节数据,该数据由对电压有说明。因为SD卡的工作电压我们是已经知道的,所以这里对读取的4个字节数据直接忽略。当然也可以进行处理。

4、发送完CMD8后。就要发送CMD55.CMD55是向SD卡说明,下一个发送的命令是特殊命令。这个要注意,图上没有说明。然后接着发送ACMD41,ACMD41是特殊命令,用来初始化SD卡。如果返回响应是0x00,就说明初始化成功,否则初始化失败。ACMD41的参数是0x40000000,CRC无所谓。至此SD卡的初始化就结束了。

这样看来,初始化其实也很简单的,就是发命令,读响应,再发命令。。。一直到所有命令发完,就初始化完了。

这里初始化完后,还可以发送命令CMD58,读取OCR内部寄存器的值。

下面是代码:

uint8_t SD_reset(void)
{
   uint8_t i;
   uint8_t response;
   uint8_t buf[4];
   uint16_t retry;
   //SD卡复位信号有效,说明现在clk时钟不能太快
   sd_reset_flag=1;    
   //发送160个clk时钟
   for(i=0; i<20; i++)
     spi_write_byte(0xff);	 		   
   //发送命令0,发送100次,如果接收不到响应,就说明发送0命令失败
   for(i=0;; i++)
   {
	  response = SD_write_command(0,0x00000000,0x95); 
	  if(response == 0x01)
	        break;
	  if(i == 100)
	  {
	     SPI_CS_HIGH();  //无效片选
		 return CMD0_ERROR;
	  }
   }
   printf("CMD0命令发送成功\n");

   //发送CMD8命令 ,回应为0x01
   response = SD_write_command(8,0x1aa,0x87); 
   if(response == 0x01)
   {
      printf("CMD8命令发送成功\n");
	  for(retry=0; retry<4; retry++)
	     buf[retry] = spi_write_byte(0xff);
	  printf("CMD8命令读取的值为:\n");
	  printf("%x  %x  %x  %x\n",buf[0],buf[1],buf[2],buf[3]);
	  if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持2.7~3.6V
  	  {
		retry=0XFFFE;
		//发送CMD55和ACMD41
		//CMD55响应为0x01
		//ACMD41响应为0x00
		do
		{
			SD_write_command(55,0,0X01);	//发送CMD55
			response=SD_write_command(41,0x40000000,0X01);//发送CMD41
		}while(response&&retry--);
		if(retry != 0)
		    printf("CMD41命令成功,初始化成功\n");
		else
		    return CMD41_ERROR;
		if(retry&&SD_write_command(58,0,0X01)==0)//鉴别SD2.0卡版本开始
		{
			for(i=0;i<4;i++)buf[i]=spi_write_byte(0XFF);//得到OCR值
			printf("CMD58命令读取的值为:\n");
            printf("%x  %x  %x  %x\n",buf[0],buf[1],buf[2],buf[3]); 
		}
	  }
	}	 
	else 
	   return CMD8_ERROR;    
   SPI_CS_HIGH();  //取消片选  
   spi_write_byte(0xff);   //片选无效后,要发送8个时钟
   sd_reset_flag=0;	//复位结束,SPI时钟加快  
   return RESET_SUCCESS;
}

代码也比较简单。按照这个流程下来,初始化应该是没有问题的。初始化后,就可以进行SD卡的数据读取和数据写入了。

首先是数据读取。

时序图:

clip_image005

这个是读取一个扇区的数据。即一次读取一个扇区的512字节数据。后面会说道读取多个扇区的数据。

时序图也比较清楚,首先拉低CS。发送命令17,然后等待响应0x00.响应后,又在等待读取数据起始令牌0xfe。然后就可以读取512字节数据了。读取后,还要在读取两个字节的CRC,不过这里不关心这数据是什么。然后拉高片选即可。这样就完成了读扇区数据。

有时候,我们是需要读取多个连续扇区数据的,那怎么做了。

首先还是上图。

clip_image007

读取连续扇区的命令式18.和17命令一样的时序,只不过是读取完两字节的CRC后,不要拉高CS。而是等待读取数据起始令牌0xfe。等到后,又继续读取数据。这样就实现了连续读取了。那如果我不想读了怎么办,只要你读完数据后,发送命令12,就停止读取数据了。

这里CMD17和CMD18的参数是扇区地址,CRC无所谓。

代码如下:

//从SD的一个扇区读取数据
//输入参数1   读取扇区地址
//输入参数2   存储读取的512个数据的数组
//输入参数3   读取扇区的个数
//返回值是读取成功还是不成功
uint8_t  SD_read_sector_data(uint32_t sector, uint8_t data[], uint32_t count)
{
   uint16_t i;
   uint32_t j;
   uint32_t add=0;
   SPI_CS_HIGH();  //无效上一次片选
   spi_write_byte(0xff);

   if(count == 1)
   {
       //发送17命令,即读一个扇区命令
       //正常响应应该为0x00
	   if(SD_write_command(17,sector,0xff))
       {
	       SPI_CS_HIGH();  //无效片选
	       return READ_SECTOR_DATA_ERROR;
       }
   }
   else
   {
       //发送18命令,即读多个扇区命令
       //正常响应应该为0x00
	   if(SD_write_command(18,sector,0xff))
       {
	       SPI_CS_HIGH();  //无效片选
	       return READ_SECTOR_DATA_ERROR;
       }
   }
  
   for(j=1; ; j++)
   {
        //等待读响应令牌 
        while(spi_write_byte(0xff) != 0xfe);
		for(i=0; i<512; i++)
		{
		  data[add+i] = spi_write_byte(0xff);
		}
		//读取CRC码
		spi_write_byte(0xff);
		spi_write_byte(0xff);
		if(j == count )
		{
		   //发送数据停止读命令
		   if(count != 1) SD_write_command(12,0,0xff);
		   break;
   		}
		else
		   add += 512;
   }
   SPI_CS_HIGH();  //无效片选

   //片选无效之后,在发送8个时钟
   spi_write_byte(0xff);
   return READ_SECTOR_SUCCESS;
}

代码也比较简单,首先判断是读取单个扇区还是多个扇区,单个扇区就发送CMD17,多个扇区就发送CMD18。如果是读取多个扇区,那么读完后,要发送CMD12.

然后就是数据写了:

首先还是上时序图:

clip_image009

和读取数据时序差不多。首先是写单个扇区。

首先拉低CS,然后发送命令24,即写单个扇区。然后等待响应信号,如果响应为0x00说明命令发送成功。然后发送数据写入起始令牌0xfe。SD卡认为0xfe是写数据的起始令牌,即这个数据后,后面的数据才是真正写入的数据。然后在发送512字节的数据,最后在写两个CRC,这个CRC写什么都可以。然后读取响应,这里第一个响应的最后5位为00101.接收到这个响应后,就等待接收的数据不是0x00,因为0x00表示SD卡正在处理写入的数据。接收到的数据不是0x00后,就拉高CS。就完成了一个扇区的数据写入。

当然,我们是可以写多个扇区的。那多个扇区是怎么处理的。先上图:

clip_image011

对于写多个扇区,命令是25.和写单个扇区时序差不多,不过不一样的地方是,写多个扇区的起始令牌不是0xfe了,而是0xfc。这里要注意。另外当最后读取的数据不是0x00后,不要拉高CS,而是继续写数据。这样就实现了多个扇区的数据写入。数据写完后,只要拉高CS就可以了,不用发送其他命令。

CMD24和CMD25的参数都是扇区的地址。对于CMD24,这扇区地址就是写入数据的扇区地址,而对于CM25,扇区地址就是写入扇区的首地址。

代码如下:

//向SD中的一个扇区写数据
//输入参数1	 写入扇区地址
//输入参数2  存储512个写入数据的数组
//输入参数3  写入扇区的个数
//返回值是写入成功还是不成功
uint8_t  SD_write_sector_data(uint32_t sector ,const uint8_t data[], uint32_t count)
{
   uint16_t i;
   uint8_t response;
   uint32_t j;
   uint32_t add=0;
    SPI_CS_HIGH();  //无效上一次片选
    spi_write_byte(0xff);

   if(count == 1)  //单个扇区数据写入
   {
	  //发送24命令,即写扇区命令
      //正常响应应该为0x00
	   if(SD_write_command(24,sector,0xff))
	   {
		  SPI_CS_HIGH();  //无效片选
		  return WRITE_SECTOR_DATA_ERROR;
	   }
	   spi_write_byte(0xff);
	   //写数据起始令牌
	   spi_write_byte(0xfe);//发送数据块标志0xfe	
	   for(i=0; i<512; i++)
	   {
		 spi_write_byte(data[add + i]);
	   }	
	   //发送CRC码
	   spi_write_byte(0xff);
	   spi_write_byte(0xff);	
	   for(i=0; ; i++)
	   {
		 response = spi_write_byte(0xff);
		 if((response & 0x0f) == 0x05)
		 	break;
		 if(i==100)
		 {
		    SPI_CS_HIGH();  //无效片选
		    return WRITE_SECTOR_DATA_ERROR;
		 }
	   }
   }
   else		 //多个扇区数据写入
   {
	   //发送25命令,即写多个扇区命令
       //正常响应应该为0x00
	   if(SD_write_command(25,sector,0xff))
	   {
		  SPI_CS_HIGH();  //无效片选
		  return WRITE_SECTOR_DATA_ERROR;
	   }

   for(j=1; ; j++)
   {
	   spi_write_byte(0xff);
	   //写数据起始令牌
	   spi_write_byte(0xfc);//发送数据块标志0xfc	
	   for(i=0; i<512; i++)
	   {
		 spi_write_byte(data[add + i]);
	   }	
	   //发送CRC码
	   spi_write_byte(0xff);
	   spi_write_byte(0xff);	
	   for(i=0; ; i++)
	   {
		 response = spi_write_byte(0xff);
		 if((response & 0x0f) == 0x05)
		 	break;
		 if(i==100)
		 {
		    SPI_CS_HIGH();  //无效片选
		    return WRITE_SECTOR_DATA_ERROR;
		 }
	   }
	   while(spi_write_byte(0xff) != 0xff);
	   add += 512;
       if(j == count)
	   {
		   spi_write_byte(0xFD);
		   break;
	   }
   }
   }
   SPI_CS_HIGH();  //无效片选
   spi_write_byte(0xff);
   return WRITE_SECTOR_SUCCESS; 
}

对写单个扇区和多个扇区分开进行处理。也是比较容易的。

这样,通过上面两个函数,就能实现对SD卡进行读写了。不过还有一个命令也是有用的,就是擦除命令。

SD卡支持扇区擦除。能一次擦除多个扇区。

clip_image012

可以看出,擦除的话,需要发送3个命令,CMD32,CMD33,CMD38。

代码如下:

//输入参数1  擦除的起始扇区地址
//输入参数2  擦除的结束扇区地址
//返回值     擦除扇区是否成功
uint8_t SD_erase_sector(uint32_t sector_start, uint32_t sector_stop)
{
    
	SPI_CS_HIGH();  //无效上一次片选
    spi_write_byte(0xff);
	
	//发送命令32,设置擦除扇区的起始地址
    //返回应该为00
	if(SD_write_command(32,sector_start,0xff))
	   {
		  SPI_CS_HIGH();  //无效片选
		  return ERASE_SECTOR_ERROR;
	   }

	//发送命令33,设置擦除扇区的终止地址
    //返回应该为00
	if(SD_write_command(33,sector_stop,0xff))
	   {
		  SPI_CS_HIGH();  //无效片选
		  return ERASE_SECTOR_ERROR;
	   }
	//发送命令38,擦除所选扇区
    //返回应该为00
	if(SD_write_command(38,0,0xff))
	   {
		  SPI_CS_HIGH();  //无效片选
		  return ERASE_SECTOR_ERROR;
	   }
	SPI_CS_HIGH();  //无效片选
    return ERASE_SECTOR_SUCCESS;
}

代码也比较容易了,就发送三个命令就行了。

这样,就完成了整个SD卡的驱动了。对于外部使用,首先调用初始化函数,对SD卡进行初始化,然后就可以调用读,写,擦除函数对SD卡进行操作了。