基于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卡进行驱动使用。

 

水平有限,仅供参考,错误之处以及不足之处还望多多指教。

 

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

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