基於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的配置,我只是改了下面兩個宏:

#define _VOLUMES	5 //支持的邏輯設備數
#define _FS_NORTC	0 //暫時不加入RTC,先關閉, 不然編譯報錯;因爲打開的話要實現get_fattime()來獲取RCT時間

關於其他的宏暫時不改動,每個宏所起的作用在源碼裏有詳細的英文說明,可以瞭解一下。
再說一下diskio.c文件,裏面共有5個函數分別是:

/*
功能:設備初始化函數
參數:pdrc是設備號,fatfs系統可以同時掛載多個設備(SD卡、MMC等)
*/
DSTATUS disk_initialize (BYTE pdrv);
/*獲取設備狀態*/
DSTATUS disk_status (BYTE pdrv);
/*
功能:從設備讀取若干個扇區的數據
buff: 讀取的數據存放的地址
sector:扇區地址
count:所讀取的扇區總個數
*/
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
/*
功能:往設備寫入若干個扇區的數據
buff: 寫入的數據地址
sector:扇區地址
count:所寫的扇區總個數
*/
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
/*
功能:設備控制,或獲取設備的參數
pdrv:設備號
cmd:命令
buff:發送/接收緩衝區指針
*/
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);

 上面提到的扇區可能大家會有疑問,不同的設備,扇區大小不一樣。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.軟件設計

編程要點:

  1. 配置一路usart串口,用來輸出printf打印信息。
  2. 配置一個TIM計時器,提供系統滴答,用來滿足超時的設計。
  3. 初始化spi外設,配置合適的參數。
  4. 根據SD卡的通訊協議,初始化SD卡,並實現一些相關的讀/寫操作函數。

打開源碼工程:

先說一下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卡進行驅動使用。

 

水平有限,僅供參考,錯誤之處以及不足之處還望多多指教。

 

《路漫漫其修遠兮,吾將上下而求索。 -------屈原》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章