一、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.h和diskio.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
支持长文件名,并指定使用栈空间为缓冲区
- CODE_PAGE(Code page on target): Simplified Chinese GBK(DBCS,OEM,Windows)
缓存工作区为什么放在栈?其实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
指定扇区大小的最小值
- VOLUMES(Logical drivers): 2
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 ID
uint16_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 N
和 USER 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