当前位置: 首页 > news >正文

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 hereW25Q64_Read(buff, sector<<12, count<<12); //1 sector == 4096 bytesres = 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 == 0DRESULT 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);  // 修正为GPIOEHAL_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=0hspi4.Init.NSS = SPI_NSS_SOFT;               // 需手动控制PF6hspi4.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;  // 10.5MHzhspi4.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 ConfigurationPE2     ------> SPI4_SCKPE5     ------> SPI4_MISOPE6     ------> SPI4_MOSIPE4     ------> 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;  // 复用功能AF5HAL_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. 释放CSHAL_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_ALWAYS0x08总是创建新文件(覆盖现有)​
A_WRITE0x02只写模式
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写就一定不要擦除,同时擦除时,一定要注意擦出的是什么,自己代码里写的是擦除扇区的地址还是什么,形参不要搞错。

http://www.xdnf.cn/news/1458037.html

相关文章:

  • 什么是静态住宅IP 跨境电商为什么要用静态住宅IP
  • More Effective C++ 条款28:智能指针
  • 稠密矩阵和稀疏矩阵的对比
  • 神马 M21 31T 矿机解析:性能、规格与市场应用
  • Python多序列同时迭代完全指南:从基础到高并发系统实战
  • vcruntime140_1.dll缺失?5个高效解决方法
  • 手机秒变全栈IDE:Claude Code UI的深度体验
  • SpringBoot实现国际化(多语言)配置
  • MySQL 8.0 主从复制原理分析与实战
  • 深入解析Java HashCode计算原理 少看大错特错的面试题
  • 多线程——线程状态
  • 并发编程——17 CPU缓存架构详解高性能内存队列Disruptor实战
  • ResNet(残差网络)-彻底改变深度神经网络的训练方式
  • linux——自定义协议
  • 多Agent协作案例:用AutoGen实现“写代码+测Bug”的自动开发流程
  • 秒店功能更新:多维度优化升级,助力商家经营
  • 当 LLM 遇上真实世界:MCP-Universe 如何撕开大模型 “工具能力” 的伪装?
  • 记录相机触发相关
  • 机器学习入门,第一个MCP示例
  • (D题|矿井突水水流漫延模型与逃生方案)2025年高教杯全国大学生数学建模国赛解题思路|完整代码论文集合
  • 生成式引擎优化(GEO):数字营销新标配,企业如何抢占AI搜索流量高地?
  • Trae + MCP : 一键生成专业封面的高阶玩法——自定义插件、微服务编排与性能调优
  • 设计模式六大原则2-里氏替换原则
  • Linux —— 环境变量
  • mysql中find_in_set()函数的使用, ancestors字段,树形查询
  • AI视频画质提升效果实用指南:提升清晰度的完整路径
  • [论文阅读] 软件工程 | REST API模糊测试的“标准化革命”——WFC与WFD如何破解行业三大痛点
  • 【论文阅读】-《Besting the Black-Box: Barrier Zones for Adversarial Example Defense》
  • AutoLayout与Masonry:简化iOS布局
  • (E题|AI 辅助智能体测)2025年高教杯全国大学生数学建模国赛解题思路|完整代码论文集合