基于stm32、spi协议的Fatfs文件系统移植(附完整代码下载)
開發環境:Window 7 32bit
開發工具:Keil uVision4
硬件:stm32f103vct6
目錄
1.硬件設計:
2.軟件設計
1.SPI收發數據
2.向SD卡發送的命令格式:
3.SD卡應答命令的響應
4.SD卡初始化流程
?3.下載驗證
4.注意事項
5.實驗可改進的地方?
?
前言:已經有段時間沒有寫博客了,可能是事有點多(是我懶...額),最近又想來寫一些;這次做的是stm32和SD卡的應用。SD卡的使用都很普遍,但是在單片機上的應用卻少;我們知道單片機的處理速度有限,在大文件、大數據面前,根本是發揮不了作用的。但是因為SD卡價格優惠性價比很高,而在某些場合需要常年工作的單片機,可用它來記錄單片機收集的數據;同時也可以通過SD卡給單片機更新自身程序(iap升級)等。接下來要做的是,利用stm32通過spi外設,驅動SD卡;當然如果要從SD卡上讀取、寫入文件,還需要移植文件系統,我選的是Fatfs(一個免費開源的文件系統)。
點擊下載SD卡2.0協議
點擊下載本實驗源碼
下載fatfs系統源碼:
官網地址:http://elm-chan.org/fsw/ff/00index_e.html,拉到下面點擊?Previous Releases,選擇0.11a版本點擊下載。
下載解壓后,有兩個文件夾,doc文件夾是幫助文檔,src里面是是源碼。
doc里面很多資料,詳細介紹了fatfs系統的架構和使用說明,一些接口函數不明白怎么使用的話可以在里面找到說明。下面介紹src文件:
option文件夾:可選的的擴展功能,比如支持中文。我這次沒有用到它。
00history:版本記錄。官網每發布一次版本都會記錄更改或者添加了那些功能,里面還有日期,可以看到它進化的歷程。
00readme:這個文件里面就是做著我現在做的事情,說明每個文件的作用。
diskio.c: 這個是接口層文件,與芯片外設相關,里面有些函數需要我們實現,需要我們修改。
diskio.h:頭文件里面聲明的函數是讓ff.c文件調用的,不需要我們修改。
ff.c:fatfs模塊源碼,核心東西,需要一定的代碼能力才能看懂,不需要我們修改。
ff.h:fatfs模塊應用接口,不需要我們修改。
ffconf.h關鍵參數配置,配置一些宏的值, 不同的值滿足不同的需求,需要我們修改。
integer.h數據類型定義,與編譯器有關,一般不需要修改。
接下我們要做兩個事情,修改diskio.c文件和ffconf.h文件。
先說一下ffconf.h的配置,我只是改了下面兩個宏:
關于其他的宏暫時不改動,每個宏所起的作用在源碼里有詳細的英文說明,可以了解一下。
再說一下diskio.c文件,里面共有5個函數分別是:
?上面提到的扇區可能大家會有疑問,不同的設備,扇區大小不一樣。SD卡每個扇區是512字節,型號W25Q128FV的spi Flash芯片每個扇區是4096字節。如果我用的是這個Flash芯片,那么在ffconf.c里面的_MAX_SS就要改大才能兼容。可見fatfs可兼容不同的扇區大小的設備。由上面參數可見讀、寫都是以扇區為單位,一個設備根據容量的不同,會分成若干個扇區。
再者,上面的每一個函數都有pdrv參數,為了兼容多個或者不同的設備,在上面每個函數里面會有一個switch分支,來區別具體要操作哪個設備,我只使用一個SD卡,所以只需要增加一個分支即可。
對于stm32來說,在提供的庫函數就有spi外設的使用接口函數,非常方便,但這僅僅是數據的收發;想要從SD卡中讀取信息,讀/寫扇區數據,還需要了解SD卡的通訊協議(通訊協議有幾個版本網上有公開資料,可自行選擇了解)。在stm32的標準庫跟fatfs系統之間還需要一個中間層。它的作用是根據通訊協議提供的命令參數,從SD卡里獲取設備型號、容量、設備狀態、讀/寫扇區等操作,這也是這次講解的重點。
diskio.c文件具體的改動這里不細說,直接看我的源碼,接下來就要開始動手了(我怕我再啰嗦的話可能就留不住人了)。
1.硬件設計:
接線如下圖:
左邊的是串口小板,接到電腦看打印信息;中間的是stm32f103vct6;右上角紅線、黑線分別是5V電源線、地線。
下方的是16G、SD卡的SPI轉接小板,?網上一搜可以買到,下面是它的原理圖:
如果你買的小板跟我的一樣,接到小板的電壓一定要5v,在這個小板上MISO、MOSI、SCK引腳接了上拉電阻。
可參照第一張圖接好線,杜邦線不宜過長;另外,我用的是J-link下載器。
2.軟件設計
編程要點:
打開源碼工程:
先說一下main.c文件,main函數比較簡單,主要調用了ff.h里面的f_mount和f_open函數。
#include "stm32f10x.h" #include "USART1.h" #include "ff.h" #include "diskio.h"void GPIO_Configuration(void); void Delay(uint32_t nCount);static FATFS g_fileSystem; /* File system object */ const TCHAR driverNumberBuffer[3U] = {'3', ':', '/'};//這里的3對應diskio.c里面的pdrv設備號int main(void) {FIL fd,outfd;GPIO_Configuration(); //配置一個led閃爍USART1_Configuration();//初始化串口,用來輸出printf信息//掛載一個設備到路徑“3:/”,這個函數里面會調用disk_initialize進行初始化SD卡if (f_mount(&g_fileSystem, driverNumberBuffer, 1)){printf("Mount volume failed.\r\n");}else{printf("Mount volume succeed.\r\n");}//打開事先在SD卡創建的readme.txt文件if(f_open(&fd, "3:/readme.txt", FA_READ) ){printf("f_open failed.\r\n");}else{printf("f_open succeed.\r\n");}while (1){GPIO_SetBits(GPIOB,GPIO_Pin_0);Delay(0xfffff);Delay(0xfffff); GPIO_ResetBits(GPIOB,GPIO_Pin_0);Delay(0xfffff);Delay(0xfffff); printf("app runing \n");} }void GPIO_Configuration(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); }void Delay(uint32_t nCount) {for(; nCount != 0; nCount--); }再說一下app_spiSD.c文件,這個是對照SD卡的通訊協議做出來的。這個文件大部分代碼是我從NXP LPC54110芯片的例程復制過來的,其中改了一些地方。下面我主要說幾個重要的點:
1.SPI收發數據
spi初始化部分這里不細說,配置參數正確就行了。下面是spi收發函數,spi發送一個字節后,必定會接收一個字節,當spi要接收SD卡響應數據時,可以發送0xFF(無效指令)來接收數據,因為SD卡是從機,時鐘線由主機控制,發送0xFF是為了產生時鐘,從機才能把數據傳出;加入超時出錯的機制,以防SD卡未插入時,程序阻塞在此處。下面是spi收發函數spi_exchange:
/* in: 發送數據的緩沖地址,不可能為NULL;即使只接收數據,也要發0xFF out:接收數據的緩沖地址,如果是NULL,代表只發送數據,不用保存接收的數據 size:收發數據的大小 */ status_t spi_exchange(uint8_t *in, uint8_t *out, uint32_t size) {uint32_t rxRemainingBytes,txRemainingBytes,tmp32;uint32_t SPITimeout;if(((in==NULL)&&(out==NULL))||size==0){return kStatus_InvalidArgument;}GPIO_ResetBits(GPIOA,SDCard_SPI_CS_PIN);//片選拉低rxRemainingBytes=out != NULL? size : 0;txRemainingBytes=in != NULL? size : 0;while(rxRemainingBytes || txRemainingBytes){SPITimeout=timer_get_current_milliseconds();while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_TXE) == RESET){if((timer_get_current_milliseconds()-SPITimeout)>SPIT_FLAG_TIMEOUT)return kStatus_Timeout;}if(txRemainingBytes){SPI_I2S_SendData(SDCard_SPI,*in);in++;txRemainingBytes--;}else{PI_I2S_SendData(SDCard_SPI,Dummy_Byte);}SPITimeout=timer_get_current_milliseconds();while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_RXNE) == RESET){if((timer_get_current_milliseconds()-SPITimeout)>SPIT_FLAG_TIMEOUT)return kStatus_Timeout;}tmp32 = SPI_I2S_ReceiveData(SDCard_SPI);if(rxRemainingBytes){*out = tmp32;out++;rxRemainingBytes--;}}while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_TXE) == RESET);GPIO_SetBits(GPIOA,SDCard_SPI_CS_PIN);//片選拉高,結束通訊return kStatus_Success; }2.向SD卡發送的命令格式:
?
命令長度共48位,包括起始位,傳輸位,命令碼,命令參數,校驗位以及停止位。在協議里面CMD0就是0,CMD16就是16,其他以此類推。下面在協議里面截取一部分命令描述:
3.SD卡應答命令的響應
?不同的命令,對應不同格式的應答;在協議里面規定,每一個發出去CMD命令都對應了一種應答格式;所以在發送命令后,我們就已經預先地知道了接下來將要接收怎么樣的應答格式,并做好接收應答準備。
Format R1 :長度為1字節,如圖: Format R1b:長度為1字節,如果R1b=0,代表SD卡處于忙碌狀態;如果R1b不為0,那么按照R1格式解讀就行。 Format R2 :長度為2個字節,是在R1格式下再加一個字節的信息,如圖:Format R3:?長度為5字節,R1(8bit)+OCR(32)寄存器的值。
Formats R4 & R5 :這兩個響應格式是為I/O模式保留的。
Format R7:長度為5個字節,第一個字節是R1格式,后4個字節包含卡的工作電壓信息和檢查模式的回顯。如下如:
以上兩個知識點體現在本實驗源碼的SDSPI_SendCommand函數,如下:
static status_t SDSPI_SendCommand(sdspi_host_t *host, sdspi_command_t *command, uint32_t timeout) {uint8_t buffer[6];uint8_t response;uint8_t i;uint8_t timingByte = 0xFFU; /* The byte need to be sent as read/write data block timing requirement */if ((kStatus_Success != SDSPI_WaitReady(host, timeout)) && (command->index != kSDMMC_GoIdleState)){return kStatus_SDSPI_WaitReadyFailed;}/* Send command. */buffer[0U] = (command->index | 0x40U);//起始位+命令碼buffer[1U] = ((command->argument >> 24U) & 0xFFU);buffer[2U] = ((command->argument >> 16U) & 0xFFU);buffer[3U] = ((command->argument >> 8U) & 0xFFU);buffer[4U] = (command->argument & 0xFFU);buffer[5U] = ((SDSPI_GenerateCRC7(buffer, 5U, 0U) << 1U) | 1U);//crc+停止位if (host->exchange(buffer, NULL, sizeof(buffer))){return kStatus_SDSPI_ExchangeFailed;}//等待應答,最多接收9個字節,若接收不到正確應答,當做錯誤處理for (i = 0U; i < 9U; i++){if (kStatus_Success != host->exchange(&timingByte, &response, 1U)){return kStatus_SDSPI_ExchangeFailed;}//當接收到的一個字節的最左邊的位是0,那么就是正確的應答,退出循環。往下繼續接收剩下的應答信息if (!(response & 0x80U)){break;}}if (response & 0x80U) //這個條件滿足,意味著應答錯誤{return kStatus_SDSPI_ResponseError;}command->response[0U] = response;//將應答的第一個字節保存,接著接收其余字節或返回。switch (command->responseType)//根據預先知道的應答類型接收應答{case kSDSPI_ResponseTypeR1:break;case kSDSPI_ResponseTypeR1b:if (kStatus_Success != SDSPI_WaitReady(host, timeout)){return kStatus_SDSPI_WaitReadyFailed;}break;case kSDSPI_ResponseTypeR2:if (kStatus_Success != host->exchange(&timingByte, &(command->response[1U]), 1U)){return kStatus_SDSPI_ExchangeFailed;}break;case kSDSPI_ResponseTypeR3:case kSDSPI_ResponseTypeR7:/* Left 4 bytes in response type R3 and R7(total 5 bytes in SPI mode) */if (kStatus_Success != host->exchange(&timingByte, &(command->response[1U]), 4U)){return kStatus_SDSPI_ExchangeFailed;}break;default:return kStatus_Fail;}return kStatus_Success; }4.SD卡初始化流程
SD卡SPI模式的初始化流程在SD卡協議文檔的106頁,下面是中文的流程以及多了一些說明,同時可以對照著diskio.c文件里的SDSPI_Init函數來理解這個流程圖:
本實驗代碼只支持對SD2.0版本的檢測,我手上也只有一張卡;初始化函數里某一個環節出錯都會立即返回,如果出現初始化不成功,可以設置斷點,排查錯誤在哪里個環節發生。SDSPI_Init函數:?
status_t SDSPI_Init(sdspi_card_t *card) {sdspi_host_t *host;uint32_t applicationCommand41Argument = 0U;uint32_t startTime;uint32_t currentTime;uint32_t elapsedTime;uint8_t response[5U];uint8_t applicationCommand41Response[5U];bool likelySdV1 = false;host = card->host;/* Card must be initialized in 400KHZ. */if (host->setFrequency(SDMMC_CLOCK_400KHZ)){return kStatus_SDSPI_SetFrequencyFailed;}/* Reset the card by CMD0. */if (kStatus_Success != SDSPI_GoIdle(card)){return kStatus_SDSPI_GoIdleFailed;}/* Check the card's supported interface condition. */if (kStatus_Success != SDSPI_SendInterfaceCondition(card, 0xAAU, response)){likelySdV1 = true;}else if ((response[3U] == 0x1U) || (response[4U] == 0xAAU)){applicationCommand41Argument |= kSD_OcrHostCapacitySupportFlag;}else{return kStatus_SDSPI_SendInterfaceConditionFailed;}/* Set card's interface condition according to host's capability and card's supported interface condition */startTime = host->getCurrentMilliseconds();do{if (kStatus_Success !=SDSPI_ApplicationSendOperationCondition(card, applicationCommand41Argument, applicationCommand41Response)){return kStatus_SDSPI_SendOperationConditionFailed;}currentTime = host->getCurrentMilliseconds();elapsedTime = (currentTime - startTime);if (elapsedTime > 500U){return kStatus_Timeout;}if (!applicationCommand41Response[0U]){break;}} while (applicationCommand41Response[0U] & kSDSPI_R1InIdleStateFlag);if (!likelySdV1){if (kStatus_Success != SDSPI_ReadOcr(card)){return kStatus_SDSPI_ReadOcrFailed;}if (card->ocr & kSD_OcrCardCapacitySupportFlag){card->flags |= kSDSPI_SupportHighCapacityFlag;}}/* Force to use 512-byte length block, no matter which version. */if (kStatus_Success != SDSPI_SetBlockSize(card, 512U)){return kStatus_SDSPI_SetBlockSizeFailed;}if (kStatus_Success != SDSPI_SendCsd(card)){return kStatus_SDSPI_SendCsdFailed;}/* Set to max frequency according to the max frequency information in CSD register. */SDSPI_SetMaxFrequencyNormalMode(card);/* Save capacity, read only attribute and CID, SCR registers. */SDSPI_CheckCapacity(card);SDSPI_CheckReadOnly(card);if (kStatus_Success != SDSPI_SendCid(card)){return kStatus_SDSPI_SendCidFailed;}if (kStatus_Success != SDSPI_SendScr(card)){return kStatus_SDSPI_SendCidFailed;}return kStatus_Success; }在讀取SD卡的CSD寄存器后,可得到該SD卡所支持的SPI的最大波特率,然后根據其最大的波特率和本地SPI設備所支持的最大波特率兩者選其中較小一個。下圖的busBandRate是設置本地支持的最大波特率,若SD卡支持的波特率大于它,那么就選用它。雖然stm32的spi最大支持36M,但是這里我設置了18M,可能是距離太長不能用36M的。如果有條件可以自己試一下36M,當然你用的SD卡支持的波特率一定要大于它才能用。
?3.下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板“USB轉串口”接口及電腦,在電腦端打開串口助手,把編譯好的程序下載到開發板。我用的是J-LINK下載器,不知道是不是我的電腦的原因,下載器輸出的電源只有3V多,SD卡的轉接板需要5V電源,所以我用另外一個5V的電源給開發板和SD卡的轉接板供電。SD卡和開發板的電源要接到一起,共地。程序下載后可以點調試運行,也可以斷電重啟;觀察串口助手打印的信息:
打印信息顯示SD卡初始化正常,可讀取“readme.txt”文件。
關于SD卡的其他信息我沒有打印出來,在SD卡初始化的時候已經把所有信息讀取保存在g_card變量,只需要根據SD卡的協議去解讀這里面的值就行;當然如果有J-Link調試器,可以將g_card添加到Watch窗口觀察。
設置斷點調試,如果沒有調試工具可以不做:
?
?由上圖可看出我用的SD卡所支持的最大頻率是0x03473BC0,即55MHz。這里只是舉個例子,在初始化過程中,如果出現錯誤返回,可以通過設置斷點來定位錯誤的位置,同時把關鍵的變量添加到Watch里觀察,記得把View-Periodic Window Update打開。
4.注意事項
- 我第一次驗證的時候也遇到了很多問題,一開始我懷疑是spi收發的設計問題,后來發現是SD卡供電的問題,導致SD卡初始化一直不成功,而且斷點調試時出錯返回的節點位置不定。所以一定要保證SD卡引腳的供電是2.0-3.6V,我買的轉接板供電要5V,之前我給轉接板供電3V多時,就一直初始化不成功。
- 開發板連接到SD卡轉接板的杜邦線不能太長,因為SPI傳輸的距離短,屬于板載通訊,不適合拉線;距離太遠,會有線耗,導致通訊不穩定。
- 確保SD卡的文件系統是FAT12、FAT16、FAT32,否則無法識別。若格式不符合,可將重要數據備份后把SD卡格式化成FAT32格式。在電腦上查看SD卡的文件系統格式:
5.實驗可改進的地方?
- 在ff.h里面有很多文件的操作函數,我這里只調用了f_mount和f_open,main函數可以進一步開發,加入f_write和f_read等應用接口,對SD卡進行文件讀寫數據的操作,測試數據的傳輸效率。
- 實驗所用的spi波特率最大是18M,改stm32芯片支持36M,但由于我接線過長的原因不能使用36M,可優化電路,將SD卡座直接與開發板焊接,縮短傳輸距離;再驗證是否可用36M傳輸,從而大大提高數據傳輸的效率。?
- 若要支持中文編碼,需要把option文件夾里面的cc936.c文件加入到工程,并在ffconf.c里配置_USE_LFN 宏和_CODE_PAGE宏。但由于我用的芯片flash不足,無法通過編譯。
- 增加SD卡插入檢測,本實驗對SD卡的初始化是上電默認進行的,但是如果在MCU運行中,插入SD卡將無法調用SD卡的初始化函數。所以加入SD卡插入檢測引腳后,當SD卡插入信號發生時,自動調用f_mount函數,對SD卡進行驅動使用。
?
水平有限,僅供參考,錯誤之處以及不足之處還望多多指教。
?
《路漫漫其修遠兮,吾將上下而求索。 -------屈原》
總結
以上是生活随笔為你收集整理的基于stm32、spi协议的Fatfs文件系统移植(附完整代码下载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: stm32之实时时钟RTC(掉电计时保持
- 下一篇: Qt 实现串口终端控制台,适配RT-Th