SPI flash挂载fatfs文件系统
本实验是在观看野火视频的基础上进行的,可以在进行实验前将这部分的代码观看完【野火瑞萨教程】56-FatFs文件系统移植实验(第1节)——FatFs文件系统介绍_哔哩哔哩_bilibili
1.实验平台
开发板stm32F427 SPI flash是w25q64大小:64Mbit/8Mbyte
w25q64 块大小: 64KB, 块个数128 8*1024/64=128块
扇区大小:4KB, 扇区个数 64/4=16块 一块可以分为16个扇区
页大小: 256字节 页个数 4*1024/256=16 一个扇区可以分为14页
实现的功能:通过fatfs文件系统,第一遍实现对spi flash(w25q64)的写和读,w25q64有数据掉电不丢失的特点,因此为了确保数据真正写进去,第二遍断电上电重新读数据,如果能读到数据,说明数据在第一遍已经成功写进去。
2.需要准备的材料
(1)FATFS文件系统源码
源码地址:http://elm-chan.org/fsw/ff/00index_e.html 进来以后是以下界面:

往下翻,点击Previous Releases,选择自己想要的版本,

这里我使用的是最新版本,无论哪个版本都可以

解压以后,文件内容是这个样子,我们实际工程中使用的是souce文件中的代码

souce文件打开就是这些

(2)w25q64驱动代码
bsp_w25q64.c/bsp_w25q64.h 这个底层代码得自己写,且必须写完整写对,否则后续就会出现挂载失败,格式挂失败,文件打不开的问题
这部分驱动代码我会放在最后面进行讲解
3.实验步骤
1.将fatfs文件系统添加进工程,这部分视频中有讲解

2.修改diskio.c文件
diskio.c驱动代码包含一下几个部分:
DSTATUS disk_status获取磁盘状态 DSTATUS disk_initialize 磁盘初始化
DRESULT disk_read 磁盘读 DRESULT disk_write 磁盘写 DRESULT disk_ioctl磁盘控制 DWORD get_fattime 时间戳函数
这部分代码是主要的核心代码,它能够真正实现 fatfs文件系统到w25q64底层代码的桥梁。
(1)DSTATUS disk_status获取磁盘状态
相关底层代码调用: W25Q64_Wait_Busy(); //等待Flash芯片内部操作完成 这个函数后面会给出
后面进行讲解
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
switch (pdrv) {
case SPI_FLASH :
W25Q64_Wait_Busy(); //等待Flash芯片内部操作完成
stat = RES_OK;
return stat;
}
return STA_NOINIT;
}
(2)DSTATUS disk_initialize 磁盘初始化
相关底层代码调用: W25Q64_Init();这个函数后面会给出
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
switch (pdrv) {
case SPI_FLASH :
W25Q64_Init();
stat = RES_OK;
return stat;
}
return STA_NOINIT;
}
(3)DRESULT disk_read 磁盘读
相关底层代码调用:W25Q64_Read(buff, sector<<12, count<<12);
buff:读取的数据缓存区
sector<<12:读取的扇区地址
count<<12:读取的字节数
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 SPI_FLASH :
// translate the arguments here
W25Q64_Read(buff, sector<<12, count<<12); //1 sector == 4096 bytes
res = RES_OK;
return res;
}
return RES_PARERR;
}
(4)DRESULT disk_write 磁盘写
相关底层代码调用: W25Q64_EraseSector(write_addr);
W25Q64_Write((uint8_t *)buff, write_addr, count<<12);
这里也很有说法,
write_addr = sector << 12; 这里的write_addr指的是数据要写入的地址 左移12,相当于乘以4096.而这里的4096,就是w25q64扇区的大小,将逻辑扇区号转换为物理地址
W25Q64_EraseSector(write_addr); 擦除指定地址的Flash扇区
W25Q64_Write((uint8_t *)buff, write_addr, count<<12); 将数据写入Flash
(uint8_t *)buff 形参含义:写入数据的缓存区
write_addr 形参含义:写入数据的物理地址
count<<12 形参含义:写入的总字节数 count指的是扇区的个数,count*4096
总字节数 = 扇区数量 × 每个扇区的大小
注意: Flash必须先擦除后才能写入,同时也要注意这里已经擦除后再写的话,底层代码 W25Q64_Write函数中就不能再有擦出的函数,一定要注意!!
#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;
uint32_t write_addr;
switch (pdrv) {
case SPI_FLASH :
write_addr = sector << 12;
W25Q64_EraseSector(write_addr);
W25Q64_Write((uint8_t *)buff, write_addr, count<<12);
res = RES_OK;
return res;
}
return RES_PARERR;
}
#endif
(5)DRESULT disk_ioctl磁盘控制
这里的三个变量一定要设置正确
相关底层代码调用: W25Q64_Wait_Busy(); // 等待Flash操作完成
由于我使用的w25q64的大小为8Mbyte,FATFS默认使用512字节或4096字节的扇区,在这里我们使用的是4096个字节,这样做是为了保证最终划分与w25q64的大小相同。
它的扇区大小为4kb,4*1024=4096,因此扇区大小GET_SECTOR_SIZE=4096;
为了保证大小为8Mb,扇区个数GET_SECTOR_COUNT:2048个
计算公式: 2048*4096/1024/1024=8MB
获取Flash芯片的擦除块大小:GET_BLOCK_SIZE=1
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT status = RES_PARERR;
switch (pdrv) {
case SPI_FLASH:
switch(cmd)
{
case CTRL_SYNC: // 同步命令
W25Q64_Wait_Busy(); // 等待Flash操作完成
break;
/* 扇区数量 2048*4096/1024/1024=8MB */
case GET_SECTOR_COUNT:
*(DWORD *)buff = 2048;
break;
/* 扇区大小 */
case GET_SECTOR_SIZE:
*(WORD *)buff = 4096;
break;
/* 同时擦除扇区个数 */
case GET_BLOCK_SIZE:
*(DWORD *)buff = 1;
break;
}
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
}
(6)DWORD get_fattime 时间戳函数
这里直接复制粘贴,不重要
//时间戳函数(FATFS必需)
DWORD get_fattime(void) {
// 返回当前时间(格式:YYYY-MM-DD HH:MM:SS)
return ((DWORD)(2025 - 1980) << 25) | // 年(2025)
((DWORD)8 << 21) | // 月(8月)
((DWORD)22 << 16) | // 日(22日)
((DWORD)14 << 11) | // 时(14时)
((DWORD)30 << 5) | // 分(30分)
((DWORD)0 >> 1); // 秒/2
}
(7)添加设备号 SPI_FLASH 0
这里的SPI_FLASH 0 指的就是我们要使用的w25q64,0指的是设备驱动号

3.修改ffconfig.h中的相关参数
修改以下几个参数
#define FF_USE_MKFS 1 允许使用 f_mkfs() 函数对磁盘进行格式化
#define FF_CODE_PAGE 936 支持中文文件名和文件内容
#define FF_USE_LFN 2 启用长文件名支持
#define FF_VOLUMES 1 设置支持的卷(磁盘)数量 我这里只使用了一个spi flash
#define FF_MAX_SS 4096 设置扇区大小 4096正好匹配W25Q64的物理扇区大小
4.检查bsp_w25q64.c底层代码书写完整
在代码进行书写之前,我们要明白w25q64的书写逻辑
写入操作时:
a.写入操作时,必须先进行写使能
b.每个数位只能由1改为0,不能由0改为1
c.写入数据前必须先擦除,擦除后所有数据位变为1
d.擦除按最小擦除单位进行(所在扇区的4096)个字节全部擦掉
e.连续写入多字节时,最多写入一页的数据,超过页尾位置的数据会回到页首覆盖(尽量不要超过页的边沿)
f.写入结束后,芯片进入忙状态,不响应新的读写操作
读取时:无需使能,无需额外操作。
(1) HAL_StatusTypeDef W25Q64_Init(void) 初始化
在初始化函数中,我也加入了读取芯片Id的操作
//1初始化函数
HAL_StatusTypeDef W25Q64_Init(void) {
// 1. 初始化SPI硬件
MX_SPI4_Init();
// 2. 正确配置CS引脚(GPIOE_PIN4)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); // 修正为GPIOE
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET); // 初始不选中
// 3. 验证Flash通信(带超时和重试)
uint32_t retry = 3; // 重试次数
uint16_t id = 0;
while(retry--) {
id = W25Q64_ReadID();
if(id == 0xEF16) return HAL_OK; // 成功
}
}
SPI.C这里需要注意的是这里使用模式0或者模式3
void MX_SPI4_Init(void) {
hspi4.Instance = SPI4;
hspi4.Init.Mode = SPI_MODE_MASTER;
hspi4.Init.Direction = SPI_DIRECTION_2LINES;
hspi4.Init.DataSize = SPI_DATASIZE_8BIT;
hspi4.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=0(模式0)
hspi4.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=0
hspi4.Init.NSS = SPI_NSS_SOFT; // 需手动控制PF6
hspi4.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 10.5MHz
hspi4.Init.FirstBit = SPI_FIRSTBIT_MSB;
if (HAL_SPI_Init(&hspi4) != HAL_OK) {
Error_Handler();
}
/* USER CODE BEGIN SPI4_Init 2 */
/* USER CODE END SPI4_Init 2 */
}
这里我在别的视频里看MISO使用的是上拉输入,但是我在这里设置上拉输入,读取不到ID,将引脚端口设为 GPIO_MODE_AF_PP复用推挽输出,可以收到数据。
同时这里CS片选引脚设置为 GPIO_MODE_OUTPUT_PP即可。
else if(spiHandle->Instance==SPI4) {
/* SPI4 clock enable */
__HAL_RCC_SPI4_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
/**SPI4 GPIO Configuration
PE2 ------> SPI4_SCK
PE5 ------> SPI4_MISO
PE6 ------> SPI4_MOSI
PE4 ------> SPI4 NSS
*/
GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_6; // 不包含PE4(CS)
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI4;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_5; // PE5 (MISO)
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL; // 关键!启用内部上拉或不拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI4; // 复用功能AF5
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 关键修正!
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET); // 初始拉高CS
}
}
(2)uint16_t W25Q64_ReadID(void) 读取id
读取id时,先拉低片选信号,延迟,再发送指令和地址,再接收,再释放cs返回实际id.
在这里有一个点需要注意,最初我的代码是,先拉低片选信号,再发送指令和地址,再接收,再释放cs,代码逻辑没有问题,但是并没有返回flash的id,后面才知道拉低片选引脚代码没有问题,但是实际硬件有没有拉低片选信号是一个问题,也可能拉低了一下,片选信号瞬间拉高,因此我在后面加了延迟us或者ms,拉低片选选延长时间,才保证数据实现真正地发送和接收,接收后再拉高片选信号。
uint16_t W25Q64_ReadID(void)
{
uint8_t tx_buf[4] = {0x90, 0x00, 0x00, 0x00}; // 读ID指令+3字节地址
uint8_t rx_buf[2] = {0}; // 用于接收ID数据
/* 1. 拉低CS片选信号 */
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
delay_us(10); // 缩短延时(µs级足够)
/* 2. 发送读ID指令和地址 */
if (HAL_SPI_Transmit(&hspi4, tx_buf, 4, 100) != HAL_OK) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return 0xFFFF; // 传输失败
}
/* 3. 接收2字节ID数据 */
if (HAL_SPI_Receive(&hspi4, rx_buf, 2, 100) != HAL_OK) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return 0xFFFF; // 接收失败
}
/* 4. 释放CS并返回实际ID */
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return (rx_buf[0] << 8) | rx_buf[1]; // 合并为16位ID
}
(3)void SPI_WriteByte(uint8_t tx_data) 写入一个字节
//3.发送字节数据
void SPI_WriteByte(uint8_t tx_data) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
delay_ms(3); // 缩短延时至微秒级(Flash典型识别时间)
if (HAL_SPI_Transmit(&hspi4, &tx_data, 1, 10) != HAL_OK) {
Error_Handler();
}
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
}
(4)void W25Q64_Write_Enable(void) 写使能
4.写使能
void W25Q64_Write_Enable(void)
{
SPI_WriteByte(W25X_WriteEnable);
}
(5)HAL_StatusTypeDef W25Q64_WritePage(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite) 页写入
变量uint8_t *pBuffer缓存区地址
uint32_t WriteAddr要写入的地址
uint16_t NumByteToWrite写入数据的字节数量
数据写入是先写进缓存区,再写入实际地址,写之前先进行写使能,写完之后进入忙状态,这里不需要擦除,擦除指的是扇区擦除,这里我们是在页写入。
//5.页写入
//变量uint8_t *pBuffer页缓存区指针,uint32_t WriteAddr要写入的地址,uint16_t NumByteToWrite写入数据的字节数量
HAL_StatusTypeDef W25Q64_WritePage(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
// 1. 参数安全检查
if(pBuffer == NULL) return HAL_ERROR;
if(NumByteToWrite == 0 || NumByteToWrite > 256) return HAL_ERROR;
// 2. 构造指令包
uint8_t cmd[4] = {
W25X_PageProgram,//整页写入
(uint8_t)(WriteAddr >> 16),//写入的地址高八位
(uint8_t)(WriteAddr >> 8),//写入的地址中八位
(uint8_t)WriteAddr//写入的地址低八位
};
// 3. 写使能
W25Q64_Write_Enable();
// 4. 传输数据
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
// HAL_Delay(3); // 确保Flash识别CS信号
delay_ms(3);
if(HAL_SPI_Transmit(&hspi4, cmd, 4, 100) != HAL_OK ||
HAL_SPI_Transmit(&hspi4, pBuffer, NumByteToWrite, 500) != HAL_OK) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return HAL_ERROR;
}
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
W25Q64_Wait_Busy();//页数据写进去后,芯片进入忙状态
return HAL_OK;
}
(6)HAL_StatusTypeDef W25Q64_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite) 扇区写入
这里的代码比较复杂,但是我们可以简单地划分为以下几种情况,就像小朋友写作业
可以简单分为5种情况:
a.从第一页起始行开始写 没写够一页
b.从第一页起始行开始写,写超过了一页即写了多页,同时剩余的不满足一页
c.从第一页任意位置写,写的内容写到的第二页,实际上写的内容不足一页,因此把第一页剩余的空白行写完,再把剩余的内容写到第二页
d.从第一页任意位置写,剩余的内容能在第一页写完
e.从第一页任意位置写,写了多页,在后面某一页某一个位置写完
//6.扇区写入
HAL_StatusTypeDef W25Q64_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
// 1. 参数检查
if (pBuffer == NULL || NumByteToWrite == 0) {
return HAL_ERROR;
}
const uint16_t PageSize = 256; // W25Q64页大小
uint16_t PageOffset = WriteAddr % PageSize; // 当前页内偏移
uint16_t RemainingInPage = PageSize - PageOffset; // 当前页剩余空间
uint16_t NumOfPage = NumByteToWrite / PageSize; // 完整页数
uint16_t NumOfSingle = NumByteToWrite % PageSize; // 剩余字节数
HAL_StatusTypeDef status;
// 地址按页对齐的情况
if (PageOffset == 0)
{
// 数据长度小于一页
if (NumOfPage == 0)
{
status = W25Q64_WritePage(pBuffer, WriteAddr, NumByteToWrite);
if (status != HAL_OK) return status;
}
else // 数据长度大于一页
{
// 写入完整页
while (NumOfPage--)
{
status = W25Q64_WritePage(pBuffer, WriteAddr, PageSize);
if (status != HAL_OK) return status;
WriteAddr += PageSize;
pBuffer += PageSize;
}
// 写入剩余的不满一页的数据
if (NumOfSingle != 0)
{
status = W25Q64_WritePage(pBuffer, WriteAddr, NumOfSingle);
if (status != HAL_OK) return status;
}
}
}
else // 地址不对齐的情况
{
// 数据长度小于一页
if (NumOfPage == 0)
{
// 当前页剩余空间不够写入所有数据
if (NumOfSingle > RemainingInPage)
{
uint16_t temp = NumOfSingle - RemainingInPage;
// 先写满当前页剩余空间
status = W25Q64_WritePage(pBuffer, WriteAddr, RemainingInPage);
if (status != HAL_OK) return status;
WriteAddr += RemainingInPage;
pBuffer += RemainingInPage;
// 再写剩余数据到下一页
status = W25Q64_WritePage(pBuffer, WriteAddr, temp);
if (status != HAL_OK) return status;
}
else // 当前页剩余空间足够写入所有数据
{
status = W25Q64_WritePage(pBuffer, WriteAddr, NumByteToWrite);
if (status != HAL_OK) return status;
}
}
else // 数据长度大于一页
{
// 先写入第一页的剩余空间
status = W25Q64_WritePage(pBuffer, WriteAddr, RemainingInPage);
if (status != HAL_OK) return status;
// 更新计数器和指针
NumByteToWrite -= RemainingInPage;
WriteAddr += RemainingInPage;
pBuffer += RemainingInPage;
// 重新计算页数和剩余字节数
NumOfPage = NumByteToWrite / PageSize;
NumOfSingle = NumByteToWrite % PageSize;
// 写入完整页
while (NumOfPage--)
{
status = W25Q64_WritePage(pBuffer, WriteAddr, PageSize);
if (status != HAL_OK) return status;
WriteAddr += PageSize;
pBuffer += PageSize;
}
// 写入剩余的不满一页的数据
if (NumOfSingle != 0)
{
status = W25Q64_WritePage(pBuffer, WriteAddr, NumOfSingle);
if (status != HAL_OK) return status;
}
}
}
return HAL_OK;
}
(7)HAL_StatusTypeDef W25Q64_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead) 读
HAL_StatusTypeDef W25Q64_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
uint8_t cmd[4];
// 0. 等待Flash就绪
W25Q64_Wait_Busy();
// 1. 构造指令包
cmd[0] = 0x03; // 读数据指令
cmd[1] = (ReadAddr >> 16) & 0xFF;
cmd[2] = (ReadAddr >> 8) & 0xFF;
cmd[3] = ReadAddr & 0xFF;
// 2. 拉低CS(缩短延时)
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
delay_us(10); // 几个空指令足够
// 3. 发送指令和地址
if (HAL_SPI_Transmit(&hspi4, cmd, 4, 10) != HAL_OK) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return HAL_ERROR;
}
// 4. 接收数据
if (HAL_SPI_Receive(&hspi4, pBuffer, NumByteToRead, 100) != HAL_OK) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return HAL_ERROR;
}
// 5. 释放CS
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return HAL_OK;
}
(8) HAL_StatusTypeDef W25Q64_EraseSector(uint32_t SectorAddr) 扇区擦除
HAL_StatusTypeDef W25Q64_EraseSector(uint32_t SectorAddr) {
uint8_t cmd[4] = {0x20, (SectorAddr >> 16) & 0xFF, (SectorAddr >> 8) & 0xFF, SectorAddr & 0xFF};
// 写使能
W25Q64_Write_Enable();
// 发送擦除指令
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_StatusTypeDef status = HAL_SPI_Transmit(&hspi4, cmd, 4, 100);
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
if (status != HAL_OK) return status;
// 等待擦除完成
W25Q64_Wait_Busy();
return HAL_OK;
}
(9)uint8_t W25Q64_ReadSR(void) 读取状态寄存器
uint8_t W25Q64_ReadSR(void)
{
uint8_t cmd = 0x05; // 读状态寄存器1指令
uint8_t status = 0;
/* 1. 拉低CS片选信号 */
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_RESET);
delay_ms(3);
/* 2. 发送指令并接收状态 */
HAL_SPI_Transmit(&hspi4, &cmd, 1, 10); // 发送读状态指令
HAL_SPI_Receive(&hspi4, &status, 1, 10); // 接收状态字节
/* 3. 释放CS片选信号 */
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET);
return status;
}
(10)void W25Q64_Wait_Busy(void) 等待忙状态退出
void W25Q64_Wait_Busy(void)
{
uint8_t status;
do {
status = W25Q64_ReadSR(); // 循环读取状态
} while (status & 0x01); // 等待BUSY位清零 bit0=1(忙状态)→ 继续循环,bit0=0(空闲状态)→ 退出循环
}
5.main.c主函数代码书写
第一遍上电,初始化w25q64, 挂载,格式挂之后再卸载挂载,再创建文件打开文件进行写操作,关闭文件;再打开文件进行读操作,关闭文件,卸载文件系统。相关的几个函数
(1)f_mount
挂载函数, 0代表逻辑驱动器,我们前面将spi_flash设置为0,1为立刻挂载
res_flash = f_mount(&fs, "0:", 1);
(2)f_mkfs
0代表逻辑驱动器
NULL 或 0 表示使用默认参数,常用选项:
| 值 | 说明 |
|---|---|
NULL | 使用默认参数(推荐) |
FM_FAT | 强制格式化为FAT12/16 |
FM_FAT32 | 强制格式化为FAT32 |
FM_SFD | 创建SFD格式(超级软盘) |
work 工作缓冲区指针 格式化过程中使用的临时工作缓冲区
和这个有关Byte work[FF_MAX_SS]; // 通常为一个扇区大小(4096字节)
res_flash = f_mkfs("0:", NULL, work, sizeof(work));
(3)f_mount
卸载文件系统,将第一个参数改为我NULL即可
f_mount(NULL, "0:", 1);
(4)f_open
形参 &fnew - 文件对象指针,指定要写入的文件对象
"0:FatFs.txt" - 文件路径,指定要打开或创建的文件路径 格式:"驱动器号:文件名.扩展名"
FA_CREATE_ALWAYS | 0x08 | 总是创建新文件(覆盖现有) |
A_WRITE | 0x02 | 只写模式 |
res_flash = f_open(&fnew, "0:FatFs.txt", FA_CREATE_ALWAYS | FA_WRITE);
(5)f_write
形参 &fnew - 文件对象指针,指定要写入的文件对象
fileWriteBuffer - 数据缓冲区指针,提供要写入文件的数据
sizeof(fileWriteBuffer) - 要写入的字节数
&fnum - 实际写入字节数指针
res_flash = f_write(&fnew, fileWriteBuffer, sizeof(fileWriteBuffer), &fnum);
(6)f_sync
将文件缓冲区中的数据立即强制写入物理存储器flash,确保数据不会因意外断电而丢失。
f_write() → 数据进入缓存 → f_sync() → 立即写入Flash → 数据安全
f_sync(&fnew);
(7)f_close 关闭文件
f_close(&fnew);
(8)f_read
&fnew - 文件对象指针
fileReadBuffer - 数据缓冲区指针,提供存储读取数据的缓冲区,指向用于接收文件数据的内存区域
sizeof(fileReadBuffer) 要读取的字节数,返回实际成功读取的字节数
res_flash = f_read(&fnew, fileReadBuffer, sizeof(fileReadBuffer), &fnum);
6.实验结果
(1)第一遍上电写和读
FATFS fs; /* FatFs文件系统对象 */
FIL fnew; /* 文件对象 */
UINT fnum; /* 文件成功读写数量 */
FRESULT res_flash; /* 文件操作结果 */
BYTE fileReadBuffer[1024]; /* 读缓冲区 */
BYTE fileWriteBuffer[] = /* 写缓冲区 */
"hello hejiaoru";
BYTE work[FF_MAX_SS];
W25Q64_Init();
/* 挂载文件系统 */
res_flash = f_mount(&fs, "0:", 1);
if (res_flash == FR_NO_FILESYSTEM)
{
/* 格式化 */
res_flash = f_mkfs("0:", NULL, work, sizeof(work));
if (res_flash == FR_OK)
{
/* 格式化后,先取消挂载 */
f_mount(NULL, "0:", 1);
res_flash = f_mount(&fs, "0:", 1);
}
else
{
/* 格式化失败,进入错误处理 */
while (1);
}
}
else if (res_flash != FR_OK)
{
/* 挂载失败,进入错误处理 */
while (1);
}
/* 打开/创建文件并写入数据 */
res_flash = f_open(&fnew, "0:FatFs.txt", FA_CREATE_ALWAYS | FA_WRITE);
if (res_flash == FR_OK)
{
res_flash = f_write(&fnew, fileWriteBuffer, sizeof(fileWriteBuffer), &fnum);
f_sync(&fnew); // 强制同步元数据
f_close(&fnew);
}
/* 重新打开文件并读取数据 */
res_flash = f_open(&fnew, "0:FatFs.txt", FA_OPEN_EXISTING | FA_READ);
if (res_flash == FR_OK)
{
res_flash = f_read(&fnew, fileReadBuffer, sizeof(fileReadBuffer), &fnum);
f_close(&fnew);
}
/* 卸载文件系统 */
f_mount(NULL, "0:", 1);
可以观察到结果,fileReadBuffer里面有数据。数据就是我们写入的数据 BYTE fileWriteBuffer[] = /* 写缓冲区 */
"hello hejiaoru";

(2)第二遍断电上电读
修改代码,注意读的时候,要先挂载才可以,不要格式化,格式化会导致第一遍写入的数据丢失,
/* 挂载文件系统 */
res_flash = f_mount(&fs, "0:", 1);
/* 重新打开文件并读取数据 */
res_flash = f_open(&fnew, "0:FatFs.txt", FA_OPEN_EXISTING | FA_READ);
if (res_flash == FR_OK)
{
res_flash = f_read(&fnew, fileReadBuffer, sizeof(fileReadBuffer), &fnum);
f_close(&fnew);
}
/* 卸载文件系统 */
f_mount(NULL, "0:", 1);
第二遍直接读,可以观察到读的内容是fileReadBuffer= "hello hejiaoru";说明数据已经真正写到flash里了

7.需要注意的问题
我们需要知道整体的调用写的逻辑是这样子,
f_write("Hello") → FATFS处理 → disk_write(扇区数据) → W25Q64_Write(原始数据)
↑ ↑ ↑ ↑
字符串 文件系统结构 物理扇区数据 Flash原始数据
读的逻辑是这样子
W25Q64 Flash → W25Q64_Read() → disk_read() → FATFS → f_read() → fileReadBuffer
↑ ↑ ↑ ↑ ↑ ↑
物理存储 硬件驱动 磁盘接口 文件系统 文件接口 应用缓冲区
因此要想实现真正的读写,底层·W25Q64_Write和 W25Q64_Read()函数必须写正确,注意如果再diskio.c的 disk_write中有 W25Q64_EraseSector(write_addr);W25Q64_Write((uint8_t *)buff, write_addr, count<<12);即擦除后再写,那么底层·W25Q64_Write写就一定不要擦除,同时擦除时,一定要注意擦出的是什么,自己代码里写的是擦除扇区的地址还是什么,形参不要搞错。
更多推荐



所有评论(0)