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_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写就一定不要擦除,同时擦除时,一定要注意擦出的是什么,自己代码里写的是擦除扇区的地址还是什么,形参不要搞错。