STM32利用SPI讀寫SD卡的程式詳解
SD卡的讀寫驅動程式是運用FATFS的基礎,學了FATFS就可以在SD卡上建立資料夾及檔案了。
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檔案系統了。