1)實驗平台:正點原子STM32mini開發闆
2)摘自《正點原子STM32 不完全手冊(HAL 庫版)》關注官方微信号公衆号,擷取更多資料:正點原子

第三十三章 SD 卡實驗
很多單片機系統都需要大容量儲存設備,以存儲資料。目前常用的有 U 盤,FLASH 晶片,
SD 卡等。他們各有優點,綜合比較,最适合單片機系統的莫過于 SD 卡了,它不僅容量可以做
到很大(32Gb 以上),而且支援 SPI 接口,友善移動,并且有幾種體積的尺寸可供選擇(标準
的 SD 卡尺寸,以及 TF 卡尺寸等),能滿足不同應用的要求。
隻需要 4 個 IO 口即可外擴一個最大達 32GB 以上的外部存儲器,容量從幾十 M 到幾十 G
選擇尺度很大,更換也很友善,程式設計也簡單,是單片機大容量外部存儲器的首選。
ALIENTKE MiniSTM32 開發闆自帶了标準的 SD 卡接口(在背面),可使用 STM32 自帶的
SPI 接口驅動,本章我們使用 SPI 驅動,最高通信速度可達 18Mbps,每秒可傳輸資料 2M 位元組
以上,對于一般應用足夠了。在本章中,我們将向大家介紹,如何在 ALIENTEK MiniSTM32
開發闆上實作 SD 卡的讀取。本章分為如下幾個部分:
33.1 SD 卡簡介
33.2 硬體設計
33.3 軟體設計
33.4 下載下傳驗證
33.1 SD 卡簡介
SD 卡(Secure Digital Memory Card)中文翻譯為安全數位卡,它是在 MMC 的基礎上發
展而來,是一種基于半導體快閃記憶器的新一代記憶裝置,它被廣泛地于便攜式裝置上使用,
例如數位相機、個人數位助理(PDA)和多媒體播放器等。SD 卡由日本松下、東芝及美國 SanDisk
公司于 1999 年 8 月共同開發研制。大小猶如一張郵票的 SD 記憶卡,重量隻有 2 克,但卻擁
有高記憶容量、快速資料傳輸率、極大的移動靈活性以及很好的安全性。按容量分類,可以将
SD 卡分為 3 類:SD 卡、SDHC 卡、SDXC 卡。如表 33.1.1 所示:
表 33.1.1 SD 卡按容量分類
SD 卡和 SDHC 卡協定基本相容,但是 SDXC 卡,同這兩者差別就比較大了,本章我們讨
論的主要是 SD/SDHC 卡(簡稱 SD 卡)。
SD 卡一般支援 2 種操作模式:
1,SD 卡模式(通過 SDIO 通信);
2,SPI 模式;
主機可以選擇以上任意一種模式同 SD 卡通信,SD 卡模式允許 4 線的高速資料傳輸。SPI
模式允許簡單的通過 SPI 接口來和 SD 卡通信,這種模式同 SD 卡模式相比就是喪失了速度。
SD 卡的引腳排序如下圖 33.1.1 所示:
圖 33.1.1 SD 卡引腳排序圖
SD 卡引腳功能描述如表 33.1.2 所示:
表 33.1.2 SD 卡引腳功能表
SD 卡隻能使用 3.3V 的 IO 電平,是以,MCU 一定要能夠支援 3.3V 的 IO 端口輸出。注意:
在 SPI 模式下,CS/MOSI/MISO/CLK 都需要加 10~100K 左右的上拉電阻。
SD 卡有 5 個寄存器,如表 33.1.3 所示:
表 33.1.3 SD 卡相關寄存器
關于這些寄存器的較長的描述,請參考CD光牒相關 SD 卡資料。我們在這裡就不描述了。接下
來,我們看看 SD 卡的指令格式,如表 33.1.4 所示:
表 33.1.4 SD 卡指令格式
SD 卡的指令由 6 個位元組組成,位元組 1 的最高 2 位固定為 01,低 6 位為指令号(比如 CMD16,
為 10000B 即 16 進制的 0X10,完整的 CMD16,第一個位元組為 01010000,即 0X10+0X40)。
位元組 2~5 為指令參數,有些指令是沒有參數的。
位元組 6 的高七位為 CRC 值,最低位恒定為 1。
SD 卡的指令總共有 12 類,分為 Class0~Class11,本章,我們僅介紹幾個比較重要的指令,
如表 33.1.5 所示:
表 33.1.5 SD 卡部分指令
上表中,大部分的指令是初始化的時候用的。表中的 R1、R3 和 R7 等是 SD 卡的回應,SD
卡和單片機的通信采用發送應答機制,如圖 33.1.2 所示:
圖 33.1.2 SD 卡指令傳輸過程
每發送一個指令,SD 卡都會給出一個應答,以告知主機該指令的執行情況,或者傳回主
機需要擷取的資料。SPI 模式下,SD 卡針對不同的指令,應答可以是 R1~R7,R1 的應答,各位描述如表 33.1.6 所示:
表 33.1.6 R1 響應各位描述
R2~R7 的響應,我們就不介紹了,請的大家參考 SD 卡 2.0 協定。接下來,我們看看 SD 卡
初始化過程。因為我們使用的是 SPI 模式,是以先得讓 SD 卡進入 SPI 模式。方法如下:在 SD
卡收到複位指令(CMD0)時,CS 為有效電平(低電平)則 SPI 模式被啟用。不過在發送 CMD0
之前,要發送>74 個時鐘,這是因為 SD 卡内部有個供電電壓上升時間,大概為 64 個 CLK,剩
下的 10 個 CLK 用于 SD 卡同步,之後才能開始 CMD0 的操作,在卡初始化的時候,CLK 時鐘
最大不能超過 400Khz!。
接着我們看看 SD 卡的初始化,SD 卡的典型初始化過程如下:
1、初始化與 SD 卡連接配接的硬體條件(MCU 的 SPI 配置,IO 口配置);
2、上電延時(>74 個 CLK);
3、複位卡(CMD0),進入 IDLE 狀态;
4、發送 CMD8,檢查是否支援 2.0 協定;
5、根據不同協定檢查 SD 卡(指令包括:CMD55、CMD41、CMD58 和 CMD1 等);
6、取消片選,發多 8 個 CLK,結束初始化
這樣我們就完成了對 SD 卡的初始化,注意末尾發送的 8 個 CLK 是提供 SD 卡額外的時鐘,
完成某些操作。通過 SD 卡初始化,我們可以知道 SD 卡的類型(V1、V2、V2HC 或者 MMC),
在完成了初始化之後,就可以開始讀寫資料了。
SD 卡讀取資料,這裡通過 CMD17 來實作,具體過程如下:
1、發送 CMD17;
2、接收卡響應 R1;
3、接收資料起始令牌 0XFE;
4、接收資料;
5、接收 2 個位元組的 CRC,如果不使用 CRC,這兩個位元組在讀取後可以丢掉。
6、禁止片選之後,發多 8 個 CLK;
以上就是一個典型的讀取 SD 卡資料過程,SD 卡的寫于讀資料差不多,寫資料通過 CMD24
來實作,具體過程如下:
1、發送 CMD24;
2、接收卡響應 R1;
3、發送寫資料起始令牌 0XFE;
4、發送資料;
5、發送 2 位元組的僞 CRC;
6、禁止片選之後,發多 8 個 CLK;
以上就是一個典型的寫 SD 卡過程。關于 SD 卡的介紹,我們就介紹到這裡,更詳細的介
紹請參考CD光牒資料→ 7,硬體資料→SD 卡資料→ SD 卡 V2.0 協定。
33.2 硬體設計
本章實驗功能簡介:開機的時候先初始化 SD 卡,如果 SD 卡初始化完成,則提示 LCD 初
始化成功。按下 KEY0,讀取 SD 卡扇區 0 的資料,然後通過序列槽發送到電腦。如果沒初始化
通過,則在 LCD 上提示初始化失敗。 同樣用 DS0 來訓示程式正在運作。
本實驗用到的硬體資源有:
1) 訓示燈 DS0
2) KEY0 按鍵
3) 序列槽
4) TFTLCD 子產品
5) SD 卡
前面四部分,在之前的執行個體已經介紹過了,這裡我們介紹一下 MiniSTM32 開發闆闆載的
SD 卡接口和 STM32 的連接配接關系,如圖 33.2.1 所示:
圖33.2.1 SD卡接口與STM32連接配接原理圖
從圖中可以看出,SD卡通過4根信号線與STM32連接配接,SD卡的片選(SD_CS)連接配接PA3,
SD卡的SPI接口,連接配接在STM32的SPI1上面,硬體連接配接就這麼簡單,這裡要注意的是SPI1被3
個外設共用了:SD卡、W25Q64和NRF24L01,在使用SD卡的時候,必須禁止其他外設的片
選,以防幹擾。
33.3 軟體設計
打開上一章的工程,由于本章還需要用到SPI功能,是以,先添加spi.c。然後,在HARDWARE
檔案夾下建立一個 SD 的檔案夾。然後建立一個 MMC_SD.C 和 MMC_SD.H 的檔案儲存在 SD
檔案夾下,并将這個檔案夾加入頭檔案包含路徑。
打開 MMC_SD.C 檔案,在該檔案裡面,我們輸入與 SD 卡相關的操作代碼,這裡由于篇
幅限制,我們不貼出所有代碼,僅介紹兩個最重要的函數,第一個是 SD_Initialize 函數,該函
數源碼如下:
//初始化 SD 卡
u8 SD_Init(void)
{
u8 r1;
// 存放 SD 卡的傳回值
u16 retry;
// 用來進行逾時計數
u8 buf[4];
u16 i;
SD_SPI_Init();
//初始化 IO
SD_SPI_SpeedLow(); //設定到低速模式
for(i=0;i<10;i++)SD_SPI_ReadWriteByte(0XFF);//發送最少 74 個脈沖
retry=20;
do
{
r1=SD_SendCmd(CMD0,0,0x95);//進入 IDLE 狀态
}while((r1!=0X01) && retry--);
SD_Type=0;//預設無卡
if(r1==0X01)
{
if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0
{
for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);
//Get trailing return value of R7 resp
if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支援 2.7~3.6V
{
retry=0XFFFE;
do
{
SD_SendCmd(CMD55,0,0X01); //發送 CMD55
r1=SD_SendCmd(CMD41,0x40000000,0X01);//發送 CMD41
}while(r1&&retry--);
if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//鑒别 SD2.0 卡版本開始
{
for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);//得到 OCR 值
if(buf[0]&0x40)SD_Type=SD_TYPE_V2HC; //檢查 CCS
else SD_Type=SD_TYPE_V2;
}
}
}else//SD V1.x/ MMC V3
{
SD_SendCmd(CMD55,0,0X01);
//發送 CMD55
r1=SD_SendCmd(CMD41,0,0X01);
//發送 CMD41
if(r1<=1)
{
SD_Type=SD_TYPE_V1;
retry=0XFFFE;
do //等待退出 IDLE 模式
{
SD_SendCmd(CMD55,0,0X01); //發送 CMD55
r1=SD_SendCmd(CMD41,0,0X01);//發送 CMD41
}while(r1&&retry--);
}else//MMC 卡不支援 CMD55+CMD41 識别
{
SD_Type=SD_TYPE_MMC;//MMC V3
retry=0XFFFE;
do //等待退出 IDLE 模式
{
r1=SD_SendCmd(CMD1,0,0X01);//發送 CMD1
}while(r1&&retry--);
}
if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)SD_Type=SD_TYPE_ERR;
//錯誤的卡
}
}
SD_DisSelect();//取消片選
SD_SPI_SpeedHigh();//高速
if(SD_Type)return 0;
else if(r1)return r1;
return 0xaa;//其他錯誤
}
該函數先設定與 SD 相關的 IO 口及 SPI 初始化,然後發送 CMD0,進入 IDLE 狀态,并設定
SD 卡為 SPI 模式通信,然後判斷 SD 卡類型,完成 SD 卡的初始化,注意該函數調用的 SD_SPI_Init
等函數,實際是對 SPI1 的相關函數進行了一層封裝,友善移植。另外一個要介紹的函數是
SD_ReadDisk,該函數用于從 SD 卡讀取一個扇區的資料(這裡一般為 512 位元組),該函數代碼如
下:
//讀 SD 卡
//buf:資料緩存區
//sector:扇區
//cnt:扇區數
//傳回值:0,ok;其他,失敗.
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt)
{
u8 r1;
if(SD_Type!=SD_TYPE_V2HC)sector <<= 9;//轉換為位元組位址
if(cnt==1)
{
r1=SD_SendCmd(CMD17,sector,0X01);//讀指令
if(r1==0) r1=SD_RecvData(buf,512);//指令發送成功,接收 512 個位元組
}else
{
r1=SD_SendCmd(CMD18,sector,0X01);//連續讀指令
do
{
r1=SD_RecvData(buf,512); //接收 512 個位元組
buf+=512;
}while(--cnt && r1==0);
SD_SendCmd(CMD12,0,0X01); //發送停止指令
}
SD_DisSelect();//取消片選
return r1;
}
此函數根據要讀取扇區的多少,發送 CMD17/CMD18 指令,然後讀取一個/多個扇區的資料,
詳細見代碼,這裡我們就不多介紹了。儲存 MMC_SD.C 檔案,并加入到 HARDWARE 組下,
然後打開 MMC_SD.H,在該檔案裡面輸入如下代碼:
#ifndef _MMC_SD_H_
#define _MMC_SD_H_
#include "sys.h"
#include
// SD 卡類型定義
#define SD_TYPE_ERR 0X00
#define SD_TYPE_MMC
0X01
#define SD_TYPE_V1 0X02
#define SD_TYPE_V2 0X04
#define SD_TYPE_V2HC 0X06
// SD 卡指令表
#define CMD0 0 //卡複位
#define CMD1 1
#define CMD8 8 //指令 8 ,SEND_IF_COND
#define CMD9 9 //指令 9 ,讀 CSD 資料
#define CMD10 10 //指令 10,讀 CID 資料
#define CMD12 12 //指令 12,停止資料傳輸
#define CMD16 16 //指令 16,設定 SectorSize 應傳回 0x00
#define CMD17 17 //指令 17,讀 sector
#define CMD18 18 //指令 18,讀 Multi sector
#define CMD23 23 //指令 23,設定多 sector 寫入前預先擦除 N 個 block
#define CMD24 24 //指令 24,寫 sector
#define CMD25 25 //指令 25,寫 Multi sector
#define CMD41 41 //指令 41,應傳回 0x00
#define CMD55 55 //指令 55,應傳回 0x01
#define CMD58 58 //指令 58,讀 OCR 資訊
#define CMD59 59 //指令 59,使能/禁止 CRC,應傳回 0x00
//資料寫入回應字意義
#define MSD_DATA_OK
0x05
#define MSD_DATA_CRC_ERROR 0x0B
#define MSD_DATA_WRITE_ERROR 0x0D
#define MSD_DATA_OTHER_ERROR 0xFF
//SD 卡回應标記字
#define MSD_RESPONSE_NO_ERROR 0x00
#define MSD_IN_IDLE_STATE
0x01
#define MSD_ERASE_RESET
0x02
#define MSD_ILLEGAL_COMMAND 0x04
#define MSD_COM_CRC_ERROR 0x08
#define MSD_ERASE_SEQUENCE_ERROR 0x10
#define MSD_ADDRESS_ERROR 0x20
#define MSD_PARAMETER_ERROR 0x40
#define MSD_RESPONSE_FAILURE 0xFF
//這部分應根據具體的連線來修改!
//MiniSTM32 開發闆使用的是 PA3 作為 SD 卡的 CS 腳.
#define SD_CS PAout(3)
//SD 卡片選引腳
u8 SD_SPI_ReadWriteByte(u8 data);
void SD_SPI_SpeedLow(void);
void SD_SPI_SpeedHigh(void);
u8 SD_WaitReady(void);
//等待 SD 卡準備
u8 SD_GetResponse(u8 Response);
//獲得相應
u8 SD_Initialize(void);
//初始化
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt);
//讀塊
u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt);
//寫塊
u32 SD_GetSectorCount(void);
//讀扇區數
u8 SD_GetCID(u8 *cid_data); //讀 SD 卡 CID
u8 SD_GetCSD(u8 *csd_data); //讀 SD 卡 CSD
#endif
該部分代碼主要是一些指令的宏定義以及函數聲明,在這裡我們設定了 SD 卡的 CS 管腳為
PA3。儲存 MMC_SD.H,就可以在主函數裡面編寫我們的應用代碼了,打開 main.c,輸入如下代
碼:
//讀取 SD 卡的指定扇區的内容,并通過序列槽 1 輸出
//sec:扇區實體位址編号
void SD_Read_Sectorx(u32 sec)
{
u8 *buf;
u16 i;
buf=mymalloc(512);
//申請記憶體
if(SD_ReadDisk(buf,sec,1)==0) //讀取 0 扇區的内容
{
LCD_ShowString(60,190,200,16,16,"USART1 Sending Data...");
printf("SECTOR 0 DATA:");
for(i=0;i<512;i++)printf("%x ",buf[i]);//列印 sec 扇區資料
printf("DATA ENDED");
LCD_ShowString(60,190,200,16,16,"USART1 Send Data Over!");
}
myfree(buf);//釋放記憶體
}
int main(void)
{
u8 key; u8 t=0;
u32 sd_size;
HAL_Init();
//初始化 HAL 庫
Stm32_Clock_Init(RCC_PLL_MUL9); //設定時鐘,72M
delay_init(72);
//初始化延時函數
uart_init(115200);
//初始化序列槽
LED_Init();
//初始化 LED
KEY_Init();
//初始化按鍵
LCD_Init();
//初始化 LCD
mem_init();
//初始化記憶體池
POINT_COLOR=RED;
//設定字型為紅色
LCD_ShowString(60,50,200,16,16,"Mini STM32");
LCD_ShowString(60,70,200,16,16,"SD CARD TEST");
LCD_ShowString(60,90,200,16,16,"[email protected]");
LCD_ShowString(60,110,200,16,16,"2019/11/16");
LCD_ShowString(60,130,200,16,16,"KEY0:Read Sector 0");
while(SD_Initialize())//檢測不到 SD 卡
{
LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(500);
LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500);
LED0=!LED0;//DS0 閃爍
}
POINT_COLOR=BLUE;//設定字型為藍色
//檢測 SD 卡成功
LCD_ShowString(60,150,200,16,16,"SD Card OK ");
LCD_ShowString(60,170,200,16,16,"SD Card Size: MB");
sd_size=SD_GetSectorCount();//得到扇區數
LCD_ShowNum(164,170,sd_size>>11,5,16);//顯示 SD 卡容量
while(1)
{
key=KEY_Scan(0);
if(key==KEY0_PRES)SD_Read_Sectorx(0);//KEY0 按,讀取 SD 卡扇區 0 的内容
delay_ms(10);t++;
if(t==20) { LED0=!LED0; t=0; }
}
}
這裡總共 2 個函數,其中 SD_Read_Sectorx 用于讀取 SD 卡指定扇區的資料,并将讀到的
資料通過序列槽 1 輸出。然後 main 函數則通過 SD_GetSectorCount 函數來得到 SD 卡的扇區數,
間接得到 SD 卡容量,然後在液晶上顯示出來,接着我們通過按鍵 KEY0 控制讀取 SD 卡的扇區 0,
然後把讀到的資料通過序列槽列印出來。另外,我們對上一章學過的記憶體管理小試牛刀,稍微用
了下,以後我們會盡量使用記憶體管理來設計。
最後,我們将 SD_Read_Sectorx 函數加入 USMART 控制,這樣,我們就可以通過序列槽調
試助手,讀取 SD 卡任意一個扇區的資料,友善大家測試。
33.4 下載下傳驗證
在代碼編譯成功之後,我們通過下載下傳代碼到 ALIENTEK MiniSTM32 開發闆上,可以看到
LCD 顯示如圖 33.4.1 所示的内容(預設 SD 卡已經接上了):
圖 33.4.1 程式運作效果圖
打開序列槽調試助手,按下 KEY0 就可以看到從開發闆發回來的資料了,如圖 33.4.2 所示:
圖 33.4.2 序列槽收到的 SD 卡扇區 0 内容
這裡請大家注意,不同的 SD 卡,讀出來的扇區 0 是不盡相同的,是以不要因為你讀出來
的資料和圖 33.4.2 不同而感到驚訝。