跳至内容
fatfs文件系统移植

fatfs文件系统移植

2025-01-01

使用的开发板:RTT&正点原子的潘多拉loT Board开发板、普中Z-100开发板。主控:STM32L475VET6、STM32F103ZET6

软件:CubeMX 6.7.0、Keil 5.38、FatFs R0.15

学习路线:

  1. 学会FATFS文件系统的移植;
  2. 学会API的使用;

第一部分:移植

FatFs 是一个通用的文件系统(FAT/exFAT)模块,用于在小型嵌入式系统中实现FAT文件系统。

FatFs 组件的编写遵循ANSI C(C89),完全分离于磁盘 I/O 层l,因此不依赖于硬件平台。它可以嵌入到资源有限的微控制器中,如 8051, PIC, AVR, ARM, Z80, RX等等,不需要做任何修改。

1. 准备源码

FATFS网站:http://elm-chan.org/fsw/ff/00index_e.html

从官网中即可下载源码。这里选择最新版本R0.15

1.1

下载压缩包解压后目录如下:

  • ff15
    • documents(官网网页,没啥用可删除)
    • source(源码)
      • diskio.c \ diskio.h
      • ``ff.c \ ff.h`
      • ffconf.h
      • ``ffsystem.h`
      • ffunicode.c
      • 00history.txt
      • 00readme.txt
    • LICENSE.txt(开源协议)

其中:

  1. ff.c / ff.h:文件系统协议层,几乎不用修改;
  2. ffconf.h:FatFs模块的配置文件;
  3. diskio.c \ diskio.h:将现有磁盘I/O模块连接到FatFs的粘合功能,即适配低层硬件接口读写操作,是主要移植修改的重点;
  4. ffunicode.c:字符编码层,可以选择utf8、ascii、GBK。根据需要选择。
  5. ffsystem.c:可选O/S相关功能的示例。

这是 FatFs 官方所提供的程序之间的依赖网络。其中虚线框不是必须的,为用户自行编写定义。

在官方提供的文件中,ff.c、ff.h、diskio.h 是不需要操作的。

1.2

需要修改的有:

  • ffconf.h:通过修改其中的宏定义可裁剪 FatFs 的部分功能。
  • diskio.c:底层驱动函数。

具体文件如何配置或修改看移植部分。

2. TF卡 SDIO 模式移植(尚有问题未解决)

这里使用普中Z-100开发板,主控芯片为STM32F103ZET6。开发板上TF卡与主控的通信采用SDIO通信

原理图如下:

2.1

2.1 创建工程

使用CubeMX创建一个工程。需要配置的项目有:

  • 时钟
  • 程序调试接口
  • 串口(用于观察调试信息)
  • SDIO接口(带DMA)

其中 SDIO 接口的配置如下:

2.2

  • Clock transition on which the bit capture is made:Rising transition。主时钟 SDIOCLK 产生 CLK 引脚时钟有效沿选择,可选上升沿或下降沿,它设定 SDIO 时钟控制寄存器(SDIO_CLKCR)的 NEGEDGE 位的值,一般选择设置为上升沿

  • SDIO Clock divider bypass:Disable。时钟分频旁路使用,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 BYPASS 位。如果使能旁路,SDIOCLK 直接驱动 CLK 线输出时钟;如果禁用,使用 SDIO_CLKCR 寄存器的 CLKDIV 位值分频 SDIOCLK,然后输出到 CLK 线。一般选择禁用时钟分频旁路。

  • SDIO Clock output enable when the bus is idle: Disable the power save for the clock。节能模式选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 PWRSAV 位的值。如果使能节能模式,CLK 线只有在总线激活时才有时钟输出;如果禁用节能模式,始终使能 CLK 线输出时钟。

  • SDIO hardware flow control: The hardware control flow is disabled。硬件流控制选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 HWFC_EN 位的值。硬件流控制功能可以避免 FIFO 发送上溢和下溢错误。

  • SDIOCLK clock divide factor: 2。时钟分频系数,它设定 SDIO_CLKCR 寄存器的 CLKDIV 位的值,设置 SDIOCLK 与 CLK 线输出时钟分频系数:CLK 线时钟频率=SDIOCLK/([CLKDIV+2])。

SDIO相关的时钟分为两部分:

  1. SDIO适配器时钟(SDIOCLK),用于驱动SDIO适配器,可用于产生SDIO_CK时钟。在STM32F1中,SDIOCLK来自HCLK(72MHz)。改时钟经过上述的分频后产生SDIO_CK时钟。

  2. AHB总线接口时钟(HCLK/2),该时钟用于驱动SDIO的AHB总线接口,其频率为HCLK/2(36MHz)()在cubemx时钟配置页面可以看得到。

需要注意的是,不同的SD卡对时钟的最高频率范围不同。因此建议将**SDIOCLK clock divide factor:**从0开始慢慢增大试试,可以读写数据成功即可。

我这里用的卡,经过测试设置为0时,卡信息读写成功,数据读写失败。设置为2时,均可成功。

DMA配置如下:(一般应用中都是用DMA传输)

2.3

此处,可以发现,DMA只能配置一个方向的,即两个方向共用了一个传输通道。在使用时,实际上可以通过重新初始化DMA来更改传输方向。这里先随便设置一个方向即可。

最后勾选上中断:

2.4


生成工程后:

修改sdio.c文件中的void MX_SDIO_SD_Init(void)函数。正确的如下:

void MX_SDIO_SD_Init(void)
{
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B;	// 注意要修改这里为 SDIO_BUS_WIDE_1B
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 2;
  if (HAL_SD_Init(&hsd) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)
  {
    Error_Handler();
  }
}

如果生成的代码中 本来就是 hsd.Init.BusWide = SDIO_BUS_WIDE_1B; 那就不用管。

如果生成的默认是 4B,则要修改为 1B,否则此函数执行的时候不会通过。

(不同固件包生成的不一样,因此要确认一下)

2.2 SD卡读写验证

2.2.1 sdio_sdcard.h

两个 test 函数,测试完成之后不需要的话可以删除。跟后续移植关系不是很大。

#ifndef __SDIO_SDCARD_H
#define __SDIO_SDCARD_H


#include "sdio.h"
#include "stdint.h"


void SD_info_print(void);
void SD_read_writer_test(void);
HAL_StatusTypeDef SDIO_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);
HAL_StatusTypeDef SDIO_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);
void SD_read_writer_dma_test(void);


#endif	/* __SDIO_SDCARD_H */

2.2.2 sdio_sdcard.c

#include "sdio_sdcard.h"

#include "stdio.h"  // 调试信息输出
#include "sdio.h"   // cubemx生成的


extern SD_HandleTypeDef hsd;
extern DMA_HandleTypeDef hdma_sdio;


/**
 * @brief   打印SD卡信息
 *
 * @param   none
 *
 * @return  none
 */
void SD_info_print(void)
{
    HAL_SD_CardInfoTypeDef  SDCardInfo;		// SD卡信息结构体
	HAL_SD_CardCIDTypeDef sdcard_cid;		// 
    
	
    HAL_SD_GetCardInfo(&hsd, &SDCardInfo);	// 得到卡的信息
	// 打印SD卡基本信息
	printf("SD card information! \r\n");
    printf("Card Type:%d \r\n",			SDCardInfo.CardType);			// 卡类型
    printf("Card CardVersion:%d \r\n",	SDCardInfo.CardVersion);		// CardVersion
	
	printf("Card Class:%d \r\n",		SDCardInfo.Class);				// 卡Class
	printf("Card RCA:%d \r\n",			SDCardInfo.RelCardAdd);			// 卡相对地址
	
	printf("Card BlockNumber: %d \r\n", 	SDCardInfo.BlockNbr);		// 有多少个块
	printf("Card BlockSize(Byte): %d \r\n",	SDCardInfo.BlockSize);		// 块大小 默认512个字节
	printf("Card Capacity(Byte): %llu \r\n", ((unsigned long long)SDCardInfo.BlockSize * SDCardInfo.BlockNbr));	// 容量
	
    printf("Card Capacity:%d MB \r\n", (uint32_t)(SDCardInfo.LogBlockNbr) >> 11); // 显示容量
    printf("Card BlockSize:%d \r\n", 	SDCardInfo.LogBlockSize);       // 显示块大小

	
	// 读取并打印SD卡的CID信息
	HAL_SD_GetCardCID(&hsd, &sdcard_cid);
	printf("ManufacturerID: %d \r\n", sdcard_cid.ManufacturerID);	// 制造商
}

/**
 * @brief   SD卡读写测试
 *
 * @param   none
 *
 * @return  none
 */
void SD_read_writer_test(void)
{
    /* SD卡状态 */
    int sdcard_status = 0;

    uint8_t read_buf[512];      // 读数据缓存
    uint8_t write_buf[512];     // 写数据缓存

    /* 读取未操作之前的数据 */
    printf("------------------- Read SD card block data Test ------------------\r\n");
    /*
        读一个扇区的数据:
        0: 从第0个扇区开始。
        1:读一个扇区的数据。
        0xffff:等待时间。
        note:也就是只读了第0个扇区。
    */
    sdcard_status = HAL_SD_ReadBlocks(&hsd, (uint8_t *)read_buf, 0, 1, 0xffff);
    if(sdcard_status == HAL_OK)
    {
        printf("Read block data ok! \r\n");
        for(int i = 0; i < 512; i++)
        {
            printf("0x%02x ", read_buf[i]);
            if((i + 1) % 16 == 0)
            {
                printf("\r\n");
            }
        }
    }
    else
    {
        printf("Read block data fail! status = %d \r\n", sdcard_status);
    }

    /* 向SD卡块写入数据 */
    printf("------------------- Write SD card block data Test ------------------\r\n");
    /* 填充缓冲区数据 */
    for(int i = 0; i < 512; i++)
    {
        write_buf[i] = i % 256;
    }
    // 开始写入数据
    /*
        写一个扇区的数据:
        0: 从第0个扇区开始。
        1:写一个扇区的数据。
        0xffff:等待时间。
        note:也就是只写了第0个扇区。
    */
    sdcard_status = HAL_SD_WriteBlocks(&hsd, (uint8_t *)write_buf, 0, 1, 0xffff);
    if(sdcard_status == HAL_OK)
    {
        /* 传输完成不代表写入完成,因此要等待SD卡状态变为可传输状态。擦除操作也是一样。 */
        printf("Writing block data. state = %d \r\n", HAL_SD_GetCardState(&hsd));
        while(HAL_SD_GetCardState(&hsd) == HAL_SD_CARD_PROGRAMMING);
        printf("Write block data ok,state = %d \r\n", HAL_SD_GetCardState(&hsd));
    }
    else
    {
        printf("Write block data fail! status = %d \r\n", sdcard_status);
    }

    /* 读取写入之后的数据 */
    printf("------------------- Read SD card block data after Write ------------------\r\n");
    sdcard_status = HAL_SD_ReadBlocks(&hsd, (uint8_t *)read_buf, 0, 1, 0xffff);
    if(sdcard_status == HAL_OK)
    {
        printf("Read block data ok! \r\n");
        for(int i = 0; i < 512; i++)
        {
            printf("0x%02x ", read_buf[i]);
            if((i + 1) % 16 == 0)
            {
                printf("\r\n");
            }
        }
    }
    else
    {
        printf("Read block data fail! status = %d \r\n", sdcard_status);
    }

    /* 擦除SD卡块 */
    printf("------------------- Block Erase -------------------------------\r\n");
    /*
        擦除512个扇区的数据:
        0: 从第0个扇区开始。
        1:一直擦除到512扇区。
        note:擦除第0到第512个扇区数据,也包括0和512,也就是一共512个。
    */
    sdcard_status = HAL_SD_Erase(&hsd, 0, 512);
    // 等待擦除完毕
    if(sdcard_status == HAL_OK)
    {
        printf("Erasing block. state = %d \r\n", HAL_SD_GetCardState(&hsd));
        while(HAL_SD_GetCardState(&hsd) == HAL_SD_CARD_PROGRAMMING);
        printf("Erase block ok state = %d \r\n", HAL_SD_GetCardState(&hsd));
    }
    else
    {
        printf("Erase block fail! status = %d \r\n", sdcard_status);
    }

    /* 读取擦除之后的数据 */
    printf("------------------- Read SD card block data after Erase ------------------\r\n");
    sdcard_status = HAL_SD_ReadBlocks(&hsd, (uint8_t *)read_buf, 0, 1, 0xffff);
    if(sdcard_status == HAL_OK)
    {
        printf("Read block data ok \r\n");
        for(int i = 0; i < 512; i++)
        {
            printf("0x%02x ", read_buf[i]);
            if((i + 1) % 16 == 0)
            {
                printf("\r\n");
            }
        }
    }
    else
    {
        printf("Read block data fail! status = %d \r\n", sdcard_status);
    }
}

/**
 * @brief   SD卡DMA读数据(开始前重新初始化DMA,更改传输方向)
 *
 * @param   hsd
 * @param   pData
 * @param   BlockAdd
 * @param   NumberOfBlocks
 *
 * @return  none
 */
HAL_StatusTypeDef SDIO_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
{
    HAL_StatusTypeDef Return_Status;
    HAL_SD_CardStateTypeDef SD_Card_Status;

	// 读之前,确保SD卡处于可传输状态
    do
    {
        SD_Card_Status = HAL_SD_GetCardState(hsd);
    } while(SD_Card_Status != HAL_SD_CARD_TRANSFER);

	// 重新初始化MDA传输方向
    HAL_DMA_DeInit(&hdma_sdio);
    hdma_sdio.Instance = DMA2_Channel4;
    hdma_sdio.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_sdio.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_sdio.Init.MemInc = DMA_MINC_ENABLE;
    hdma_sdio.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma_sdio.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_sdio.Init.Mode = DMA_NORMAL;
    hdma_sdio.Init.Priority = DMA_PRIORITY_LOW;
    if(HAL_DMA_Init(&hdma_sdio) != HAL_OK)
    {
        Error_Handler();
    }
    __HAL_LINKDMA(hsd, hdmarx, hdma_sdio);

	// 开始一次DMA传输
    Return_Status = HAL_SD_ReadBlocks_DMA(hsd, pData, BlockAdd, NumberOfBlocks);

    return Return_Status;
}

/**
 * @brief   SD卡DMA写数据(开始前重新初始化DMA,更改传输方向)
 *
 * @param   hsd
 * @param   pData
 * @param   BlockAdd
 * @param   NumberOfBlocks
 *
 * @return  none
 */
HAL_StatusTypeDef SDIO_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
{
    HAL_StatusTypeDef Return_Status;
    HAL_SD_CardStateTypeDef SD_Card_Status;

    do
    {
        SD_Card_Status = HAL_SD_GetCardState(hsd);
    } while(SD_Card_Status != HAL_SD_CARD_TRANSFER);

    // 重新初始化MDA传输方向
    HAL_DMA_DeInit(&hdma_sdio);
    hdma_sdio.Instance = DMA2_Channel4;
    hdma_sdio.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_sdio.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_sdio.Init.MemInc = DMA_MINC_ENABLE;
    hdma_sdio.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma_sdio.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_sdio.Init.Mode = DMA_NORMAL;
    hdma_sdio.Init.Priority = DMA_PRIORITY_LOW;
    if(HAL_DMA_Init(&hdma_sdio) != HAL_OK)
    {
        Error_Handler();
    }
    __HAL_LINKDMA(hsd, hdmatx, hdma_sdio);

	// 开始一次DMA传输
    Return_Status = HAL_SD_WriteBlocks_DMA(hsd, pData, BlockAdd, NumberOfBlocks);

    return Return_Status;
}

/**
 * @brief   SD卡DMA读写测试
 *
 * @param   none
 *
 * @return  none
 */
void SD_read_writer_dma_test(void)
{
    /* SD卡状态 */
    int sdcard_status = 0;

    uint8_t read_buf[512];      // 读数据缓存
    uint8_t write_buf[512];     // 写数据缓存

    /* 读取未操作之前的数据 */
    printf("------------------- Read SD card block data Test ------------------\r\n");
    /*
        读一个扇区的数据:
        0: 从第0个扇区开始。
        1:读一个扇区的数据。
        note:也就是只读了第0个扇区。
    */
    sdcard_status = SDIO_ReadBlocks_DMA(&hsd, (uint8_t *)read_buf, 0, 1);

    if(sdcard_status == HAL_OK)
    {
        printf("Read block data ok! \r\n");
        for(int i = 0; i < 512; i++)
        {
            printf("0x%02x ", read_buf[i]);
            if((i + 1) % 16 == 0)
            {
                printf("\r\n");
            }
        }
    }
    else
    {
        printf("Read block data fail! status = %d \r\n", sdcard_status);
    }

    /* 向SD卡块写入数据 */
    printf("------------------- Write SD card block data Test ------------------\r\n");
    /* 填充缓冲区数据 */
    for(int i = 0; i < 512; i++)
    {
        write_buf[i] = i % 256;
    }
    // 开始写入数据
    /*
        写一个扇区的数据:
        0: 从第0个扇区开始。
        1:写一个扇区的数据。
        note:也就是只写了第0个扇区。
    */
    sdcard_status = SDIO_WriteBlocks_DMA(&hsd, (uint8_t *)write_buf, 0, 1);

    if(sdcard_status == HAL_OK)
    {
        /* 传输完成不代表写入完成,因此要等待SD卡状态变为可传输状态。擦除操作也是一样。 */
        printf("Writing block data. state = %d \r\n", HAL_SD_GetCardState(&hsd));
        while(HAL_SD_GetCardState(&hsd) == HAL_SD_CARD_PROGRAMMING);
        printf("Write block data ok,state = %d \r\n", HAL_SD_GetCardState(&hsd));
    }
    else
    {
        printf("Write block data fail! status = %d \r\n", sdcard_status);
    }

    /* 读取写入之后的数据 */
    printf("------------------- Read SD card block data after Write ------------------\r\n");

    sdcard_status = SDIO_ReadBlocks_DMA(&hsd, (uint8_t *)read_buf, 0, 1);

    if(sdcard_status == HAL_OK)
    {
        printf("Read block data ok! \r\n");
        for(int i = 0; i < 512; i++)
        {
            printf("0x%02x ", read_buf[i]);
            if((i + 1) % 16 == 0)
            {
                printf("\r\n");
            }
        }
    }
    else
    {
        printf("Read block data fail! status = %d \r\n", sdcard_status);
    }
}

2.2.3 main.c

添加printf支持,在usart.c中添加如下代码

// 需要调用stdio.h文件
#include <stdio.h>
// 取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)// 标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 
}; 
FILE __stdout;       
void _sys_exit(int x) // 定义_sys_exit()以避免使用半主机模式
{ 
	x = x;
} 
void _ttywrch(int ch)	// 这个函数一般情况下应该是不需要的,但是在这里需要
{
	ch = ch;
}
int fputc(int ch, FILE *f)
{  
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

main.c中while之前运行:

SD_info_print();
SD_read_writer_test();    // 阻塞式读写测试
SD_read_writer_dma_test();	// DMA读写测试

如果均能正常执行,即表示配置无误。

如果第二个函数读写失败:参照修改2.1小节SDIOCLK clock divide factor参数。

如果生成的初始化函数运行卡死:参照修改2.1小节中的void MX_SDIO_SD_Init(void)函数。

注意:如果按照上述流程配置下来,SD_read_writer_test();写出错,而SD_read_writer_dma_test();正常运行是没什么问题的。

因为上面的时钟频率设置的挺高的(SDIOCLK clock divide factor: 2),在此情况下,只能使用DMA模式。

如果不放心,可以把这个参数调大一点之后,然后验证阻塞式读写。验证可以之后再调回来(实际应用中肯定希望速度快一点)。

2.3 移植 FATFS 系统

2.3.1 添加源码

在确保SD卡读写无误之后,开始移植。

在工程目录创建文件夹并放入FATFS系统源码

  • Middlewares\FATFS_R0.15
    • ffunicode.c
    • ffsystem.c
    • ffconf.h
    • ff.h
    • ff.c
    • diskio.h
    • diskio.c
    • 00readme.txt
    • 00history.txt

并在 Keil 中添加文件和头文件路径。

2.5


然后开始适配工作。

2.3.2 修改配置 ffconf.h

一些声明的含义可以参考一下网址:http://elm-chan.org/fsw/ff/doc/config.html。

以下内容是把原有的英文注释换成了中文的。并且修改了自己需要的配置。

需要注意的点(需要进行适配):

  • #define FF_CODE_PAGE 936 // 支持中文
  • #define FF_USE_LFN 3 // 支持长文件名,且存在堆区
  • #define FF_VOLUMES 1 // 一共有一个卷
/* 以下是全部默认拥有的配置 */

/* 版本ID */
#define FFCONF_DEF	80286	/* Revision ID */

/* 设置是否只读。建议为0,否则很多API用不了(0:可读写。1:只读)*/
#define FF_FS_READONLY	0

/* 
	此选项定义了最小化级别,以删除一些基本API函数。 
	
	0:基本功能已完全启用。建议为0
	1:f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename() 被移除
	2:在 1 的基础上 f_opendir(), f_readdir() and f_closedir() 被移除
	3:在 2 的基础上 f_lseek() 被移除
*/

#define FF_FS_MINIMIZE	0

/* 
	设置是否使用查找功能 f_findfirst() and f_findnext(). 
	
	0:不启用
	1:启用
	2:同时启用匹配altname[]
*/
#define FF_USE_FIND		0

/* 
	是否启用格式化功能 f_mkfs(),对于没有格式化为Fat32的U盘或者SD卡,设置为1,否则无法正常初始化。
	
	0:禁止使用 f_mkfs()
	1:允许使用 f_mkfs()
*/
#define FF_USE_MKFS		1

/* 设置是否使用快速查找功能(0:不使用。1:使用)*/
#define FF_USE_FASTSEEK	1

/* 设置是否使用拓展功能(0:不使用。1:使用) */
#define FF_USE_EXPAND	1

/* 切换属性操作函数 f_chmod()和 f_utime()(0:不使用。1:使用)仅在 FF_FS_READONLY 模式下可用*/
#define FF_USE_CHMOD	0

/* 切换卷标函数 f_getlabel() and f_setlabel() (0:不使用。1:使用)*/
#define FF_USE_LABEL	1

/* 此选项切换 f_forward()函数。(0:禁用或1:启用) */
#define FF_USE_FORWARD	0

/* 设置对字符串的操作 */
#define FF_USE_STRFUNC	1
#define FF_PRINT_LLI	1
#define FF_PRINT_FLOAT	1
#define FF_STRF_ENCODE	3
/* FF_USE_STRFUNC switches string functions, f_gets(), f_putc(), f_puts() and f_printf().
/
/   0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect.
/   1: Enable without LF-CRLF conversion.
/   2: Enable with LF-CRLF conversion.
/
/  FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2
/  makes f_printf() support floating point argument. These features want C99 or later.
/  When FF_LFN_UNICODE >= 1 with LFN enabled, string functions convert the character
/  encoding in it. FF_STRF_ENCODE selects assumption of character encoding ON THE FILE
/  to be read/written via those functions.
/
/   0: ANSI/OEM in current CP
/   1: Unicode in UTF-16LE
/   2: Unicode in UTF-16BE
/   3: Unicode in UTF-8
*/

/* 设置语言支持(932:中文支持。437:英文支持)*/
#define FF_CODE_PAGE	932
/* This option specifies the OEM code page to be used on the target system.
/  Incorrect code page setting can cause a file open failure.
/   437 - U.S.
/   720 - Arabic
/   737 - Greek
/   771 - KBL
/   775 - Baltic
/   850 - Latin 1
/   852 - Latin 2
/   855 - Cyrillic
/   857 - Turkish
/   860 - Portuguese
/   861 - Icelandic
/   862 - Hebrew
/   863 - Canadian French
/   864 - Arabic
/   865 - Nordic
/   866 - Russian
/   869 - Greek 2
/   932 - Japanese (DBCS)
/   936 - Simplified Chinese (DBCS)
/   949 - Korean (DBCS)
/   950 - Traditional Chinese (DBCS)
/     0 - Include all code pages above and configured by f_setcp()
*/

#define FF_USE_LFN		0
#define FF_MAX_LFN		255
/* The FF_USE_LFN switches the support for LFN (long file name).
/
/   0: Disable LFN. FF_MAX_LFN has no effect.(默认最长12个字符)
/   1: Enable LFN with static  working buffer on the BSS. Always NOT thread-safe.
/   2: Enable LFN with dynamic working buffer on the STACK.
/   3: Enable LFN with dynamic working buffer on the HEAP.
/
/  To enable the LFN, ffunicode.c needs to be added to the project. The LFN function
/  requiers certain internal working buffer occupies (FF_MAX_LFN + 1) * 2 bytes and
/  additional (FF_MAX_LFN + 44) / 15 * 32 bytes when exFAT is enabled.
/  The FF_MAX_LFN defines size of the working buffer in UTF-16 code unit and it can
/  be in range of 12 to 255. It is recommended to be set it 255 to fully support LFN
/  specification.
/  When use stack for the working buffer, take care on stack overflow. When use heap
/  memory for the working buffer, memory management functions, ff_memalloc() and
/  ff_memfree() exemplified in ffsystem.c, need to be added to the project. */

#define FF_LFN_UNICODE	0
/* This option switches the character encoding on the API when LFN is enabled.
/
/   0: ANSI/OEM in current CP (TCHAR = char)
/   1: Unicode in UTF-16 (TCHAR = WCHAR)
/   2: Unicode in UTF-8 (TCHAR = char)
/   3: Unicode in UTF-32 (TCHAR = DWORD)
/
/  Also behavior of string I/O functions will be affected by this option.
/  When LFN is not enabled, this option has no effect. */

#define FF_LFN_BUF		255
#define FF_SFN_BUF		12
/* This set of options defines size of file name members in the FILINFO structure
/  which is used to read out directory items. These values should be suffcient for
/  the file names to read. The maximum possible length of the read file name depends
/  on character encoding. When LFN is not enabled, these options have no effect. */

#define FF_FS_RPATH		0
/* This option confatfs文件系统移植 support for relative path.
/
/   0: Disable relative path and remove related functions.
/   1: Enable relative path. f_chdir() and f_chdrive() are available.
/   2: f_getcwd() function is available in addition to 1.
*/

/* Number of volumes (logical drives) to be used. (1-10) */
#define FF_VOLUMES		1

#define FF_STR_VOLUME_ID	0
#define FF_VOLUME_STRS		"RAM","NAND","CF","SD","SD2","USB","USB2","USB3"
/* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings.
/  When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive
/  number in the path name. FF_VOLUME_STRS defines the volume ID strings for each
/  logical drives. Number of items must not be less than FF_VOLUMES. Valid
/  characters for the volume ID strings are A-Z, a-z and 0-9, however, they are
/  compared in case-insensitive. If FF_STR_VOLUME_ID >= 1 and FF_VOLUME_STRS is
/  not defined, a user defined volume string table is needed as:
/
/  const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",...
*/

#define FF_MULTI_PARTITION	0
/* This option switches support for multiple volumes on the physical drive.
/  By default (0), each logical drive number is bound to the same physical drive
/  number and only an FAT volume found on the physical drive will be mounted.
/  When this function is enabled (1), each logical drive number can be bound to
/  arbitrary physical drive and partition listed in the VolToPart[]. Also f_fdisk()
/  function will be available. */

#define FF_MIN_SS		512
#define FF_MAX_SS		512
/* This set of options confatfs文件系统移植 the range of sector size to be supported. (512,
/  1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/  harddisk, but a larger value may be required for on-board flash memory and some
/  type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is configured
/  for variable sector size mode and disk_ioctl() function needs to implement
/  GET_SECTOR_SIZE command. */

#define FF_LBA64		0
/* This option switches support for 64-bit LBA. (0:Disable or 1:Enable)
/  To enable the 64-bit LBA, also exFAT needs to be enabled. (FF_FS_EXFAT == 1) */

#define FF_MIN_GPT		0x10000000
/* Minimum number of sectors to switch GPT as partitioning format in f_mkfs and
/  f_fdisk function. 0x100000000 max. This option has no effect when FF_LBA64 == 0. */

#define FF_USE_TRIM		0
/* This option switches support for ATA-TRIM. (0:Disable or 1:Enable)
/  To enable Trim function, also CTRL_TRIM command should be implemented to the
/  disk_ioctl() function. */

#define FF_FS_TINY		0
/* This option switches tiny buffer configuration. (0:Normal or 1:Tiny)
/  At the tiny configuration, size of file object (FIL) is shrinked FF_MAX_SS bytes.
/  Instead of private sector buffer eliminated from the file object, common sector
/  buffer in the filesystem object (FATFS) is used for the file data transfer. */

#define FF_FS_EXFAT		0
/* This option switches support for exFAT filesystem. (0:Disable or 1:Enable)
/  To enable exFAT, also LFN needs to be enabled. (FF_USE_LFN >= 1)
/  Note that enabling exFAT discards ANSI C (C89) compatibility. */

#define FF_FS_NORTC		0
#define FF_NORTC_MON	1
#define FF_NORTC_MDAY	1
#define FF_NORTC_YEAR	2022
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/  an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/  timestamp feature. Every object modified by FatFs will have a fixed timestamp
/  defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/  To enable timestamp function (FF_FS_NORTC = 0), get_fattime() function need to be
/  added to the project to read current time form real-time clock. FF_NORTC_MON,
/  FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/  These options have no effect in read-only configuration (FF_FS_READONLY = 1). */

#define FF_FS_NOFSINFO	0
/* If you need to know correct free space on the FAT32 volume, set bit 0 of this
/  option, and f_getfree() function at the first time after volume mount will force
/  a full FAT scan. Bit 1 controls the use of last allocated cluster number.
/
/  bit0=0: Use free cluster count in the FSINFO if available.
/  bit0=1: Do not trust free cluster count in the FSINFO.
/  bit1=0: Use last allocated cluster number in the FSINFO if available.
/  bit1=1: Do not trust last allocated cluster number in the FSINFO.
*/

#define FF_FS_LOCK		0
/* The option FF_FS_LOCK switches file lock function to control duplicated file open
/  and illegal operation to open objects. This option must be 0 when FF_FS_READONLY
/  is 1.
/
/  0:  Disable file lock function. To avoid volume corruption, application program
/      should avoid illegal open, remove and rename to the open objects.
/  >0: Enable file lock function. The value defines how many files/sub-directories
/      can be opened simultaneously under file lock control. Note that the file
/      lock control is independent of re-entrancy. */

#define FF_FS_REENTRANT	0
#define FF_FS_TIMEOUT	1000
/* The option FF_FS_REENTRANT switches the re-entrancy (thread safe) of the FatFs
/  module itself. Note that regardless of this option, file access to different
/  volume is always re-entrant and volume control functions, f_mount(), f_mkfs()
/  and f_fdisk() function, are always not re-entrant. Only file/directory access
/  to the same volume is under control of this featuer.
/
/   0: Disable re-entrancy. FF_FS_TIMEOUT have no effect.
/   1: Enable re-entrancy. Also user provided synchronization handlers,
/      ff_mutex_create(), ff_mutex_delete(), ff_mutex_take() and ff_mutex_give()
/      function, must be added to the project. Samples are available in ffsystem.c.
/
/  The FF_FS_TIMEOUT defines timeout period in unit of O/S time tick.
*/

这里设置了支持长文件名,并且相关内容要申请堆中的内存,因此这里需要增大堆内存的空间。

2.6

也可以直接从启动文件处修改

2.7

2.3.3 底层适配 diskio.c

需要适配的函数如下

2.8

第一步:设备编号定义

这里只有一个设备(SD卡),把DEV_MMC编号改为0。(要求从0开始按顺序增加)

其他的可以按顺序编号,当然也可以直接删掉。但是为了以后增加,建议保留。

/* Definitions of physical drive number for each drive */
#define DEV_RAM		1	/* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC		0	/* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB		2	/* Example: Map USB MSD to physical drive 2 */

这里名字其实是可以改的,比如改为(正点原子就是这样的):

#define SD_CARD	 	0  			// SD卡,卷标为0
#define EX_FLASH 	1			// 外部spi flash,卷标为1

如果改了,下面的适配函数也都一样改就可以了。

第二步:添加头文件

因为需要用到我们前面写好的驱动以及printf调试输出,把头文件添加进来。

#include "spi_sdcard.h"
#include "stdio.h"

第三步:函数适配

这里直接默认返回OK,没什么问题,其他类型的设备注释掉即可。需要用的话再修改。

DSTATUS disk_status (
	BYTE pdrv		/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case DEV_RAM :	// 还没用到,先注释掉
		// result = RAM_disk_status();

		// translate the reslut code here

		return stat;

	case DEV_MMC :
	
		return 0;	// 默认没什么问题

	case DEV_USB :	// 还没用到,先注释掉
		// result = USB_disk_status();

		// translate the reslut code here

		return stat;
	}
	return STA_NOINIT;
}

初始化调用我们前面写好的驱动

DSTATUS disk_initialize (
	BYTE pdrv				/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case DEV_RAM :	// 还没用到,先注释掉
		// result = RAM_disk_initialize();

		// translate the reslut code here

		return stat;

	case DEV_MMC :
		
		if(SD_Init() == 0)    // SD_Init()是SD初始化函数,无错误返回0
            return 0;
        else
            return STA_NOINIT;

	case DEV_USB :	// 还没用到,先注释掉
		// result = USB_disk_initialize();

		// translate the reslut code here

		return stat;
	}
	return STA_NOINIT;
}

读扇区函数,一样的调用前面的驱动

DRESULT disk_read (
	BYTE pdrv,		/* Physical drive nmuber to identify the drive */
	BYTE *buff,		/* Data buffer to store read data */
	LBA_t sector,	/* Start sector in LBA */
	UINT count		/* Number of sectors to read */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :
		// translate the arguments here

		// result = RAM_disk_read(buff, sector, count);

		// translate the reslut code here

		return res;

	case DEV_MMC :
		
		res = (DRESULT) SD_ReadDisk(buff, sector, count);
	
		while(res)	// 读出错
		{
			printf("sd read error:%d\r\n", res);
			SD_Init();	// 重新初始化SD卡
			res =  (DRESULT) SD_ReadDisk(buff,sector,count);
		}

		return res;

	case DEV_USB :
		// translate the arguments here

		// result = USB_disk_read(buff, sector, count);

		// translate the reslut code here

		return res;
	}

	return RES_PARERR;
}

写扇区函数,还是调用写好的驱动

DRESULT disk_write (
	BYTE pdrv,			/* Physical drive nmuber to identify the drive */
	const BYTE *buff,	/* Data to be written */
	LBA_t sector,		/* Start sector in LBA */
	UINT count			/* Number of sectors to write */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :
		// translate the arguments here

		// result = RAM_disk_write(buff, sector, count);

		// translate the reslut code here

		return res;

	case DEV_MMC :
		
		res = (DRESULT) SD_WriteDisk((uint8_t *)buff, sector, count);

		while(res)	// 写出错
		{
			printf("sd write error:%d\r\n", res);
			SD_Init();	// 重新初始化SD卡
			res = (DRESULT) SD_WriteDisk((uint8_t *)buff, sector, count);			
		}

		return res;

	case DEV_USB :
		// translate the arguments here

		// result = USB_disk_write(buff, sector, count);

		// translate the reslut code here

		return res;
	}

	return RES_PARERR;
}

命令函数

DRESULT disk_ioctl (
	BYTE pdrv,		/* Physical drive nmuber (0..) */
	BYTE cmd,		/* Control code */
	void *buff		/* Buffer to send/receive control data */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :

		// Process of the command for the RAM drive

		return res;

	case DEV_MMC :

		switch(cmd)
	    {
#if FF_FS_READONLY == 0
		    case CTRL_SYNC:
				res = RES_OK; 
		        break;
#endif
#if FF_USE_MKFS == 1
		    case GET_SECTOR_COUNT:
		        *(DWORD*)buff = SD_GetSectorCount();
		        res = RES_OK;
		        break;
		    case GET_BLOCK_SIZE:
				*(WORD*)buff = 8;
		        res = RES_OK;
		        break;
#endif
#if FF_MAX_SS != FF_MIN_SS
		    case GET_SECTOR_SIZE:
				*(DWORD*)buff = 512; 
		        res = RES_OK;
		        break;	 
#endif
		    default:
		        res = RES_PARERR;
		        break;
	    }
		return res;

	case DEV_USB :

		// Process of the command the USB drive

		return res;
	}

	return RES_PARERR;
}

在此文件的最后添加一个获取时间的函数,默认返回0

DWORD get_fattime(void)
{				 
	return 0;
}

2.3.4 验证 main.c

把以下代码放在main函数中执行,如果成功执行,说明没问题。

这里执行 f_res = f_mount(&fs, “0:”, 1),标号0就是diskio.c开头定义的标号。

	FATFS fs;				/* FatFs 文件系统对象 */
	FRESULT f_res;			/* 文件操作结果 */
	BYTE work[FF_MAX_SS];

	FIL file;                       /* 文件对象 */
	UINT fnum;                      /* 文件成功读写数量 */
	BYTE ReadBuffer[1024] = {0};    /* 读缓冲区 */
	BYTE WriteBuffer[] = "This is STM32 working with FatFs \r\n";	/* 写缓冲区 */

	// 在外部 SD 卡挂载文件系统,文件系统挂载时会对 SD 卡初始化
	// note:必须先要保证SD卡正常拥有FAT文件系统,如果没有会失败。
    f_res = f_mount(&fs, "0:", 1);
	
	/* 如果没有文件系统就格式化创建创建文件系统 */
    if(f_res == FR_NO_FILESYSTEM)
    {
        printf("The SD card does not yet have a file system and is about to be formatted... \r\n");
        /* 格式化 */
		f_res = f_mkfs("0:", NULL, work, sizeof(work));	/* Create a FAT volume */
		
        if(f_res == FR_OK)
        {
            printf("The SD card successfully formatted the file system\r\n");
            /* 格式化后,先取消挂载 */
            f_res = f_mount(NULL, "0:", 1);
            /* 重新挂载 */
            f_res = f_mount(&fs, "0:", 1);
        }
        else
        {
            printf("The format failed\r\n");
            while(1);
        }
    }
    else if(f_res != FR_OK)
    {
        printf("mount error : %d \r\n", f_res);
        while(1);
    }
    else
    {
        printf("mount sucess!!! \r\n");
    }
	
	/*----------------------- 文件系统测试:写测试 -----------------------------*/
    /* 打开文件,如果文件不存在则创建它 */
    printf("\r\n ****** Create and Open new text file objects with write access ****** \r\n");
    
	f_res = f_open(&file, "0:FatFs STM32cube.txt", FA_CREATE_ALWAYS | FA_WRITE);
    if(f_res == FR_OK)
    {
        printf("open file sucess!!! \r\n");
        /* 将指定存储区内容写入到文件内 */
        printf("\r\n****** Write data to the text files ******\r\n");
        f_res = f_write(&file, WriteBuffer, sizeof(WriteBuffer), &fnum);
        if(f_res == FR_OK)
        {
            printf("write file sucess!!! (%d)\r\n", fnum);
            printf("write Data : %s\r\n", WriteBuffer);
        }
        else
        {
            printf("write file error : %d\r\n", f_res);
        }
        /* 不再读写,关闭文件 */
        f_close(&file);
    }
    else
    {
        printf("open file error : %d\r\n", f_res);
    }
    
    /*------------------- 文件系统测试:读测试 ------------------------------------*/
    printf("\r\n****** Read data from the text files ******\r\n");
    f_res = f_open(&file, "0:FatFs STM32cube.txt", FA_OPEN_EXISTING | FA_READ);
    if(f_res == FR_OK)
    {
        printf("open file sucess!!! \r\n");
        f_res = f_read(&file, ReadBuffer, sizeof(ReadBuffer), &fnum);
        if(f_res == FR_OK)
        {
            printf("read sucess!!! (%d)\r\n", fnum);
            printf("read Data : %s\r\n", ReadBuffer);
        }
        else
        {
            printf(" read error!!! %d\r\n", f_res);
        }
    }
    else
    {
        printf("open file error : %d\r\n", f_res);
    }
    /* 不再读写,关闭文件 */
    f_close(&file);
    /* 不再使用文件系统,取消挂载文件系统 */
    f_mount(NULL, "0:", 1);
    /* 操作完成,停机 */

其他 API 的使用参考 FATFS 官网:http://elm-chan.org/fsw/ff/00index_e.html

3. TF卡 SPI 模式移植

这里使用RTT&正点原子的潘多拉loT Board开发板,主控芯片为STM32L475VET6。开发板上TF卡与主控的通信采用SPI通信

原理图如下:

3.1

  • SPI1_SCK:PA5
  • SPI1_MISO:PA6
  • SPI1_MOSI:PA7
  • SD_CS:PC3

3.1 创建工程

使用CubeMX创建一个工程。需要配置的项目有:

  • 时钟
  • 程序调试接口
  • 串口(用于观察调试信息)
  • SPI 接口
  • 片选引脚CS(推挽输出,默认高电平,速度最快,上拉)

其中 SPI 接口的配置如下:

3.2

SPI 与 SD卡 通信,在卡识别阶段,时钟频率不能超过400K。在传输数据阶段可以更高,最多不超过25M。不同的卡上限不太一样。

这里初始化时先设置一个较低的速度,识别完成之后更改 SPI 波特率即可。

3.2 SD卡读写验证

3.2.1 spi_sdcard.h

#ifndef __SPI_SDCARD_H
#define __SPI_SDCARD_H


#include "stdint.h"

/*
	SD_CS:		PC3		// 片选
*/
#define	SD_CS_SET(n)	(n?HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_RESET))


// SD卡类型定义  
#define SD_TYPE_ERR     0X00
#define SD_TYPE_MMC     0X01
#define SD_TYPE_V1      0X02
#define SD_TYPE_V2      0X04
#define SD_TYPE_V2HC    0X06
// SD卡指令表  	   
#define CMD0    0       // 卡复位
#define CMD1    1
#define CMD8    8       // 命令8 ,SEND_IF_COND
#define CMD9    9       // 命令9 ,读CSD数据
#define CMD10   10      // 命令10,读CID数据
#define CMD12   12      // 命令12,停止数据传输
#define CMD16   16      // 命令16,设置SectorSize 应返回0x00
#define CMD17   17      // 命令17,读sector
#define CMD18   18      // 命令18,读Multi sector
#define CMD23   23      // 命令23,设置多sector写入前预先擦除N个block
#define CMD24   24      // 命令24,写sector
#define CMD25   25      // 命令25,写Multi sector
#define CMD41   41      // 命令41,应返回0x00
#define CMD55   55      // 命令55,应返回0x01
#define CMD58   58      // 命令58,读OCR信息
#define CMD59   59      // 命令59,使能/禁止CRC,应返回0x00
// 数据写入回应字意义
#define MSD_DATA_OK                0x05
#define MSD_DATA_CRC_ERROR         0x0B
#define MSD_DATA_WRITE_ERROR       0x0D
#define MSD_DATA_OTHER_ERROR       0xFF
// SD卡回应标记字
#define MSD_RESPONSE_NO_ERROR      0x00
#define MSD_IN_IDLE_STATE          0x01
#define MSD_ERASE_RESET            0x02
#define MSD_ILLEGAL_COMMAND        0x04
#define MSD_COM_CRC_ERROR          0x08
#define MSD_ERASE_SEQUENCE_ERROR   0x10
#define MSD_ADDRESS_ERROR          0x20
#define MSD_PARAMETER_ERROR        0x40
#define MSD_RESPONSE_FAILURE       0xFF


/* ========================== 以下通信部分修改区 ============================*/
void SD_SPI_SetSpeed(uint8_t SPI_BaudRatePrescaler);
uint8_t SD_SPI_ReadWriteByte(uint8_t TxData);
uint8_t SD_SPI_WriteByte(uint8_t TxData);
/* ========================== 以上通信部分修改区 ============================*/

/* ========================== 以下SD卡通信修改区 ============================*/
extern uint8_t  SD_Type;			//SD卡的类型

uint8_t SD_ReadWriteByte(uint8_t data);
void SD_SpeedLow(void);
void SD_SpeedHigh(void);
/* ========================== 以上SD卡通信修改区 ============================*/

/* ========================== 以下SD卡通信实现区(无需修改) ============================*/
uint8_t SD_WaitReady(void);										// 等待SD卡准备
void SD_DisSelect(void);										// 取消片选
uint8_t SD_Select(void);										// 片选选中
uint8_t SD_GetResponse(uint8_t Response);						// 获得响应
uint8_t SD_RecvData(uint8_t *buf, uint16_t len);				// 读sd卡512字节(一个扇区)
uint8_t SD_SendBlock(uint8_t *buf, uint8_t cmd);				// 向sd卡写512字节(一个扇区)
uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc);		// 发送命令
uint8_t SD_GetCID(uint8_t *cid_data);							// 读SD卡CID
uint8_t SD_GetCSD(uint8_t *csd_data);							// 读SD卡CSD
uint32_t SD_GetSectorCount(void);								// 读扇区数
uint8_t SD_Init(void);											// 初始化
uint8_t SD_ReadDisk(uint8_t *buf, uint32_t sector, uint8_t cnt);	// 写块,连续读扇区
uint8_t SD_WriteDisk(uint8_t *buf, uint32_t sector, uint8_t cnt);	// 读块,连续写扇区
/* ========================== 以上SD卡通信实现区(无需修改) ============================*/


#endif	/* __SPI_SDCARD_H */

3.2.2 spi_sdcard.c

#include "spi_sdcard.h"
#include "stdio.h"	// 调试信息输出

#include "spi.h"    // cubemx生成的
#include "usart.h"  // cubemx生成的


/* ========================== 以下通信部分修改区 ============================*/

extern SPI_HandleTypeDef hspi1;
#define SD_SPI hspi1            // 减少移植时需要修改的地方


/**
 * @brief   SD_SPI 速度设置函数
 *
 * @param   SPI_BaudRate_Prescaler  SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_256(2的倍数)
 *
 * @return  void
 *
 * @remark  SD卡初始化时,波特率不能超过400k,传输数据时可以很高(不同SD卡的最高范围不同)。
 *
 */
void SD_SPI_SetSpeed(uint8_t SPI_BaudRatePrescaler)
{
    assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler)); // 判断有效性

    __HAL_SPI_DISABLE(&SD_SPI);            // 先关闭SPI

    // hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;    // 设置波特率

    SD_SPI.Instance->CR1 &= 0XFFC7;                 // 位3-5清零,用来设置波特率
    SD_SPI.Instance->CR1 |= SPI_BaudRatePrescaler;  // 设置SPI速度

    __HAL_SPI_ENABLE(&SD_SPI);             // 重新使能SPI
}

/**
 * @brief   SD_SPI 读写一个字节
 *
 * @param   TxData      要写入的字节
 *
 * @return  uint8_t     读取到的字节
 */
uint8_t SD_SPI_ReadWriteByte(uint8_t TxData)
{
    uint8_t Rxdata;
    HAL_SPI_TransmitReceive(&SD_SPI, &TxData, &Rxdata, 1, 1000);
    return Rxdata;
}

/**
 * @brief   SD_SPI 写入一个字节
 *
 * @param   TxData      要写入的字节
 *
 * @return  uint8_t     0:写入成功,其他:写入失败
 */
uint8_t SD_SPI_WriteByte(uint8_t TxData)
{
    return HAL_SPI_Transmit(&SD_SPI, &TxData, 1, 1000);
}

/* ========================== 以上通信部分修改区 ============================*/


/* ========================== 以下SD卡通信修改区 ============================*/

uint8_t  SD_Type = 0;   // SD卡的类型

/**
 * @brief   SD卡底层读写一个字节
 *
 * @param   data        要写入的字节
 *
 * @return  uint8_t     读取到的字节
 *
 * @remark  调用SPI读写
 */
uint8_t SD_ReadWriteByte(uint8_t data)
{
    return SD_SPI_ReadWriteByte(data);
}

/**
 * @brief   SD卡初始化的时候,需要低速(400K以下)
 *
 * @param   void
 *
 * @return  void
 *
 * @remark  根据自己的芯片选择合适的分频系数 STM32L475VET6在这个分频系数下对应的是312.5K
 */
void SD_SpeedLow(void)
{
    // 初始化时设置到低速模式
    SD_SPI_SetSpeed(SPI_BAUDRATEPRESCALER_256);
}

/**
 * @brief   SD卡正常工作的时候,可以高速
 *
 * @param   void
 *
 * @return  void
 *
 * @remark  根据自己的芯片选择合适的分频系数 STM32L475VET6在这个分频系数下对应的是40M
 */
void SD_SpeedHigh(void)
{
    // 传输数据时设置到高速模式
    SD_SPI_SetSpeed(SPI_BAUDRATEPRESCALER_2);   // STM32L475VET6对应的是40M
}

/* ========================== 以上SD卡通信修改区 ============================*/


/* ========================== 以下SD卡通信实现区(无需修改) ============================*/

/**
 * @brief   等待卡准备好
 *
 * @param   void
 *
 * @return  uint8_t 0,准备好了;其他,错误代码
 */
uint8_t SD_WaitReady(void)
{
    uint32_t t = 0;
    do
    {
        if(SD_SPI_ReadWriteByte(0XFF) == 0XFF)
			return 0;		// OK
        t++;
    } while(t < 0XFFFFFF);	// 等待
    return 1;
}

/**
 * @brief   取消选择,释放SPI总线
 *
 * @param   void
 *
 * @return  void
 */
void SD_DisSelect(void)
{
    SD_CS_SET(1);
    SD_ReadWriteByte(0xff); // 提供额外的8个时钟
}

/**
 * @brief   选择SD卡,并且等待卡准备OK
 *
 * @param   void
 *
 * @return  0,成功;1,失败;
 */
uint8_t SD_Select(void)
{
    SD_CS_SET(0);
    
	if(SD_WaitReady() == 0)
        return 0;	// 等待成功
	
    SD_DisSelect();
	
    return 1;	// 等待失败
}

/**
 * @brief   等待SD卡回应
 *
 * @param   Response	要得到的回应值
 *
 * @return  uint8_t		0,成功得到了该回应值。其他,得到回应值失败。
 */
uint8_t SD_GetResponse(uint8_t Response)
{
    uint16_t Count = 0xFFFF; // 等待次数
    while((SD_SPI_ReadWriteByte(0XFF) != Response) && Count)
		Count--; 	// 等待得到准确的回应
    if(Count == 0)
		return MSD_RESPONSE_FAILURE;	// 得到回应失败
    else 
		return MSD_RESPONSE_NO_ERROR;	// 正确回应
}

/**
 * @brief   从sd卡读取一个数据包的内容
 *
 * @param   buf     	数据缓存区
 * @param   len     	要读取的数据长度
 *
 * @return  uint8_t     0,成功;其他,失败
 */
uint8_t SD_RecvData(uint8_t *buf, uint16_t len)
{
    if(SD_GetResponse(0xFE))
		return 1;	// 等待SD卡发回数据起始令牌0xFE
    while(len--)	// 开始接收数据
    {
        *buf = SD_ReadWriteByte(0xFF);
        buf++;
    }
    //下面是2个伪CRC(dummy CRC)
    SD_SPI_ReadWriteByte(0xFF);
    SD_SPI_ReadWriteByte(0xFF);
    return 0; // 读取成功
}

/**
 * @brief   向sd卡写入一个数据包的内容 512字节
 *
 * @param   buf     	数据缓存区
 * @param   cmd     	指令
 *
 * @return  uint8_t     0,成功;其他,失败
 */
uint8_t SD_SendBlock(uint8_t *buf, uint8_t cmd)
{
    uint16_t t;
	
    if(SD_WaitReady())
		return 1;	// 等待准备失效
	
    SD_ReadWriteByte(cmd);
	
    if(cmd != 0XFD)	// 不是结束指令
    {
        for(t = 0; t < 512; t++)
			SD_ReadWriteByte(buf[t]);		// 提高速度,减少函数传参时间
		
        SD_ReadWriteByte(0xFF);	// 忽略crc
        SD_ReadWriteByte(0xFF);
        
		t = SD_SPI_ReadWriteByte(0xFF);	// 接收响应
        
		if((t & 0x1F) != 0x05)
			return 2;	// 响应错误
    }
    return 0;	// 写入成功
}

/**
 * @brief   向SD卡发送一个命令
 *
 * @param   cmd     	命令
 * @param   arg     	命令参数
 * @param   crc     	crc校验值
 *
 * @return  uint8_t     SD卡返回的响应
 */
uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc)
{
    uint8_t r1;
    uint8_t Retry = 0;
    SD_DisSelect();		// 取消上次片选
    if(SD_Select())
		return 0XFF;	// 片选失效
    
	// 发送
    SD_ReadWriteByte(cmd | 0x40);	// 分别写入命令
    SD_ReadWriteByte(arg >> 24);
    SD_ReadWriteByte(arg >> 16);
    SD_ReadWriteByte(arg >> 8);
    SD_ReadWriteByte(arg);
    SD_ReadWriteByte(crc);
	
    if(cmd == CMD12)
		SD_ReadWriteByte(0xff); // Skip a stuff byte when stop reading
    // 等待响应,或超时退出
    Retry = 0X1F;
	do
    {
        r1 = SD_ReadWriteByte(0xFF);
    } while((r1 & 0X80) && Retry--);
    
    return r1;	// 返回状态值
}

/**
 * @brief   获取SD卡的CID信息,包括制造商信息
 *
 * @param   cid_data    存放CID的内存,至少16Byte
 *
 * @return  uint8_t		0:NO_ERR。1:错误
 */
uint8_t SD_GetCID(uint8_t *cid_data)
{
    uint8_t r1;
    // 发CMD10命令,读CID
    r1 = SD_SendCmd(CMD10, 0, 0x01);
    if(r1 == 0x00)
    {
        r1 = SD_RecvData(cid_data, 16); // 接收16个字节的数据
    }
	
    SD_DisSelect();	// 取消片选
	
    if(r1)
		return 1;
    else
		return 0;
}

/**
 * @brief   获取SD卡的CSD信息,包括容量和速度信息
 *
 * @param   cid_data    存放CSD的内存,至少16Byte
 *
 * @return  uint8_t		0:NO_ERR。1:错误
 */
uint8_t SD_GetCSD(uint8_t *csd_data)
{
    uint8_t r1;
    r1 = SD_SendCmd(CMD9, 0, 0x01);		// 发CMD9命令,读CSD
    if(r1 == 0)
    {
        r1 = SD_RecvData(csd_data, 16); // 接收16个字节的数据
    }
    SD_DisSelect();//取消片选
    if(r1)return 1;
    else return 0;
}

/**
 * @brief   获取SD卡的总扇区数(扇区数)每扇区的字节数必为512,因为如果不是512,则初始化不能通过.
 *
 * @param   void
 *
 * @return  uint32_t	0: 取容量出错。其他:SD卡的容量(扇区数/512字节)
 */
uint32_t SD_GetSectorCount(void)
{
    uint8_t csd[16];
    uint32_t Capacity;
    uint8_t n;
    uint16_t csize;
	
    // 取CSD信息,如果期间出错,返回0
    if(SD_GetCSD(csd) != 0)
		return 0;
	
    printf("SD_GetCSD(csd):[%d]\r\n", csd[0]);
	
    // 如果为SDHC卡,按照下面方式计算
    if((csd[0] & 0xC0) == 0x40)	// V2.00的卡
    {
        csize = csd[9] + ((uint16_t)csd[8] << 8) + 1;
        Capacity = (uint32_t)csize << 10;	// 得到扇区数
    }
    else // V1.XX的卡
    {
        n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2;
        csize = (csd[8] >> 6) + ((uint16_t)csd[7] << 2) + ((uint16_t)(csd[6] & 3) << 10) + 1;
        Capacity = (uint32_t)csize << (n - 9); // 得到扇区数
    }
    return Capacity;
}

/**
 * @brief   初始化SD卡
 *
 * @param   void
 *
 * @return  uint8_t     0:初始化成功,其他:初始化错误
 */
uint8_t SD_Init(void)
{
    uint8_t r1;			// 存放SD卡的返回值
    uint16_t retry;		// 用来进行超时计数
    uint8_t buf[4];
    uint16_t i;

    SD_SpeedLow();		// 设置到低速模式
	
    for(i = 0; i < 10; i++)
		SD_ReadWriteByte(0XFF);	// 发送最少74个脉冲
	
    retry = 20;
    do
    {
        r1 = SD_SendCmd(CMD0, 0, 0x95); // 进入IDLE状态
    } while((r1 != 0X01) && retry--);
	
    SD_Type = 0;	// 默认无卡
    if(r1 == 0X01)
    {
        if(SD_SendCmd(CMD8, 0x1AA, 0x87) == 1)	// SD V2.0
        {
            for(i = 0; i < 4; i++)
				buf[i] = SD_ReadWriteByte(0XFF);	// Get trailing return value of R7 resp
            if(buf[2] == 0X01 && buf[3] == 0XAA)		// 卡是否支持2.7~3.6V
            {
                retry = 0XFFFE;
                do
                {
                    SD_SendCmd(CMD55, 0, 0X01);		// 发送CMD55
                    r1 = SD_SendCmd(CMD41, 0x40000000, 0X01);	// 发送CMD41
                } while(r1 && retry--);
                if(retry && SD_SendCmd(CMD58, 0, 0X01) == 0)	// 鉴别SD2.0卡版本开始
                {
                    for(i = 0; i < 4; i++)
						buf[i] = SD_ReadWriteByte(0XFF);	// 得到OCR值
                    if(buf[0] & 0x40)
						SD_Type = SD_TYPE_V2HC;					// 检查CCS
                    else 
						SD_Type = SD_TYPE_V2;
                }
            }
        }
        else // SD V1.x/ MMC V3
        {
            SD_SendCmd(CMD55, 0, 0X01);			// 发送CMD55
            r1 = SD_SendCmd(CMD41, 0, 0X01);	// 发送CMD41
            if(r1 <= 1)
            {
                SD_Type = SD_TYPE_V1;
                retry = 0XFFFE;
                do // 等待退出IDLE模式
                {
                    SD_SendCmd(CMD55, 0, 0X01);			// 发送CMD55
                    r1 = SD_SendCmd(CMD41, 0, 0X01);	// 发送CMD41
                } while(r1 && retry--);
            }
            else // MMC卡不支持CMD55+CMD41识别
            {
                SD_Type = SD_TYPE_MMC; // MMC V3
                retry = 0XFFFE;
                do // 等待退出IDLE模式
                {
                    r1 = SD_SendCmd(CMD1, 0, 0X01); // 发送CMD1
                } while(r1 && retry--);
            }
            if(retry == 0 || SD_SendCmd(CMD16, 512, 0X01) != 0)SD_Type = SD_TYPE_ERR; // 错误的卡
        }
    }
    SD_DisSelect();		// 取消片选
    SD_SpeedHigh();		// 高速
    if(SD_Type)
		return 0;
    else if(r1)
		return r1;
	
    return 0xaa;	// 其他错误
}

/**
 * @brief   读SD卡,连续读扇区
 *
 * @param   buf     	数据缓存区
 * @param   sector  	扇区
 * @param   cnt     	扇区数
 *
 * @return  uint8_t     0,OK;其他,失败.
 */
uint8_t SD_ReadDisk(uint8_t *buf, uint32_t sector, uint8_t cnt)
{
    uint8_t r1;
    if(SD_Type != SD_TYPE_V2HC)
		sector <<= 9;	// 转换为字节地址
    if(cnt == 1)
    {
        r1 = SD_SendCmd(CMD17, sector, 0X01); // 读命令
        if(r1 == 0) // 指令发送成功
        {
            r1 = SD_RecvData(buf, 512); // 接收512个字节
        }
    }
    else
    {
        r1 = SD_SendCmd(CMD18, sector, 0X01);	// 连续读命令
        do
        {
            r1 = SD_RecvData(buf, 512);			// 接收512个字节
            buf += 512;
        } while(--cnt && r1 == 0);
        SD_SendCmd(CMD12, 0, 0X01);				// 发送停止命令
    }
    SD_DisSelect();	// 取消片选
	
    return r1;
}

/**
 * @brief   写SD卡,连续写扇区
 *
 * @param   buf     数据缓存区
 * @param   sector  起始扇区
 * @param   cnt     扇区数
 *
 * @return  uint8_t     0,OK;其他,失败.
 */
uint8_t SD_WriteDisk(uint8_t *buf, uint32_t sector, uint8_t cnt)
{
    uint8_t r1;
    if(SD_Type != SD_TYPE_V2HC)
		sector *= 512;	// 转换为字节地址
    if(cnt == 1)
    {
        r1 = SD_SendCmd(CMD24, sector, 0X01); // 读命令
        if(r1 == 0) // 指令发送成功
        {
            r1 = SD_SendBlock(buf, 0xFE);	// 写512个字节
        }
    }
    else
    {
        if(SD_Type != SD_TYPE_MMC)
        {
            SD_SendCmd(CMD55, 0, 0X01);
            SD_SendCmd(CMD23, cnt, 0X01);		// 发送指令
        }
        r1 = SD_SendCmd(CMD25, sector, 0X01);	// 连续读命令
        if(r1 == 0)
        {
            do
            {
                r1 = SD_SendBlock(buf, 0xFC);	// 接收512个字节
                buf += 512;
            } while(--cnt && r1 == 0);
            r1 = SD_SendBlock(0, 0xFD);			// 接收512个字节
        }
    }
    SD_DisSelect();	// 取消片选
    return r1;
}

/* ========================== 以上SD卡通信实现区(无需修改) ============================*/

3.2.3 main.c

添加printf支持,在usart.c中添加如下代码

// 需要调用stdio.h文件
#include <stdio.h>
// 取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)// 标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 
}; 
FILE __stdout;       
void _sys_exit(int x) // 定义_sys_exit()以避免使用半主机模式
{ 
	x = x;
} 
void _ttywrch(int ch)	// 这个函数一般情况下应该是不需要的,但是在这里需要
{
	ch = ch;
}
int fputc(int ch, FILE *f)
{  
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

把以下代码放在main函数中执行

uint8_t buf[512];
uint8_t sta = 0;

while(SD_Init())	// 检测不到SD卡
{
    printf("initing...!\r\n");
    HAL_Delay(100);
}

printf("SD Card Size = %d MB \r\n", SD_GetSectorCount() >> 11);	// 每个扇区512字节
printf("SD Card Capacity = %d\r\n", SD_GetSectorCount());		// 每个扇区512字节

// 从0扇区开始,读取一个扇区的数据
if(SD_ReadDisk(buf, 0, 1) == 0)
{
    printf("USART1 Sending Data...\r\n");
    printf("SECTOR 0 DATA:\r\n");

    for(uint32_t sd_size = 0; sd_size < 512; sd_size++)
        printf("%x ", buf[sd_size]); // 打印0扇区数据

    printf("\r\n USART1 Send Data Over! \r\n");
}

// 修改数组中的数据
for(uint32_t i = 0; i < 512; i++)
    buf[i] = i * 3;		// 初始化写入的数据,是3的倍数.

// 从第0个扇区开始读取一个扇区的数据
sta = SD_WriteDisk(buf, 0, 1);
if(sta == 0)
    printf("Write over!\r\n");
else
    printf("err:%d\r\n", sta);

// 从0扇区开始,读取一个扇区的数据(验证是否是写入的数据)
if(SD_ReadDisk(buf, 0, 1) == 0)
{
    printf("USART1 Sending Data...\r\n");
    printf("SECTOR 0 DATA:\r\n");

    for(uint32_t sd_size = 0; sd_size < 512; sd_size++)
        printf("%x ", buf[sd_size]); // 打印0扇区数据

    printf("\r\n USART1 Send Data Over! \r\n");
}

如果程序正常执行,说明SD卡读写没问题。可以进行下一步。

3.3 移植 FATFS 系统

3.3.1 添加源码

在工程目录创建文件夹并放入FATFS系统源码

  • Middlewares\FATFS_R0.15
    • ffunicode.c
    • ffsystem.c
    • ffconf.h
    • ff.h
    • ff.c
    • diskio.h
    • diskio.c
    • 00readme.txt
    • 00history.txt

并在 Keil 中添加文件和头文件路径。

3.3.2 修改配置 ffconf.h

一些声明的含义可以参考一下网址:http://elm-chan.org/fsw/ff/doc/config.html。

以下内容是把原有的英文注释换成了中文的。并且修改了自己需要的配置。

需要注意的点(需要进行适配):

  • #define FF_CODE_PAGE 936 // 支持中文
  • #define FF_USE_LFN 3 // 支持长文件名,且存在堆区
  • #define FF_VOLUMES 1 // 一共有一个卷
/* 版本ID */
#define FFCONF_DEF	80286	/* Revision ID */


/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/

/* 设置是否只读。建议为0,否则很多API用不了(0:可读写。1:只读)*/
#define FF_FS_READONLY	0

/* 
	此选项定义了最小化级别,以删除一些基本API函数。 
	
	0:基本功能已完全启用。建议为0
	1:f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename() 被移除
	2:在 1 的基础上 f_opendir(), f_readdir() and f_closedir() 被移除
	3:在 2 的基础上 f_lseek() 被移除
*/
#define FF_FS_MINIMIZE	0

/* 
	设置是否使用查找功能 f_findfirst() and f_findnext(). 
	
	0:不启用
	1:启用
	2:同时启用匹配altname[]
*/
#define FF_USE_FIND		0

/* 
	是否启用格式化功能 f_mkfs(),对于没有格式化为Fat32的U盘或者SD卡,设置为1,否则无法正常初始化。
	
	0:禁止使用 f_mkfs()
	1:允许使用 f_mkfs()
*/
#define FF_USE_MKFS		1

/* 设置是否使用快速查找功能(0:不使用。1:使用)*/
#define FF_USE_FASTSEEK	1

/* 设置是否使用拓展功能(0:不使用。1:使用) */
#define FF_USE_EXPAND	1

/* 切换属性操作函数 f_chmod()和 f_utime()(0:不使用。1:使用)仅在 FF_FS_READONLY 模式下可用*/
#define FF_USE_CHMOD	0

/* 切换卷标函数 f_getlabel() and f_setlabel() (0:不使用。1:使用)*/
#define FF_USE_LABEL	1

/* 此选项切换 f_forward()函数。(0:禁用或1:启用) */
#define FF_USE_FORWARD	0

/* 设置对字符串的操作 */
#define FF_USE_STRFUNC	1
#define FF_PRINT_LLI	1
#define FF_PRINT_FLOAT	1
#define FF_STRF_ENCODE	3
/* FF_USE_STRFUNC switches string functions, f_gets(), f_putc(), f_puts() and f_printf().
/
/   0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect.
/   1: Enable without LF-CRLF conversion.
/   2: Enable with LF-CRLF conversion.
/
/  FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2
/  makes f_printf() support floating point argument. These features want C99 or later.
/  When FF_LFN_UNICODE >= 1 with LFN enabled, string functions convert the character
/  encoding in it. FF_STRF_ENCODE selects assumption of character encoding ON THE FILE
/  to be read/written via those functions.
/
/   0: ANSI/OEM in current CP
/   1: Unicode in UTF-16LE
/   2: Unicode in UTF-16BE
/   3: Unicode in UTF-8
*/


/*---------------------------------------------------------------------------/
/ Locale and Namespace Configurations
/---------------------------------------------------------------------------*/


/* 设置语言支持(936:中文支持。437:英文支持)*/
#define FF_CODE_PAGE	936
/* This option specifies the OEM code page to be used on the target system.
/  Incorrect code page setting can cause a file open failure.
/   437 - U.S.
/   720 - Arabic
/   737 - Greek
/   771 - KBL
/   775 - Baltic
/   850 - Latin 1
/   852 - Latin 2
/   855 - Cyrillic
/   857 - Turkish
/   860 - Portuguese
/   861 - Icelandic
/   862 - Hebrew
/   863 - Canadian French
/   864 - Arabic
/   865 - Nordic
/   866 - Russian
/   869 - Greek 2
/   932 - Japanese (DBCS)
/   936 - Simplified Chinese (DBCS)
/   949 - Korean (DBCS)
/   950 - Traditional Chinese (DBCS)
/     0 - Include all code pages above and configured by f_setcp()
*/

/* 
	设置是否支持长文件名(如果设置了支持中文,建议一定要使用,中文一个字符两个字节)
	
	0:不启用(最长12个字符,FF_MAX_LFN参数不生效)
	1:启用(工作空间在BSS段,线程不安全。可以使用)
	2:启用(使用动态缓存在栈区。不推荐)
	3:启用(使用动态缓存在堆区。推荐使用)
*/
#define FF_USE_LFN		3
#define FF_MAX_LFN		255

/* 
	当FF_USE_LFN启用时,此处设置字符编码。(IO函数也会被影响)

	0: ANSI/OEM in current CP (TCHAR = char)
	1: Unicode in UTF-16 (TCHAR = WCHAR)
	2: Unicode in UTF-8 (TCHAR = char)
	3: Unicode in UTF-32 (TCHAR = DWORD)
*/
#define FF_LFN_UNICODE	0

/* 
	这组选项定义FILINFO结构中用于读取目录项的文件名成员的大小。
	这些值应该足以读取文件名。读取文件名的最大可能长度取决于字符编码。
	当LFN未启用时,这些选项无效。
*/
#define FF_LFN_BUF		255
#define FF_SFN_BUF		12

/* 
	设置是否支持相对路径

	0: 不支持相对路径,并删除相关函数
	1: 支持相对路径,f_chdir()和f_chdrive()可用。
	2: 在1的基础上,f_getcwd() 可用。
*/
#define FF_FS_RPATH		0


/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/

/*
	一共有几个卷。(1-10)
	有几个存储设备就设置为几,比如只有SD卡就是1;如果有SD卡和W25Q128就是2。
*/
#define FF_VOLUMES		1

/*
	FF_STR_VOLUME_ID:卷标从几开始。
	当 FF_STR_VOLUME_ID 设置为1或2时,路径名称中的驱动器号可以使用任意字符串。
	
	FF_VOLUME_STRS 定义每个逻辑驱动器的卷ID字符串。项目数量不得小于FF_VOLUMES。
	卷ID字符串的有效字符为A-Z、A-Z和0-9,但是,会对它们进行不区分大小写的比较。
	如果 FF_STR_VOLUME_ID>=1 且未定义 FF_VOLUME_STRS,则需要用户定义的卷字符串表,如下所示:
	const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",...
*/
#define FF_STR_VOLUME_ID	0
#define FF_VOLUME_STRS		"RAM","NAND","CF","SD","SD2","USB","USB2","USB3"

/* 
	多分区支持。(将一个物理设备分为多个磁盘)
	0:不支持。默认每个设备就是一个磁盘分区。
	1:支持。每个逻辑驱动器号都可以绑定到VolToPart[]中列出的任意物理驱动器和分区。
		并且支持 f_fdisk() 函数
*/
#define FF_MULTI_PARTITION	0


/*
	设置设备扇区的大小范围。(对于SD卡,固定就是512)
	
	如果 FF_MAX_SS > FF_MIN_SS。	FatFs被配置为可变扇区大小模式。
	disk_ioctl() 函数需要实现 GET_SECTOR_SIZE 命令
*/
#define FF_MIN_SS		512
#define FF_MAX_SS		512


/*
	此选项切换对64位LBA的支持。
	0:禁用
	1:启用
	要启用64位LBA,还需要启用exFAT(FF_FS_EXFAT==1)
*/
#define FF_LBA64		0

/*
	在f_mkfs和f_fdisk函数中将GPT切换为分区格式的最小扇区数。
	0x100000000最大值。当FF_LBA64==0时,此选项无效。
*/
#define FF_MIN_GPT		0x10000000

/*
	此选项切换对ATA-TRIM的支持。
	0:禁用
	1:启用
	要启用Trim函数,还应将CTRL_TRIM命令实现到disk_ioctl()函数。
*/
#define FF_USE_TRIM		0


/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/


/*
	此选项切换小缓冲区配置。
	0:正常
	1:微小。	在微小配置下,文件对象(FIL)的大小收缩为FF_MAX_SS字节。
		文件系统对象(FATFS)中的公共扇区缓冲区用于文件数据传输,而不是从文件对象中删除私有扇区缓冲区
*/
#define FF_FS_TINY		0

/*
	此选项切换对exFAT文件系统的支持。
	0:禁用
	1:启用
	要启用exFAT,还需要启用LFN。(FF_USE_LFN>=1)
*/
#define FF_FS_EXFAT		0

/*
	FF_FS_NORTC:系统是否有RTC时钟,以支持时间戳功能。(FF_FS_READONLY = 1时,该选项不受影响)
	0:有RTC,支持时间戳。需要添加 get_fattime() 函数。
		FF_NORTC_YEAR、FF_NORTC_MDAY、FF_NORTC_MON 不受影响
	1:没有RTC,不支持时间戳。
		使用默认的FF_NORTC_YEAR、FF_NORTC_MDAY、FF_NORTC_MON
	
*/
#define FF_FS_NORTC		0
#define FF_NORTC_MON	1
#define FF_NORTC_MDAY	1
#define FF_NORTC_YEAR	2022

/*
	如果您需要知道FAT32卷上的正确可用空间,请设置此选项的位0,
	并且在卷装载后的第一时间执行f_getfree()函数将强制进行完整的FAT扫描。
	位1控制最后分配的簇号的使用。
	
	bit0=0: Use free cluster count in the FSINFO if available.
	bit0=1: Do not trust free cluster count in the FSINFO.
	bit1=0: Use last allocated cluster number in the FSINFO if available.
	bit1=1: Do not trust last allocated cluster number in the FSINFO.
*/
#define FF_FS_NOFSINFO	0

/*
	选项 FF_FS_LOCK 切换文件锁定功能以控制重复文件打开和非法操作以打开对象。
	当 FF_FS_READONLY 为1时,此选项必须为0。
	
	0:禁用文件锁定功能。为了避免卷损坏,应用程序应避免非法打开、删除和重命名打开的对象。
	!0:启用文件锁定功能。该值定义在文件锁定控制下可以同时打开的文件/子目录的数量。
		请注意,文件锁定控制与重新进入无关。
*/
#define FF_FS_LOCK		0

/*
	选项FF_FS_REENTRANT切换FatFs模块本身的再入(线程安全)。
	请注意,无论此选项如何,对不同卷的文件访问始终是可重入的,
	卷控制函数f_mount()、f_mkfs()和f_fdisk()始终不是可重入函数。
	只有对同一卷的文件/目录访问受此功能控制。
	
	0:禁用重入。FF_FS_TIMEOUT无效。
	1:启用重新进入。
		此外,必须将用户提供的同步处理程序ff_mutex_create()、ff_mutexdelete()
		ff_mutex_take()和ff_mutex_give()函数添加到项目中。
		样品可在ffsystem.c中获得。
		
	FF_FS_TIMEOUT 以 O/S 时间刻度为单位定义超时时间。
*/
#define FF_FS_REENTRANT	0
#define FF_FS_TIMEOUT	1000

这里设置了支持长文件名,并且相关内容要申请堆中的内存,因此这里需要增大堆内存的空间。

3.3

也可以直接从启动文件处修改

3.4

3.3.3 底层适配 diskio.c

需要适配的函数如下

2.8

第一步:设备编号定义

这里只有一个设备(SD卡),把DEV_MMC编号改为0。(要求从0开始按顺序增加)

其他的可以按顺序编号,当然也可以直接删掉。但是为了以后增加,建议保留。

/* Definitions of physical drive number for each drive */
#define DEV_RAM		1	/* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC		0	/* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB		2	/* Example: Map USB MSD to physical drive 2 */

这里名字其实是可以改的,比如改为(正点原子就是这样的):

#define SD_CARD	 	0  			// SD卡,卷标为0
#define EX_FLASH 	1			// 外部spi flash,卷标为1

如果改了,下面的适配函数也都一样改就可以了。

第二步:添加头文件

因为需要用到我们前面写好的驱动以及printf调试输出,把头文件添加进来。

#include "spi_sdcard.h"
#include "stdio.h"

第三步:函数适配

这里直接默认返回OK,没什么问题,其他类型的设备注释掉即可。需要用的话再修改。

DSTATUS disk_status (
	BYTE pdrv		/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case DEV_RAM :	// 还没用到,先注释掉
		// result = RAM_disk_status();

		// translate the reslut code here

		return stat;

	case DEV_MMC :
	
		return 0;	// 默认没什么问题

	case DEV_USB :	// 还没用到,先注释掉
		// result = USB_disk_status();

		// translate the reslut code here

		return stat;
	}
	return STA_NOINIT;
}

初始化调用我们前面写好的驱动

DSTATUS disk_initialize (
	BYTE pdrv				/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case DEV_RAM :	// 还没用到,先注释掉
		// result = RAM_disk_initialize();

		// translate the reslut code here

		return stat;

	case DEV_MMC :
		
		if(SD_Init() == 0)    // SD_Init()是SD初始化函数,无错误返回0
            return 0;
        else
            return STA_NOINIT;

	case DEV_USB :	// 还没用到,先注释掉
		// result = USB_disk_initialize();

		// translate the reslut code here

		return stat;
	}
	return STA_NOINIT;
}

读扇区函数,一样的调用前面的驱动

DRESULT disk_read (
	BYTE pdrv,		/* Physical drive nmuber to identify the drive */
	BYTE *buff,		/* Data buffer to store read data */
	LBA_t sector,	/* Start sector in LBA */
	UINT count		/* Number of sectors to read */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :
		// translate the arguments here

		// result = RAM_disk_read(buff, sector, count);

		// translate the reslut code here

		return res;

	case DEV_MMC :
		
		res = (DRESULT) SD_ReadDisk(buff, sector, count);
	
		while(res)	// 读出错
		{
			printf("sd read error:%d\r\n", res);
			SD_Init();	// 重新初始化SD卡
			res =  (DRESULT) SD_ReadDisk(buff,sector,count);
		}

		return res;

	case DEV_USB :
		// translate the arguments here

		// result = USB_disk_read(buff, sector, count);

		// translate the reslut code here

		return res;
	}

	return RES_PARERR;
}

写扇区函数,还是调用写好的驱动

DRESULT disk_write (
	BYTE pdrv,			/* Physical drive nmuber to identify the drive */
	const BYTE *buff,	/* Data to be written */
	LBA_t sector,		/* Start sector in LBA */
	UINT count			/* Number of sectors to write */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :
		// translate the arguments here

		// result = RAM_disk_write(buff, sector, count);

		// translate the reslut code here

		return res;

	case DEV_MMC :
		
		res = (DRESULT) SD_WriteDisk((uint8_t *)buff, sector, count);

		while(res)	// 写出错
		{
			printf("sd write error:%d\r\n", res);
			SD_Init();	// 重新初始化SD卡
			res = (DRESULT) SD_WriteDisk((uint8_t *)buff, sector, count);			
		}

		return res;

	case DEV_USB :
		// translate the arguments here

		// result = USB_disk_write(buff, sector, count);

		// translate the reslut code here

		return res;
	}

	return RES_PARERR;
}

命令函数

DRESULT disk_ioctl (
	BYTE pdrv,		/* Physical drive nmuber (0..) */
	BYTE cmd,		/* Control code */
	void *buff		/* Buffer to send/receive control data */
)
{
	DRESULT res;

	switch (pdrv) {
	case DEV_RAM :

		// Process of the command for the RAM drive

		return res;

	case DEV_MMC :

		switch(cmd)
	    {
#if FF_FS_READONLY == 0
		    case CTRL_SYNC:
				res = RES_OK; 
		        break;
#endif
#if FF_USE_MKFS == 1
		    case GET_SECTOR_COUNT:
		        *(DWORD*)buff = SD_GetSectorCount();
		        res = RES_OK;
		        break;
		    case GET_BLOCK_SIZE:
				*(WORD*)buff = 8;
		        res = RES_OK;
		        break;
#endif
#if FF_MAX_SS != FF_MIN_SS
		    case GET_SECTOR_SIZE:
				*(DWORD*)buff = 512; 
		        res = RES_OK;
		        break;	 
#endif
		    default:
		        res = RES_PARERR;
		        break;
	    }
		return res;

	case DEV_USB :

		// Process of the command the USB drive

		return res;
	}

	return RES_PARERR;
}

在此文件的最后添加一个获取时间的函数,默认返回0

DWORD get_fattime(void)
{				 
	return 0;
}

3.3.4 验证 main.c

把以下代码放在main函数中执行,如果成功执行,说明没问题。

这里执行 f_res = f_mount(&fs, “0:”, 1),标号0就是diskio.c开头定义的标号。

	FATFS fs;				/* FatFs 文件系统对象 */
	FRESULT f_res;			/* 文件操作结果 */
	BYTE work[FF_MAX_SS];

	FIL file;                       /* 文件对象 */
	UINT fnum;                      /* 文件成功读写数量 */
	BYTE ReadBuffer[1024] = {0};    /* 读缓冲区 */
	BYTE WriteBuffer[] = "This is STM32 working with FatFs \r\n";	/* 写缓冲区 */

	// 在外部 SD 卡挂载文件系统,文件系统挂载时会对 SD 卡初始化
	// note:必须先要保证SD卡正常拥有FAT文件系统,如果没有会失败。
    f_res = f_mount(&fs, "0:", 1);
	
	/* 如果没有文件系统就格式化创建创建文件系统 */
    if(f_res == FR_NO_FILESYSTEM)
    {
        printf("The SD card does not yet have a file system and is about to be formatted... \r\n");
        /* 格式化 */
		f_res = f_mkfs("0:", NULL, work, sizeof(work));	/* Create a FAT volume */
		
        if(f_res == FR_OK)
        {
            printf("The SD card successfully formatted the file system\r\n");
            /* 格式化后,先取消挂载 */
            f_res = f_mount(NULL, "0:", 1);
            /* 重新挂载 */
            f_res = f_mount(&fs, "0:", 1);
        }
        else
        {
            printf("The format failed\r\n");
            while(1);
        }
    }
    else if(f_res != FR_OK)
    {
        printf("mount error : %d \r\n", f_res);
        while(1);
    }
    else
    {
        printf("mount sucess!!! \r\n");
    }
	
	/*----------------------- 文件系统测试:写测试 -----------------------------*/
    /* 打开文件,如果文件不存在则创建它 */
    printf("\r\n ****** Create and Open new text file objects with write access ****** \r\n");
    
	f_res = f_open(&file, "0:FatFs STM32cube.txt", FA_CREATE_ALWAYS | FA_WRITE);
    if(f_res == FR_OK)
    {
        printf("open file sucess!!! \r\n");
        /* 将指定存储区内容写入到文件内 */
        printf("\r\n****** Write data to the text files ******\r\n");
        f_res = f_write(&file, WriteBuffer, sizeof(WriteBuffer), &fnum);
        if(f_res == FR_OK)
        {
            printf("write file sucess!!! (%d)\r\n", fnum);
            printf("write Data : %s\r\n", WriteBuffer);
        }
        else
        {
            printf("write file error : %d\r\n", f_res);
        }
        /* 不再读写,关闭文件 */
        f_close(&file);
    }
    else
    {
        printf("open file error : %d\r\n", f_res);
    }
    
    /*------------------- 文件系统测试:读测试 ------------------------------------*/
    printf("\r\n****** Read data from the text files ******\r\n");
    f_res = f_open(&file, "0:FatFs STM32cube.txt", FA_OPEN_EXISTING | FA_READ);
    if(f_res == FR_OK)
    {
        printf("open file sucess!!! \r\n");
        f_res = f_read(&file, ReadBuffer, sizeof(ReadBuffer), &fnum);
        if(f_res == FR_OK)
        {
            printf("read sucess!!! (%d)\r\n", fnum);
            printf("read Data : %s\r\n", ReadBuffer);
        }
        else
        {
            printf(" read error!!! %d\r\n", f_res);
        }
    }
    else
    {
        printf("open file error : %d\r\n", f_res);
    }
    /* 不再读写,关闭文件 */
    f_close(&file);
    /* 不再使用文件系统,取消挂载文件系统 */
    f_mount(NULL, "0:", 1);
    /* 操作完成,停机 */

其他 API 的使用参考 FATFS 官网:http://elm-chan.org/fsw/ff/00index_e.html

4. W25Q128 (Q)SPI模式移植

这里不同开发板与外部FALSH(W25Q128)的通信方式有一丢丢的区别。

两种流程是一样的,只有驱动部分有一丢丢区别。两个开发板都整一份。

4.1 W25QXX系列芯片

W25QXX系列芯片,共有八个引脚,可以使用不同的通信方式。

引脚编号QSPI通信备注SPI通信备注
1CS##片选引脚。低电平表示选中。CS##片选引脚。低电平表示选中。
2IO1数据线1,QSPI_BK1_IO1SOSPI数据输出接口
3IO2数据线2,QSPI_BK1_IO2WP##硬件写保护引脚,输入高电平可以正常写入数据,输入低电平禁止写入。
4GND公共地GND公共地
5IO0数据线0,QSPI_BK1_IO0SISPI数据输入接口
6CLK时钟线,QSPI_BK1_CLKCLKSPI时钟接口
7IO3数据线3,QSPI_BK1_IO3HOLD状态保存接口,输入低电平禁止操作芯片,输入高电平可正常操作芯片。
8VCC电源接口,2.7-3.6电源VCC电源接口,2.7-3.6电源

以W25Q64为例,其容量规划如下:

单位大小比例数量
256字节最小单位
扇区4K字节(4096字节)1扇区=16个页2048个
64K字节1块=16个扇区128个

其总容量为 64 * 1024 * 128 = 8388608字节 = 8192K字节 = 8M 字节。

对于W25Q128,其共拥有256个块,总容量就为 16M 字节。

芯片每次最小擦写单位为一个扇区,即4K字节。

4.2 创建工程

使用CubeMX创建一个工程。需要配置的项目有:

  • 时钟
  • 程序调试接口
  • 串口(用于观察调试信息)
  • (Q)SPI。这里需要根据不同的通讯方式创建工程。

如果使用QSPI创建工程(潘多拉loT Board开发板),则配置如下:

4.1

得到的初始化代码如下:

其中代码生成后需要手动修改的为:hqspi.Init.FlashSize,根据使用的Flash的大小修改。

void MX_QUADSPI_Init(void)
{
  hqspi.Instance = QUADSPI;
  hqspi.Init.ClockPrescaler = 0;	// QPSI分频比,W25Q128最大频率为104M,这里为80MHz
  hqspi.Init.FifoThreshold = 4;		// FIFO阈值为4个字节
  hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;	// 采样移位半个周期(DDR模式下,必须设置为0)
  hqspi.Init.FlashSize = POSITION_VAL(0x1000000)-1;		// SPI FLASH大小,W25Q128大小为16M字节
  hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_4_CYCLE;	// 片选高电平时间为4个时钟(12.5*4=50ns),即手册里面的tSHSL参数
  hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0;	// 模式0
  if (HAL_QSPI_Init(&hqspi) != HAL_OK)
  {
    Error_Handler();
  }
}

5. 多磁盘移植

这里我们直接在第三章的基础上改。

第一步:创建工程根据两种存储介质配置相应的接口。

第二步:添加每种存储介质的驱动、FATFS的源码

第三步:修改配置文件 ffconf.h

相较于上面的配置,需要修改的有

#define FF_VOLUMES		1	// 有几个存储介质,这里就改为几(1-10)

第四步:修改底层接口 diskio.c

设备定义。有几个就写几个,名字可以自己拟,但是标号要从0开始,这个标号就是挂载时的编号。

如果自拟了名字,那后面的函数中switch (pdrv) 的选项名字要对应的改一下

/* Definitions of physical drive number for each drive */
#define DEV_RAM		1	/* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC		0	/* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB		2	/* Example: Map USB MSD to physical drive 2 */

其他需要配置的接口如下,直接照抄上面,在需要的 case 中添加对应存储介质的实现即可。

DSTATUS disk_status (BYTE pdrv);
DSTATUS disk_initialize (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE *buff, LBA_t sector,	UINT count);
DRESULT disk_write (BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff);
DWORD get_fattime(void);

第五步:挂载

有几个存储设备,就挂载几个。其余操作和原来的一样。

FATFS fs0, fs1;
f_res = f_mount(&fs0, "0:", 1);
if()	// 判断是否成功
{    
}
f_res = f_mount(&fs1, "1:", 1);
if()	// 判断是否成功
{    
}