硬汉嵌入式论坛

 找回密码
 立即注册
查看: 535|回复: 7
收起左侧

[技术讨论] flash能否不拆除直接写入?

[复制链接]

1

主题

5

回帖

8

积分

新手上路

积分
8
发表于 2024-5-11 15:37:01 | 显示全部楼层 |阅读模式
之前一直没有对flash进行过读写操作,有些想请教一下各位大佬
1、对flash写入之前必须擦除,然而单片机擦除flash是以页为单位,擦除这样一片空间的时间太久。有什么办法能缩短时间吗?
2、对一页flash写入之后,剩下的空间下次如何继续利用呢。比方说我只对前面的一百个字节写入数据,后面的空间如果想再次写入数据,要对整页进行擦除操作,这就引发另外一个问题,前面保存的数据应该怎么办?
如果有高手愿意指导一下,感激不尽
回复

使用道具 举报

0

主题

13

回帖

13

积分

新手上路

积分
13
发表于 2024-5-11 16:22:48 | 显示全部楼层
flash 一般而言只有Program 和 Erase两个概念,Erase是把0的位变成1(需要不一样的单片机电压,所以时间会变长),Program是把1的位写成0没法把0变成1。当Erase整页完成时,整页Flash的数据都变成0xff就是全为1,这样Program把正确的位变成0然后成为你想写入的数。举例来说,你可以用program的操作0xff,program成0xee,然后再将0xee 二次program 为0x00,实际上st官方的flash模拟eeprom库就是这样干的,写数过程中将页面首字节写成0xeeee,然后program页面满了就写成0x0000。所以位的1到0的操作,是可以不erase就进行Program的,理解了Erase是把0的位变成1、Program是把1的位写成0,就可以知道如何节省时间了,如果不erase,对0xee,写入0x11会导致直接把这个数据变成0x00而不是期望的0x11。至于flash如何高效的利用空间,参考st官方的flash模拟eeprom库,这个库的实现就相当高效的利用了flash空间,建议直接参考阅读官方库的实现。   

https://www.st.com/en/embedded-software/x-cube-eeprom.html
回复

使用道具 举报

8

主题

29

回帖

53

积分

初级会员

积分
53
发表于 2024-5-12 11:23:10 | 显示全部楼层
针对第二个问题,可以查看正点原子的战舰精英版的官方例程,他用SPI与Flash通信,先把Flash中前面的字节保存在内存里,然后把想要的新写入的内容,通过偏移,加载到原有内容后面,最后先擦除Flsah对应的一页,然后将相关内存一次性写入Flash的一页。

//写SPI FLASH  
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)                                               
//NumByteToWrite:要写入的字节数(最大65535)   
u8 W25QXX_BUFFER[4096];                 
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{
        u32 secpos;
        u16 secoff;
        u16 secremain;          
        u16 i;   
        u8 * W25QXX_BUF;          
           W25QXX_BUF=W25QXX_BUFFER;             
        secpos=WriteAddr/4096;//扇区地址  
        secoff=WriteAddr%4096;//在扇区内的偏移
        secremain=4096-secoff;//扇区剩余空间大小   
        //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
        if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
        while(1)
        {       
                W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
                for(i=0;i<secremain;i++)//校验数据
                {
                        if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除            
                }
                if(i<secremain)//需要擦除
                {
                        W25QXX_Erase_Sector(secpos);                //擦除这个扇区
                        for(i=0;i<secremain;i++)                           //复制
                        {
                                W25QXX_BUF[i+secoff]=pBuffer[i];          
                        }
                        W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区  

                }else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.                                   
                if(NumByteToWrite==secremain)break;//写入结束了
                else//写入未结束
                {
                        secpos++;//扇区地址增1
                        secoff=0;//偏移位置为0          

                           pBuffer+=secremain;                                  //指针偏移
                        WriteAddr+=secremain;                                //写地址偏移          
                           NumByteToWrite-=secremain;                        //字节数递减
                        if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完
                        else secremain=NumByteToWrite;                //下一个扇区可以写完了
                }         
        };         
}
详细建议参考正点原子STM32F103开发板原码
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
107778
QQ
发表于 2024-5-12 16:51:47 | 显示全部楼层
楼主的问题,我记得我昨天回复了,今天看了下这个帖子,竟然没有回复成功。

问题1:这个回复就是2楼坛友的回复
问题2:这个就是3楼坛友的回复。

我这里综合下,将他们这两个都做了支持,也就是我们设计的bsp_spi_flash.C文件,里面做了一writebuffer方式,支持2楼坛友的高效擦写,支持3楼坛友复制粘贴。

也就是说这个函数,可以像使用内部SRAM一样指定任意地址进行写入,无需用户操心,直接调用即可。


https://www.armbbs.cn/forum.php? ... 3255&extra=page%3D1

V6-011_串行SPI Flash W25QXX读写例程(查询方式).rar
V6-012_串行SPI Flash W25QXX读写例程(中断方式).rar
V6-013_串行SPI Flash W25QXX读写例程(DMA方式).rar


22.png


[C] 纯文本查看 复制代码
/*
*********************************************************************************************************
*        函 数 名: sf_NeedErase
*        功能说明: 判断写PAGE前是否需要先擦除。
*        形    参:   _ucpOldBuf : 旧数据。
*                           _ucpNewBuf : 新数据。
*                           _uiLen :数据个数。
*        返 回 值: 0 : 不需要擦除, 1 :需要擦除
*********************************************************************************************************
*/
static uint8_t sf_NeedErase(uint8_t * _ucpOldBuf, uint8_t *_ucpNewBuf, uint16_t _usLen)
{
        uint16_t i;
        uint8_t ucOld;

        /*
        算法第1步:old 求反, new 不变
              old    new
                  1101   0101
        ~     1111
                = 0010   0101

        算法第2步: old 求反的结果与 new 位与
                  0010   old
        &          0101   new
                 =0000

        算法第3步: 结果为0,则表示无需擦除. 否则表示需要擦除
        */

        for (i = 0; i < _usLen; i++)
        {
                ucOld = *_ucpOldBuf++;
                ucOld = ~ucOld;

                /* 注意错误的写法: if (ucOld & (*_ucpNewBuf++) != 0) */
                if ((ucOld & (*_ucpNewBuf++)) != 0)
                {
                        return 1;
                }
        }
        return 0;
}

/*
*********************************************************************************************************
*        函 数 名: sf_AutoWriteSector
*        功能说明: 写1个扇区并校验,如果不正确则再重写两次,本函数自动完成擦除操作。
*        形    参:          _pBuf : 数据源缓冲区;
*                                _uiWriteAddr :目标区域首地址
*                                _usSize :数据个数,不能超过扇区大小。
*        返 回 值: 0 : 错误, 1 : 成功
*********************************************************************************************************
*/
static uint8_t sf_AutoWriteSector(uint8_t *_ucpSrc, uint32_t _uiWrAddr, uint16_t _usWrLen)
{
        uint16_t i;
        uint16_t j;                                        /* 用于延时 */
        uint32_t uiFirstAddr;                /* 扇区首址 */
        uint8_t ucNeedErase;                /* 1表示需要擦除 */
        uint8_t cRet;

        /* 长度为0时不继续操作,直接认为成功 */
        if (_usWrLen == 0)
        {
                return 1;
        }

        /* 如果偏移地址超过芯片容量则退出 */
        if (_uiWrAddr >= g_tSF.TotalSize)
        {
                return 0;
        }

        /* 如果数据长度大于扇区容量,则退出 */
        if (_usWrLen > g_tSF.SectorSize)
        {
                return 0;
        }

        /* 如果FLASH中的数据没有变化,则不写FLASH */
        sf_ReadBuffer(s_spiBuf, _uiWrAddr, _usWrLen);
        if (memcmp(s_spiBuf, _ucpSrc, _usWrLen) == 0)
        {
                return 1;
        }

        /* 判断是否需要先擦除扇区 */
        /* 如果旧数据修改为新数据,所有位均是 1->0 或者 0->0, 则无需擦除,提高Flash寿命 */
        ucNeedErase = 0;
        if (sf_NeedErase(s_spiBuf, _ucpSrc, _usWrLen))
        {
                ucNeedErase = 1;
        }

        uiFirstAddr = _uiWrAddr & (~(g_tSF.SectorSize - 1));

        if (_usWrLen == g_tSF.SectorSize)                /* 整个扇区都改写 */
        {
                for        (i = 0; i < g_tSF.SectorSize; i++)
                {
                        s_spiBuf[i] = _ucpSrc[i];
                }
        }
        else                                                /* 改写部分数据 */
        {
                /* 先将整个扇区的数据读出 */
                sf_ReadBuffer(s_spiBuf, uiFirstAddr, g_tSF.SectorSize);

                /* 再用新数据覆盖 */
                i = _uiWrAddr & (g_tSF.SectorSize - 1);
                memcpy(&s_spiBuf[i], _ucpSrc, _usWrLen);
        }

        /* 写完之后进行校验,如果不正确则重写,最多3次 */
        cRet = 0;
        for (i = 0; i < 3; i++)
        {

                /* 如果旧数据修改为新数据,所有位均是 1->0 或者 0->0, 则无需擦除,提高Flash寿命 */
                if (ucNeedErase == 1)
                {
                        sf_EraseSector(uiFirstAddr);                /* 擦除1个扇区 */
                }

                /* 编程一个扇区 */
                sf_PageWrite(s_spiBuf, uiFirstAddr, g_tSF.SectorSize);

                if (sf_CmpData(_uiWrAddr, _ucpSrc, _usWrLen) == 0)
                {
                        cRet = 1;
                        break;
                }
                else
                {
                        if (sf_CmpData(_uiWrAddr, _ucpSrc, _usWrLen) == 0)
                        {
                                cRet = 1;
                                break;
                        }

                        /* 失败后延迟一段时间再重试 */
                        for (j = 0; j < 10000; j++);
                }
        }

        return cRet;
}

/*
*********************************************************************************************************
*        函 数 名: sf_WriteBuffer
*        功能说明: 写1个扇区并校验,如果不正确则再重写两次,本函数自动完成擦除操作。
*        形    参:  _pBuf : 数据源缓冲区;
*                           _uiWrAddr :目标区域首地址
*                           _usSize :数据个数,任意大小,但不能超过芯片容量。
*        返 回 值: 1 : 成功, 0 : 失败
*********************************************************************************************************
*/
uint8_t sf_WriteBuffer(uint8_t* _pBuf, uint32_t _uiWriteAddr, uint32_t _usWriteSize)
{
        uint32_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

        Addr = _uiWriteAddr % g_tSF.SectorSize;
        count = g_tSF.SectorSize - Addr;
        NumOfPage =  _usWriteSize / g_tSF.SectorSize;
        NumOfSingle = _usWriteSize % g_tSF.SectorSize;

        if (Addr == 0) /* 起始地址是扇区首地址  */
        {
                if (NumOfPage == 0) /* 数据长度小于扇区大小 */
                {
                        if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0)
                        {
                                return 0;
                        }
                }
                else         /* 数据长度大于等于扇区大小 */
                {
                        while (NumOfPage--)
                        {
                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0)
                                {
                                        return 0;
                                }
                                _uiWriteAddr +=  g_tSF.SectorSize;
                                _pBuf += g_tSF.SectorSize;
                        }
                        if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0)
                        {
                                return 0;
                        }
                }
        }
        else  /* 起始地址不是扇区首地址  */
        {
                if (NumOfPage == 0) /* 数据长度小于扇区大小 */
                {
                        if (NumOfSingle > count)  /* (_usWriteSize + _uiWriteAddr) > SPI_FLASH_PAGESIZE */
                        {
                                temp = NumOfSingle - count;

                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0)
                                {
                                        return 0;
                                }

                                _uiWriteAddr +=  count;
                                _pBuf += count;

                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, temp) == 0)
                                {
                                        return 0;
                                }
                        }
                        else
                        {
                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0)
                                {
                                        return 0;
                                }
                        }
                }
                else        /* 数据长度大于等于扇区大小 */
                {
                        _usWriteSize -= count;
                        NumOfPage =  _usWriteSize / g_tSF.SectorSize;
                        NumOfSingle = _usWriteSize % g_tSF.SectorSize;
                        if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0)
                        {
                                return 0;
                        }

                        _uiWriteAddr +=  count;
                        _pBuf += count;

                        while (NumOfPage--)
                        {
                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0)
                                {
                                        return 0;
                                }
                                _uiWriteAddr +=  g_tSF.SectorSize;
                                _pBuf += g_tSF.SectorSize;
                        }

                        if (NumOfSingle != 0)
                        {
                                if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0)
                                {
                                        return 0;
                                }
                        }
                }
        }
        return 1;        /* 成功 */
}

回复

使用道具 举报

1

主题

5

回帖

8

积分

新手上路

积分
8
 楼主| 发表于 2024-5-12 17:40:31 | 显示全部楼层
感谢各位的积极指导,很有帮助
回复

使用道具 举报

1

主题

5

回帖

8

积分

新手上路

积分
8
 楼主| 发表于 2024-5-13 10:44:06 | 显示全部楼层
eric2013 发表于 2024-5-12 16:51
楼主的问题,我记得我昨天回复了,今天看了下这个帖子,竟然没有回复成功。

问题1:这个回复就是2楼坛友 ...

另外想请教下硬汉哥,如果在rtos内应用这类操作flash,用查询还是中断还是DMA方式更好呢
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
107778
QQ
发表于 2024-5-15 10:06:49 | 显示全部楼层
JM001 发表于 2024-5-13 10:44
另外想请教下硬汉哥,如果在rtos内应用这类操作flash,用查询还是中断还是DMA方式更好呢

DMA更推荐,可以充分解放CPU,

当然,如果你每次的操作的数据量非常小,可以考虑中断或者查询。
回复

使用道具 举报

8

主题

163

回帖

187

积分

初级会员

积分
187
发表于 2024-5-16 12:38:02 | 显示全部楼层
带ECC校验的单片机,必须通过寄存器地址查询是否是擦除态。
不带ECC的,只要是0xff,你就可以写。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2024-6-13 20:55 , Processed in 0.232825 second(s), 30 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表