天天看点

SPI 读取不同长度 寄存器_【STM32】SD卡读写(四)-STM32利用SPI读写SD卡的程序详解...

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

我们先从main文件了解一下程序的执行流程

  1. int main(void)
  2. {
  3. u16 i;
  4. USART1_Config();
  5. for(i=0;i<1536;i++)
  6. send_data[i]='D';
  7. switch(SD_Init())
  8. {
  9. case 0:
  10. USART1_Puts("SD Card Init Success!");
  11. break;
  12. case 1:
  13. USART1_Puts("Time Out!");
  14. break;
  15. case 99:
  16. USART1_Puts("No Card!");
  17. break;
  18. default: USART1_Puts("unknown err");
  19. break;
  20. }
  21. SD_WriteSingleBlock(30,send_data);
  22. SD_ReadSingleBlock(30,receive_data);
  23. if(Buffercmp(send_data,receive_data,512))
  24. {
  25. USART1_Puts(" single read and write success ");
  26. //USART1_Puts(receive_data);
  27. }
  28. SD_WriteMultiBlock(50,send_data,3);
  29. SD_ReadMultiBlock(50,receive_data,3);
  30. if(Buffercmp(send_data,receive_data,1536))
  31. {
  32. USART1_Puts("multi read and write success ");
  33. //USART1_Puts(receive_data);
  34. }
  35. while(1);
  36. }

这里程序流程比较简单:

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

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

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

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

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

下面我们开始对main函数中涉及到的用户函数的层层调用详细说明

SD初始化函数SD_Init()

为使程序更简洁,故只对SD卡进行检测,放弃对MMC卡的支持(此种卡市面上已几乎不再使用,本人手上也没有这种卡,所以写出驱动程序,也没有硬件进行检测是否可用)。

下面程序是部分对SD2.0卡检测的代码,完整代码中还有对1.0版本SD卡的初始化,可下载完整代码查看。

  1. u8 SD_Init(void)
  2. {
  3. u16 i;
  4. u8 r1;
  5. u16 retry;
  6. u8 buff[6];
  7. SPI_ControlLine();
  8. //SD卡初始化时时钟不能超过400KHz
  9. SPI_SetSpeed(SPI_SPEED_LOW);
  10. //CS为低电平,片选置低,选中SD卡
  11. SD_CS_ENABLE();
  12. //纯延时,等待SD卡上电稳定
  13. for(i=0;i<0xf00;i++);
  14. //先产生至少74个脉冲,让SD卡初始化完成
  15. for(i=0;i<10;i++)
  16. {
  17. //参数可随便写,经过10次循环,产生80个脉冲
  18. SPI_ReadWriteByte(0xff);
  19. }
  20. //-----------------SD卡复位到idle状态----------------
  21. //循环发送CMD0,直到SD卡返回0x01,进入idle状态
  22. //超时则直接退出
  23. retry=0;
  24. do
  25. {
  26. //发送CMD0,CRC为0x95
  27. r1=SD_SendCommand(CMD0,0,0x95);
  28. retry++;
  29. }
  30. while((r1!=0x01)&&(retry<200));
  31. //跳出循环后,检查跳出原因,
  32. if(retry==200)//说明已超时
  33. {
  34. return 1;
  35. }
  36. //如果未超时,说明SD卡复位到idle结束
  37. //发送CMD8命令,获取SD卡的版本信息
  38. r1=SD_SendCommand(CMD8,0x1aa,0x87);
  39. //下面是SD2.0卡的初始化
  40. if(r1==0x01)
  41. {
  42. // V2.0的卡,CMD8命令后会传回4字节的数据,要跳过再结束本命令
  43. buff[0] = SPI_ReadWriteByte(0xFF);
  44. buff[1] = SPI_ReadWriteByte(0xFF);
  45. buff[2] = SPI_ReadWriteByte(0xFF);
  46. buff[3] = SPI_ReadWriteByte(0xFF);
  47. SD_CS_DISABLE();
  48. //多发8个时钟
  49. SPI_ReadWriteByte(0xFF);
  50. retry = 0;
  51. //发卡初始化指令CMD55+ACMD41
  52. do
  53. {
  54. r1 = SD_SendCommand(CMD55, 0, 0);
  55. //应返回0x01
  56. if(r1!=0x01)
  57. return r1;
  58. r1 = SD_SendCommand(ACMD41, 0x40000000, 1);
  59. retry++;
  60. if(retry>200)
  61. return r1;
  62. }
  63. while(r1!=0);
  64. //初始化指令发送完成,接下来获取OCR信息
  65. //----------鉴别SD2.0卡版本开始-----------
  66. //读OCR指令
  67. r1 = SD_SendCommand_NoDeassert(CMD58, 0, 0);
  68. //如果命令没有返回正确应答,直接退出,返回应答
  69. if(r1!=0x00)
  70. return r1;
  71. //应答正确后,会回传4字节OCR信息
  72. buff[0] = SPI_ReadWriteByte(0xFF);
  73. buff[1] = SPI_ReadWriteByte(0xFF);
  74. buff[2] = SPI_ReadWriteByte(0xFF);
  75. buff[3] = SPI_ReadWriteByte(0xFF);
  76. //OCR接收完成,片选置高
  77. SD_CS_DISABLE();
  78. SPI_ReadWriteByte(0xFF);
  79. //检查接收到的OCR中的bit30位(CSS),确定其为SD2.0还是SDHC
  80. //CCS=1:SDHC CCS=0:SD2.0
  81. if(buff[0]&0x40)
  82. {
  83. SD_Type = SD_TYPE_V2HC;
  84. }
  85. else
  86. {
  87. SD_Type = SD_TYPE_V2;
  88. }
  89. //-----------鉴别SD2.0卡版本结束-----------
  90. SPI_SetSpeed(1); //设置SPI为高速模式
  91. }
  92. }

以上函数是根据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卡。

SPI读写一字节数据

在这里,先介绍一个相对底层的函数。

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

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

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

  1. u8 SPI_ReadWriteByte(u8 TxData)
  2. {
  3. while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);
  4. SPI_I2S_SendData(SPI1,TxData);
  5. while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);
  6. return SPI_I2S_ReceiveData(SPI1);
  7. }

这个函数在所有主机与SD卡通信的函数中都会被调用到。

从SD卡中读回指定长度的数据

在SD卡读写试验中,我们会遇到很多需要读取SD卡各个寄存器数据的情况。

SD卡返回的数据长度并不都相同,所以需要一个函数来实现这个功能。

函数中多次调用了读写一字节数据的函数SPI_ReadWriteByte。

这个功能由函数 u8 SD_ReceiveData()来实现。

  1. u8 SD_ReceiveData(u8 *data, u16 len, u8 release)
  2. {
  3. u16 retry;
  4. u8 r1;
  5. //启动一次传输
  6. SD_CS_ENABLE();
  7. retry = 0;
  8. do
  9. {
  10. r1 = SPI_ReadWriteByte(0xFF);
  11. retry++;
  12. if(retry>4000) //4000次等待后没有应答,退出报错(可多试几次)
  13. {
  14. SD_CS_DISABLE();
  15. return 1;
  16. }
  17. }
  18. //等待SD卡发回数据起始令牌0xFE
  19. while(r1 != 0xFE);
  20. //跳出循环后,开始接收数据
  21. while(len--)
  22. {
  23. *data = SPI_ReadWriteByte(0xFF);
  24. data++;
  25. }
  26. //发送2个伪CRC
  27. SPI_ReadWriteByte(0xFF);
  28. SPI_ReadWriteByte(0xFF);
  29. //按需释放总线
  30. if(release == RELEASE)
  31. {
  32. SD_CS_DISABLE();
  33. SPI_ReadWriteByte(0xFF);
  34. }
  35. return 0;
  36. }

此函数有3个输入参数:

u8 * data为保存读回数据的变量

len为需要保存的的数据个数

release 为当程序结束后是否释放总线的标志。

给SD卡发送命令

在初始化函数中,我们需要做的最多的就是给SD卡发送各种命令以及接收各种响应,从而判断卡片的类型,操作条件等相关信息。

一个命令包括6个段:

SPI 读取不同长度 寄存器_【STM32】SD卡读写(四)-STM32利用SPI读写SD卡的程序详解...

给SD卡发送命令的程序有2个。

区别为一个发送完命令后失能片选,一个为发送完命令不失能片选(后续还有数据传回)。

  1. u8 SD_SendCommand(u8 cmd,u32 arg,u8 crc)
  2. {
  3. unsigned char r1;
  4. unsigned int Retry = 0;
  5. SD_CS_DISABLE();
  6. //发送8个时钟,提高兼容性
  7. SPI_ReadWriteByte(0xff);
  8. //选中SD卡
  9. SD_CS_ENABLE();
  10. //cmd参数的第二位为传输位,数值为1,所以或0x40
  11. SPI_ReadWriteByte(cmd | 0x40);
  12. //参数段第24-31位数据[31..24]
  13. SPI_ReadWriteByte((u8)(arg >> 24));
  14. //参数段第16-23位数据[23..16]
  15. SPI_ReadWriteByte((u8)(arg >> 16));
  16. //参数段第8-15位数据[15..8]
  17. SPI_ReadWriteByte((u8)(arg >> 8));
  18. //参数段第0-7位数据[7..0]
  19. SPI_ReadWriteByte((u8)arg);
  20. SPI_ReadWriteByte(crc);
  21. //等待响应或超时退出
  22. while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
  23. {
  24. Retry++;
  25. if(Retry > 800)break; //超时次数
  26. }
  27. //关闭片选
  28. SD_CS_DISABLE();
  29. //在总线上额外发送8个时钟,让SD卡完成剩下的工作
  30. SPI_ReadWriteByte(0xFF);
  31. //返回状态值
  32. return r1;
  33. }
  1. u8 SD_SendCommand_NoDeassert(u8 cmd, u32 arg,u8 crc)
  2. {
  3. unsigned char r1;
  4. unsigned int Retry = 0;
  5. SD_CS_DISABLE();
  6. //发送8个时钟,提高兼容性
  7. SPI_ReadWriteByte(0xff);
  8. //选中SD卡
  9. SD_CS_ENABLE();
  10. SPI_ReadWriteByte(cmd | 0x40);
  11. SPI_ReadWriteByte((u8)(arg >> 24));
  12. SPI_ReadWriteByte((u8)(arg >> 16));
  13. SPI_ReadWriteByte((u8)(arg >> 8));
  14. SPI_ReadWriteByte((u8)arg);
  15. SPI_ReadWriteByte(crc);
  16. //等待响应或超时退出
  17. while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
  18. {
  19. Retry++;
  20. if(Retry > 800)break;
  21. }
  22. return r1;
  23. }

以上两个函数就是根据SD卡在SPI模式下发送指令的时序编写的

取CID寄存器数据

  1. u8 SD_GetCID(u8 *cid_data)
  2. {
  3. u8 r1;
  4. //发CMD10命令,读取CID信息
  5. r1 = SD_SendCommand(CMD10, 0, 0xFF);
  6. if(r1 != 0x00)
  7. return r1; //响应错误,退出
  8. //接收16个字节的数据
  9. SD_ReceiveData(cid_data, 16, RELEASE);
  10. return 0;
  11. }

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

获取SD卡容量信息

SD卡容量的信息主要是通过查询CSD寄存器的一些相关数据,并根据数据手册进行计算得出的。

该函数虽然较为复杂,但可先精读SPI操作SD卡的理论知识篇,看懂程序的算法为何是这样实现的,也就容易理解程序的编写原理了。

  1. u32 SD_GetCapacity(void)
  2. {
  3. u8 csd[16];
  4. u32 Capacity;
  5. u8 r1;
  6. u16 i;
  7. u16 temp;
  8. //取CSD信息,如果出错,返回0
  9. if(SD_GetCSD(csd)!=0)
  10. return 0;
  11. //如果是CSD寄存器是2.0版本,按下面方式计算
  12. if((csd[0]&0xC0)==0x40)
  13. {
  14. Capacity=((u32)csd[8])<<8;
  15. Capacity+=(u32)csd[9]+1;
  16. Capacity = (Capacity)*1024;//得到扇区数
  17. Capacity*=512;//得到字节数
  18. }
  19. else//CSD寄存器是1.0版本
  20. {
  21. i = csd[6]&0x03;
  22. i<<=8;
  23. i += csd[7];
  24. i<<=2;
  25. i += ((csd[8]&0xc0)>>6);
  26. r1 = csd[9]&0x03;
  27. r1<<=1;
  28. r1 += ((csd[10]&0x80)>>7);
  29. r1+=2;
  30. temp = 1;
  31. while(r1)
  32. {
  33. temp*=2;
  34. r1--;
  35. }
  36. Capacity = ((u32)(i+1))*((u32)temp);
  37. i = csd[5]&0x0f;
  38. temp = 1;
  39. while(i)
  40. {
  41. temp*=2;
  42. i--;
  43. }
  44. //最终结果
  45. Capacity *= (u32)temp;
  46. //字节为单位
  47. }
  48. return (u32)Capacity;
  49. }

此函数计算出来的容量是Kbyte,结果除以1024就是Mbyte,再除以1024就是GByte。

2G的卡,结果可能是1.8G,8G的卡结果可能是7.6G,代表用户可用容量。

读单块block和读多块block

SD卡读单块和多块的命令分别为CMD17和CMD18,他们的参数即要读的区域的开始地址。

因为考虑到一般SD卡的读写要求地址对齐,所以一般我们都将地址转为块,并以扇区(块)(512Byte)为单位进行读写,比如读扇区0参数就为0,读扇区1参数就为1<<9(即地址512),读扇区2参数就为2<<9(即地址1024),依此类推。

读单块:

  1. u8 SD_ReadSingleBlock(u32 sector, u8 *buffer)
  2. {
  3. u8 r1;
  4. //高速模式
  5. SPI_SetSpeed(SPI_SPEED_HIGH);
  6. if(SD_Type!=SD_TYPE_V2HC)//如果不是SDHC卡
  7. {
  8. sector = sector<<9;//512*sector即物理扇区的边界对其地址
  9. }
  10. r1 = SD_SendCommand(CMD17, sector, 1);//发送CMD17 读命令
  11. if(r1 != 0x00)return r1;
  12. r1 = SD_ReceiveData(buffer, 512, RELEASE);//一个扇区为512字节
  13. if(r1 != 0)
  14. return r1; //读数据出错
  15. else
  16. return 0; //读取正确,返回0
  17. }

读多块:

  1. u8 SD_ReadMultiBlock(u32 sector, u8 *buffer, u8 count)
  2. {
  3. u8 r1;
  4. SPI_SetSpeed(SPI_SPEED_HIGH);
  5. if(SD_Type != SD_TYPE_V2HC)
  6. {
  7. sector = sector<<9;
  8. }
  9. r1 = SD_SendCommand(CMD18, sector, 1);//读多块命令
  10. if(r1 != 0x00)return r1;
  11. do//开始接收数据
  12. {
  13. if(SD_ReceiveData(buffer, 512, NO_RELEASE) != 0x00)
  14. {
  15. break;
  16. }
  17. buffer += 512;
  18. } while(--count);
  19. SD_SendCommand(CMD12, 0, 1);//全部传输完成,发送停止命令
  20. SD_CS_DISABLE();//释放总线
  21. SPI_ReadWriteByte(0xFF);
  22. if(count != 0)
  23. return count; //如果没有传完,返回剩余个数
  24. else
  25. return 0;
  26. }

写单块和写多块

SD卡用CMD24和CMD25来写单块和多块,参数的定义和读操作是一样的。

忙检测:SD卡写入数据并自编程时,数据线上读到0x00表示SD卡正忙,当读到0xff表示写操作完成。

  1. u8 SD_WaitReady(void)
  2. {
  3. u8 r1;
  4. u16 retry=0;
  5. do
  6. {
  7. r1 = SPI_ReadWriteByte(0xFF);
  8. retry++;
  9. if(retry==0xfffe)
  10. return 1;
  11. }while(r1!=0xFF);
  12. return 0;
  13. }

写单块流程:

1.发送CMD24,收到0x00表示成功

2.发送若干时钟

3.发送写单块开始字节0xFE

4.发送512个字节数据

5.发送2字节CRC(可以均为0xff)

6.连续读直到读到XXX00101表示数据写入成功

7.继续读进行忙检测(读到0x00表示SD卡正忙),当读到0xff表示写操作完成

  1. u8 SD_WriteSingleBlock(u32 sector, const u8 *data)
  2. {
  3. u8 r1;
  4. u16 i;
  5. 16 retry;
  6. //高速模式
  7. SPI_SetSpeed(SPI_SPEED_HIGH);
  8. //如果不是SDHC卡,将sector地址转为byte地址
  9. if(SD_Type!=SD_TYPE_V2HC)
  10. {
  11. sector = sector<<9;
  12. }
  13. //写扇区指令
  14. r1 = SD_SendCommand(CMD24, sector, 0x00);
  15. if(r1 != 0x00)
  16. {
  17. //应答错误,直接返回
  18. return r1;
  19. }
  20. //开始准备数据传输
  21. SD_CS_ENABLE();
  22. //先发3个空数据,等待SD卡准备好
  23. SPI_ReadWriteByte(0xff);
  24. SPI_ReadWriteByte(0xff);
  25. SPI_ReadWriteByte(0xff);
  26. //放起始令牌0xFE
  27. SPI_ReadWriteByte(0xFE);
  28. //发一个sector数据
  29. for(i=0;i<512;i++)
  30. {
  31. SPI_ReadWriteByte(*data++);
  32. }
  33. //发送2个伪CRC校验
  34. SPI_ReadWriteByte(0xff);
  35. SPI_ReadWriteByte(0xff);
  36. //等待SD卡应答
  37. r1 = SPI_ReadWriteByte(0xff);
  38. //如果为0x05说明数据写入成功
  39. if((r1&0x1F)!=0x05)
  40. {
  41. SD_CS_DISABLE();
  42. return r1;
  43. }
  44. //等待操作完成
  45. retry = 0;
  46. //卡自编程时,数据线被拉低
  47. while(!SPI_ReadWriteByte(0xff))
  48. {
  49. retry++;
  50. if(retry>65534) //如果长时间没有写入完成,退出报错
  51. {
  52. SD_CS_DISABLE();
  53. return 1; //写入超时,返回1
  54. }
  55. }
  56. //写入完成,片选置1
  57. SD_CS_DISABLE();
  58. SPI_ReadWriteByte(0xff);
  59. return 0;
  60. }

写多块流程:

1.发送CMD25,收到0x00表示成功

2.发送若干时钟

3.发送写多块开始字节0xFC

4.发送512字节数据

5.发送两个CRC(可以均为0xff)

6.连续读直到读到XXX00101表示数据写入成功

7.继续读进行忙检测,直到读到0xFF表示写操作完成

8.如果想读下一扇区重复2-7步骤

9.发送写多块停止字节0xFD来停止写操作

10.进行忙检测直到读到0xFF

  1. u8 SD_WriteMultiBlock(u32 sector, const u8 *data, u8 count)
  2. {
  3. u8 r1;
  4. u16 i;
  5. SPI_SetSpeed(SPI_SPEED_HIGH);
  6. if(SD_Type != SD_TYPE_V2HC)
  7. {
  8. sector = sector<<9;
  9. }
  10. if(SD_Type != SD_TYPE_MMC)
  11. {
  12. //启用ACMD23指令使能预擦除
  13. r1 = SD_SendCommand(ACMD23, count, 0x01);
  14. }
  15. //写多块指令CMD25
  16. r1 = SD_SendCommand(CMD25, sector, 0x01);
  17. //应答不正确,直接返回
  18. if(r1 != 0x00)return r1;
  19. //开始准备数据传输
  20. SD_CS_ENABLE();
  21. //放3个空数据让SD卡准备好
  22. SPI_ReadWriteByte(0xff);
  23. SPI_ReadWriteByte(0xff);
  24. SPI_ReadWriteByte(0xff);
  25. //下面是N个sector循环写入的部分
  26. do
  27. {
  28. //放起始令牌0xFC,表明是多块写入
  29. SPI_ReadWriteByte(0xFC);
  30. //发1个sector的数据
  31. for(i=0;i<512;i++)
  32. {
  33. SPI_ReadWriteByte(*data++);
  34. }
  35. //发2个伪CRC
  36. SPI_ReadWriteByte(0xff);
  37. SPI_ReadWriteByte(0xff);
  38. //等待SD卡回应
  39. r1 = SPI_ReadWriteByte(0xff);
  40. //0x05表示数据写入成功
  41. if((r1&0x1F)!=0x05)
  42. {
  43. SD_CS_DISABLE();
  44. return r1;
  45. }
  46. //检测SD卡忙信号
  47. if(SD_WaitReady()==1)
  48. {
  49. SD_CS_DISABLE(); //长时间写入未完成,退出
  50. return 1;
  51. }
  52. }
  53. while(--count);
  54. //发送传输结束令牌0xFD
  55. SPI_ReadWriteByte(0xFD);
  56. //等待准备好
  57. if(SD_WaitReady())
  58. {
  59. SD_CS_DISABLE();
  60. return 1;
  61. }
  62. //写入完成,片选置1
  63. SD_CS_DISABLE();
  64. SPI_ReadWriteByte(0xff);
  65. //返回count值,如果写完,则count=0,否则count=未写完的sector数
  66. return count;
  67. }

SD卡的基本读写程序就是这些,编写的思路就是由最底层的SPI 读写一字节数据的程序作为基本程序,然后根据SD卡不同时序进行相应的组合。

掌握了这个例程的读写SD卡的函数原理,就可以着手运用到FATFS文件系统了。

继续阅读