1. 程式人生 > >STM32利用SPI讀寫SD卡的程式詳解

STM32利用SPI讀寫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);
}

這裡程式流程比較簡單:

1)配置串列埠,用作程式的除錯輸出

2)填充將要給SD卡寫入資料的陣列send_data。

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

4)單塊讀寫實驗,並比對讀寫出的資料是否相同。

5)多塊讀寫實驗,並比對讀寫出的資料是否相同。

下面我們開始對main函式中涉及到的使用者函式的層層呼叫詳細說明

SD初始化函式SD_Init()

為使程式更簡潔,故只對SD卡進行檢測,放棄對MMC卡的支援(此種卡市面上已幾乎不再使用,本人手上也沒有這種卡,所以寫出驅動程式,也沒有硬體進行檢測是否可用)。

下面程式是部分對SD2.0卡檢測的程式碼,完整程式碼中還有對1.0版本SD卡的初始化,可下載完整程式碼檢視。

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為高速模式
 }
}

以上函式是根據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)作為函式的引數。

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);
}

這個函式在所有主機與SD卡通訊的函式中都會被呼叫到。

從SD卡中讀回指定長度的資料

在SD卡讀寫試驗中,我們會遇到很多需要讀取SD卡各個暫存器資料的情況。

SD卡返回的資料長度並不都相同,所以需要一個函式來實現這個功能。

函式中多次呼叫了讀寫一位元組資料的函式SPI_ReadWriteByte。

這個功能由函式 u8 SD_ReceiveData()來實現。

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;
}

此函式有3個輸入引數:

u8 * data為儲存讀回資料的變數

len為需要儲存的的資料個數

release 為當程式結束後是否釋放匯流排的標誌。

給SD卡傳送命令

在初始化函式中,我們需要做的最多的就是給SD卡傳送各種命令以及接收各種響應,從而判斷卡片的型別,操作條件等相關資訊。

一個命令包括6個段:

給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;		
}
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;
}

以上兩個函式就是根據SD卡在SPI模式下發送指令的時序編寫的

取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資料。

獲取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;
}

此函式計算出來的容量是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),依此類推。

讀單塊:

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
}

讀多塊:

u8 SD_ReadMultiBlock(u32 sector, u8 *buffer, u8 count)
{
  u8 r1;	 			 
  SPI_SetSpeed(SPI_SPEED_HIGH);
  if(SD_Type != SD_TYPE_V2HC)
  {
    	sector = sector<<9;
  } 
  r1 = SD_SendCommand(CMD18, sector, 1);		//讀多塊命令
  if(r1 != 0x00)	return r1;	 
  do	//開始接收資料 
  {
    	if(SD_ReceiveData(buffer, 512, NO_RELEASE) != 0x00)
    	{
       		break;
    	}
    	buffer += 512;
  	      } 	while(--count);		 
  
  SD_SendCommand(CMD12, 0, 1);	//全部傳輸完成,傳送停止命令
  SD_CS_DISABLE();	//釋放匯流排
  SPI_ReadWriteByte(0xFF);    
  if(count != 0)
    return count;   //如果沒有傳完,返回剩餘個數
  else 
    return 0;	
}

寫單塊和寫多塊

SD卡用CMD24和CMD25來寫單塊和多塊,引數的定義和讀操作是一樣的。

忙檢測:SD卡寫入資料並自程式設計時,資料線上讀到0x00表示SD卡正忙,當讀到0xff表示寫操作完成。

u8 SD_WaitReady(void)
{
  u8 r1;
  u16 retry=0;
  do
  {
    r1 = SPI_ReadWriteByte(0xFF);
    retry++;
    if(retry==0xfffe)
    	return 1; 
  }while(r1!=0xFF); 
    	return 0;
}

寫單塊流程

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

2.傳送若干時鐘

3.傳送寫單塊開始位元組0xFE

4.傳送512個位元組資料

5.傳送2位元組CRC(可以均為0xff)

6.連續讀直到讀到XXX00101表示資料寫入成功

7.繼續讀進行忙檢測(讀到0x00表示SD卡正忙),當讀到0xff表示寫操作完成

u8 SD_WriteSingleBlock(u32 sector, const u8 *data)
{
  	u8 r1;
  	u16 i;
  	16 retry;
        //高速模式
  	SPI_SetSpeed(SPI_SPEED_HIGH);
	//如果不是SDHC卡,將sector地址轉為byte地址
        if(SD_Type!=SD_TYPE_V2HC)		
  	{
     		sector = sector<<9;
  	}
	//寫扇區指令
        r1 = SD_SendCommand(CMD24, sector, 0x00);	
  	if(r1 != 0x00)
  	{
		//應答錯誤,直接返回
    		return r1;  
  	}
	//開始準備資料傳輸
        SD_CS_ENABLE();	
  	//先發3個空資料,等待SD卡準備好
  	SPI_ReadWriteByte(0xff);
  	SPI_ReadWriteByte(0xff);
  	SPI_ReadWriteByte(0xff);
  	//放起始令牌0xFE
        SPI_ReadWriteByte(0xFE);
        //發一個sector資料
  	for(i=0;i<512;i++)
  	{
     		SPI_ReadWriteByte(*data++);
  	}
  	//傳送2個偽CRC校驗
  	SPI_ReadWriteByte(0xff);
  	SPI_ReadWriteByte(0xff);
  	//等待SD卡應答
  	r1 = SPI_ReadWriteByte(0xff);
	//如果為0x05說明資料寫入成功
  	if((r1&0x1F)!=0x05)		
  	{
     		SD_CS_DISABLE();
     		return r1;
  	}
    	//等待操作完成
  	retry = 0;
        //卡自程式設計時,資料線被拉低
  	while(!SPI_ReadWriteByte(0xff))		
  	{
     		retry++;
     		if(retry>65534)        //如果長時間沒有寫入完成,退出報錯
     		{
        		SD_CS_DISABLE();
        		return 1;           //寫入超時,返回1
     		}
  	}
        //寫入完成,片選置1
  	SD_CS_DISABLE();
  	SPI_ReadWriteByte(0xff);
        return 0;
}

寫多塊流程:

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

2.傳送若干時鐘

3.傳送寫多塊開始位元組0xFC

4.傳送512位元組資料

5.傳送兩個CRC(可以均為0xff)

6.連續讀直到讀到XXX00101表示資料寫入成功

7.繼續讀進行忙檢測,直到讀到0xFF表示寫操作完成

8.如果想讀下一扇區重複2-7步驟

9.傳送寫多塊停止位元組0xFD來停止寫操作

10.進行忙檢測直到讀到0xFF

u8 SD_WriteMultiBlock(u32 sector, const u8 *data, u8 count)
{
  	u8 r1;
  	u16 i;	 		 
  	SPI_SetSpeed(SPI_SPEED_HIGH); 
  	if(SD_Type != SD_TYPE_V2HC)
        {
    	    sector = sector<<9;
        }
  	if(SD_Type != SD_TYPE_MMC) 
        {
	      //啟用ACMD23指令使能預擦除
    	     r1 = SD_SendCommand(ACMD23, count, 0x01);		 
        }  
        //寫多塊指令CMD25
        r1 = SD_SendCommand(CMD25, sector, 0x01);	
        //應答不正確,直接返回
  	if(r1 != 0x00)	return r1;	
	//開始準備資料傳輸
  	SD_CS_ENABLE();
	//放3個空資料讓SD卡準備好		
	SPI_ReadWriteByte(0xff);	
  	SPI_ReadWriteByte(0xff); 
        SPI_ReadWriteByte(0xff);   
  	//下面是N個sector迴圈寫入的部分
  	do
  	{
    	    //放起始令牌0xFC,表明是多塊寫入
     	    SPI_ReadWriteByte(0xFC);	  
     	    //發1個sector的資料
     	    for(i=0;i<512;i++)
     	    {
        	SPI_ReadWriteByte(*data++);
     	    }
     	    //發2個偽CRC
	    SPI_ReadWriteByte(0xff);
    	    SPI_ReadWriteByte(0xff);
            //等待SD卡迴應
     	    r1 = SPI_ReadWriteByte(0xff);
	    //0x05表示資料寫入成功
     	    if((r1&0x1F)!=0x05)			
     	    {
        	SD_CS_DISABLE();    
        	return r1;
     	    }
            //檢測SD卡忙訊號
     	    if(SD_WaitReady()==1)
     	   {
        	SD_CS_DISABLE();    //長時間寫入未完成,退出
        	return 1;
     	   }	   
       } 
       while(--count);	
       //傳送傳輸結束令牌0xFD
       SPI_ReadWriteByte(0xFD);	
       //等待準備好   
       if(SD_WaitReady()) 		
       {
           SD_CS_DISABLE();
           return 1;  
        }
       //寫入完成,片選置1
       SD_CS_DISABLE();
       SPI_ReadWriteByte(0xff);
       //返回count值,如果寫完,則count=0,否則count=未寫完的sector數  
       return count;   
}

SD卡的基本讀寫程式就是這些,編寫的思路就是由最底層的SPI 讀寫一位元組資料的程式作為基本程式,然後根據SD卡不同時序進行相應的組合。

想細緻研究STM32的SPI在這個例程中的配置還是需要下載原始碼仔細閱讀的。

掌握了這個例程的讀寫SD卡的函式原理,下一步就可以著手運用到FATFS檔案系統了。