项目中使用到了flash,而Nordic为操作flash准备了很丰富的工具,包括fstorage 和fds,两者都是异步返回真实操作结果的,其中fds依赖于fstorage,是一个简单易用的文件系统。
使用fds之前一般都需要初始化一下,包括使用 fds_register 函数来注册一个事件回调,用来获得异步操作的结果。然后还需要使用 fds_init 函数进行初始化,完了之后就可以进行各种文件操作了。
为了与物理地址隔离,fds使用了虚拟页的概念。使用fds之前需要配置好一个虚拟页分配多大空间,必须是物理页的空间(一般是4096 byte)的整数倍。然后还需要设置总共使用多少个虚拟页,可用的虚拟页是分配的数量减一,因为其中有一页被用作GC的缓存。
这里从fds_init 开始追溯。
为了防止反复初始化,fds_init 会首先检查是否已经初始化过了,如果已经做过初始化操作,则直接返回FDS_SUCCESS。从这里可以看出,fds_init函数可以反复调用,来确保自己的模块在使用fds的时候将其正确的初始化。
fds依赖于fstorage,任意的fds操作最终都会转化为fstorage操作,这里首先需要有一个fstorage的句柄。
NRF_FSTORAGE_DEF(nrf_fstorage_t m_fs) =
{
// The flash area boundaries are set in fds_init().
.evt_handler = fs_event_handler,
};
其中,fs_event_handler为fstorage的异步回调函数。
if (m_flags.initialized)
{
// No initialization is necessary. Notify the application immediately.
event_send(&evt_success);
return FDS_SUCCESS;
}
if (nrf_atomic_flag_set_fetch(&m_flags.initializing))
{
// If we were already initializing, return.
return FDS_SUCCESS;
}
接下来的一个操作是flash_subsystem_init,字面上无法理解是干嘛的。进去一看,该函数主要有两个操作,一个是flash_bounds_set,然后是nrf_fstorage_init。
因为fds依赖于fstorage,因此fds的初始化操作必然要进行fstorage的初始化。
然后从函数名可以知道flash_bounds_set是确定如何分配fds的真实页面地址和边界,这里隐含的信息就是,fds所分配的真实页面是一块连续的区域。
flash_bounds_set函数主要内容如下:
static void flash_bounds_set(void)
{
uint32_t flash_size = (FDS_PHY_PAGES * FDS_PHY_PAGE_SIZE * sizeof(uint32_t));
m_fs.end_addr = flash_end_addr();
m_fs.start_addr = m_fs.end_addr - flash_size;
}
这里的FDS_PHY_PAGE_SIZE 是以一个 word为单位的,占用4个字节, flash_size即为fds所占用的真实的flash空间大小,这里通过如下的映射关系来确定flash_size的大小:
// The size of a physical page, in 4-byte words.
#if defined(NRF51)
#define FDS_PHY_PAGE_SIZE (256)
#else
#define FDS_PHY_PAGE_SIZE (1024)
#endif
// The number of physical pages to be used. This value is configured indirectly.
#define FDS_PHY_PAGES ((FDS_VIRTUAL_PAGES * FDS_VIRTUAL_PAGE_SIZE) / FDS_PHY_PAGE_SIZE)
然后通过flash最末尾的地址间接计算出真实页的起始地址。flash_end_addr函数用于得到flash的末尾的地址,如果flash中含有bootloader,则该地址为bootloader的起始地址。
static uint32_t flash_end_addr(void)
{
uint32_t const bootloader_addr = NRF_UICR->NRFFW[0];
uint32_t const page_sz = NRF_FICR->CODEPAGESIZE;
#ifndef NRF52810_XXAA
uint32_t const code_sz = NRF_FICR->CODESIZE;
#else
// Number of flash pages, necessary to emulate the NRF52810 on NRF52832.
uint32_t const code_sz = 48;
#endif
return (bootloader_addr != 0xFFFFFFFF) ? bootloader_addr : (code_sz * page_sz);
}
这里NRF_UICR->NRFFW[0] 具体保存的是什么值,datasheet中并未说明,但是既然是uicr的域,应该是存储什么值都是可以的。但是sdk中怎么都搜索不到这个域。然后不死心去bootloader的源码中搜索,居然也没有!本着打破砂锅问到底的原则,不死心的重新去研究datasheet,注意到UICR的base address是 0x10001000,然后 NRFFW[0]的offset是 0x14。这下反应过来了,之前研究bootloader源码的时候,有一个define 是NRF_UICR_BOOTLOADER_START_ADDRESS,在nrf_bootloader_info.h中。查看定义:
/**
* @brief Bootloader start address in UICR.
*
* Register location in UICR where the bootloader start address is stored.
*
* @note If the value at the given location is 0xFFFFFFFF, the bootloader address is not set.
*/
#define NRF_UICR_BOOTLOADER_START_ADDRESS (NRF_UICR_BASE + 0x14)
哈,这下清楚了,NRF_UICR->NRFFW[0] 指向的就是bootloader的起始地址,由bootloader负责初始化这个值,只要烧录的文件中含有bootloader,这里就会有一个不为0xFFFFFFFF的值。
然后,初始化操作队列,nordic提供了 atfifo供大家使用。nordic的sdk中存在很多预定义的宏,使用起来极为方便,这里一句话就搞定了队列的初始化。
然后,初始化的最后一步,是对页面进行初始化。这里需要将虚拟页一页一页的映射到真实的物理页,分配的虚拟页面中,有一页需要作为在GC的缓存页。
完整的页面初始化函数如下:
// This function is called during initialization to setup the page structure (m_pages) and
// provide additional information regarding eventual further initialization steps.
static fds_init_opts_t pages_init(void)
{
uint32_t ret = NO_PAGES;
uint16_t page = 0;
uint16_t total_pages_available = FDS_VIRTUAL_PAGES;
bool swap_set_but_not_found = false;
for (uint16_t i = 0; i < FDS_VIRTUAL_PAGES; i++)
{
uint32_t const * const p_page_addr = (uint32_t*)m_fs.start_addr + (i * FDS_PAGE_SIZE);
fds_page_type_t const page_type = page_identify(p_page_addr);
switch (page_type)
{
case FDS_PAGE_UNDEFINED:
{
if (page_is_erased(p_page_addr))
{
if (m_swap_page.p_addr != NULL)
{
// If a swap page is already set, flag the page as erased (in m_pages)
// and try to tag it as data (in flash) later on during initialization.
m_pages[page].page_type = FDS_PAGE_ERASED;
m_pages[page].p_addr = p_page_addr;
m_pages[page].write_offset = FDS_PAGE_TAG_SIZE;
// This is a candidate for a potential new swap page, in case the
// current swap is going to be promoted to complete a GC instance.
m_gc.cur_page = page;
page++;
}
else
{
// If there is no swap page yet, use this one.
m_swap_page.p_addr = p_page_addr;
m_swap_page.write_offset = FDS_PAGE_TAG_SIZE;
swap_set_but_not_found = true;
}
ret |= PAGE_ERASED;
}
else
{
// The page contains non-FDS data.
// Do not initialize or use this page.
total_pages_available--;
m_pages[page].p_addr = p_page_addr;
m_pages[page].page_type = FDS_PAGE_UNDEFINED;
page++;
}
} break;
case FDS_PAGE_DATA:
{
m_pages[page].page_type = FDS_PAGE_DATA;
m_pages[page].p_addr = p_page_addr;
// Scan the page to compute its write offset and determine whether or not the page
// can be garbage collected. Additionally, update the latest kwown record ID.
page_scan(p_page_addr, &m_pages[page].write_offset, &m_pages[page].can_gc);
ret |= PAGE_DATA;
page++;
} break;
case FDS_PAGE_SWAP:
{
if (swap_set_but_not_found)
{
m_pages[page].page_type = FDS_PAGE_ERASED;
m_pages[page].p_addr = m_swap_page.p_addr;
m_pages[page].write_offset = FDS_PAGE_TAG_SIZE;
page++;
}
m_swap_page.p_addr = p_page_addr;
// If the swap is promoted, this offset should be kept, otherwise,
// it should be set to FDS_PAGE_TAG_SIZE.
page_scan(p_page_addr, &m_swap_page.write_offset, NULL);
ret |= (m_swap_page.write_offset == FDS_PAGE_TAG_SIZE) ?
PAGE_SWAP_CLEAN : PAGE_SWAP_DIRTY;
} break;
default:
// Shouldn't happen.
break;
}
}
if (total_pages_available < 2)
{
ret &= NO_PAGES;
}
return (fds_init_opts_t)ret;
}
这里没什么好说的,初始化flash当然要日常遍历一下,需要注意的是,每个页面的地址是由(uint32_t*)m_fs.start_addr+ i*1024,注意这里将start_addr转换成了 uint32_t 的指针类型,所以每增加一个1024,实际的地址增量是 1024*4。
uint32_t const * const p_page_addr = (uint32_t*)m_fs.start_addr + (i * FDS_PAGE_SIZE);
然后,得到每个页面的状态,可见fds会在每个页面的预设地址上标记页面的状态,就和fds的每个record都有一个header一样,可以理解为每个页面也有一个类似header的这么个玩意。page_indentify函数没有什么好说的,就是查询p_page_addr对应的某个固定便宜地址上的tag是什么值。
fds_page_type_t const page_type = page_identify(p_page_addr);
正常情况下,烧录程序前会彻底擦除一遍flash,因此这里所有的页面都是FDS_PAGE_UNDEFINED 状态(0xFF)。但是这里考虑了非fds存入数据的情况,比如在非fds标记页面头的位置写入了一些数据。所以这里page_is_erased(p_page_addr) 函数会暴力遍历该页面的每一个字节,只有整个页面完全为空的情况下才会将本页面分配给fds。其实月面头的标记就两个,一个是数据页,一个是交换页,被标记为交换页的页面用于GC缓存。
到这里,页面初始化的操作就完成了,后面还有一些队列的操作就会彻底完成fds的初始化。fds还有很多实现细节后边如果有时间写会有后续更新。