http://www.amobbs.com/forum.php?mod=viewthread&tid=5464257&highlight=STM32%2BFatFS%2B%E7%A7%BB%E6%A4%8D%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB
前言與廢話
做項目時網找資料,不會的東西上網查閱一下多半可以解決,一些尚未解決的問題也會有所啟發。最近由於項目的需要,仔細閱讀了SD卡相關內容,順藤摸瓜學習FatFS。網上關於SD卡和FatFS的內容非常的多,重復的部分我就不介紹了,我把移植和使用部分的經驗和大家分享一下。
剛開始的時候,我找來一些現成的代碼研究一下,不用說看的是一頭霧水。看FatFS示例代碼,也不知如何移植。最后還是下定決心,慢慢的閱讀FatFS的相關文檔和范例代碼,對於移植部分一點一點的研究,相信一定會有所收獲。
一、硬件准備
開始移植之前,你必須要有一塊SD卡。從形狀上來說,有普通的SD卡,有很小的microSD卡,microSD卡就是手機中長見的TF卡。購買microSD卡的時候,往往會附帶一個SD卡套,那么小個頭的microSD卡就變成了普通的SD卡,接口都是一樣的。
但是還是您注意了,建議大家購買2G以下的SD卡(如果可以的話,買個128M的SD卡就可以達到實驗的效果,價格也非常便宜)。剛開始移植的時候,我使用了4G的SD卡,但是發現程序無法完成SD卡的初始化。查閱網上相關的資料,發現SD卡技術已2G作為分界線,大於或者等於4G的卡屬於高速SD卡,和小於或者等於4G的SD卡略有區別。
二、軟件准備
在進行移植之前,先編寫一些最簡單的STM32程序。在調試之前,我都會完成USART的初始化和發送函數,通過串口把STM32的運行狀態打印出來,這樣配合Jlink硬件調試,可以很快的找到錯誤。由於SD卡可以使用SPI進行讀寫操作,所以還需要完成SPI的初始化工作。
先來說一下USART的操作,我個人比較喜歡使用系統的printf函數,所以還需要引入stdio頭文件。在IAR中必須設定option的某個選項。如下圖所示。

除了完成USART的初始化工作以外,還需要重寫fputc函數,具體的代碼如下。
- int fputc(int ch, FILE * f)
- {
- USART_SendData(USART1, (uint8_t)ch);
- while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET );
- return ch;
- }
然后說一下SPI的初始化工作。閱讀網上的代碼,發現STM32 V2的庫函數和V3函數中,關於SPI端口初始化的部分還是有些出入的。
V2庫中,把SCK,MOSI,MISO全部設置為復用輸出。而V3庫中,SCK,MOSI設置為復用輸出,而MISO設置為浮動輸入。在SD的SPI接口中,SCK,MOSI和MOSI,甚至包括CS都使用了上拉電阻。
您需要注意一下幾點
1. 沒有上拉電阻時 MISO應該如何設置
由於我的開發板中沒有使用上拉電阻,若設定MISO為浮動輸入的話,或許會有某些問題,由於SD卡的輸出端口驅動能力很弱,很有可能就接收不到返回數據,事實也正是如此。所以MISO最后被我甚至成了上拉輸入模式,具體的代碼如下。(所以還是要相信過來人的電路圖,老實的加一個上拉電阻。)
2. SPI的模式應該如何選擇
SPI的速度不能太快,在初始化時時鍾設為400k以下為宜。
3. SPI的速度應該如何選擇
SD卡使用SPI的模式0和模式3,這兩個模式是等價的。
- void SPI1_Config(void)
- {
- //使能APB2上相關時鍾
- //使能SPI時鍾,使能GPIOA時鍾
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 |\
- RCC_APB2Periph_GPIOA ,ENABLE );
- //定義一個GPIO結構體
- GPIO_InitTypeDef GPIO_InitStructure;
- //SPI SCK MOSI
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//復用推挽輸出
- GPIO_Init(GPIOA, &GPIO_InitStructure);
- //SPI MISO
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉輸入
- GPIO_Init(GPIOA, &GPIO_InitStructure);
- //自定義SPI結構體
- SPI_InitTypeDef SPI_InitStructure;
- //雙線雙向全雙工
- SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
- //主機模式
- SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
- //8位幀結構
- SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
- //時鍾空閑時為低
- SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
- //第一個上升沿捕獲數據。模式,0
- SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
- //MSS 端口軟件控制,實際沒有使用
- SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
- //SPI時鍾72Mhz / 256 = 281.25K < 400K
- SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
- //數據傳輸高位在前
- SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
- SPI_InitStructure.SPI_CRCPolynomial = 7;//
- //初始化SPI1
- SPI_Init(SPI1, &SPI_InitStructure);
- //使能SPI1
- SPI_Cmd(SPI1, ENABLE);
- }
除了初始化操作以外,還需要一個SPI發送函數和一個SPI接收函數。由於SPI是同步通信方式,所以SPI接收函數,實際上只需要發送0xFF就可以,具體的代碼如下。
- uint8_t SPI1_SendByte(uint8_t byte)
- {
- //等待發送緩沖寄存器為空
- while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
- //發送數據
- SPI_I2S_SendData(SPI1, byte);
- //等待接收緩沖寄存器為非空
- while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
- //返回從SPI通信中接收到的數據
- return SPI_I2S_ReceiveData(SPI1);
- }
- uint8_t SPI1_ReceiveByte()
- {
- //等待發送緩沖寄存器為空
- while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
- //發送數據,通過發送xff,獲得返回數據
- SPI_I2S_SendData(SPI1, 0xff);
- //等待接收緩沖寄存器為非空
- while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
- //返回從SPI通信中接收到的數據
- return SPI_I2S_ReceiveData(SPI1);
- }
三、移植前的“心靈”准備
“移植”實際上就是研究他人的代碼,你必須敏銳的看清代碼的核心內容,了解你必須要做什么,哪一些可以以后再實現。在移植的初步階段,我建議您使用最簡單的方法完成某些內容,而不是去重視代碼效率。例如,在移植過程中需要使用到延時函數,可以使用軟件延時,可以配合Systick延時,甚至可以使用uCOS的延時函數。但是我建議您在面對選擇的時候選擇最簡單的函數——軟件延時,雖然它不准確效率也不高,但是您可以把更多的精力投入到其他重要的內容中去,你會覺得移植是那么簡單,而延時函數的效率提高是錦上添花的事情。
例如您在移植之前會查看FatFS中關於STM32的移植范例。在該范例中,有關於SD卡插入,SD卡上電控制,SD卡寫保護檢測的函數。除了這些函數之外,代碼中通過宏定義的方法,可以選擇使用DMA來傳送SPI數據,初始化SD卡時使用低速SPI,讀寫塊的時候使用高速SPI,雖然這些改動讓您覺得代碼強大而高效,但是對您的移植一定用處都沒有。您需要從最簡單的generic開始,如果從這個文件開始,您會覺得移植是那么的簡單,僅需要十幾分鍾。我相信您看完文章就會了,其實非常的簡單。
四、移植開始——從generic開始
您所需要操作的只是mmcbb文件,里面主要包括SD卡的初始化、讀塊和寫塊函數。其實修改僅需要三步。
第一步,修改宏定義,添加合適的頭文件,添加延時函數
第二步,修改多字節發送函數
第三步,修改多字節接收函數
下面我通過原代碼和移植代碼的比較,來說明這個移植問題。
4.1 修改頭文件和宏定義
原代碼如下
- /* Include device specific declareation file here */
- #include <device.h>
- /* Initialize MMC control port (CS/CLK/DI:output, DO/WP/INS:input) */
- #define INIT_PORT() { init_port(); }
- /* Delay n microseconds */
- #define DLY_US(n) { dly_us(n); }
- #define CS_H() bset(P0) /* Set MMC CS "high" */
- #define CS_L() bclr(P0) /* Set MMC CS "low" */
- #define CK_H() bset(P1) /* Set MMC SCLK "high" */
- #define CK_L() bclr(P1) /* Set MMC SCLK "low" */
- #define DI_H() bset(P2) /* Set MMC DI "high" */
- #define DI_L() bclr(P2) /* Set MMC DI "low" */
- #define DO btest(P3) /* Get MMC DO value (high:true, low:false) */
- /* Socket: Card is inserted (yes:true, no:false, default:true) */
- #define INS (1)
- /* Socket: Card is write protected (yes:true, no:false, default:false) */.
- #define WP (0)
==========修改后的代碼如下==========
- /* Include device specific declareation file here */
- #include "stm32f10x.h"
- #include "spi1.h"
- #include <stdio.h>
- /* Initialize MMC control port (CS/CLK/DI:output, DO/WP/INS:input) */
- #define INIT_PORT() { init_port(); }
- /* Set MMC CS "high" */
- #define CS_H() GPIO_SetBits(GPIOE,GPIO_Pin_7)
- /* Set MMC CS "low" */
- #define CS_L() GPIO_ResetBits(GPIOE,GPIO_Pin_7)
- /* Delay n microseconds */
- #define DLY_US(n) { dly_us(n); }
- /* Socket: Card is inserted (yes:true, no:false, default:true) */
- #define INS (1)
- /* Socket: Card is write protected (yes:true, no:false, default:false) */
- #define WP (0)
使用STM32時需要包含STM3210x頭文件;spi1.h包括了spi相關操作函數。修改了CS操作的宏定義。
除了一個宏定義外,還需要些一個延時函數和一個初始化函數。延時函數使用軟件延時,很不精確,但是可以說明問題。初始化函數,只是配置CS端口,而SPI初始化工作在調用fatfs API函數時已完成初始化。(若是SPI初始化也完成了CS的操作,init_port()可以省略)
- //初始化端口
- void init_port()
- {
- //初始化時鍾GPIOE
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE ,ENABLE );
- //配置GPIOE.7
- //定義一個GPIO結構體
- GPIO_InitTypeDef GPIO_InitStructure;
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
- GPIO_Init(GPIOE, &GPIO_InitStructure);
- }
- //軟件演示函數
- void dly_us(uint16_t n)
- {
- for( ; n > 0 ; n--)
- for(uint8_t i = 100 ; i > 0 ; i--);
- }
4.2 多字節發送函數
原代碼和修改后的代碼如下。
- static
- void xmit_mmc (
- const BYTE* buff, /* Data to be sent */
- UINT bc /* Number of bytes to send */
- )
- {
- BYTE d;
- do {
- d = *buff++; /* Get a byte to be sent */
- if (d & 0x80) DI_H(); else DI_L(); /* bit7 */
- CK_H(); CK_L();
- if (d & 0x40) DI_H(); else DI_L(); /* bit6 */
- CK_H(); CK_L();
- if (d & 0x20) DI_H(); else DI_L(); /* bit5 */
- CK_H(); CK_L();
- if (d & 0x10) DI_H(); else DI_L(); /* bit4 */
- CK_H(); CK_L();
- if (d & 0x08) DI_H(); else DI_L(); /* bit3 */
- CK_H(); CK_L();
- if (d & 0x04) DI_H(); else DI_L(); /* bit2 */
- CK_H(); CK_L();
- if (d & 0x02) DI_H(); else DI_L(); /* bit1 */
- CK_H(); CK_L();
- if (d & 0x01) DI_H(); else DI_L(); /* bit0 */
- CK_H(); CK_L();
- } while (--bc);
- }
==========修改后的代碼===========
- static void xmit_mmc (const BYTE* buff, UINT bc)
- {
- BYTE d;
- do {
- /* Get a byte to be sent */
- d = *buff++;
- //通過SPI發送
- SPI1_SendByte(d);
- } while (--bc);
- }
4.3 多字節接收函數
原代碼和修改后的代碼如下。
- static
- void rcvr_mmc (
- BYTE *buff, /* Pointer to read buffer */
- UINT bc /* Number of bytes to receive */
- )
- {
- BYTE r;
- DI_H(); /* Send 0xFF */
- do {
- r = 0; if (DO) r++; /* bit7 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit6 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit5 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit4 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit3 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit2 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit1 */
- CK_H(); CK_L();
- r <<= 1; if (DO) r++; /* bit0 */
- CK_H(); CK_L();
- *buff++ = r; /* Store a received byte */
- } while (--bc);
- }
===========修改后的函數===========
- static void rcvr_mmc ( BYTE *buff, UINT bc )
- {
- BYTE r;
- do {
- //重新賦值
- r = 0;
- //通過SPI獲得數據
- r = SPI1_ReceiveByte();
- /* Store a received byte */
- *buff++ = r;
- } while (--bc);
- }
在這里多說一句,源代碼中
DI_H(); /* Send 0xFF */
作者的本意應該是把IO設為輸入狀態,51系列單片機就是這么操作的,但是寫代碼注釋寫成了發送0xFF,其實並不需要發送0xFF。
到這里就完成了fatfs的STM32移植工作,雖然只有簡單的三步,但是卻花了我整整三天的時間。我想您看了這樣的描述,不知道能否在10分鍾之內完成修改。
五 FatFS初步使用
接下來就是使用FatFS了,看了這個函數我找回了當初初學C語言的感覺,打開一個文件,然后讀一些數據,然后創建另一個文件,在文件中寫一些數據,最后關閉文件。
- int main(void)
- {
- //初始化Systick
- RCC_Config();
- //初始化串口
- USART1_Config();
- //初始化SPI1
- SPI1_Config();
- printf("start to read file\n");
- /* Register volume work area (never fails) */
- f_mount(0, &fatfs);
- printf("\nOpen a test file (test.txt).\n");
- rc = f_open(&fil, "test.txt", FA_READ);
- if (rc) die(rc);
- printf("\nType the file content.\n");
- for (;;) {
- rc = f_read(&fil, buff, sizeof(buff), &br); /* Read a chunk of file */
- if (rc || !br) break; /* Error or end of file */
- for (i = 0; i < br; i++) /* Type the data */
- putchar(buff[i]);
- }
- if (rc) die(rc);
- printf("\nClose the file.\n");
- rc = f_close(&fil);
- if (rc) die(rc);
- printf("\nCreate a new file (hello.txt).\n");
- rc = f_open(&fil, "HELLO.TXT", FA_WRITE | FA_CREATE_ALWAYS);
- if (rc) die(rc);
- printf("\nWrite a text data. (Hello world!)\n");
- rc = f_write(&fil, "Hello world!\r\n", 14, &bw);
- if (rc) die(rc);
- printf("%u bytes written.\n", bw);
- printf("\nClose the file.\n");
- rc = f_close(&fil);
- if (rc) die(rc);
- while (1)
- {
- }
- }
如果出現失敗的話,程序會進入die函數,該函數會輸出錯誤代碼,並進入一個無限循環。
通過串口的輸出結果如下所示。

我再把SD卡從目標板上拿下,查看文件中的內容。的確hello.txt文件中寫了hello world字符(應該還有回車和換行符)。

六 我的錯誤經歷
再快要移植成功的時候,我一運行程序,程序就進入die函數,並顯示錯誤1,提示應該是SD卡操作錯誤。我通過斷點調試和printf輸出,把問題定位到發送cmd0處,返回的結果為一個非法的命令。我從CMD17命令入手,查閱了網上各位大神的經驗,有說是發送命令的延時時候不夠。但是照着這個修改之后問題存在,無奈之下在電腦面前苦苦思考。直到我的女朋友,在愚人節那天“玩”我,當時我正在仔細的檢查代碼,她和我說某某老師要找我並提醒我一定要拿手機,我收拾起我凌亂的思緒,立刻跑過去時,她卻打電話給我說愚人節快樂。我很無奈但也有點開心的回到電腦面前,一動鼠標就看到了某些異樣。
#define CMD17 (7) /* READ_SINGLE_BLOCK */
我把CMD17命令的宏定義寫成了7,而實際上是17。就這么一個簡答的錯誤,花費了我一天的時間。也非常感謝女朋友的這個愚人節玩笑,沒有她或許就無法發現這個問題。
一個尚未解決的問題!
還有一個比較特殊的地方請聰明的你注意一下,在generic中man函數中,把這些定義在了main函數里面。這些定義如下
- FRESULT rc; /* Result code */
- FATFS fatfs; /* File system object */
- FIL fil; /* File object */
- DIR dir; /* Directory object */
- FILINFO fno; /* File information object */
- UINT bw, br, i;
- BYTE buff[128];
如果把這些變量的聲明都放在main函數中的話,系統將會運行到一個異常中,如下圖所示。這個錯誤會讓人非常的沮喪。雖然我沒有找到原因,但是我找到了解決的方法。把這些變量的聲明放在函數之外。
<IGNORE_JS_OP>
IAR版本 V5.5
