DataFlash 驱动代码小议

最近比较闲,有时间把以前的一些想法实现出来了。Atmel 的DataFlash 我自己没有用过,不过公司的好几个项目中都用到了。我没事时也去翻看过别人实现的代码,感觉实现的功能都太基本,使用起来不方便,因此就趁着最近空闲将这部分代码改造一番了。

DataFlash是美国Atmel公司新推出的大容量串行Flash存储器产品,采用NOR技术制造,采用SPI接口进行读写,内部页面尺寸较小,8Mb容量的页面尺寸为264字节,16Mb和32Mb容量的页面尺寸为512字节,64Mb容量的页面尺寸为1056字节,128Mb容量和256Mb容量的页面尺寸为2112字节。另外,AT45DBxxxx系列存储器内部集成了两个与主存页面相同大小的SRAM缓存。

简单的说,DataFlash与普通的Flash 芯片最大的区别是内部包含了两个 SRAM 的buffer,每个Buffer 的大小都与 Flash 的 Page 相同。因此,改写 DataFlash 中的几个字节就变得非常的方便,不需要我们维护个外部的 Buffer。

我手里现有的程序基本上就是将芯片手册中提到的那几种操作用函数封装了一下。也就是说实现了下面几个函数。

void PageRead(uint8_t buffer[], uint16_t pageNo, uint16_t byteAddr, uint16_t size);
void BufferRead(uint8_t buffer[], uint8_t bufferNo, uint16_t byteAddr, uint16_t size);
void BufferWrite(uint8_t buffer[], uint8_t bufferNo, uint16_t byteAddr, uint16_t size);
void BufferToPage(uint8_t bufferNo, uint16_t pageNo);
void PageToBuffer(uint8_t bufferNo, uint16_t pageNo);
void PageErase(uint16_t pageNo);
void BlockErase(uint16_t blockNo);

上面几个函数的函数名已经很直白的表明了这几个函数的作用,就不用我多说了。我做的工作是将上面的这些基本函数进一步的封装,使其接口更加的友好。这里需要提前说明的是我增加功能代码主要是面向无 RTOS、无文件系统的环境中的应用。如果考虑上文件系统的话,这里的代码可能就没有什么参考价值了。另外,这里的代码都是示意性的,为了突出主线,我的代码中没有给出任何的异常处理机制(所有函数都没有返回值)。因此,在没有补全必要的异常处理功能之前,代码不适合在严肃的项目中使用。

首先来说说我心目中理想的DataFlash的驱动接口应该是什么样子的。对于写应用代码的程序员来说,使用DataFlash 应该像普通的文件读写那样简单。写上层代码的程序员不需要知道DataFlash 中有几个 Buffer, 更不需要考虑何时需要将 Buffer中的数据写回到 Flash 中。这些 Dirty Work 都应该封装在驱动内部。

因此,驱动接口应该像下面这样:

void DF_Read(uint8_t buffer[], uint32_t addr,  uint32_t size);
void DF_write(uint8_t buffer[], uint32_t addr,  uint32_t size);

当然,上面的接口有些过于理想,DataFlash 毕竟与其他的Flash 还是有些显著的区别的。其中,对程序员来说最应该注意的是 DataFlash的Page 的大小。比如AT45DB161有4096个Page,需要用12个Bit 来寻址 PA11-PA0。每个Page 大小是528 个字节,比512 字节多了16个字节,因此一个字符在 Page 中的位置需要用10 bit 来表示BA9-BA0。这样要想寻址每个字节,就需要用个22 bit 的地址。可是如果用 22 bit 的地址也有问题,这样在地址空间中会有许多的洞(4096个洞),每个洞的大小是 512-16 = 496 字节。对这些洞的访问都是无效的。要想让地址空间变得连续,可以每个Page 都放弃最后的16个字节,这样每个Page 都只使用前512个字节,用 9bit 寻址,加上寻址 page 的 12 个 bit,总共用21 个bit 来表示每个字节。

对于需要用全 528 个字节的场合,我们还需要提供额外的接口:

void DF_PageWrite(uint8_t buffer[], uint16_t pageNo, uint16_t byteAddr, uint16_t size);
void DF_PageRead(uint8_t buffer[], uint16_t pageNo, uint16_t byteAddr, uint16_t size);

这两个函数可以分别读写(改写)每一个 page 中的任意多个字节。

下面可以来说说我的程序是如何来实现的了。

首先,驱动代码需要自己记录当前的两个 Buffer 分别对应的哪两个 Page 的内容。还需要记录每个Buffer 被改写了多少次。当某个Buffer 被改写了一定的次数后就需要将Buffer的内容回写的Page中了(之所以这样做是防止突然断电等情况导致大量的数据丢失)。

因此,需要下面的变量。

static uint8_t buffer1PageNo = 0;
static uint8_t buffer2PageNo = 0;
static uint16_t buffer1WriteCount = 0;
static uint16_t buffer2WriteCount = 0;
static uint16_t writeCountMax;

然后,首先是初始化函数。指定两个Page,将其读入到Buffer中。还有设置Buffer改写多少次后需要写回 Flash中。

void DF_Init(uint16_t pageNo1, uint16_t pageNo2, uint16_t writeCntMax)
{
    buffer1PageNo = pageNo1;
    buffer2PageNo = pageNo2;
    PageToBuffer(BUFFER1, buffer1PageNo);
    PageToBuffer(BUFFER2, buffer2PageNo);
    buffer1WriteCount = 0;  /* 记录第一个 Buffer 中的内容被改写了多少次 */
    buffer2WriteCount = 0;  /* 记录第二个 Buffer 中的内容被改写了多少次 */
    writeCountMax = writeCntMax; /* 当某个 Buffer 的内容被改写了writeCountMax 次后,就会强制将 Buffer 的内容写回 Flash */
}

然后给出读取某个Page中数据的代码。代码很简单,如果对应 Page 的内容在某个Buffer中就从Buffer中读取(这时必须从Buffer 中读取,因为Buffer中的内容可能已经被更改过但是还没有写回Page中)。否则直接从Page中读取。

void DF_PageRead(uint8_t buffer[], uint16_t pageNo, uint16_t byteAddr,  uint16_t size)
{
    /* 如果读取的内容在 Buffer 中就从 Buffer 中读取*/
    if(pageNo == buffer1PageNo)
    {
        BufferRead(buffer, BUFFER1, byteAddr, size);
    }
    else if(pageNo == buffer2PageNo)
    {
        BufferRead(buffer, BUFFER2, byteAddr, size);
    }
    else /* 否则直接从Flash 中读取 */
    {
        PageRead(buffer, pageNo, byteAddr,  size);
    }
}

下面是改写 Page 代码。如果对应 Page 的内容已经读入到 Buffer中了,就直接在 Buffer 中改写。改写完之后判断一下改写的次数是否到了需要写回 Page的阈值,到了就写回 Page。

如果 Page 的内容没有在 Buffer 中就比较麻烦了,那就需要把某个 Buffer 腾出来。如果有某个 Buffer没有被改写过就直接把它腾出了,因为这样最方便。如果两个 Buffer 都已经改写过了,我采用的策略是将用的较少的那个 Buffer 腾出了放新的 Page 的内容。下面是代码。

void DF_PageWrite(uint8_t buffer[], uint16_t pageNo, uint16_t byteAddr, uint16_t size)
{
    if(pageNo == buffer1PageNo)
    {
        BufferWrite(buffer, BUFFER1, byteAddr, size);
        buffer1WriteCount++;
        if(buffer1WriteCount >= writeCountMax) /* 如果 Buffer 的改写次数超过设定的次数了,就写回 Flash 中 */
        {
            BufferToPage(BUFFER1, buffer1PageNo);
            buffer1WriteCount = 0;
        }
    }
    else if(pageNo == buffer2PageNo)
    {
        BufferWrite(buffer, BUFFER2, byteAddr, size);
        buffer2WriteCount++;
        if(buffer2WriteCount >= writeCountMax) /* 如果 Buffer 的改写次数超过设定的次数了,就写回 Flash 中 */
        {
            BufferToPage(BUFFER1, buffer2PageNo);
            buffer2WriteCount = 0;
        }
    }
    else
    {
        /* 需要改写的数据不在 Buffer 中,这时就要将某个 Buffer 腾出来 */
        /* 先判断是否有 Buffer 与 Flash 中的内容一致,如果有这样的 Buffer 就优先使用,
           因为不需要将这样的 Buffer 的内容写回 Flash,可以减少 Flash 的擦写次数,执行速度也更快 */
        if(buffer1WriteCount == 0)
        {
            buffer1PageNo = pageNo;
            PageToBuffer(BUFFER1, buffer1PageNo);
            BufferWrite(buffer, BUFFER1, byteAddr, size);
            buffer1WriteCount++;
        }
        else if(buffer2WriteCount == 0)
        {
            buffer2PageNo = pageNo;
            PageToBuffer(BUFFER2, buffer2PageNo);
            BufferWrite(buffer, BUFFER2, byteAddr, size);
            buffer2WriteCount++;
        }
        else
        {
            /* 到这里了我们就要做个抉择了,两个 Buffer 必须要拿出一个来放新的 Page 的内容。
             * 可以选择的策略有很多种,我的策略是换掉不常用的那个 Buffer。
             */
            if(buffer1WriteCount >= buffer2WriteCount)
            {
                BufferToPage(BUFFER2, buffer2PageNo);
                buffer2PageNo = pageNo;
                PageToBuffer(BUFFER2, buffer2PageNo);
                BufferWrite(buffer, BUFFER2, byteAddr, size);
                buffer2WriteCount = 1;
            }
            else
            {
                BufferToPage(BUFFER1, buffer1PageNo);
                buffer1PageNo = pageNo;
                PageToBuffer(BUFFER1, buffer1PageNo);
                BufferWrite(buffer, BUFFER1, byteAddr, size);
                buffer1WriteCount = 1;
            }
        }
    }
}

有时,我们需要强制性的将Buffer的内容写回 Page 中。因此有下面这样的代码。通常这个函数会被定时调用,以此减少由于突然断电或程序跑飞造成的数据丢失。

void DF_FlushAll(void)
{
    if(buffer1WriteCount != 0)
    {
        BufferToPage(BUFFER1, buffer1PageNo);
        buffer1WriteCount = 0;
    }
    if(buffer2WriteCount != 0)
    {
        BufferToPage(BUFFER2, buffer2PageNo);
        buffer2WriteCount = 0;
    }
}

上面的接口可以访问到每个Page 的每一个字节。下面还有两个统一地址空间下的读写函数。下面的代码是按照 AT45DB161 写成的,每个 Page 只使用前 512 个字节。

void DF_Read(uint8_t buffer[], uint32_t addr, uint32_t size)
{
    uint16_t pageNo;
    uint16_t byteAddr;
    uint16_t count;

    pageNo = addr >> 9;
    byteAddr = addr & 0x1ff;
    count = 512 - byteAddr;
    if(count > size) count = size;
    DF_PageRead(buffer, pageNo, byteAddr,  count);
    size -= count;
    buffer += count;
    byteAddr = 0;
    
    while(size > 0)
    {
        pageNo ++;
        count = size & 0x1ff;
        DF_PageRead(buffer, pageNo, byteAddr,  count);
        size -= count;
        buffer += count;
    }
}

void DF_write(uint8_t buffer[], uint32_t addr, uint32_t size)
{
    uint16_t pageNo;
    uint16_t byteAddr;
    uint16_t count;

    pageNo = addr >> 9;
    byteAddr = addr & 0x1ff;
    count = 512 - byteAddr;
    if(count > size) count = size;
    DF_PageWrite(buffer, pageNo, byteAddr,  count);
    size -= count;
    buffer += count;
    byteAddr = 0;
    
    while(size > 0)
    {
        pageNo ++;
        count = size & 0x1ff;
        DF_PageWrite(buffer, pageNo, byteAddr,  count);
        size -= count;
        buffer += count;
    }
}

到这里,所有的功能就都实现了。如果使用的不是AT45DB161, 最后的两个函数需要做些小修改。另外,请读者注意上面的代码我写完后没有测试过,不保证没有错误。




你可能感兴趣的:(DataFlash 驱动代码小议)