从 0 搭建 SPI Flash 文件系统:驱动、FatFS、读写与坑点

FatFS官网:https://elm-chan.org/fsw/ff/00index_e.html

当你用单片机做项目,代码调试靠串口、数据记录靠看屏幕、文件读写靠想象,久而久之,你会发现:没有文件系统,生活就像裸奔,哪都能跑,就是不太方便;

尤其是在一些需要长时间运行、持续采集数据的应用场景中,比如环境监测、设备日志记录、传感器数据采集等,如果没有一个可靠的文件系统来进行数据持久化存储,不仅开发调试麻烦,维护和升级也会变得困难重重;你总不能每次都靠串口打印几十KB甚至几MB的数据吧?

这时候你可能听说了一个神器:FatFS,一个轻量级的 FAT 文件系统,专为嵌入式系统设计,小巧灵活,支持 SD 卡、SPI Flash,甚至 RAMDisk;不论你用的是 STM32、GD32,还是别的 MCU 平台,都能把它“嫁接”过去;

有了文件系统,不仅可以更方便地与电脑共享数据(比如通过 U 盘或 SD 卡读取设备日志),还能按时间归档、分类管理信息,甚至在设备意外断电或异常重启时保留关键数据,提升项目的健壮性和专业程度;

那么,这篇文章就是来讲一讲:如何在你的单片机上,成功移植 FatFS,让你的 MCU 拥有读写文件的能力;

FatFS 移植流程概览

FatFS 的移植主要包括以下几个步骤:

  1. 准备底层存储驱动(如 SPI Flash 驱动)
  2. 实现 FatFS 所需的 diskio.c 接口函数
  3. 配置 ffconf.h 以满足你的文件系统需求
  4. 在主函数中初始化 FatFS,挂载文件系统
  5. 实现文件的读写操作测试

接下来,我们从第一步开始,移植 SPI Flash 驱动;

FatFS 文件系统移植到 SPI Flash

要让 FatFS 在单片机上正常工作,首先你得有一个“存储设备”能读能写;虽然 SD 卡是最常见的选择,但很多时候,SPI Flash 是更方便的一种方式:不需要外接卡座、不怕接触不良,容量也够用;

对应的驱动文件如下,将文件添加到你的工程中,驱动文件来自沁恒例程,做了一点点的补充与修改:

W25Qxx.c

#include "W25Qxx.h"

static void W25Qxx_Reset(void);

W25Qxx_Info_t W25Qxx_Info = {0};

__weak void W25Qxx_CS_Enable(void)
{

}

__weak void W25Qxx_CS_Disable(void)
{

}

// 返回1是正常
__weak uint8_t W25Qxx_ReadByte(uint8_t* RxData, uint16_t Size)
{
  (void)RxData;
  (void)Size;

  return 0;
}

// 返回1是正常
__weak uint8_t W25Qxx_WriteByte(uint8_t* TxData, uint16_t Size)
{
  (void)TxData;
  (void)Size;

  return 0;
}

__weak uint32_t W25Qxx_GetTick(void)
{
  return 0;
}

/**
  * @brief  Initializes the W25Q128FV interface.
  * @retval None
  */
uint8_t W25Qxx_Init(void)
{
  /* Reset W25Qxx */
  W25Qxx_Reset();

  return W25Qxx_GetStatus();
}

/**
  * @brief  This function reset the W25Qx.
  * @retval None
  */
static void W25Qxx_Reset(void)
{
  uint8_t cmd[2] = {RESET_ENABLE_CMD, RESET_MEMORY_CMD};

  W25Qxx_CS_Enable();
  /* Send the reset command */
  W25Qxx_WriteByte(cmd, 2);
  W25Qxx_CS_Disable();
}

/**
  * @brief  Reads current status of the W25Q128FV.
  * @retval W25Q128FV memory status
  */
uint8_t W25Qxx_GetStatus(void)
{
  uint8_t cmd[] = {READ_STATUS_REG1_CMD};
  uint8_t status;

  W25Qxx_CS_Enable();
  /* Send the read status command */
  W25Qxx_WriteByte(cmd, 1);
  /* Reception of the data */
  W25Qxx_ReadByte(&status, 1);
  W25Qxx_CS_Disable();

  /* Check the value of the register */
  if((status & W25QXX_FSR_BUSY) != 0)
  {
    return W25QXX_BUSY;
  }
  else
  {
    return W25QXX_OK;
  }
}

/**
  * @brief  This function send a Write Enable and wait it is effective.
  * @retval None
  */
uint8_t W25Qxx_WriteEnable(void)
{
  uint8_t cmd[] = {WRITE_ENABLE_CMD};
  uint32_t tickstart = W25Qxx_GetTick();

  /*Select the FLASH: Chip Select low */
  W25Qxx_CS_Enable();
  /* Send the read ID command */
  W25Qxx_WriteByte(cmd, 1);
  /*Deselect the FLASH: Chip Select high */
  W25Qxx_CS_Disable();

  /* Wait the end of Flash writing */
  while(W25Qxx_GetStatus() == W25QXX_BUSY)
  {
    /* Check for the Timeout */
    if((W25Qxx_GetTick() - tickstart) > W25QXX_TIMEOUT_VALUE)
    {
      return W25QXX_TIMEOUT;
    }
  }

  return W25QXX_OK;
}

/**
  * @brief  Read Manufacture/Device ID.
  * @param  return value address
  * @retval None
  */
void W25Qxx_Read_ID(uint8_t* ID)
{
  uint8_t cmd[4] = {READ_JEDEC_ID_CMD, 0x00, 0x00, 0x00};

  W25Qxx_CS_Enable();
  /* Send the read ID command */
  W25Qxx_WriteByte(cmd, 1);
  /* Reception of the data */
  W25Qxx_ReadByte(ID, 3);
  W25Qxx_CS_Disable();
}

void W25Qxx_IC_Check(void)
{
  uint32_t count;

  uint8_t temp_id[3];

  /* Read FLASH ID */
  W25Qxx_Read_ID(temp_id);

  W25Qxx_Info.Flash_ID = ((uint32_t)temp_id[0] << 16) | 
                         ((uint32_t)temp_id[1] << 8)  | 
                         ((uint32_t)temp_id[2]);

  W25Qxx_Info.Flash_Sector_Count = 0x00;
  W25Qxx_Info.Flash_Sector_Size  = 0x00;

  switch(W25Qxx_Info.Flash_ID)
  {
  /* W25XXX */
  case W25X10_FLASH_ID: /* 0xEF3011-----1M bit */
    count = 1;
    break;

  case W25X20_FLASH_ID: /* 0xEF3012-----2M bit */
    count = 2;
    break;

  case W25X40_FLASH_ID: /* 0xEF3013-----4M bit */
    count = 4;
    break;

  case W25X80_FLASH_ID: /* 0xEF4014-----8M bit */
    count = 8;
    break;

  case W25Q16_FLASH_ID1: /* 0xEF3015-----16M bit */
  case W25Q16_FLASH_ID2: /* 0xEF4015-----16M bit */
    count = 16;
    break;

  case W25Q32_FLASH_ID1: /* 0xEF4016-----32M bit */
  case W25Q32_FLASH_ID2: /* 0xEF6016-----32M bit */
    count = 32;
    break;

  case W25Q64_FLASH_ID1: /* 0xEF4017-----64M bit */
  case W25Q64_FLASH_ID2: /* 0xEF6017-----64M bit */
    count = 64;
    break;

  case W25Q128_FLASH_ID1: /* 0xEF4018-----128M bit */
  case W25Q128_FLASH_ID2: /* 0xEF6018-----128M bit */
    count = 128;
    break;

  case W25Q256_FLASH_ID1: /* 0xEF4019-----256M bit */
  case W25Q256_FLASH_ID2: /* 0xEF6019-----256M bit */
    count = 256;
    break;
  default:
    if((W25Qxx_Info.Flash_ID != 0xFFFFFFFF) && (W25Qxx_Info.Flash_ID != 0x00000000))
    {
      count = 16;
    }
    else
    {
      count = 0x00;
    }
    break;
  }
  count = ((uint32_t)count * 1024) * ((uint32_t)1024 / 8);

  if(count)
  {
    // 如果是内部,那么DEF_UDISK_SECTOR_SIZE是512,如果是外部,则DEF_SECTOR_SIZE是4096
    W25Qxx_Info.Flash_Sector_Count = count / DEF_SECTOR_SIZE; // DEF_SECTOR_SIZE;
    W25Qxx_Info.Flash_Sector_Size  = DEF_SECTOR_SIZE;         // DEF_SECTOR_SIZE;
    W25Qxx_Info.Flash_Page_Size = 256;                        // 全系列固定的
  }
  else
  {
//    printf ("External Flash not connected\r\n");
    //  while(1);
  }
}

/**
  * @brief  Reads an amount of data from the QSPI memory.
  * @param  pData: Pointer to data to be read
  * @param  ReadAddr: Read start address
  * @param  Size: Size of data to read
  * @retval QSPI memory status
  */
// TODO
uint8_t W25Qxx_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
{
  uint8_t cmd[4];

  /* Configure the command */
  cmd[0] = READ_CMD;
  cmd[1] = (uint8_t)(ReadAddr >> 16);
  cmd[2] = (uint8_t)(ReadAddr >> 8);
  cmd[3] = (uint8_t)(ReadAddr);

  W25Qxx_CS_Enable();
  /* Send the read ID command */
  W25Qxx_WriteByte(cmd, 4);
  /* Reception of the data */
  if(W25Qxx_ReadByte(pData, Size) == 0)
  {
    return W25QXX_ERROR;
  }
  W25Qxx_CS_Disable();
  return W25QXX_OK;
}

/**
  * @brief  Writes an amount of data to the QSPI memory.
  * @param  pData: Pointer to data to be written
  * @param  WriteAddr: Write start address
  * @param  Size: Size of data to write,No more than 256byte.
  * @retval QSPI memory status
  */
uint8_t W25Qxx_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size)
{
  uint8_t cmd[4];
  uint32_t end_addr, current_size, current_addr;
  uint32_t tickstart = W25Qxx_GetTick();

  /* Calculation of the size between the write address and the end of the page */
  current_addr = 0;

  while(current_addr <= WriteAddr)
  {
    current_addr += W25Qxx_Info.Flash_Page_Size;
  }
  current_size = current_addr - WriteAddr;

  /* Check if the size of the data is less than the remaining place in the page */
  if(current_size > Size)
  {
    current_size = Size;
  }

  /* Initialize the adress variables */
  current_addr = WriteAddr;
  end_addr = WriteAddr + Size;

  /* Perform the write page by page */
  do
  {
    /* Configure the command */
    cmd[0] = PAGE_PROG_CMD;
    cmd[1] = (uint8_t)(current_addr >> 16);
    cmd[2] = (uint8_t)(current_addr >> 8);
    cmd[3] = (uint8_t)(current_addr);

    /* Enable write operations */
    W25Qxx_WriteEnable();

    W25Qxx_CS_Enable();
    /* Send the command */
    if(W25Qxx_WriteByte(cmd, 4) == 0)
    {
      return W25QXX_ERROR;
    }

    /* Transmission of the data */
    if(W25Qxx_WriteByte(pData, current_size) == 0)
    {
      return W25QXX_ERROR;
    }
    W25Qxx_CS_Disable();
    /* Wait the end of Flash writing */
    while(W25Qxx_GetStatus() == W25QXX_BUSY)
    {
      /* Check for the Timeout */
      if((W25Qxx_GetTick() - tickstart) > W25QXX_TIMEOUT_VALUE)
      {
        return W25QXX_TIMEOUT;
      }
    }

    /* Update the address and size variables for next page programming */
    current_addr += current_size;
    pData += current_size;
    current_size = ((current_addr + W25Qxx_Info.Flash_Page_Size) > end_addr) ? (end_addr - current_addr) : W25Qxx_Info.Flash_Page_Size;
  }
  while(current_addr < end_addr);

  return W25QXX_OK;
}

/**
  * @brief  Erases the specified block of the QSPI memory.
  * @param  BlockAddress: Block address to erase
  * @retval QSPI memory status
  */
uint8_t W25Qxx_Erase_Block(uint32_t Address)
{
  uint8_t cmd[4];
  uint32_t tickstart = W25Qxx_GetTick();
  cmd[0] = SECTOR_ERASE_CMD;
  cmd[1] = (uint8_t)(Address >> 16);
  cmd[2] = (uint8_t)(Address >> 8);
  cmd[3] = (uint8_t)(Address);

  /* Enable write operations */
  W25Qxx_WriteEnable();

  /*Select the FLASH: Chip Select low */
  W25Qxx_CS_Enable();
  /* Send the read ID command */
  W25Qxx_WriteByte(cmd, 4);
  /*Deselect the FLASH: Chip Select high */
  W25Qxx_CS_Disable();

  /* Wait the end of Flash writing */
  while(W25Qxx_GetStatus() == W25QXX_BUSY)
  {
    /* Check for the Timeout */
    if((W25Qxx_GetTick() - tickstart) > W25QXX_SECTOR_ERASE_MAX_TIME)
    {
      return W25QXX_TIMEOUT;
    }
  }
  return W25QXX_OK;
}

/**
  * @brief  Erases the entire QSPI memory.This function will take a very long time.
  * @retval QSPI memory status
  */
uint8_t W25Qxx_Erase_Chip(void)
{
  uint8_t cmd[4];
  uint32_t tickstart = W25Qxx_GetTick();
  cmd[0] = CHIP_ERASE_CMD;

  /* Enable write operations */
  W25Qxx_WriteEnable();

  /*Select the FLASH: Chip Select low */
  W25Qxx_CS_Enable();
  /* Send the read ID command */
  W25Qxx_WriteByte(cmd, 1);
  /*Deselect the FLASH: Chip Select high */
  W25Qxx_CS_Disable();

  /* Wait the end of Flash writing */
  while(W25Qxx_GetStatus() != W25QXX_BUSY)
  {
    /* Check for the Timeout */
    if((W25Qxx_GetTick() - tickstart) > W25QXX_BULK_ERASE_MAX_TIME)
    {
      return W25QXX_TIMEOUT;
    }
  }
  return W25QXX_OK;
}

W25Qxx.h

#ifndef __W25QXX_H_
#define __W25QXX_H_

#include <stdint.h>

#define W25QXX_BULK_ERASE_MAX_TIME         250000
#define W25QXX_SECTOR_ERASE_MAX_TIME       3000
#define W25QXX_SUBSECTOR_ERASE_MAX_TIME    800
#define W25QXX_TIMEOUT_VALUE 1000

#define DEF_SECTOR_SIZE 4096

#define W25X10_FLASH_ID 0xEF3011   /* 1M bit */
#define W25X20_FLASH_ID 0xEF3012   /* 2M bit */
#define W25X40_FLASH_ID 0xEF3013   /* 4M bit */
#define W25X80_FLASH_ID 0xEF4014   /* 8M bit */
#define W25Q16_FLASH_ID1 0xEF3015  /* 16M bit */
#define W25Q16_FLASH_ID2 0xEF4015  /* 16M bit */
#define W25Q32_FLASH_ID1 0xEF4016  /* 32M bit */
#define W25Q32_FLASH_ID2 0xEF6016  /* 32M bit */
#define W25Q64_FLASH_ID1 0xEF4017  /* 64M bit */
#define W25Q64_FLASH_ID2 0xEF6017  /* 64M bit */
#define W25Q128_FLASH_ID1 0xEF4018 /* 128M bit */
#define W25Q128_FLASH_ID2 0xEF6018 /* 128M bit */
#define W25Q256_FLASH_ID1 0xEF4019 /* 256M bit */
#define W25Q256_FLASH_ID2 0xEF6019 /* 256M bit */

/* Reset Operations */
#define RESET_ENABLE_CMD          0x66
#define RESET_MEMORY_CMD          0x99

#define ENTER_QPI_MODE_CMD        0x38
#define EXIT_QPI_MODE_CMD         0xFF

/* Identification Operations */
#define READ_ID_CMD               0x90
#define DUAL_READ_ID_CMD          0x92
#define QUAD_READ_ID_CMD          0x94
#define READ_JEDEC_ID_CMD         0x9F

/* Read Operations */
#define READ_CMD                  0x03
#define FAST_READ_CMD             0x0B
#define DUAL_OUT_FAST_READ_CMD    0x3B
#define DUAL_INOUT_FAST_READ_CMD  0xBB
#define QUAD_OUT_FAST_READ_CMD    0x6B
#define QUAD_INOUT_FAST_READ_CMD  0xEB

/* Write Operations */
#define WRITE_ENABLE_CMD          0x06
#define WRITE_DISABLE_CMD         0x04

/* Register Operations */
#define READ_STATUS_REG1_CMD      0x05
#define READ_STATUS_REG2_CMD      0x35
#define READ_STATUS_REG3_CMD      0x15

#define WRITE_STATUS_REG1_CMD     0x01
#define WRITE_STATUS_REG2_CMD     0x31
#define WRITE_STATUS_REG3_CMD     0x11

/* Program Operations */
#define PAGE_PROG_CMD             0x02
#define QUAD_INPUT_PAGE_PROG_CMD  0x32

/* Erase Operations */
#define SECTOR_ERASE_CMD          0x20
#define CHIP_ERASE_CMD            0xC7

#define PROG_ERASE_RESUME_CMD     0x7A
#define PROG_ERASE_SUSPEND_CMD    0x75

/* Flag Status Register */
#define W25QXX_FSR_BUSY            ((uint8_t)0x01)    /*!< busy */
#define W25QXX_FSR_WREN            ((uint8_t)0x02)    /*!< write enable */
#define W25QXX_FSR_QE              ((uint8_t)0x02)    /*!< quad enable */

/* Status */
#define W25QXX_OK            ((uint8_t)0x00)
#define W25QXX_ERROR         ((uint8_t)0x01)
#define W25QXX_BUSY          ((uint8_t)0x02)
#define W25QXX_TIMEOUT       ((uint8_t)0x03)

typedef struct
{
  uint32_t Flash_ID;
  uint32_t Flash_Sector_Count;
  uint32_t Flash_Page_Size;
  uint16_t Flash_Sector_Size;
}W25Qxx_Info_t;

extern W25Qxx_Info_t W25Qxx_Info;

uint8_t W25Qxx_Init(void);  // 必须执行
uint8_t W25Qxx_GetStatus(void);
uint8_t W25Qxx_WriteEnable(void);
void W25Qxx_Read_ID(uint8_t* ID);
void W25Qxx_IC_Check(void); // 必须执行
uint8_t W25Qxx_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size);
uint8_t W25Qxx_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size);
uint8_t W25Qxx_Erase_Block(uint32_t Address);
uint8_t W25Qxx_Erase_Chip(void);

#endif

该驱动程序在SPI1上实现了对SPI FLASH的读写操作,包括初始化、读取ID、写入使能、写入禁用、读取状态寄存器、检查IC、擦除扇区、读取块、写入块等操作。

我将与单片机硬件相关的函数都使用弱定义进行了声明,这样在移植的时候,只需要在别的文件中实现硬件操作即可,不需要修改其他文件。比如,你可以在 spi.c 中实现以下函数:

void W25Qxx_CS_Enable(void)
{
  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);
}

void W25Qxx_CS_Disable(void)
{
  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);
}

// 返回1是正常
uint8_t W25Qxx_ReadByte(uint8_t* RxData, uint16_t Size)
{
  return (HAL_SPI_Receive(&hspi1, RxData, Size, 0xFF) == HAL_OK);
}

// 返回1是正常
uint8_t W25Qxx_WriteByte(uint8_t* TxData, uint16_t Size)
{
  return (HAL_SPI_Transmit(&hspi1, TxData, Size, 0xFF) == HAL_OK);
}

uint32_t W25Qxx_GetTick(void)
{
  return HAL_GetTick();
}

执行FLASH_IC_Check函数之后,函数会根据返回的芯片 ID,设置Flash_Type、Flash_ID、Flash_Sector_Count、Flash_Sector_Size等变量,以便后续操作使用;

移植 FatFS

实现 diskio.c 接口

主要就是需要编写 diskio.c 文件,实现以下函数(可以直接复制):

/*-----------------------------------------------------------------------*/
/* Low level disk I/O module SKELETON for FatFs     (C)ChaN, 2019        */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be        */
/* attached to the FatFs via a glue function rather than modifying it.   */
/* This is an example of glue functions to attach various exsisting      */
/* storage control modules to the FatFs module with a defined API.       */
/*-----------------------------------------------------------------------*/

#include "ff.h"			/* Obtains integer types */
#include "diskio.h"		/* Declarations of disk functions */

#include "W25Qxx.h"

/* Definitions of physical drive number for each drive */
#define DEV_SPIFLASH 0

/*-----------------------------------------------------------------------*/
/* Get Drive Status                                                      */
/*-----------------------------------------------------------------------*/

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

  switch(pdrv)
  {
  case DEV_SPIFLASH :
    result = W25Qxx_GetStatus();

    // translate the reslut code here
    switch(result)
    {
    case W25QXX_OK:
      stat = STA_NOINIT & (~STA_NOINIT);
      break;

    case W25QXX_ERROR:
      stat = STA_NOINIT;
      break;

    case W25QXX_BUSY:
      stat = STA_NOINIT;
      break;

    case W25QXX_TIMEOUT:
      stat = STA_NOINIT;
      break;
    }
    return stat;

  }
  return STA_NOINIT;
}



/*-----------------------------------------------------------------------*/
/* Inidialize a Drive                                                    */
/*-----------------------------------------------------------------------*/

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

  switch(pdrv)
  {
  case DEV_SPIFLASH :
    result = W25Qxx_Init();
    W25Qxx_IC_Check();

    // translate the reslut code here
    switch(result)
    {
    case W25QXX_OK:
      stat = STA_NOINIT & (~STA_NOINIT);
      break;

    case W25QXX_ERROR:
      stat = STA_NOINIT;
      break;

    case W25QXX_BUSY:
      stat = STA_NOINIT;
      break;

    case W25QXX_TIMEOUT:
      stat = STA_NOINIT;
      break;
    }
    return stat;

  }
  return STA_NOINIT;
}



/*-----------------------------------------------------------------------*/
/* Read Sector(s)                                                        */
/*-----------------------------------------------------------------------*/

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;
  uint8_t result;

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

    result = W25Qxx_Read(buff, sector * W25Qxx_Info.Flash_Sector_Size, count * W25Qxx_Info.Flash_Sector_Size);

    // translate the reslut code here
    switch(result)
    {
    case W25QXX_OK:
      res = RES_OK;
      break;

    case W25QXX_ERROR:
      res = RES_ERROR;
      break;

    case W25QXX_BUSY:
      res = RES_NOTRDY;
      break;

    case W25QXX_TIMEOUT:
      res = RES_ERROR;
      break;
    }
    return res;

  }

  return RES_PARERR;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)                                                       */
/*-----------------------------------------------------------------------*/

#if FF_FS_READONLY == 0

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;
  int result;

  switch(pdrv)
  {
  case DEV_SPIFLASH :
    // translate the arguments here
    for(UINT i = 0; i < count; i++)
    {
      W25Qxx_Erase_Block((sector + i) * W25Qxx_Info.Flash_Sector_Size);
    }
    result = W25Qxx_Write((uint8_t*)buff, sector * W25Qxx_Info.Flash_Sector_Size, count * W25Qxx_Info.Flash_Sector_Size);

    // translate the reslut code here
    switch(result)
    {
    case W25QXX_OK:
      res = RES_OK;
      break;

    case W25QXX_ERROR:
      res = RES_ERROR;
      break;

    case W25QXX_BUSY:
      res = RES_NOTRDY;
      break;

    case W25QXX_TIMEOUT:
      res = RES_ERROR;
      break;
    }
    return res;

  }

  return RES_PARERR;
}

#endif


/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions                                               */
/*-----------------------------------------------------------------------*/

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

  switch(pdrv)
  {
  case DEV_SPIFLASH :
    switch(cmd)
    {
    case GET_SECTOR_COUNT://将驱动器上可用扇区的数目返回到buff指向的DWORD变量中
    {
      *(DWORD*)buff = W25Qxx_Info.Flash_Sector_Count;
      break;
    }
    case GET_SECTOR_SIZE://将媒体的扇区大小返回到buff指向的WORD变量中
    {
      *(WORD*)buff = W25Qxx_Info.Flash_Sector_Size; //类型是WORD的类型,每个扇区是4096的大小,这里同时还需要修改MAX_SS的值
      break;
    }
    case GET_BLOCK_SIZE://将闪存介质的擦除块大小(以扇区为单位)返回到buff指向的DWORD变量中
    {
      *(DWORD*)buff = 1; //每次擦除的大小是1个扇区,因为单位是扇区
      break;
    }
    }
    // Process of the command for the RAM drive

    return res;

  }

  return RES_PARERR;
}

修改 ffconf.h 配置

#define FF_MAX_SS		4096  // 使用的是SPI FLash,所以这个需要修改为4096

#define FF_USE_MKFS		1     // 这个需要修改为1启用格式化的功能

#define FF_CODE_PAGE	936   // 可以设置成936,增加对中文的支持

#define FF_FS_NORTC		1     // 这个需要设置成1,就是先不搞RTC相关的日期功能

使用

main.c 文件中大体如下操作即可

#include "ff.h"

FATFS FsObject;

FIL fp;

static BYTE work_buffer[4096];

int main(void)
{
  FRESULT result;
  result=f_mount(&FsObject,"0:",1); // 这个0:就是路径,和#define FF_VOLUMES 1有关,设定为1则路径只有0:

  // 如果是13,则表明没有格式化,我们进行格式化
  // 如果是11,则表明数量对不上,需要去改设备个数,也就是FF_VOLUMES
  if(result == 13)
  {
    MKFS_PARM Format = {FM_FAT32, 0, 0, 0, 0};  // 为了兼容,可以改为FM_ANY,对16MB来说,FAT16最合适
    result = f_mkfs("0:", &Format, work_buffer, sizeof(work_buffer));

    result=f_mount(&FsObject,"0:",1);  // 再次挂载,其实还可以进行判断
  }

//  result = f_open(&fp, "0:test.txt", FA_OPEN_ALWAYS|FA_WRITE|FA_READ);

//  UINT test;
//  result = f_write(&fp,"test1234",sizeof("test1234"),&test);
//  f_close(&fp);

  
  result = f_open(&fp, "0:test.txt", FA_OPEN_ALWAYS|FA_WRITE|FA_READ);

  UINT test;
  uint8_t read[20];
  result=f_read(&fp, read, f_size(&fp), &test);

  while(1)
  {
    
  }

  return 0;
}

常见问题与调试技巧

Flash 相关问题

  • Q: FLASH_ReadID() 返回 0xFFFFFF 或 0x000000?
    A: SPI Flash 没有接好,或者 SPI 接口初始化未正确完成;建议检查:

    • SPI 时钟、模式是否与 Flash 兼容;
    • Flash 供电是否稳定;
    • CS 引脚是否正确拉低后开始通信;
  • Q: Flash 容量识别不对?
    A: FLASH_IC_Check() 中只处理了常见型号,若你使用的是不在列表内的型号,请根据 datasheet 添加对应的 JEDEC ID 和容量;

挂载与格式化相关

  • Q: f_mount 返回 FR_NO_FILESYSTEM(13),怎么解决?
    A: 说明当前设备上没有可识别的文件系统;应使用 f_mkfs 对 Flash 进行格式化,完成后再调用 f_mount 重新挂载;

  • Q: f_mount 返回 FR_INVALID_DRIVE(11)?
    A: 说明 FatFS 的卷编号配置有问题,请检查 ffconf.h 中 FF_VOLUMES 是否 >= 你的逻辑盘号,比如 f_mount(..., "0:", 1) 表示你至少得设置 #define FF_VOLUMES 1;

FAT 文件系统格式相关

  • Q: f_mkfs() 返回 FR_INVALID_PARAMETER?
    A: f_mkfs 参数设置不当,建议使用如下方式初始化:
MKFS_PARM fs_param = {FM_ANY, 0, 0, 0, 0};
f_mkfs("0:", &fs_param, work_buffer, sizeof(work_buffer));
  • Q: 格式化完文件系统容量很小(比如识别为1MB)?
    A: 可能是扇区大小未正确返回,检查 disk_ioctl() 中 GET_SECTOR_SIZE 和 GET_SECTOR_COUNT 是否准确计算,是否符合你实际 Flash 容量;

读写文件异常

  • Q: 文件写入后读出来的数据不对,乱码或者全 0?
    A: 可能原因

    • 写入之前没有正确擦除扇区;
    • 写操作未对齐页写入(W25系列对页写入有要求);
    • 写入数据后未调用 f_close() 或 f_sync(),导致未刷新缓存到 Flash;
    • diskio.c 中 FLASH_Erase_Sector() 和 W25XXX_WR_Block() 地址未正确计算;
  • Q: 写文件成功了,但再次打开文件内容变空?
    A: 注意写模式是否是 FA_CREATE_ALWAYS,该模式会每次打开都清空内容;如果想保留内容,改为 FA_OPEN_ALWAYS | FA_WRITE 并调用 f_lseek(&fp, f_size(&fp)) 跳到末尾再写;

文件系统行为与配置相关

  • Q: 文件名太长无法识别?
    A: 默认 FatFS 禁用长文件名(LFN),需在 ffconf.h 中配置:
#define FF_USE_LFN    1
#define FF_MAX_LFN    64
  • Q: 中文文件名乱码?
    A: 请设置正确的代码页,例如:
#define FF_CODE_PAGE 936  // 简体中文 GBK
  • Q: 同一个文件写入后再读读取不到内容?
    A: 若写入后未关闭文件或调用 f_sync(),FatFS 可能未刷新数据到底层 Flash,建议:
f_write(...);
f_sync(&fp);  // 确保数据落盘

运行异常 / 稳定性问题

  • Q: 写入操作中系统卡死或死循环?
    A: Flash 的写入/擦除是阻塞操作,部分芯片擦除单个扇区可能耗时几十毫秒;建议你:

    • 在写函数中加入 watchdog 喂狗机制;
    • 考虑用非阻塞 Flash 驱动 + 文件系统缓存策略来优化;
  • Q: Flash 写入过程中掉电,数据损坏?
    A: 推荐使用 FatFS 的事务机制,例如在写入文件时增加 f_sync(),或使用 FAT 的备用区功能(需高级配置);另外也可以借助 CRC 校验机制保证文件有效性;

调试技巧

  • 建议在初始化完成后先跑一个简单的读写测试函数
f_open(&fp, "test.txt", FA_CREATE_ALWAYS | FA_WRITE);
f_write(&fp, "hello", 5, &bw);
f_close(&fp);

f_open(&fp, "test.txt", FA_READ);
f_read(&fp, buf, 5, &br);
f_close(&fp);
  • 使用串口或者 RTT 等工具打印中间步骤结果,比如挂载结果、读写返回值、实际读出的内容,有助于快速定位问题;

  • 调试期间建议将所有错误码都打印出来对应 FR_XXX 含义,便于对照 FatFS 源码中的错误枚举;

总结

本篇博客详细介绍了如何将 FatFS 移植到 SPI Flash,并通过 W25Q128 实现文件读写功能;从驱动实现、FatFS 配置、文件操作到问题排查,整个流程强调的是「实用」与「稳定」,希望对你的嵌入式项目有所帮助;

值得注意的是,SPI Flash 天生具备“写前擦除”“页擦除/块擦除”的特性,且写入寿命有限(一般每个扇区约 10 万次擦写);这意味着在频繁写入场景下,Flash 容易出现写坏、性能衰减等问题;

为此,建议关注以下几点:

  • 磨损均衡(Wear Leveling):FatFS 本身不具备磨损均衡机制,如果使用 SPI Flash 存储频繁变更的数据(如日志、数据库),需要在应用层实现“循环覆盖”或“动态地址映射”来避免单点反复擦写;
  • 避免频繁格式化和 f_open/f_write/f_close 操作循环,应尽可能复用文件句柄,按需 flush 写入;
  • 设置合适的缓存机制,如启用 sector 缓冲,减少物理擦写次数;
  • 建议定期备份重要数据,并在系统初始化时进行 Flash 健康检查(可利用空闲位、标志位判断 Flash 是否写满或擦损);
  • 日志/配置文件等 建议使用固定格式(如简化版 TLV)写入,便于恢复和分析;

最后,虽然 FatFS 的结构设计优雅轻量,但在用它搭配 SPI Flash 构建嵌入式文件系统时,我们仍需深入理解底层 Flash 的行为特性,并结合自身项目场景做出相应调整和优化;

下一步,我计划基于 USB Composite(复合设备) 实现 STM32 同时具备串口调试和 模拟U盘功能,通过 USB MSC 协议挂载 FatFS 文件系统,让用户能够在电脑端直接读写 SPI Flash 中的数据,这将进一步提升系统的易用性和可扩展性,敬请期待!

阅读原文:从 0 搭建 SPI Flash 文件系统:驱动、FatFS、读写与坑点

posted @ 2026-05-04 17:58  EM-Time  阅读(7)  评论(0)    收藏  举报