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

在写外部FLASH的应用时发现一些问题,在这里分享一下我的想法

1、前言

原有的SPI-FLASH驱动就像野火哥那种,返回值都是空,能用的就是一个写的时候阻塞的查询那个状态位。当然跨页写的部分,我是直接复制野火哥的代码。附上网址

25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火霸道开发板 文档

这会遇到一个问题,假如我写错了怎么办,我怎么知道我写错了。没有一个返回值能告诉我,只能靠超时机制吗

/* WIP(busy)标志,FLASH内部正在写入 */
#define WIP_Flag                  0x01/**
* @brief  等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
* @param  none
* @retval none
*/
void SPI_FLASH_WaitForWriteEnd(void)
{u8 FLASH_Status = 0;/* 选择 FLASH: CS 低 */SPI_FLASH_CS_LOW();/* 发送 读状态寄存器 命令 */SPI_FLASH_SendByte(W25X_ReadStatusReg);/* 若FLASH忙碌,则等待 */do{/* 读取FLASH芯片的状态寄存器 */FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);}while ((FLASH_Status & WIP_Flag) == SET);  /* 正在写入标志 *//* 停止信号  FLASH: CS 高 */SPI_FLASH_CS_HIGH();
}

这部分代码简直就跟我工程里的原有的驱动代码一模一样。死等在这里没有超时退出机制。

我加了一个超时退出,这个时间我是当做一个经验值,平时都能写成功的一个值。

/*** @brief 等待 Flash 操作完成(轮询状态寄存器 WIP 位)*/
bool W25Q16_WaitProcessDone_timeout(void)
{uint8_t u8Status = 0;uint32_t timeoutCounter = 0; // 计数值跟自己的时钟频率有关,需要自己去尝试,或者直接查tickdo{W25Q16_CS_LOW();soft_spi_transfer(W25Q16_CMD_READ_STATUS1);u8Status = soft_spi_transfer(0Xff);W25Q16_CS_HIGH();timeoutCounter++;if(timeoutCounter > 100000){PRINT("W25Q16 timeout\r\n");return false;}}while ((u8Status & 0x01) == 1U);return true;
}

超时退出机制有了,没有的话,如果等不到这个标志会不会程序就死在这里了。

下一个问题,就是按页写作为W25Q16等的写入大小。最大就写入一页。超了写不了,需要跨页地址写。如果跨扇区(未擦的情况下),还要擦除下一个扇区。所以跨页,野火哥给解决了,擦扇区需要放在应用方面手动去计算,是不是要擦下一个扇区。比如。

先放上跨页写的接口,当然,你想写多大都是没问题的。前提是后面的扇区都擦过了

/**
* @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param   pBuffer,要写入数据的指针
* @param  WriteAddr,写入地址
* @param  NumByteToWrite,写入数据长度
* @retval 无
*/
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, 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){SPI_FLASH_PageWrite(pBuffer, WriteAddr,NumByteToWrite);}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*先把整数页都写了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr,SPI_FLASH_PageSize);WriteAddr +=  SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/SPI_FLASH_PageWrite(pBuffer, WriteAddr,NumOfSingle);}}/* 若地址与 SPI_FLASH_PageSize 不对齐  */else{/* NumByteToWrite < SPI_FLASH_PageSize */if (NumOfPage == 0){/*当前页剩余的count个位置比NumOfSingle小,一页写不完*/if (NumOfSingle > count){temp = NumOfSingle - count;/*先写满当前页*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr +=  count;pBuffer += count;/*再写剩余的数据*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);}else /*当前页剩余的count个位置能写完NumOfSingle个数据*/{SPI_FLASH_PageWrite(pBuffer, WriteAddr,NumByteToWrite);}}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*地址不对齐多出的count分开处理,不加入这个运算*/NumByteToWrite -= count;NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;/* 先写完count个数据,为的是让下一次要写的地址对齐 */SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);/* 接下来就重复地址对齐的情况 */WriteAddr +=  count;pBuffer += count;/*把整数页都写了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr,SPI_FLASH_PageSize);WriteAddr +=  SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle != 0){SPI_FLASH_PageWrite(pBuffer, WriteAddr,NumOfSingle);}}}
}

这里的关键问题,就是下一个扇区该不该擦。

这里就要用到一个计算,就是写入的地址开头与地址结尾进行特定操作后是否相等

(wr_addr_start & (~(FLASH_SECTOR_SIZE - 1))) !=  \
(wr_addr_end & (~(FLASH_SECTOR_SIZE - 1)))

这个与操作就是获得对应所在扇区的位置。

如果不相等就证明跨扇区了,需要擦下一个扇区。

这里还会遇到一个问题,如果你定义了一个地址范围,你的写入尾地址超出了,你限定存储的地址范围,此时就不是要擦下一个扇区,为了有效的存储,我们要回头擦第一个扇区,得到一个环形的存储空间。如果没有超出范围,就擦下一个扇区即可。

这里就可能会涉及到记录存储方面的内容,以后吃透了可能会讲讲。

下面就是擦,擦成功了吗?如何判定是否擦扇区成功,一般最小单位就是扇区,我们也是按最小单位来擦除的。

static bool W25Q16_VerifySectorErased(uint32_t addr) {uint8_t buffer[FLASH_SECTOR_SIZE]; // 扇区大小4KBuint32_t i;W25Q16_Read(addr, buffer, FLASH_SECTOR_SIZE); // 读取扇区数据// 遍历验证所有字节是否为0xFFfor (i = 0; i < FLASH_SECTOR_SIZE; i++) {if (buffer[i] != 0xFF) {return false; // 存在非0xFF字节,擦除失败}}return true; // 全为0xFF,擦除成功
}
/*** @brief 擦除扇区并验证(全为 0xFF)* @param addr  扇区起始地址* @return true: 擦除并验证成功;false: 擦除失败*/
bool W25Q16_EraseSectorAndVerify(uint32_t addr) {W25Q16_EraseSector(addr); // 擦除扇区if (W25Q16_VerifySectorErased(addr) == false) {// 擦除失败处理(如重试或报错)printf("Sector erase failed at 0x%08X\n", addr);return false;}return true;
}

我们都知道,SPI-FLASH擦完都是1,也就是每个字节都是0XFF,那么我们只需要把整个扇区的数据读回来,判断是不是0XFF即可。当然这都会造成一定的时间损耗,适合对准确性的要求大于效率性。擦除失败可以尝试重新擦除,这里就不再进行重擦,可以在写的地方,设置写失败,重试的次数,不然一直重擦,都嵌套了,太慢了。

2、擦除接口

当然我前面没做重试操作,在应用接口里做了重擦操作。这都是因为,我写记录打印的时候,有些时候写失败了,唉,不然我也不会去改这个接口。

/*** @brief W25Q16 芯片的存储容量为 16Mbit(2MB),它将内部存储空间分为了 32 个块,每个块包含 16 个扇区,每个扇区大小为 4KB,因此可计算出每个块的大小为 4KB×16 = 64KB* */          
/*** @brief 擦除指定地址所在的Flash扇区(带重试机制)* @param addr 要擦除的起始地址(自动对齐到扇区边界)* @return 擦除成功返回true,否则返回false* @note 内部自动重试最多5次,适用于SPI Flash扇区擦除操作*/
static bool RECORD_FLASH_ERASE(uint32_t addr)
{uint8_t write_cnt = 0;bool ret = true; // 循环重试擦除操作,最多5次do {ret = W25Q16_EraseSectorAndVerify(addr);write_cnt++;} while (ret != true && write_cnt < 5);// 返回最终擦除结果(成功或达到最大重试次数)if(write_cnt >= 5) {return ret; // 超时重试5次后仍失败 }return ret; // 成功或重试后成功
}

3、写接口

就是写失败,头疼。改完之后,还会出现写失败,但是后面重试后成功了。写5次都失败的话,估计FLASH也可以扔了,换一个吧孩子。

在外面其实会判断是否需要擦下一个扇区,但是这里我就是为了保险,就这样吧,反正扇区那么大,也不是经常擦。记录也不是经常写。

第一次尝试写入,如果失败,就直接读出整个扇区的数据到RAM里,按理说没写过的地方都是0XFF,不用担心会整个扇区写入后导致后面的还得再擦。

如果没有跨扇区,把数据组合后,就调用跨页写接口,把这个扇区的内容全部写进去。

如果跨扇区了,为了保险,数据组合写入后,还是要擦下一个扇区,然后把剩余长度的数据写入下一个扇区。重试次数为5次。

/*** @brief 向Flash写入数据(带失败恢复机制)* @param addr 写入起始地址* @param data 待写入数据缓冲区* @param data_size 数据长度(字节)* @return 写入成功返回true,失败返回false* @note 若写入失败,会自动读取扇区、擦除、合并数据并重试*/
static bool RECORD_FLASH_PROGRAM(uint32_t addr, uint8_t* data, uint16_t data_size)
{uint8_t write_cnt = 0;uint8_t read_buf[FLASH_SECTOR_SIZE]={0};                // 扇区数据缓冲区uint32_t sector_addr  = addr & (~(FLASH_SECTOR_SIZE - 1)); // 计算扇区起始地址uint16_t sector_offset = addr % FLASH_SECTOR_SIZE;        // 计算地址在扇区内的偏移bool ret = false; // 首次尝试直接写入ret = W25Q16_Write_WithReadBack(addr, data, data_size);if(ret == true)return ret;// 写失败,读取整个扇区数据W25Q16_Read(sector_addr, read_buf, FLASH_SECTOR_SIZE);// 进入重试恢复流程do {// 擦除当前扇区W25Q16_EraseSector(sector_addr);// 处理不跨扇区的情况(数据完全在当前扇区内)if(sector_offset + data_size <= FLASH_SECTOR_SIZE){// 合并新旧数据到缓冲区tmos_memcpy(&read_buf[sector_offset], data, data_size);// 写入整个扇区数据ret = W25Q16_Write_WithReadBack(sector_addr, read_buf, FLASH_SECTOR_SIZE);if(ret == true)break;}// 处理跨扇区的情况else {// 跨扇区:分两部分处理uint16_t first_part_size = FLASH_SECTOR_SIZE - sector_offset;  // 当前扇区可写入的字节数uint16_t second_part_size = data_size - first_part_size;        // 需写入下一扇区的字节数// 合并第一部分数据到缓冲区tmos_memcpy(&read_buf[sector_offset], data, first_part_size); // 擦除下一个扇区W25Q16_EraseSector(sector_addr + FLASH_SECTOR_SIZE);// 写入当前扇区的合并数据ret = W25Q16_Write_WithReadBack(sector_addr , read_buf,  FLASH_SECTOR_SIZE);if(ret == false)continue;  // 当前扇区写入失败,继续重试// 写入下一个扇区的剩余数据ret = W25Q16_Write_WithReadBack(sector_addr + FLASH_SECTOR_SIZE, &data[first_part_size],  // 指向剩余数据的起始位置second_part_size         // 剩余数据的长度);if(ret == true)break;}write_cnt++;} while (ret != true && write_cnt < 5);// 返回最终写入结果if(write_cnt >= 5) {return ret; // 超时重试5次后仍失败 }return ret; // 成功或重试后成功
}

4、当然还有一个重要的点,读数据,你怎么知道你读的是对的?

开始的时候,我想过在结构体内定义固定的标志位,比如0X55等。读的时候,先把第一位读出来,但是这样非常不保险,只能确定0x55是对的。我还想过连读两次,两次一样才判定成功,但是两次如果不一样,你怎么判定哪一次是错的?然后我还是想到了Modbus里的校验。结构体尾部加校验,存的时候把除去最后两字节的长度字节计算CRC16校验,读的时候,用读出的结果与计算的CRC校验比较即可。因为读接口是确定了,每次只读一个结构体大小的数据,而CRC又在结尾,这就不会导致,还要去找校验位在哪?

/*** @brief 从Flash读取数据并校验(记录使用)* @param addr 读取起始地址* @param data 数据缓冲区* @param data_size 读取数据长度(字节)* @return 读取并校验成功返回true,失败返回false* @note 内部使用带校验的读取函数,确保数据完整性*/
static bool RECORD_FLASH_READ(uint32_t addr, uint8_t* data, uint16_t data_size)
{uint8_t read_cnt = 0;bool ret = false;// 至少尝试读取一次do {// 1. 调用底层读取函数if (W25Q16_Read_Verify(addr, data, data_size) == false) {ret = false;// 地址越界等严重错误,不重试return false;}// 2. 提取存储的CRC值(低字节在前,高字节在后)uint16_t stored_crc = (uint16_t)data[data_size - 2] | ((uint16_t)data[data_size - 1] << 8);// 3. 计算数据部分的CRC(排除CRC字段本身)uint16_t calculated_crc = ModbusCrc16(data, data_size - 2);// 4. 比较CRC值if (calculated_crc == stored_crc) {return true; // CRC校验成功,返回true}read_cnt++;} while (read_cnt < 5);// 达到最大重试次数仍失败return false;
}

这样应该能解决我的擦写失败问题了,我不信一直擦写失败,en。而且我也不会一直去擦写。存记录的话会存在一个问题,就是磨损平衡,和数据查找完整性问题。如果你只划一个扇区给记录,存完就擦,一下全部的数据都没了,你要开始存,那这些数据是要还是不要呢?当然,你都存了,数据肯定是重要的。

此时有两种思路:当然我是抄老哥的第二种思路。以后我自己写出来会分享。

第一种:搞个备份区,开始的时候主区和备份区同时存。存满后,擦主区,备份区还都是老数据,可以读1条新数据加地址偏移去读备份区的数据,这样数据存满了,可以保持一直是满的状态,旧数据被覆盖掉。

第二种,把数据区域搞大点,比如你最多存一个扇区,你就开3个扇区用来存数据,加上一个偏移量是大于数据记录最大数的,假如第一个扇区存满了,接着存第2,3扇区,偏移量在走,但是记录数不变,读取的时候就偏移往后-最大记录数为有效数据即可,这样就算偏移满了,回头擦第1扇区的时候也不会影响第3扇区存的旧纪录。好像就是土豪做法,反正有效数据确定是那么长了,也不会频繁擦。

最初我想的就是偏移最大等于最大记录数,这样偏移娆回头把我的老数据擦没了,读出来全是0XFF,头疼。 

感谢野火哥的跨页写代码!!!

相关文章:

  • 【Dv3Admin】系统视图角色管理API文件解析
  • 在Word中使用 Microsoft Print to PDF和另存为PDF两种方式生成的 PDF文件
  • Docker 操作容器[SpringBoot之Docker实战系列] - 第538篇
  • bilibili-mcp 使用示例
  • JBank:Jucoin 推出的 Web3 去中心化自托管银行金融协议
  • 服务器硬防的应用场景都有哪些?
  • Deepin 25 安装字体
  • 第二部分-IP及子网划分
  • Golang dig框架与GraphQL的完美结合
  • 【117架AI无人机出击】乌克兰窃取4.4GB敏感数据,重创俄41架战机,损失超500亿元
  • 使用联邦学习进行CIFAR-10分类任务
  • 混合动力无人机设计与运行要点分析
  • 无人机动力系统核心技术解析
  • 无人机机器人资源整合
  • DAY 27 函数专题2:装饰器
  • 2025年ASOC SCI2区TOP,协同搜索框架自适应算法+多无人机巡检规划,深度解析+性能实测
  • 使用反汇编指令javap查看synchronized实现原理
  • 【OpenCV】双相机结构光成像与图像交叉融合实现【python篇】
  • Cursor 工具项目构建指南:MySql 数据库结构设计的 Cursor 规范
  • PCB设计教程【大师篇】——STM32开发板原理图设计(单片机最小系统)
  • 做网站知识大全/网络营销做的好的企业
  • 域名销售网站/蚁坊软件舆情监测系统
  • 推荐一些做电子的网站/谷歌外贸平台
  • 动态网站管理系统/徐州seo排名收费
  • 多语种网站营销/seo网站推广杭州
  • 外贸站群/青岛网站建设推广公司