本文讲述了Coffee文件系统移植重要一步--宏映射,分析了COFFEE_READCOFFEE_WRITECOFFEE_ERASE宏映射的源码,给出一个测试例子。最后给出个人对Coffee的一些看法。

PS:本文测试平台是IAR+STM32F103RBT6。

1. 之前的问题

博文《Coffee文件系统移植 》只是根据分析的,并没有实际跑起来。后来几乎把源码分析了一遍,期间发现Coffee有逻辑问题。现在回想起来,觉得很幼稚。

问题出在我将小组移植的代码当官方源码读了(他在源码并没有注释,后来才知道是他移植的),具体来说,就是将宏COFFEE_ERASE(sector)映射的函数逻辑功能理解错了,COFFEE_ERASE实际功能是将sector全部写入0,而不是擦除,这大概称得上代码可读性极差吧!(我通过研读cpu\stm32w108\cfs-coffee-arch.c源代码证实了这个问题)。

2. COFFEE_READ、COFFEE_WRITE和COFFEE_ERASE宏映射

2.1 概述

移植Coffee一个很重要的工作就是将COFFEE_READCOFFEE_WRITECOFFEE_ERASE宏映射到硬件相关的函数。COFFEE_READ对应于FLASH读stm32_flash_readCOFFEE_WRITE对应于文件写stm32_flash_writeCOFFEE_ERASE却不是对应于FLASH擦除,为了避免搞混,这里用stm32_coffee_erase而不是stm32_flash_erase。部分源代码如下:

//filename:cfs-coffee-arch.h
#define COFFEE_WRITE(buf, size, offset) stm32_flash_write(COFFEE_START + offset, buf, size)
#define COFFEE_READ(buf, size, offset) stm32_flash_read(COFFEE_START + offset, buf, size)
#define COFFEE_ERASE(sector) stm32_coffee_erase(sector)

2.2 stm32_coffee_erase

COFFEE_ERASE宏用于将sector的内容全部写为0,而Coffee文件系统格式化cfs_coffee_format函数就是循环调用COFFEE_ERASE将Coffee所有的sector(总共有COFFEE_SECTOR_COUNT)内容写为0,详情可参见博文《Coffee文件系统格式化cfs_coffee_format》。那么,stm32_coffee_erase实现的功能就是将sector写入0,源代码如下:

void stm32_coffee_erase(u8_t sector)
{
  u32_t data = 0;
  u32_t addr = COFFEE_START + (sector) *COFFEE_SECTOR_SIZE;
  u32_t end = addr + COFFEE_SECTOR_SIZE;

  if(!(addr >= COFFEE_START && end <= COFFEE_START + COFFEE_SIZE)) //确保地址在Coffee管理的区间
  {
    return ;
  }

  FLASH_Unlock(); /* Unlock the Flash Program Erase controller */
  for(; addr < end; addr += 4)
  {
    if(FLASH_ProgramWord(addr, data) != FLASH_COMPLETE)
    {
      PRINTF("FLASH_ProgramHalfWord Error.\n");
    }
  }
  FLASH_Lock();
}

2.3 stm32_flash_read

读取Flash的内容比较简单,因为是片上FLASH,与内存统一编址,读取FLASH可以像读取普通内存一样,这里调用IAR库函数memcpy实现,源代码如下:

void stm32_flash_read(u32_t address, void *data, u32_t length)
{
  u8_t *pdata = (u8_t*)address;
  ENERGEST_ON(ENERGEST_TYPE_FLASH_READ);
  memcpy(data, pdata, length);
  ENERGEST_OFF(ENERGEST_TYPE_FLASH_READ);
}

其中,宏ENERGEST_ONENERGEST_OFF是Contiki系统函数(在core/sys/energest.h定义),用于统计能量消耗,这里暂且忽略之。

2.4 stm32_flash_write

往FLASH写入内容就比较复杂了,首先得确保存储单元在写入前是0xFFFF(因为FLASH写入只能从1变成0的物理特性),否则会报PGERR错误(即Programming error,由硬件自动设置)。具体写入步骤是这样的:读取内容-->修改内容-->擦除FLASH-->写入内容。源代码如下:

// Allocates a buffer of FLASH_PAGE_SIZE bytes statically (rather than on the stack).
#ifndef STATIC_FLASH_BUFFER
  #define STATIC_FLASH_BUFFER 1
#endif


void stm32_flash_write(u32_t address, const void *data, u32_t length)
{
  const u32_t end = address + length;
  u32_t i;
  u32_t next_page, curr_page;
  u16_t offset;

  #if STATIC_FLASH_BUFFER
    static u8_t buf[FLASH_PAGE_SIZE];
  #else
    u8_t buf[FLASH_PAGE_SIZE];
  #endif

  for(i = address; i < end;)
  {
    next_page = (i | (FLASH_PAGE_SIZE - 1)) + 1;
    curr_page = i &~(FLASH_PAGE_SIZE - 1);
    offset = i - curr_page;
    if(next_page > end)
    {
      next_page = end;
    }
    //1.Read a page from flash and put it into a mirror buffer.
    stm32_flash_read(curr_page, buf, FLASH_PAGE_SIZE);
    //2.Update flash mirror data with new data.
    memcpy(buf + offset, data, next_page - i);
    /**********3. Erase flash page. ***************/
    ENERGEST_ON(ENERGEST_TYPE_FLASH_WRITE);
    FLASH_Unlock();
    FLASH_ErasePage(i);

    /*****4. Write modified data form mirror buffer into the flash.********/
    unsigned int j = 0;
    while(j < FLASH_PAGE_SIZE)
    {
      FLASH_ProgramWord(curr_page + j, *(u32_t*) &buf[j]);
      j += 4;
    }
    FLASH_Lock();
    ENERGEST_OFF(ENERGEST_TYPE_FLASH_WRITE);

    data = (u8_t*)data + next_page - i;
    i = next_page;
  }
}

2.5 stm32_flash_erase

Coffee进行读写前,需先进行文件系统格式化,而格式化主要是将Coffee管辖的FLASH区域全部写入0,而FLASH写入的前提是编程单元是0xFFFF,所以还需要一个函数将Coffee管辖的FLASH区域全部擦除,即由0变1。源代码如下:

//erase flash between COFFEE_START to COFFEE_START+COFFEE_SIZE
void stm32_flash_erase(void)
{
  u32_t curr_page_addr = COFFEE_START &~(FLASH_PAGE_SIZE - 1);
  u32_t last_page_addr = (COFFEE_START + COFFEE_SIZE - FLASH_PAGE_SIZE) &~(FLASH_PAGE_SIZE - 1); //用于Coffee最后一页的起始地址
  u32_t i;

  FLASH_Unlock(); /* Unlock the Flash Program Erase controller */
  for(i = curr_page_addr; i <= last_page_addr;)
  {
    FLASH_ErasePage(i); //FLASH_Status FLASH_ErasePage(uint32_t Page_Address)
    i += FLASH_PAGE_SIZE;
  }
  FLASH_Lock();
}

3. 测试

3.1 测试用例

文件Contiki\cpu\stm32w108\cfs-coffee-arch.c有个cfs_coffee_test函数,对Coffee进行多方面测试。我也是拿这个来测试Coffee,这里只举简单的例子,将字符串写入,再读出,通过串口打印观其是否相同。测试源码如下:

#include "contiki.h"
#include "debug-uart.h"
#include "cfs/cfs.h"
#include "cfs-coffee-arch.h"

PROCESS(cfs_test_process, "cfs test");
AUTOSTART_PROCESSES(&cfs_test_process);

PROCESS_THREAD(cfs_test_process, ev, data)
{
  PROCESS_BEGIN();
  usart_puts("***cfs test process start***\n");

  if(cfs_coffee_format() == - 1)
  {
    usart_puts("coffee format error.");
    return - 1;
  }

  int fd = cfs_open("CoffeeTest", CFS_WRITE | CFS_READ);
  if(fd == - 1)
  {
    usart_puts("First time open error.");
    return - 1;
  }

  char buf1[] = "Hello, World!";
  char buf2[32] = "Orignal!";

  usart_puts("The orignal buf1 and buf2 is : ");
  usart_puts(buf1);
  usart_puts(buf2);

  int size_write = cfs_write(fd, buf1, sizeof(buf1));
  cfs_seek(fd, 0, CFS_SEEK_SET); //NOTE
  int size_read = cfs_read(fd, buf2, sizeof(buf1));

  usart_puts("The update buf1 and buf2 is : ");
  usart_puts(buf1);
  usart_puts(buf2);

  cfs_close(fd);

  PROCESS_END();
}

值得注意的是,需要在main函数加入如下代码:

stm32_flash_erase();//erase flash between COFFEE_START to COFFEE_START+COFFEE_SIZE
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); /* Clear All pending flags */

3.2 运行结果

img

图1 Coffee测试运行结果

4. 一个新的BUG

细心的你会发现,这其中隐含着一个很大的BUG。试想这样情况,当空间不足调用垃圾回收时,垃圾回收调用COFFEE_ERASE(sector),而COFFEE_ERASE映射到stm32_coffee_erase,而stm32_coffee_erase往sector区全部写入0,问题就出在这里,此时sector区存储单元未必是0xFFFF(因为sector已经被用过,只是标识无效,没擦除过),在写入的时候就会产生PGERR

需要重新设计stm32_coffee_erase函数,在写入之前先擦除再写入0(类似于stm32_flash_read,但没那么繁琐,无须读出、修改)。如此,系统启动也就无须调用stm32_flash_erase函数,这个操作现在已经包含在stm32_coffee_erase函数。需要考虑COFFEE_SECTOR_SIZEFLASH_PAGE_SIZE关系,这里简化处理,假设两者相等。重新设计的stm32_coffee_erase函数源码如下:

void stm32_coffee_erase(u8_t sector)
{
  u32_t data = 0;
  u32_t addr = COFFEE_START + (sector) *COFFEE_SECTOR_SIZE;
  u32_t end = addr + COFFEE_SECTOR_SIZE;

  if(!(addr >= COFFEE_START && end <= COFFEE_START + COFFEE_SIZE)) //确保地址在Coffee管理的区间
  {
    return ;
  }

  FLASH_Unlock();
  FLASH_ErasePage(addr); //先擦除 NOTE:assume COFFEE_SECTOR_SIZE=FLASH_PAGE_SIZE
  for(; addr < end; addr += 4)
  {
    if(FLASH_ProgramWord(addr, data) != FLASH_COMPLETE)
    {
      PRINTF("FLASH_ProgramHalfWord Error.\n");
    }
  }
  FLASH_Lock();
}

通过这样改进,还可以带来额外的好处,启动系统无须先全部擦除FLASH,如果也没有进行文件系统格式化,那么,之前创建的文件还是存在的。

5. 我的一些看法

随着分析的深入,我总觉得Contiki文件系统Coffee并没有想像的那么好。就拿这个例子来说吧,系统首次运行需要将FLASH擦除(擦除一次),再进行文件系统格式化(实质是FLASH写入,事实上也包括擦除,也许这一点可以通过设计算法有效避免),此时的FLASH全部为0了(除了文件头file_header),下一次写入,肯定是要擦除的。也就是说,Coffee假定FLASH的初值为0,而FLASH物理特性只能从1变0,所以每次写入都得先擦除后写入。试想,Coffee假定FLASH的初值为1,后续写入就可直接写入了,而无须先擦后写,大大减少擦除次数。但此时的文件头file_header都为1,需要大量修改整个Coffee,包括file_headerflags各位宏定义、宏判断等等。

本文系Spark & Shine原创,转载需注明出处本文最近一次修改时间 2022-03-20 23:17

results matching ""

    No results matching ""