本文讲述了Coffee文件系统移植重要一步--宏映射,分析了COFFEE_READ
、COFFEE_WRITE
和COFFEE_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_READ
、COFFEE_WRITE
和COFFEE_ERASE
宏映射到硬件相关的函数。COFFEE_READ
对应于FLASH读stm32_flash_read
,COFFEE_WRITE
对应于文件写stm32_flash_write
,COFFEE_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_ON
和ENERGEST_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 运行结果
图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_SIZE
与FLASH_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_header
的flags
各位宏定义、宏判断等等。