打印
[其他ST产品]

STM32利用SPI读写SD卡的程序详解

[复制链接]
2183|29
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
  关于SD卡的基础知识这里不做过多陈述,如果有对这方面感兴趣的朋友可以直接百度一下,有很多讲SD卡的文章,这里主要是针对SD卡的读写程序实现做一些详细说明。

        SD卡的读写驱动程序是运用FATFS的基础,学了FATFS就可以在SD卡上创建文件夹及文件了。



我们先从main文件了解一下程序的执行流程
int main(void)
{       
  u16 i;
  USART1_Config();
  for(i=0;i<1536;i++)
     send_data[i]='D';
  switch(SD_Init())
  {
  case 0:
          USART1_Puts("\r\nSD Card Init Success!\r\n");
        break;
  case 1:  
        USART1_Puts("Time Out!\n");
        break;
  case 99:
        USART1_Puts("No Card!\n");
        break;
  default: USART1_Puts("unknown err\n");
  break;
  }
  SD_WriteSingleBlock(30,send_data);
  SD_ReadSingleBlock(30,receive_data);
  if(Buffercmp(send_data,receive_data,512))
  {
           USART1_Puts("\r\n single read and write success \r\n");
        //USART1_Puts(receive_data);
  }
  SD_WriteMultiBlock(50,send_data,3);
  SD_ReadMultiBlock(50,receive_data,3);
  if(Buffercmp(send_data,receive_data,1536))
  {
        USART1_Puts("\r\n        multi read and write success \r\n");
        //USART1_Puts(receive_data);
  }
  while(1);
}



使用特权

评论回复
沙发
gaonaiweng|  楼主 | 2023-10-19 13:05 | 只看该作者
这里程序流程比较简单:

1)配置串口,用作程序的调试输出

2)填充将要给SD卡写入数据的数组send_data。

3)初始化SD卡,根据返回SD_Init()返回值确定SD卡初始化是否完成。

4)单块读写实验,并比对读写出的数据是否相同。

5)多块读写实验,并比对读写出的数据是否相同。

使用特权

评论回复
板凳
gaonaiweng|  楼主 | 2023-10-19 13:08 | 只看该作者
SD初始化函数SD_Init()
        为使程序更简洁,故只对SD卡进行检测,放弃对MMC卡的支持(此种卡市面上已几乎不再使用,本人手上也没有这种卡,所以写出驱动程序,也没有硬件进行检测是否可用)。

        下面程序是部分对SD2.0卡检测的代码,完整代码中还有对1.0版本SD卡的初始化,可下载完整代码查看。下面我们开始对main函数中涉及到的用户函数的层层调用详细说明

使用特权

评论回复
地板
gaonaiweng|  楼主 | 2023-10-19 13:08 | 只看该作者
u8 SD_Init(void)
{
  u16 i;
  u8 r1;
  u16 retry;
  u8 buff[6];
  SPI_ControlLine();               
  //SD卡初始化时时钟不能超过400KHz
  SPI_SetSpeed(SPI_SPEED_LOW);
  //CS为低电平,片选置低,选中SD卡               
  SD_CS_ENABLE();       
  //纯延时,等待SD卡上电稳定
  for(i=0;i<0xf00;i++);
  //先产生至少74个脉冲,让SD卡初始化完成
  for(i=0;i<10;i++)               
  {
   //参数可随便写,经过10次循环,产生80个脉冲
   SPI_ReadWriteByte(0xff);        
  }
//-----------------SD卡复位到idle状态----------------
//循环发送CMD0,直到SD卡返回0x01,进入idle状态
//超时则直接退出
retry=0;
do
  {
  //发送CMD0,CRC为0x95
  r1=SD_SendCommand(CMD0,0,0x95);               
  retry++;
  }
  while((r1!=0x01)&&(retry<200));
  //跳出循环后,检查跳出原因,
  if(retry==200)        //说明已超时       
  {
    return 1;
  }
  //如果未超时,说明SD卡复位到idle结束
  //发送CMD8命令,获取SD卡的版本信息
  r1=SD_SendCommand(CMD8,0x1aa,0x87);
  //下面是SD2.0卡的初始化               
  if(r1==0x01)       
{
    // V2.0的卡,CMD8命令后会传回4字节的数据,要跳过再结束本命令
    buff[0] = SPI_ReadWriteByte(0xFF);         
    buff[1] = SPI_ReadWriteByte(0xFF);         
    buff[2] = SPI_ReadWriteByte(0xFF);         
    buff[3] = SPI_ReadWriteByte(0xFF);                      
    SD_CS_DISABLE();
    //多发8个时钟          
    SPI_ReadWriteByte(0xFF);                         
    retry = 0;
    //发卡初始化指令CMD55+ACMD41
    do
    {
      r1 = SD_SendCommand(CMD55, 0, 0);               
      //应返回0x01
      if(r1!=0x01)               
      return r1;          
      r1 = SD_SendCommand(ACMD41, 0x40000000, 1);       
      retry++;
      if(retry>200)       
      return r1;
    }
    while(r1!=0);
    //初始化指令发送完成,接下来获取OCR信息          
    //----------鉴别SD2.0卡版本开始-----------
    //读OCR指令
    r1 = SD_SendCommand_NoDeassert(CMD58, 0, 0);               
    //如果命令没有返回正确应答,直接退出,返回应答
    if(r1!=0x00)  
    return r1;                   
    //应答正确后,会回传4字节OCR信息
    buff[0] = SPI_ReadWriteByte(0xFF);
    buff[1] = SPI_ReadWriteByte(0xFF);
    buff[2] = SPI_ReadWriteByte(0xFF);
    buff[3] = SPI_ReadWriteByte(0xFF);
    //OCR接收完成,片选置高
    SD_CS_DISABLE();
    SPI_ReadWriteByte(0xFF);
    //检查接收到的OCR中的bit30位(CSS),确定其为SD2.0还是SDHC
    //CCS=1:SDHC   CCS=0:SD2.0
    if(buff[0]&0x40)
    {
      SD_Type = SD_TYPE_V2HC;
    }            
    else               
    {
      SD_Type = SD_TYPE_V2;
    }            
    //-----------鉴别SD2.0卡版本结束-----------
    SPI_SetSpeed(1);                 //设置SPI为高速模式
}
}

使用特权

评论回复
5
gaonaiweng|  楼主 | 2023-10-19 13:08 | 只看该作者
以上函数是根据SD卡的发送和响应时序进行编写的。

1)程序中配置好SPI模式和引脚后,需要先将SPI的速度设为低速,SD卡初始化时SCK时钟信号不能大于400KHz,初始化结束后再设为高速模式,这里对SPI的模式配置不在赘述,可参考SPI读写FLASH文章的相关内容。

2)将片选信号拉低,选中SD卡,上电后,需要等待至少74个时钟,使SD卡上电稳定。

3)向SD卡发送CMD0指令,SD卡如果返回0x01,说明SD卡已复位到idle状态。

4)向SD卡发送CMD8指令,SD卡如果返回0x01,说明SD卡是2.0或SDHC卡。

使用特权

评论回复
6
gaonaiweng|  楼主 | 2023-10-19 13:09 | 只看该作者
SPI读写一字节数据
在这里,先介绍一个相对底层的函数。

SPI操作SD卡时,发送和接收是同步的,所以发送和接收数据使用同一个函数。

在发送数据时,并不关心函数的返回值;

在接收数据时,可以发送并无实际意义的字节(如0xFF)作为函数的参数。

使用特权

评论回复
7
gaonaiweng|  楼主 | 2023-10-19 13:09 | 只看该作者
u8  SPI_ReadWriteByte(u8 TxData)
{
   while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);               
   SPI_I2S_SendData(SPI1,TxData);       
   while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);       
   return SPI_I2S_ReceiveData(SPI1);
}

使用特权

评论回复
8
gaonaiweng|  楼主 | 2023-10-19 13:09 | 只看该作者
这个函数在所有主机与SD卡通信的函数中都会被调用到。

使用特权

评论回复
9
gaonaiweng|  楼主 | 2023-10-19 13:09 | 只看该作者
从SD卡中读回指定长度的数据
        在SD卡读写试验中,我们会遇到很多需要读取SD卡各个寄存器数据的情况。SD卡返回的数据长度并不都相同,所以需要一个函数来实现这个功能。函数中多次调用了读写一字节数据的函数SPI_ReadWriteByte。这个功能由函数 u8 SD_ReceiveData()来实现。

使用特权

评论回复
10
gaonaiweng|  楼主 | 2023-10-19 13:09 | 只看该作者
u8 SD_ReceiveData(u8 *data, u16 len, u8 release)
{
   u16 retry;
   u8 r1;
   //启动一次传输
   SD_CS_ENABLE();
   retry = 0;                                                                                  
   do
   {
      r1 = SPI_ReadWriteByte(0xFF);               
      retry++;
      if(retry>4000)  //4000次等待后没有应答,退出报错(可多试几次)
   {
   SD_CS_DISABLE();
   return 1;
   }
}
  //等待SD卡发回数据起始令牌0xFE
  while(r1 != 0xFE);       
  //跳出循环后,开始接收数据
  while(len--)
  {
   *data = SPI_ReadWriteByte(0xFF);
   data++;
  }
  //发送2个伪CRC
  SPI_ReadWriteByte(0xFF);
  SPI_ReadWriteByte(0xFF);
  //按需释放总线
  if(release == RELEASE)
  {
    SD_CS_DISABLE();
    SPI_ReadWriteByte(0xFF);
  }                                                                                                         
  return 0;
}

使用特权

评论回复
11
gaonaiweng|  楼主 | 2023-10-19 13:10 | 只看该作者
此函数有3个输入参数:  u8 * data为保存读回数据的变量,len为需要保存的的数据个数,release 为当程序结束后是否释放总线的标志。

使用特权

评论回复
12
gaonaiweng|  楼主 | 2023-10-19 13:10 | 只看该作者
给SD卡发送命令
在初始化函数中,我们需要做的最多的就是给SD卡发送各种命令以及接收各种响应,从而判断卡片的类型,操作条件等相关信息。一个命令包括6个段:

使用特权

评论回复
13
gaonaiweng|  楼主 | 2023-10-19 13:12 | 只看该作者
给SD卡发送命令的程序有2个。区别为一个发送完命令后失能片选,一个为发送完命令不失能片选(后续还有数据传回)。

u8  SD_SendCommand(u8 cmd,u32 arg,u8 crc)
{
   unsigned char r1;
   unsigned int Retry = 0;
   SD_CS_DISABLE();
   //发送8个时钟,提高兼容性
   SPI_ReadWriteByte(0xff);       
   //选中SD卡
   SD_CS_ENABLE();               
   /*按照SD卡的命令序列开始发送命令 */
   //cmd参数的第二位为传输位,数值为1,所以或0x40  
   SPI_ReadWriteByte(cmd | 0x40);   
   //参数段第24-31位数据[31..24]
   SPI_ReadWriteByte((u8)(arg >> 24));
   //参数段第16-23位数据[23..16]       
   SPI_ReadWriteByte((u8)(arg >> 16));
   //参数段第8-15位数据[15..8]       
   SPI_ReadWriteByte((u8)(arg >> 8));       
   //参数段第0-7位数据[7..0]
   SPI_ReadWriteByte((u8)arg);   
   SPI_ReadWriteByte(crc);
   //等待响应或超时退出
   while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
   {
     Retry++;
     if(Retry > 800)        break;         //超时次数
   }   
   //关闭片选
   SD_CS_DISABLE();               
   //在总线上额外发送8个时钟,让SD卡完成剩下的工作
   SPI_ReadWriteByte(0xFF);        
   //返回状态值       
   return r1;               
}

使用特权

评论回复
14
gaonaiweng|  楼主 | 2023-10-19 13:12 | 只看该作者
u8 SD_SendCommand_NoDeassert(u8 cmd, u32 arg,u8 crc)
{
   unsigned char r1;
   unsigned int Retry = 0;
   SD_CS_DISABLE();
   //发送8个时钟,提高兼容性
   SPI_ReadWriteByte(0xff);               
   //选中SD卡
   SD_CS_ENABLE();               
   /* 按照SD卡的命令序列开始发送命令 */
   SPI_ReadWriteByte(cmd | 0x40);                     
   SPI_ReadWriteByte((u8)(arg >> 24));
   SPI_ReadWriteByte((u8)(arg >> 16));
   SPI_ReadWriteByte((u8)(arg >> 8));
   SPI_ReadWriteByte((u8)arg);   
   SPI_ReadWriteByte(crc);
   //等待响应或超时退出
   while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
   {
      Retry++;
      if(Retry > 800)break;
    }
    return r1;
}

使用特权

评论回复
15
gaonaiweng|  楼主 | 2023-10-19 13:13 | 只看该作者
以上两个函数就是根据SD卡在SPI模式下发送指令的时序编写的

使用特权

评论回复
16
gaonaiweng|  楼主 | 2023-10-19 13:14 | 只看该作者
取CID寄存器数据

u8 SD_GetCID(u8 *cid_data)
{
   u8 r1;
   //发CMD10命令,读取CID信息
   r1 = SD_SendCommand(CMD10, 0, 0xFF);
   if(r1 != 0x00)       
   return r1;          //响应错误,退出
   //接收16个字节的数据
   SD_ReceiveData(cid_data, 16, RELEASE);         
   return 0;
}

以上程序源码相对比较简单,发送了CMD10读取CID寄存器命令后,如果相应正确,即开始进入接收数据环节,这里SD_ReceiveData函数中第二个参数输入16,即表示回传128位的CID数据。

使用特权

评论回复
17
gaonaiweng|  楼主 | 2023-10-19 13:14 | 只看该作者
获取SD卡容量信息
        SD卡容量的信息主要是通过查询CSD寄存器的一些相关数据,并根据数据手册进行计算得出的。该函数虽然较为复杂,但可先精读SPI操作SD卡的理论知识篇,看懂程序的算法为何是这样实现的,也就容易理解程序的编写原理了。

u32 SD_GetCapacity(void)
{
    u8 csd[16];
    u32 Capacity;
    u8 r1;
    u16 i;
    u16 temp;
    //取CSD信息,如果出错,返回0
    if(SD_GetCSD(csd)!=0)        
    return 0;            
    //如果是CSD寄存器是2.0版本,按下面方式计算
    if((csd[0]&0xC0)==0x40)
    {                                                                          
       Capacity=((u32)csd[8])<<8;
       Capacity+=(u32)csd[9]+1;         
       Capacity = (Capacity)*1024;        //得到扇区数
       Capacity*=512;        //得到字节数   
    }
    else                //CSD寄存器是1.0版本
   {                    
       i = csd[6]&0x03;
       i<<=8;
       i += csd[7];
       i<<=2;
       i += ((csd[8]&0xc0)>>6);
       r1 = csd[9]&0x03;
       r1<<=1;
       r1 += ((csd[10]&0x80)>>7);         
       r1+=2;
       temp = 1;
       while(r1)
       {
        temp*=2;
         r1--;
       }
   Capacity = ((u32)(i+1))*((u32)temp);         
   i = csd[5]&0x0f;
   temp = 1;
   while(i)
   {
      temp*=2;
      i--;
   }
   //最终结果
   Capacity *= (u32)temp;               
   //字节为单位  
  }
   return (u32)Capacity;
}

使用特权

评论回复
18
gaonaiweng|  楼主 | 2023-10-19 13:18 | 只看该作者
此函数计算出来的容量是Kbyte,结果除以1024就是Mbyte,再除以1024就是GByte。2G的卡,结果可能是1.8G,8G的卡结果可能是7.6G,代表用户可用容量。

使用特权

评论回复
19
gaonaiweng|  楼主 | 2023-10-19 13:18 | 只看该作者
读单块block和读多块block
SD卡读单块和多块的命令分别为CMD17和CMD18,他们的参数即要读的区域的开始地址。因为考虑到一般SD卡的读写要求地址对齐,所以一般我们都将地址转为块,并以扇区(块)(512Byte)为单位进行读写,比如读扇区0参数就为0,读扇区1参数就为1<<9(即地址512),读扇区2参数就为2<<9(即地址1024),依此类推。

使用特权

评论回复
20
gaonaiweng|  楼主 | 2023-10-19 13:18 | 只看该作者
读单块:
u8 SD_ReadSingleBlock(u32 sector, u8 *buffer)
{
  u8 r1;
  //高速模式
  SPI_SetSpeed(SPI_SPEED_HIGH);
  if(SD_Type!=SD_TYPE_V2HC)        //如果不是SDHC卡
  {
    sector = sector<<9;        //512*sector即物理扇区的边界对其地址
  }
   r1 = SD_SendCommand(CMD17, sector, 1);        //发送CMD17 读命令
   if(r1 != 0x00)        return r1;                                                                              
   r1 = SD_ReceiveData(buffer, 512, RELEASE);        //一个扇区为512字节
   if(r1 != 0)
     return r1;   //读数据出错
   else
     return 0;                 //读取正确,返回0
}

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

69

主题

697

帖子

3

粉丝