第37章 基於SD卡的FatFs文件系統
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
上一章我們已經全面介紹了SD卡的識別和簡單的數據讀寫,也進行了簡單的讀寫測試,不過像這樣直接操作SD卡存儲單元,在實際應用中是不現實的。SD卡一般用來存放文件,所以都需要加載文件系統到里面。類似於串行Flash芯片,我們移植FatFs文件系統到SD卡內。
對於FatFs文件系統的介紹和具體移植過程參考"基於串行Flash的FatFs文件系統",這里就不做過多介紹,重點放在SD卡與FatFs接口函數編寫上。與串行Flash的FatFs文件系統移植例程相比,FatFs文件系統部分的代碼只有diskio.c文件有所不同,其他的不用修改,所以一個簡易的移植方法是利用原來工程進行修改。下面講解利用原來工程實現SD卡的FatFs文件系統。
37.1 FatFs移植步驟
上一章我們已經完成了SD卡驅動程序以及進行了簡單的讀寫測試。該工程有很多東西是現在可以使用的,所以我們先把上一章的工程文件完整的拷貝一份,並修改文件夾名為"SDIO-FatFs移植與讀寫測試",如果此時使用KEIL軟件打開該工程,應該是編譯無錯誤並實現上一章的測試功能。
接下來,我們到串行Flash文件系統移植工程文件的"\SPI—FatFs移植與讀寫測試\User"文件夾下拷貝"FATFS"整個文件夾到現在工程文件的"\SDIO—FatFs移植與讀寫測試\User"文件夾下,如圖 371。該文件夾是FatFs文件系統的所有代碼文件,在串行Flash移植FatFs文件系統時我們對部分文件做了修改,這里主要是想要保留之前的配置,而不是使用FatFs官方源碼還需要重新配置。
圖 371 拷貝FatFs文件夾
現在就可以使用KEIL軟件打開"SDIO-FatFs移植與讀寫測試"工程文件,並把FatFs相關文件添加到工程內,同時把sdio_test.c文件移除,參考圖 372。
圖 372 FatFs工程文件結構
添加文件之后還必須打開工程選項對話框添加相關路徑,參考圖 373。
圖 373 添加FatFs路徑到工程
操作到這來,工程文件結構就算完整了,接下來就是修改文件代碼了。這來有兩個文件需要修改,為diskio.c文件和main.c文件。main.c文件內容可以參考"SPI—FatFs移植與讀寫測試"工程中的main.c文件,只有做小細節修改而已。這來重點講解diskio.c文件,也是整個移植的重點。
37.2 FatFs接口函數
FatFs文件系統與存儲設備的連接函數在diskio.c文件中,主要有5個函數需要我們編寫的。
宏定義和存儲設備狀態獲取函數
代碼清單 371 宏定義和disk_status函數
1 //宏定義
2 #define ATA 0 // SD卡
3 #define SPI_FLASH 1 // 預留外部SPI Flash使用
4 // SD卡塊大小
5 #define SD_BLOCKSIZE 512
6
7 //存儲設備狀態獲取
8 DSTATUS disk_status (
9 BYTE pdrv /* 物理編號 */
10 )
11 {
12 DSTATUS status = STA_NOINIT;
13 switch (pdrv) {
14 case ATA: /* SD CARD */
15 status &= ~STA_NOINIT;
16 break;
17
18 case SPI_FLASH: /* SPI Flash */
19 break;
20
21 default:
22 status = STA_NOINIT;
23 }
24 return status;
25 }
FatFs支持同時掛載多個存儲設備,通過定義為不同編號以區別。SD卡一般定義為編號0,編號1預留給串行Flash芯片使用。使用宏定義方式給出SD卡塊大小,方便修改。實際上,SD卡塊大小一般都是設置為512字節的,不管是標准SD卡還是高容量SD卡。
disk_status函數要求返回存儲設備的當前狀態,對於SD卡一般返回SD卡插入狀態,這里直接返回正常狀態。
存儲設備初始化函數
代碼清單 372 disk_initialize函數
1 DSTATUS disk_initialize (
2 BYTE pdrv /* 物理編號 */
3 )
4 {
5 DSTATUS status = STA_NOINIT;
6 switch (pdrv) {
7 case ATA: /* SD CARD */
8 if (SD_Init()==SD_OK) {
9 status &= ~STA_NOINIT;
10 } else {
11 status = STA_NOINIT;
12 }
13
14 break;
15
16 case SPI_FLASH: /* SPI Flash */
17 break;
18
19 default:
20 status = STA_NOINIT;
21 }
22 return status;
23 }
該函數用於初始化存儲設備,一般包括相關GPIO初始化、外設環境初始化、中斷配置等等。對於SD卡,直接調用SD_Init函數實現對SD卡初始化,如果函數返回SD_OK說明SD卡正確插入,並且控制器可以與之正常通信。
存儲設備數據讀取函數
代碼清單 373 disk_read函數
1 DRESULT disk_read (
2 BYTE pdrv, /* 設備物理編號(0..) */
3 BYTE *buff, /* 數據緩存區 */
4 DWORD sector, /* 扇區首地址 */
5 UINT count /* 扇區個數(1..128) */
6 )
7 {
8 DRESULT status = RES_PARERR;
9 SD_Error SD_state = SD_OK;
10
11 switch (pdrv) {
12 case ATA: /* SD CARD */
13 if ((DWORD)buff&3) {
14 DRESULT res = RES_OK;
15 DWORD scratch[SD_BLOCKSIZE / 4];
16
17 while (count--) {
18 res = disk_read(ATA,(void *)scratch, sector++, 1);
19
20 if (res != RES_OK) {
21 break;
22 }
23 memcpy(buff, scratch, SD_BLOCKSIZE);
24 buff += SD_BLOCKSIZE;
25 }
26 return res;
27 }
28
29 SD_state=SD_ReadMultiBlocks(buff,sector*SD_BLOCKSIZE,
30 SD_BLOCKSIZE,count);
31 if (SD_state==SD_OK) {
32 /* Check if the Transfer is finished */
33 SD_state=SD_WaitReadOperation();
34 while (SD_GetStatus() != SD_TRANSFER_OK);
35 }
36 if (SD_state!=SD_OK)
37 status = RES_PARERR;
38 else
39 status = RES_OK;
40 break;
41
42 case SPI_FLASH:
43 break;
44
45 default:
46 status = RES_PARERR;
47 }
48 return status;
49 }
disk_read函數用於從存儲設備指定地址開始讀取一定的數量的數據到指定存儲區內。對於SD卡,最重要是使用SD_ReadMultiBlocks函數讀取多塊數據到存儲區。這里需要注意的地方是SD卡數據操作是使用DMA傳輸的,並設置數據尺寸為32位大小,為實現數據正確傳輸,要求存儲區是4字節對齊。在某些情況下,FatFs提供的buff地址不是4字節對齊,這會導致DMA數據傳輸失敗,所以為保證數據傳輸正確,可以先判斷存儲區地址是否是4字節對齊,如果存儲區地址已經是4字節對齊,無需其他處理,直接使用SD_ReadMultiBlocks函數執行多塊讀取即可。如果判斷得到地址不是4字節對齊,則先申請一個4字節對齊的臨時緩沖區,即局部數組變量scratch,通過定義為DWORD類型可以使得其自動4字節對齊,scratch所占的總存儲空間也是一個塊大小,這樣把一個塊數據讀取到scratch內,然后把scratch存儲器內容拷貝到buff地址空間上就可以了。
SD_ReadMultiBlocks函數用於從SD卡內讀取多個塊數據,它有四個形參,分別為存儲區地址指針、起始塊地址、塊大小以及塊數量。為保證數據傳輸完整,還需要調用SD_WaitReadOperation函數和SD_GetStatus函數檢測和保證傳輸完成。
存儲設備數據寫入函數
代碼清單 374 disk_write函數
1 #if _USE_WRITE
2 DRESULT disk_write (
3 BYTE pdrv, /* 設備物理編號(0..) */
4 const BYTE *buff, /* 欲寫入數據的緩存區 */
5 DWORD sector, /* 扇區首地址 */
6 UINT count /* 扇區個數(1..128) */
7 )
8 {
9 DRESULT status = RES_PARERR;
10 SD_Error SD_state = SD_OK;
11
12 if (!count) {
13 return RES_PARERR; /* Check parameter */
14 }
15
16 switch (pdrv) {
17 case ATA: /* SD CARD */
18 if ((DWORD)buff&3) {
19 DRESULT res = RES_OK;
20 DWORD scratch[SD_BLOCKSIZE / 4];
21
22 while (count--) {
23 memcpy( scratch,buff,SD_BLOCKSIZE);
24 res = disk_write(ATA,(void *)scratch, sector++, 1);
25 if (res != RES_OK) {
26 break;
27 }
28 buff += SD_BLOCKSIZE;
29 }
30 return res;
31 }
32
33 SD_state=SD_WriteMultiBlocks((uint8_t *)buff,sector*SD_BLOCKSIZE,
34 SD_BLOCKSIZE,count);
35 if (SD_state==SD_OK) {
36 /* Check if the Transfer is finished */
37 SD_state=SD_WaitReadOperation();
38
39 /* Wait until end of DMA transfer */
40 while (SD_GetStatus() != SD_TRANSFER_OK);
41 }
42 if (SD_state!=SD_OK)
43 status = RES_PARERR;
44 else
45 status = RES_OK;
46 break;
47
48 case SPI_FLASH:
49 break;
50
51 default:
52 status = RES_PARERR;
53 }
54 return status;
55 }
56 #endif
disk_write函數用於向存儲設備指定地址寫入指定數量的數據。對於SD卡,執行過程與disk_read函數是非常相似,也必須先檢測存儲區地址是否是4字節對齊,如果是4字節對齊則直接調用SD_WriteMultiBlocks函數完成多塊數據寫入操作。如果不是4字節對齊,申請一個4字節對齊的臨時緩沖區,先把待寫入的數據拷貝到該臨時緩沖區內,然后才寫入到SD卡。
SD_WriteMultiBlocks函數是向SD卡寫入多個塊數據,它有四個形參,分別為存儲區地址指針、起始塊地址、塊大小以及塊數量,它與SD_ReadMultiBlocks函數執行相互過程。最后也是需要使用相關函數保存數據寫入完整才退出disk_write函數。
其他控制函數
代碼清單 375 disk_ioctl函數
1 #if _USE_IOCTL
2 DRESULT disk_ioctl (
3 BYTE pdrv, /* 物理編號 */
4 BYTE cmd, /* 控制指令 */
5 void *buff /* 寫入或者讀取數據地址指針 */
6 )
7 {
8 DRESULT status = RES_PARERR;
9 switch (pdrv) {
10 case ATA: /* SD CARD */
11 switch (cmd) {
12 // Get R/W sector size (WORD)
13 case GET_SECTOR_SIZE :
14 *(WORD * )buff = SD_BLOCKSIZE;
15 break;
16 // Get erase block size in unit of sector (DWORD)
17 case GET_BLOCK_SIZE :
18 *(DWORD * )buff = SDCardInfo.CardBlockSize;
19 break;
20
21 case GET_SECTOR_COUNT:
22 *(DWORD*)buff = SDCardInfo.CardCapacity/SDCardInfo.CardBlockSize;
23 break;
24 case CTRL_SYNC :
25 break;
26 }
27 status = RES_OK;
28 break;
29
30 case SPI_FLASH:
31 break;
32
33 default:
34 status = RES_PARERR;
35 }
36 return status;
37 }
38 #endif
disk_ioctl函數有三個形參,pdrv為設備物理編號,cmd為控制指令,包括發出同步信號、獲取扇區數目、獲取扇區大小、獲取擦除塊數量等等指令,buff為指令對應的數據指針。
對於SD卡,為支持格式化功能,需要用到獲取扇區數量(GET_SECTOR_COUNT)指令和獲取塊尺寸(GET_BLOCK_SIZE)。另外,SD卡扇區大小為512字節,串行Flash芯片一般設置扇區大小為4096字節,所以需要用到獲取扇區大小(GET_SECTOR_SIZE)指令。
至此,基於SD卡的FatFs文件系統移植就已經完成了,最重要就是diskio.c文件中5個函數的編寫。接下來就編寫FatFs基本的文件操作檢測移植代碼是否可以正確執行。
37.3 FatFs功能測試
主要的測試包括格式化測試、文件寫入測試和文件讀取測試三個部分,主要程序都在main.c文件中實現。
變量定義
代碼清單 376 變量定義
1 FATFS fs; /* FatFs文件系統對象 */
2 FIL fnew; /* 文件對象 */
3 FRESULT res_sd; /* 文件操作結果 */
4 UINT fnum; /* 文件成功讀寫數量 */
5 BYTE ReadBuffer[1024]= {0}; /* 讀緩沖區 */
6 BYTE WriteBuffer[] = /* 寫緩沖區*/
7 "歡迎使用野火STM32 F429開發板今天是個好日子,新建文件系統測試文件\r\n";
FATFS是在ff.h文件定義的一個結構體類型,針對的對象是物理設備,包含了物理設備的物理編號、扇區大小等等信息,一般我們都需要為每個物理設備定義一個FATFS變量。
FIL也是在ff.h文件定義的一個結構體類型,針對的對象是文件系統內具體的文件,包含了文件很多基本屬性,比如文件大小、路徑、當前讀寫地址等等。如果需要在同一時間打開多個文件進行讀寫,才需要定義多個FIL變量,不然一般定義一個FIL變量即可。
FRESULT是也在ff.h文件定義的一個枚舉類型,作為FatFs函數的返回值類型,主要管理FatFs運行中出現的錯誤。總共有19種錯誤類型,包括物理設備讀寫錯誤、找不到文件、沒有掛載工作空間等等錯誤。這在實際編程中非常重要,當有錯誤出現是我們要停止文件讀寫,通過返回值我們可以快速定位到錯誤發生的可能地點。如果運行沒有錯誤才返回FR_OK。
fnum是個32位無符號整形變量,用來記錄實際讀取或者寫入數據的數組。
buffer和textFileBuffer分別對應讀取和寫入數據緩存區,都是8位無符號整形數組。
主函數
代碼清單 377 main函數
1 int main(void)
2 {
3 /* 禁用WiFi模塊 */
4 BL8782_PDN_INIT();
5
6 /* 初始化LED */
7 LED_GPIO_Config();
8 LED_BLUE;
9
10 /* 初始化調試串口,一般為串口1 */
11 Debug_USART_Config();
12 printf("\r\n****** 這是一個SD卡文件系統實驗 ******\r\n");
13
14 //在外部SPI Flash掛載文件系統,文件系統掛載時會對SPI設備初始化
15 res_sd = f_mount(&fs,"0:",1);
16
17 /*----------------------- 格式化測試 ---------------------------*/
18 /* 如果沒有文件系統就格式化創建創建文件系統 */
19 if (res_sd == FR_NO_FILESYSTEM) {
20 printf("》SD卡還沒有文件系統,即將進行格式化...\r\n");
21 /* 格式化 */
22 res_sd=f_mkfs("0:",0,0);
23
24 if (res_sd == FR_OK) {
25 printf("》SD卡已成功格式化文件系統。\r\n");
26 /* 格式化后,先取消掛載 */
27 res_sd = f_mount(NULL,"0:",1);
28 /* 重新掛載 */
29 res_sd = f_mount(&fs,"0:",1);
30 } else {
31 LED_RED;
32 printf("《《格式化失敗。》》\r\n");
33 while (1);
34 }
35 } else if (res_sd!=FR_OK) {
36 printf("!!SD卡掛載文件系統失敗。(%d)\r\n",res_sd);
37 printf("!!可能原因:SD卡初始化不成功。\r\n");
38 while (1);
39 } else {
40 printf("》文件系統掛載成功,可以進行讀寫測試\r\n");
41 }
42
43 /*--------------------- 文件系統測試:寫測試 -----------------------*/
44 /* 打開文件,如果文件不存在則創建它 */
45 printf("\r\n****** 即將進行文件寫入測試... ******\r\n");
46 res_sd=f_open(&fnew,"0:FatFs讀寫測試文件.txt",FA_CREATE_ALWAYS|FA_WRITE);
47 if ( res_sd == FR_OK ) {
48 printf("》打開/創建FatFs讀寫測試文件.txt文件成功,向文件寫入數據。\r\n");
49 /* 將指定存儲區內容寫入到文件內 */
50 res_sd=f_write(&fnew,WriteBuffer,sizeof(WriteBuffer),&fnum);
51 if (res_sd==FR_OK) {
52 printf("》文件寫入成功,寫入字節數據:%d\n",fnum);
53 printf("》向文件寫入的數據為:\r\n%s\r\n",WriteBuffer);
54 } else {
55 printf("!!文件寫入失敗:(%d)\n",res_sd);
56 }
57 /* 不再讀寫,關閉文件 */
58 f_close(&fnew);
59 } else {
60 LED_RED;
61 printf("!!打開/創建文件失敗。\r\n");
62 }
63
64 /*------------------ 文件系統測試:讀測試 --------------------------*/
65 printf("****** 即將進行文件讀取測試... ******\r\n");
66 res_sd=f_open(&fnew,"0:FatFs讀寫測試文件.txt",FA_OPEN_EXISTING|FA_READ);
67 if (res_sd == FR_OK) {
68 LED_GREEN;
69 printf("》打開文件成功。\r\n");
70 res_sd = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
71 if (res_sd==FR_OK) {
72 printf("》文件讀取成功,讀到字節數據:%d\r\n",fnum);
73 printf("》讀取得的文件數據為:\r\n%s \r\n", ReadBuffer);
74 } else {
75 printf("!!文件讀取失敗:(%d)\n",res_sd);
76 }
77 } else {
78 LED_RED;
79 printf("!!打開文件失敗。\r\n");
80 }
81 /* 不再讀寫,關閉文件 */
82 f_close(&fnew);
83
84 /* 不再使用文件系統,取消掛載文件系統 */
85 f_mount(NULL,"0:",1);
86
87 /* 操作完成,停機 */
88 while (1) {
89 }
90 }
首先,調用BL8782_PDN_INIT函數禁用WiFi模塊,接下來初始化RGB彩燈和調試串口,用來指示程序進程。
FatFs的第一步工作就是使用f_mount函數掛載工作區。f_mount函數有三個形參,第一個參數是指向FATFS變量指針,如果賦值為NULL可以取消物理設備掛載。第二個參數為邏輯設備編號,使用設備根路徑表示,與物理設備編號掛鈎,在代碼清單 371中我們定義SD卡物理編號為0,所以這里使用"0:"。第三個參數可選0或1,1表示立即掛載,0表示不立即掛載,延遲掛載。 f_mount函數會返回一個FRESULT類型值,指示運行情況。
如果f_mount函數返回值為FR_NO_FILESYSTEM,說明SD卡沒有FAT文件系統。我們就必須對SD卡進行格式化處理。使用f_mkfs函數可以實現格式化操作。f_mkfs函數有三個形參,第一個參數為邏輯設備編號;第二參數可選0或者1,0表示設備為一般硬盤,1表示設備為軟盤。第三個參數指定扇區大小,如果為0,表示通過代碼清單 375中disk_ioctl函數獲取。格式化成功后需要先取消掛載原來設備,再重新掛載設備。
在設備正常掛載后,就可以進行文件讀寫操作了。使用文件之前,必須使用f_open函數打開文件,不再使用文件必須使用f_close函數關閉文件,這個跟電腦端操作文件步驟類似。f_open函數有三個形參,第一個參數為文件對象指針。第二參數為目標文件,包含絕對路徑的文件名稱和后綴名。第三個參數為訪問文件模式選擇,可以是打開已經存在的文件模式、讀模式、寫模式、新建模式、總是新建模式等的或運行結果。比如對於寫測試,使用FA_CREATE_ALWAYS和FA_WRITE組合模式,就是總是新建文件並進行寫模式。
f_close函數用於不再對文件進行讀寫操作關閉文件,f_close函數只要一個形參,為文件對象指針。f_close函數運行可以確保緩沖區完全寫入到文件內。
成功打開文件之后就可以使用f_write函數和f_read函數對文件進行寫操作和讀操作。這兩個函數用到的參數是一致的,只不過一個是數據寫入,一個是數據讀取。f_write函數第一個形參為文件對象指針,使用與f_open函數一致即可。第二個參數為待寫入數據的首地址,對於f_read函數就是用來存放讀出數據的首地址。第三個參數為寫入數據的字節數,對於f_read函數就是欲讀取數據的字節數。第四個參數為32位無符號整形指針,這里使用fnum變量地址賦值給它,在運行讀寫操作函數后,fnum變量指示成功讀取或者寫入的字節個數。
最后,不再使用文件系統時,使用f_mount函數取消掛載。
下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發板。程序開始運行后,RGB彩燈為藍色,在串口調試助手可看到格式化測試、寫文件檢測和讀文件檢測三個過程;最后如果所有讀寫操作都正常,RGB彩燈會指示為綠色,如果在運行中FatFs出現錯誤RGB彩燈指示為紅色。正確執行例程程序后可以使用讀卡器將SD卡在電腦端打開,我們可以在SD卡根目錄下看到"FatFs讀寫測試文件.txt"文件,這與程序設計是相吻合的。
37.4 每課一問
3. 根據"基於串行Flash的FatFs文件系統"章節內容,將FatFs功能使用實驗移植到SD卡上運行。