【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("\\r\\nSD Card Init Success!\\r\\n");
  11. break;
  12. case 1:
  13. USART1_Puts("Time Out!\\n");
  14. break;
  15. case 99:
  16. USART1_Puts("No Card!\\n");
  17. break;
  18. default: USART1_Puts("unknown err\\n");
  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("\\r\\n single read and write success \\r\\n");
  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("\\r\\nmulti read and write success \\r\\n");
  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個段:


【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. /*按照SD卡的命令序列開始發送命令 */
  11. //cmd參數的第二位為傳輸位,數值為1,所以或0x40
  12. SPI_ReadWriteByte(cmd | 0x40);
  13. //參數段第24-31位數據[31..24]
  14. SPI_ReadWriteByte((u8)(arg >> 24));
  15. //參數段第16-23位數據[23..16]
  16. SPI_ReadWriteByte((u8)(arg >> 16));
  17. //參數段第8-15位數據[15..8]
  18. SPI_ReadWriteByte((u8)(arg >> 8));
  19. //參數段第0-7位數據[7..0]
  20. SPI_ReadWriteByte((u8)arg);
  21. SPI_ReadWriteByte(crc);
  22. //等待響應或超時退出
  23. while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
  24. {
  25. Retry++;
  26. if(Retry > 800)break; //超時次數
  27. }
  28. //關閉片選
  29. SD_CS_DISABLE();
  30. //在總線上額外發送8個時鐘,讓SD卡完成剩下的工作
  31. SPI_ReadWriteByte(0xFF);
  32. //返回狀態值
  33. return r1;
  34. }
  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. /* 按照SD卡的命令序列開始發送命令 */
  11. SPI_ReadWriteByte(cmd | 0x40);
  12. SPI_ReadWriteByte((u8)(arg >> 24));
  13. SPI_ReadWriteByte((u8)(arg >> 16));
  14. SPI_ReadWriteByte((u8)(arg >> 8));
  15. SPI_ReadWriteByte((u8)arg);
  16. SPI_ReadWriteByte(crc);
  17. //等待響應或超時退出
  18. while((r1 = SPI_ReadWriteByte(0xFF))==0xFF)
  19. {
  20. Retry++;
  21. if(Retry > 800)break;
  22. }
  23. return r1;
  24. }

以上兩個函數就是根據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文件系統了。


分享到:


相關文章: