一、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

三、SPI1

3.1 参数配置

Connectivity 中选择 SPI1 设置,并选择 Full-Duplex Master 全双工主模式,不开启 NSS 即不使用硬件片选信号

原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。

在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output纠正:野火STM32F103指南者开发板SPI1 NSS须配置为PC0

修改输出高电平 High,标签为 W25Q64_CHIP_SELECT

SPI 为默认设置不作修改。只需注意一下,Prescaler 分频系数最低为 4,波特率 (Baud Rate) 为 18.0 MBits/s。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。

  • Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
  • Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。

    根据 FLASH 芯片的说明,它支持 SPI 模式0模式 3,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。

    所以这里配置 CPOL 为 Low,CPHA 为 1 Edge 即 SPI 模式0

四、FATFS

4.1 参数配置

Middleware 中选择 FATFS 设置,并勾选 User-defined 因为 SPI Flash 在上面没有

  • 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): 4096 指定扇区大小的最大值。SD 卡扇区大小一般都为 512 字节,SPI Flash 芯片扇区大小一般设置为 4096 字节,所以需要把 _MAX_SS 改为 4096
    • MIN_SS(Minimum Sector Size): 512 指定扇区大小的最小值

4.2 增大栈空间

将最小栈空间改到 0x1000

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

4.3 生成代码

输入项目名和项目路径

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

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

点击 GENERATE CODE 生成代码

五、添加SPI Flash操作函数

user_diskio.c 中加入

//#define SPI_FLASH_PageSize            4096
#define SPI_FLASH_PageSize              256
#define SPI_FLASH_PerWritePageSize      256#define ManufactDeviceID_CMD    0x90
#define READ_STATU_REGISTER_1   0x05
#define READ_STATU_REGISTER_2   0x35
#define READ_DATA_CMD           0x03
#define WRITE_ENABLE_CMD        0x06
#define WRITE_DISABLE_CMD       0x04
#define SECTOR_ERASE_CMD        0x20
#define CHIP_ERASE_CMD          0xc7
#define PAGE_PROGRAM_CMD        0x02#define SPI_FLASH_CS_LOW()      HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
#define SPI_FLASH_CS_HIGH()     HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);extern SPI_HandleTypeDef hspi1; /*** @brief    SPI发送指定长度的数据* @param    buf  —— 发送数据缓冲区首地址* @param    size —— 要发送数据的字节数* @retval   成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);
}
/*** @brief   SPI接收指定长度的数据* @param   buf  —— 接收数据缓冲区首地址* @param   size —— 要接收数据的字节数* @retval  成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);
}
/*** @brief   SPI在发送数据的同时接收指定长度的数据* @param   send_buf  —— 接收数据缓冲区首地址* @param   recv_buf  —— 接收数据缓冲区首地址* @param   size —— 要发送/接收数据的字节数* @retval  成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}/*等待超时时间*/
#define SPIT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT         ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
static __IO uint32_t  SPITimeout = SPIT_LONG_TIMEOUT;
/*** @brief  等待超时回调函数* @param  None.* @retval None.*/
static  uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{/* 等待超时后的处理,输出错误信息 */printf("SPI 等待超时!errorCode = %d",errorCode);return 0;
}/*** @brief  使用SPI发送一个字节的数据* @param  byte:要发送的数据* @retval 返回接收到的数据*/
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待发送缓冲区为空,TXE事件 */while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_TXE ) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);}/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */WRITE_REG(hspi1.Instance->DR, byte);SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待接收缓冲区非空,RXNE事件 */while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_RXNE ) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);}/* 读取数据寄存器,获取接收缓冲区数据 */return READ_REG(hspi1.Instance->DR);
}/*** @brief   读取Flash内部的ID* @param   none* @retval  成功返回device_id*/
uint16_t W25QXX_ReadID(void)
{uint8_t recv_buf[2] = {0};    //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device IDuint16_t device_id = 0;uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00};   //待发送数据,命令+地址/* 使能片选 */SPI_FLASH_CS_LOW();/* 发送并读取数据 */if (HAL_OK == SPI_Transmit(send_data, 4)) {if (HAL_OK == SPI_Receive(recv_buf, 2)) {device_id = (recv_buf[0] << 8) | recv_buf[1];}}/* 取消片选 */SPI_FLASH_CS_HIGH();return device_id;
}
/*** @brief     读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器* @param     reg  —— 状态寄存器编号(1~2)* @retval    状态寄存器的值*/
static uint8_t W25QXX_ReadSR(uint8_t reg)
{uint8_t result = 0; uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};switch(reg){case 1:send_buf[0] = READ_STATU_REGISTER_1;case 2:send_buf[0] = READ_STATU_REGISTER_2;case 0:default:send_buf[0] = READ_STATU_REGISTER_1;}/* 使能片选 */SPI_FLASH_CS_LOW();if (HAL_OK == SPI_Transmit(send_buf, 4)) {if (HAL_OK == SPI_Receive(&result, 1)) {/* 取消片选 */SPI_FLASH_CS_HIGH();return result;}}/* 取消片选 */SPI_FLASH_CS_HIGH();return 0;
}
/*** @brief   阻塞等待Flash处于空闲状态* @param   none* @retval  none*/
static void W25QXX_Wait_Busy(void)
{while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
/*** @brief    W25QXX写使能,将S1寄存器的WEL置位* @param    none* @retval*/
void W25QXX_Write_Enable(void)
{uint8_t cmd= WRITE_ENABLE_CMD;SPI_FLASH_CS_LOW();SPI_Transmit(&cmd, 1);SPI_FLASH_CS_HIGH();W25QXX_Wait_Busy();}
/*** @brief    W25QXX写禁止,将WEL清零* @param    none* @retval    none*/
void W25QXX_Write_Disable(void)
{uint8_t cmd = WRITE_DISABLE_CMD;SPI_FLASH_CS_LOW();SPI_Transmit(&cmd, 1);SPI_FLASH_CS_HIGH();W25QXX_Wait_Busy();
}
/*** @brief    W25QXX擦除一个扇区* @param   sector_addr    —— 扇区地址 根据实际容量设置* @retval  none* @note    阻塞操作*/
void W25QXX_Erase_Sector(uint32_t sector_addr)
{   W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能W25QXX_Wait_Busy();     //等待写使能完成/* 使能片选 */SPI_FLASH_CS_LOW();/* 发送扇区擦除指令*/SPI_FLASH_SendByte(SECTOR_ERASE_CMD);/*发送擦除扇区地址的高位*/SPI_FLASH_SendByte((sector_addr & 0xFF0000) >> 16);/* 发送擦除扇区地址的中位 */SPI_FLASH_SendByte((sector_addr & 0xFF00) >> 8);/* 发送擦除扇区地址的低位 */SPI_FLASH_SendByte(sector_addr & 0xFF);/* 取消片选 */SPI_FLASH_CS_HIGH();W25QXX_Wait_Busy();       //等待扇区擦除完成
}
/*** @brief    页写入操作* @param    dat —— 要写入的数据缓冲区首地址* @param    WriteAddr —— 要写入的地址* @param   byte_to_write —— 要写入的字节数(0-256)* @retval    none*/
void W25QXX_PageProgram(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes)
{uint8_t cmd = PAGE_PROGRAM_CMD;//    WriteAddr <<= 8;W25QXX_Write_Enable();/* 使能片选 */SPI_FLASH_CS_LOW();SPI_Transmit(&cmd, 1);//    SPI_Transmit((uint8_t*)&WriteAddr, 3);uint8_t addr;HAL_StatusTypeDef status;/* 发送 读 地址高位 */addr = (WriteAddr & 0xFF0000) >> 16;status = SPI_Transmit(&addr, 1);/* 发送 读 地址中位 */addr = (WriteAddr & 0xFF00) >> 8;status = SPI_Transmit(&addr, 1);/* 发送 读 地址低位 */addr = WriteAddr & 0xFF;status = SPI_Transmit(&addr, 1);SPI_Transmit(dat, nbytes);/* 取消片选 */SPI_FLASH_CS_HIGH();W25QXX_Wait_Busy();
}
/*** @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区* @param   pBuffer,要写入数据的指针* @param  WriteAddr,写入地址* @param  NumByteToWrite,写入数据长度* @retval 无*/
void W25QXX_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{uint8_t NumOfPage = 0;uint8_t NumOfSingle = 0;uint8_t Addr = 0;uint8_t count = 0;uint8_t temp = 0;/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/Addr = WriteAddr % SPI_FLASH_PageSize;/*差count个数据值,刚好可以对齐到页地址*/count = SPI_FLASH_PageSize - Addr;  /*计算出要写多少整数页*/NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;/*mod运算求余,计算出剩余不满一页的字节数*/NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;/* Addr=0,则WriteAddr 刚好按页对齐 aligned  */if(Addr == 0) {/* NumByteToWrite < SPI_FLASH_PageSize */if(NumOfPage == 0) {W25QXX_PageProgram(pBuffer, WriteAddr, NumByteToWrite);}/* NumByteToWrite > SPI_FLASH_PageSize */else{/*先把整数页都写了*/while(NumOfPage--){W25QXX_PageProgram(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr +=  SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/W25QXX_PageProgram(pBuffer, WriteAddr, NumOfSingle);}}/* 若地址与 SPI_FLASH_PageSize 不对齐  */else {/* NumByteToWrite < SPI_FLASH_PageSize */if(NumOfPage == 0) {/*当前页剩余的count个位置比NumOfSingle小,写不完*/if(NumOfSingle > count) {temp = NumOfSingle - count;/*先写满当前页*/W25QXX_PageProgram(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;/*再写剩余的数据*/W25QXX_PageProgram(pBuffer, WriteAddr, temp);}/*当前页剩余的count个位置能写完NumOfSingle个数据*/else{               W25QXX_PageProgram(pBuffer, WriteAddr, NumByteToWrite);}}/* NumByteToWrite > SPI_FLASH_PageSize */else{/*地址不对齐多出的count分开处理,不加入这个运算*/NumByteToWrite -= count;NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;W25QXX_PageProgram(pBuffer, WriteAddr, count);WriteAddr +=  count;pBuffer += count;/*把整数页都写了*/while(NumOfPage--){W25QXX_PageProgram(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr +=  SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/if(NumOfSingle != 0){W25QXX_PageProgram(pBuffer, WriteAddr, NumOfSingle);}}}
}/*** @brief  读取FLASH数据* @param  pBuffer,存储读出数据的指针* @param   ReadAddr,读取地址* @param   NumByteToRead,读取数据长度* @retval 无*/
void W25QXX_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{  W25QXX_Wait_Busy();/* 选择FLASH: CS低电平 */SPI_FLASH_CS_LOW();/* 发送 读 指令 */uint8_t cmd = READ_DATA_CMD;SPI_Transmit(&cmd, 1);// 不知道为什么连起来发不行
//    ReadAddr = ReadAddr << 8;
//    SPI_Transmit((uint8_t*)&ReadAddr, 3);uint8_t addr;HAL_StatusTypeDef status;/* 发送 读 地址高位 */addr = (ReadAddr & 0xFF0000) >> 16;status = SPI_Transmit(&addr, 1);/* 发送 读 地址中位 */addr = (ReadAddr& 0xFF00) >> 8;status = SPI_Transmit(&addr, 1);/* 发送 读 地址低位 */addr = ReadAddr & 0xFF;status = SPI_Transmit(&addr, 1);if(HAL_OK == status){SPI_Receive(pBuffer, NumByteToRead);}/* 停止信号 FLASH: CS 高电平 */SPI_FLASH_CS_HIGH();
}

六、修改diskio接口函数

user_diskio.c 中修改以下几个函数:

/* Private functions ---------------------------------------------------------*//*** @brief  Initializes a Drive* @param  pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_initialize (BYTE pdrv           /* Physical drive nmuber to identify the drive */
)
{/* USER CODE BEGIN INIT *//* 延时一小段时间 */uint16_t i;i = 500;while(--i);Stat = STA_NOINIT;if(W25QXX_ReadID() != 0){Stat &= ~STA_NOINIT;}      return Stat;/* USER CODE END INIT */
}/*** @brief  Gets Disk Status* @param  pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_status (BYTE pdrv       /* Physical drive number to identify the drive */
)
{/* USER CODE BEGIN STATUS */Stat &= ~STA_NOINIT;return Stat;/* USER CODE END STATUS */
}/*** @brief  Reads Sector(s)* @param  pdrv: Physical drive number (0..)* @param  *buff: Data buffer to store read data* @param  sector: Sector address (LBA)* @param  count: Number of sectors to read (1..128)* @retval DRESULT: Operation result*/
DRESULT USER_read (BYTE pdrv,      /* Physical drive nmuber to identify the drive */BYTE *buff,     /* Data buffer to store read data */DWORD sector,   /* Sector address in LBA */UINT count      /* Number of sectors to read */
)
{/* USER CODE BEGIN READ */DRESULT status = RES_PARERR;if(!count){return RES_PARERR; //count不能等于0,否则返回参数错误}// /* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */// sector += 512;      W25QXX_BufferRead(buff, sector << 12, count << 12);status = RES_OK;return status;/* USER CODE END READ */
}/*** @brief  Writes Sector(s)* @param  pdrv: Physical drive number (0..)* @param  *buff: Data to be written* @param  sector: Sector address (LBA)* @param  count: Number of sectors to write (1..128)* @retval DRESULT: Operation result*/
#if _USE_WRITE == 1
DRESULT USER_write (BYTE pdrv,          /* Physical drive nmuber to identify the drive */const BYTE *buff,   /* Data to be written */DWORD sector,       /* Sector address in LBA */UINT count          /* Number of sectors to write */
)
{/* USER CODE BEGIN WRITE */uint32_t write_addr; DRESULT status = RES_PARERR;if(!count) {return RES_PARERR;        /* Check parameter */}///* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 *///sector += 512;write_addr = sector << 12;    W25QXX_Erase_Sector(write_addr);W25QXX_BufferWrite((uint8_t *)buff, write_addr, count << 12);status = RES_OK;return status;/* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 *//*** @brief  I/O control operation* @param  pdrv: Physical drive number (0..)* @param  cmd: Control code* @param  *buff: Buffer to send/receive control data* @retval DRESULT: Operation result*/
#if _USE_IOCTL == 1
DRESULT USER_ioctl (BYTE pdrv,      /* Physical drive nmuber (0..) */BYTE cmd,       /* Control code */void *buff      /* Buffer to send/receive control data */
)
{/* USER CODE BEGIN IOCTL */DRESULT status = RES_OK;switch(cmd) {case CTRL_SYNC :break;/* 扇区数量:1536*4096/1024/1024=6(MB) */case GET_SECTOR_COUNT:*(DWORD * )buff = 1536;      break;/* 扇区大小  */case GET_SECTOR_SIZE :*(WORD * )buff = 4096;break;/* 同时擦除扇区个数 */case GET_BLOCK_SIZE :*(DWORD * )buff = 1;break;        case CTRL_TRIM:break;default:status = RES_PARERR;break;      }return status;/* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */

七、修改main函数

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
UINT fnum;                          /* 文件成功读写数量 */
BYTE ReadBuffer[1024] = {0};        /* 读缓冲区 */
BYTE WriteBuffer[]= "Hello World!\n";/* USER CODE END 0 *//*** @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_SPI1_Init();MX_FATFS_Init();/* USER CODE BEGIN 2 */printf("****** 这是一个SPI FLASH 文件系统实验 ******\r\n");// 在外部SPI Flash挂载文件系统,文件系统挂载时会对SPI设备初始化retUSER = f_mount(&USERFatFS, USERPath, 1);/*----------------------- 格式化测试 -----------------*/  /* 如果没有文件系统就格式化创建创建文件系统 */if(retUSER == FR_NO_FILESYSTEM){printf("》FLASH还没有文件系统,即将进行格式化...\r\n");/* 格式化 */retUSER = f_mkfs(USERPath, 0, 0);      if(retUSER == FR_OK){printf("》FLASH已成功格式化文件系统。\r\n");/* 格式化后,先取消挂载 */retUSER = f_mount(NULL, USERPath, 1);          /* 重新挂载 */          retUSER = f_mount(&USERFatFS, USERPath, 1);}else{printf("《《格式化失败。》》\r\n");while(1);}}else if(retUSER != FR_OK){printf("!!外部Flash挂载文件系统失败。(%d)\r\n", retUSER);printf("!!可能原因:SPI Flash初始化不成功。\r\n");while(1);}else{printf("》文件系统挂载成功,可以进行读写测试\r\n");}/*----------------------- 文件系统测试:写测试 -------------------*//* 打开文件,每次都以新建的形式打开,属性为可写 */printf("\r\n****** 即将进行文件写入测试... ******\r\n"); retUSER = f_open(&USERFile, "test.txt", FA_CREATE_ALWAYS | FA_WRITE);if(retUSER == FR_OK){printf("》打开/创建FatFs读写测试文件.txt文件成功,向文件写入数据。\r\n");/* 将指定存储区内容写入到文件内 */retUSER = f_write(&USERFile, WriteBuffer, sizeof(WriteBuffer), &fnum);if(retUSER == FR_OK){printf("》文件写入成功,写入字节数据:%d\n", fnum);printf("》向文件写入的数据为:\r\n%s\r\n", WriteBuffer);}else{printf("!!文件写入失败:(%d)\n", retUSER);}    /* 不再读写,关闭文件 */f_close(&USERFile);}else{   printf("!!打开/创建文件失败。\r\n");}/*------------------- 文件系统测试:读测试 --------------------------*/printf("****** 即将进行文件读取测试... ******\r\n");retUSER = f_open(&USERFile, "test.txt",FA_OPEN_EXISTING | FA_READ);   if(retUSER == FR_OK){printf("》打开文件成功。\r\n");retUSER = f_read(&USERFile, ReadBuffer, sizeof(ReadBuffer), &fnum); if(retUSER==FR_OK){printf("》文件读取成功,读到字节数据:%d\r\n",fnum);printf("》读取得的文件数据为:\r\n%s \r\n", ReadBuffer); }else{printf("!!文件读取失败:(%d)\n",retUSER);}        }else{printf("!!打开文件失败。\r\n");}/* 不再读写,关闭文件 */f_close(&USERFile);    /* 不再使用文件系统,取消挂载文件系统 */f_mount(NULL,"1:",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/1WAfBj091e7IJVLwaL1HyHA 提取码:w24p

十、注意事项

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


• 由 Leung 写于 2021 年 4 月 2 日

• 参考:【STM32CubeMx你不知道的那些事】第九章:STM32CubeMx的SPI外置FLASH+文件系统(FATFS)
    STM32CubeMX系列|FATFS文件系统
    使用STM32CUBEMX生成FatFS代码,操作SPI FLASH
    STM32CUBEIDE之SPI读写FLASH进阶串行FLASH文件系统FatFs
    3.1、CUBEMX使用FATFS读写SPI_FLASH

STM32CubeMX学习笔记(25)——FatFs文件系统使用(操作SPI Flash)相关推荐

  1. STM32CubeMX学习笔记(27)——FatFs文件系统使用(操作SD卡)

    一.FatFs简介 FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统.它完全是由 ANSI C 语言编写并且完全独立于底层的 I/O 介质.因此它可以很容易地不加修改地移植到其他的处理器 ...

  2. C++学习笔记之对文件的操作2

    转载自** https://www.cnblogs.com/uniqueliu/archive/2011/08/03/2126680.html ** 什么都不说了,继续<C++学习笔记之对文件的 ...

  3. STM32CubeMX学习笔记(24)——通用定时器接口使用(电容按键检测)

    一.电容按键简介 电容器(简称为电容)就是可以容纳电荷的器件,两个金属块中间隔一层绝缘体就可以构成一个最简单的电容.如图 32-1(俯视图),有两个金属片,之间有一个绝缘介质,这样就构成了一个电容.这 ...

  4. STM32CubeMX学习笔记(38)——FSMC接口使用(TFT-LCD屏显示)

    一.TFT-LCD简介 TFT-LCD(Thin Film Transistor-Liquid Crystal Display) 即薄膜晶体管液晶显示器.TFT-LCD 与无源 TN-LCD. STN ...

  5. STM32CubeMX学习笔记(9)——I2C接口使用(读写EEPROM AT24C02)

    一.I2C简介 I2C(Inter-Integrated Circuit ,内部集成电路) 总线是一种由飞利浦 Philip 公司开发的串行总线.是两条串行的总线,它由一根数据线(SDA)和一根 时钟 ...

  6. STM32CubeMX学习笔记(22)——CRC接口使用

    一.CRC简介 CRC(Cyclic Redundancy Check),即循环冗余校验,是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种信道编码技术,主要用来检测或校验数据传输或者 ...

  7. STM32CubeMX学习笔记(28)——FreeRTOS实时操作系统使用(任务管理)

    一.FreeRTOS简介 FreeRTOS 是一个可裁剪.可剥夺型的多任务内核,而且没有任务数限制.FreeRTOS 提供了实时操作系统所需的所有功能,包括资源管理.同步.任务通信等. FreeRTO ...

  8. MySQL学习笔记05【多表操作、三大范式、数据库的备份和还原】

    MySQL 文档-黑马程序员(腾讯微云):https://share.weiyun.com/RaCdIwas 1-MySQL基础.pdf.2-MySQL约束与设计.pdf.3-MySQL多表查询与事务 ...

  9. STM32CubeMX学习笔记(15)——电源管理(PWR)低功耗睡眠模式

    一.低功耗模式简介 系统提供了多个低功耗模式,可在 CPU 不需要运行时(例如等待外部事件时)节省功耗.由用户根据应用选择具体的低功耗模式,以在低功耗.短启动时间和可用唤醒源之间寻求最佳平衡. 睡眠模 ...

最新文章

  1. Java学习笔记(5)——泛型
  2. **上海铁路局2004年最新时刻发布!**
  3. linux screen -ls,Linux screen命令详解
  4. Android面试常规问答
  5. python使用opencv会蓝屏_Opencv和DirectShow一起做蓝屏抠像
  6. Android Studio来了,它能取代Eclipse吗?
  7. 利用思科 Webex中的3个漏洞,以 ghost 用户身份参会
  8. nginx 负载均衡 404_Nginx+.Net Core实现项目负载均衡
  9. NYOJ243 - 交换输出
  10. 如何在Mac上使用听写进行语音到文本的键入
  11. linux运维必学python吗_Python学习资源整理
  12. 遗传算法实例解析(python)
  13. php微商系统,产品营销推广神器Thinkphp核心微商新版零售系统源代码
  14. Android多国语言包
  15. 边框border-radium属性
  16. pyttsx3设置男性声音
  17. Python输入账号密码判断是否正确并输出,典型案例-百钱买百鸡的两个程序代码
  18. 【产品实战-乘风游旅游App】3.0 乘风游的产品设计之用户角色
  19. 阿雪的学习记录|解决Linux下PPPoE拨号上网不稳定的问题
  20. android 删除一个工程,Android Studio该怎样删除项目?

热门文章

  1. php gd imageaffine
  2. 十一届蓝桥模拟赛 元素之间最大的元素距离 JAVA
  3. 任总讲话,说明华为对5G有清醒的认识
  4. Office 365小型企业版同时安装 Office Project 2016 教程
  5. ZOJ 3557 (插板法+Lucas定理)
  6. NB-IoT应用3年,揭秘它为何被称 “为抄表而生”
  7. Star CCM+ Interface踩坑——表面发射率
  8. 四川大学2019计算机考研,四川大学考研全攻略篇(各学院2019年考研详解、报考建议...)...
  9. html多重表格设置密码,日记坊-excel协同办公:一张表格设置多个不同密码,限制多区域编辑权限...
  10. socket编程基础知识