STM32CubeMX学习笔记:使用FatFs文件系统操作SD卡(第27期)

一、FatFs简介

FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统。它完全是由 ANSI C 语言编写并且完全独立于底层的 I/O 介质。因此它可以很容易地不加修改地移植到其他的处理器当中,如 8051、PIC、AVR、SH、Z80、H8、ARM 等。FatFs 支持 FAT12、FAT16、FAT32 等格式,所以我们利用前面写好的 SPI Flash 芯片驱动,把 FatFs 文件系统代码移植到工程之中,就可以利用文件系统的各种函数,对 SPI Flash 芯片以“文件”格式进行读写操作了。

FatFs 文件系统的源码可以从 fatfs 官网下载:
http://elm-chan.org/fsw/ff/00index_e.html

1.1 FatFs文件系统布局

簇是文件存储的最小单元,FAT32分区大小与对应簇空间大小关系如下表示:

分区空间大小 簇空间大小 每个簇包含的扇区数
< 8GB 4KB 8
[ 8GB, 16GB ) 8KB 16
[ 16GB, 32GB ) 16KB 32
>= 32GB 32KB 64

例如:创建一个50字节的test.txt文件,文件大小是50字节,但是占用磁盘空间为4096字节(一个簇)

1.2 FatFs层次结构

  • 最顶层是应用层:使用者只需要调用FATFS模块提供给用户的一系列应用接口函数(如f_open, f_read, f_write和f_close等),就可以像在PC上读写文件那样简单

  • 中间层FATFS模块:实现了FAT文件读写协议;它提供了ff.c和ff.h文件,一般情况下不用修改,使用时将头文件包含进去即可

  • 最底层是FATFS模块的底层接口:包括存储媒介读写接口和供给文件创建修改时间的实时时钟,需要在移植时编写对应的代码

  • FATFS源码相关文件介绍如下表示;移植FATFS模块时,一般只需要修改2个文件(即ffconf.hdiskio.c

    与平台无关:

    文件 说明
    ffconf.h FATFS模块配置文件
    ff.h FATFS和应用模块公用的包含文件
    ff.c FATFS模块
    diskio.h FATFS和disk I/O模块公用的包含文件
    interger.h 数据类型定义
    option 可选的外部功能(比如支持中文)

    与平台相关:

    文件 说明
    diskio.c FATFS和disk I/O模块接口层文件

    1.3 FatFs API

    1.3.1 f_mount

    功能 在FatFs模块上注册、注销一个工作区(文件系统对象)
    函数定义 FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt)
    参数 fs:工作区(文件系统对象)指针
    path:注册/注销工作区的逻辑驱动器号
    opt:注册或注销选项
    返回 操作结果

    1.3.2 f_open

    功能 创建/打开一个文件对象
    函数定义 FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode)
    参数 fp:将被创建的文件对象结构的指针
    path:文件名指针,指定将创建或打开的文件名
    mode:访问类型和打开方法,由一下标准的一个组合指定的
    返回 操作结果
    模式 描述
    FA_READ 指定读访问对象。可以从文件中读取数据。 与FA_WRITE 结 合可以进行读写访问。
    FA_WRITE 指定写访问对象。可以向文件中写入数据。与FA_READ 结合 可以进行读写访问。
    FA_OPEN_EXISTING 打开文件。如果文件不存在,则打开失败。(默认)
    FA_OPEN_ALWAYS 如果文件存在,则打开;否则,创建一个新文件。
    FA_CREATE_NEW 创建一个新文件。如果文件已存在,则创建失败。
    FA_CREATE_ALWAYS 创建一个新文件。如果文件已存在,则它将被截断并覆盖。

    1.3.3 f_close

    功能 关闭一个打开的文件
    函数定义 FRESULT f_close(FIL* fp)
    参数 fp:指向将被关闭的已打开的文件对象结构的指针
    返回 操作结果

    1.3.4 f_read

    功能 从一个打开的文件中读取数据
    函数定义 FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br)
    参数 fp:指向将被读取的已打开的文件对象结构的指针
    buff:指向存储读取数据的缓冲区的指针
    btr:要读取的字节数
    br:指向返回已读取字节数的UINT变量的指针,返回为实际读取的字节数
    返回 操作结果

    1.3.5 f_write

    功能 写入数据到一个已打开的文件
    函数定义 FRESULT f_write(FIL* fp, void* buff, UINT btw, UINT* bw)
    参数 fp:指向将被写入的已打开的文件对象结构的指针
    buff:指向存储写入数据的缓冲区的指针
    btw:要写入的字节数
    bw:指向返回已写入字节数的UINT变量的指针,返回为实际写入的字节数
    返回 操作结果

    另外FatFs还有很多API操作函数,在这里不再作详细的介绍,详细信息请查看FatFs文件系统官网。

    二、新建工程

    1. 打开 STM32CubeMX 软件,点击“新建工程”

    2. 选择 MCU 和封装

    3. 配置时钟
    RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)

    选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
    修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置

    4. 配置调试模式
    非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
    SYS 设置,选择 Debug 为 Serial Wire

    三、SDIO

    STM32 控制器可以控制使用单线或 4 线传输,本开发板设计使用 4 线传输。

    3.1 参数配置

    Connetivity 中选择 SDIO 设置,并选择 SD 4 bits Wide bus 四线SD模式

    此时 SDIO 对应的管脚也被选中。

    Parameter Settings 进行具体参数配置。

    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: 6。时钟分频系数,它设定 SDIO_CLKCR 寄存器的 CLKDIV 位的值,设置 SDIOCLK 与 CLK 线输出时钟分频系数:CLK 线时钟频率=SDIOCLK/([CLKDIV+2])。

    SDIO_CK 引脚的时钟信号在卡识别模式时要求不超过 400KHz,而在识别后的数据传输模式时则希望有更高的速度(最大不超过 25MHz),所以会针对这两种模式配置 SDIOCLK 的时钟。

    这里参数描述建议将SDIOCLK clock divede factor 参数使用默认值为0,SDIOCLK为72MHz,可以得到最大频率36MHz,但请注意,有些型号的SD卡可能不支持36MHz这么高的频率,所以还是要以实际情况而定。

    3.2 配置DMA

    SDIO 外设支持生成 DMA 请求,使用 DMA 传输可以提高数据传输效率,因此在 SDIO 的控制代码中,可以把它设置为 DMA 传输模式或轮询模式,ST 标准库提供 SDIO 示例中针对这两个模式做了区分处理。应用中一般都使用DMA 传输模式。

    点击 DMA Settings 添加 SDIO 对应 DMA2 的通道4。DMA模式选择循环模式,方向选为内存到外设。

  • Priority
    当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通 道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
  • Mode
    Normal 表示单次传输,传输一次后终止传输。
    Circular 表示循环传输,传输完成后又重新开始继续传输,不断循环永不停止。
  • Increment Address
    Peripheral 表示外设地址自增。
    Memory 表示内存地址自增。
  • Data Width
    Byte 一个字节。
    Half Word 半个字,等于两字节。
    Word 一个字,等于四字节。
  • 3.3 配置NVIC

    DMA及SDIO中断设置,原则是全局中断优先级高于DMA中断

    四、FATFS

    4.1 参数配置

    Middleware 中选择 FATFS 设置,并勾选 SD Card 配置为SD卡

  • Function Parameters 跳过

  • Locale and Namespace Parameters:

  • CODE_PAGE(Code page on target): Simplified Chinese GBK(DBCS,OEM,Windows) 支持简体中文编码
  • USE_LFN(Use Long Filename): Enabled with dynamic working buffer on the STACK 支持长文件名,并指定使用栈空间为缓冲区
  • 缓存工作区为什么放在栈?其实fatfs提供了三个选项:BSS,STACK , HEAP,根据个人情况选一个。
    在BSS上启用带有静态工作缓冲区的LFN,不能动态分配。
    如果选择了HEAP(堆)且自己有属于自己的malloc就去重写ff_memalloc  ff_memfree函数。如果是库的malloc就不需要。
    一般都选择使用STACK(栈),能动态分配。
    当使用堆栈作为工作缓冲区时,请注意堆栈溢出。

  • Physical Drive Parameters:
  • VOLUMES(Logical drivers): 2 指定物理设备数量,这里设置为 2,包括预留 SD 卡和 SPI Flash 芯片
  • MAX_SS(Maximum Sector Size): 512 指定扇区大小的最大值。SD 卡扇区大小一般都为 512 字节,SPI Flash 芯片扇区大小一般设置为 4096 字节,所以需要把 _MAX_SS 改为 512
  • MIN_SS(Minimum Sector Size): 512 指定扇区大小的最小值
  • 4.2 配置SD卡检测引脚

    SD卡插入检测引脚,如果不配置一个引脚生成文件时会报错,所以这里即使没有硬件连接,也可以任意设置一引脚使用,生成工程后注释代码。

    4.3 增大栈空间

    将最小栈空间改到 0x1000

    注意:由于刚才设置长文件名动态缓存存储在堆中,故需要增大堆大小,如果不修改则程序运行时堆会生成溢出,程序进入硬件错误中断(HardFault),死循环。

    4.4 生成代码

    输入项目名和项目路径

    选择应用的 IDE 开发环境 MDK-ARM V5

    每个外设生成独立的 ’.c/.h’ 文件
    不勾:所有初始化代码都生成在 main.c
    勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。

    点击 GENERATE CODE 生成代码

    五、屏蔽SD卡插入检测代码

    由于没有检测SD卡插入的硬件引脚,打开 bsp_driver_sd.c 文件,修改 BSP_SD_IsDetected() 函数,始终返回SD_PRESENT:

    六、修改main函数

    定义相关变量:

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

    修改main函数:

    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_USART1_UART_Init();
      MX_SDIO_SD_Init();
      MX_FATFS_Init();
      /* USER CODE BEGIN 2 */
        printf("\r\n****** FatFs Example ******\r\n\r\n");
        
        //在外部 SD 卡挂载文件系统,文件系统挂载时会对 SD 卡初始化
        f_res = f_mount(&fs, "0:", 1);
        
        /*----------------------- 格式化测试 ---------------------------*/
        printf("\r\n****** Register the file system object to the FatFs module ******\r\n");
        /* 如果没有文件系统就格式化创建创建文件系统 */
        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:", 0, 0);
            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)\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)\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);
        /* 操作完成,停机 */
    
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    

    七、查看打印

    串口打印功能查看 STM32CubeMX学习笔记(6)——USART串口使用

    八、工程代码

    链接:https://pan.baidu.com/s/1kgJ7AmbnW89yHcrgBzD_1w 提取码:kg5a

    九、注意事项

    f_open、f_write、f_read如果偶尔有问题;f_mkfs报错 FS_DISK_ERR,可以加上了SDIO硬件流使能试试。

    用户代码要加在 USER CODE BEGIN NUSER CODE END N 之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。


    • 由 Leung 写于 2021 年 12 月 7 日

    • 参考:STM32CubeMX系列教程18:文件系统FATFS
        STM32CubeMX系列|FATFS文件系统
        1.6 Cubemx_STM32F103_NOOS SDIO_DMA_FATFS基于SD卡的FATFS测试(一)
        【STM32Cube_20】在SD卡上移植FATFS文件系统
        STM32CubeMX之SD卡+FatFs

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32CubeMX学习笔记:使用FatFs文件系统操作SD卡(第27期)

    发表评论