Bluetooth MCU Development Tour: Exploration of Nordic fds File System Source Code

Posted by zander213 on Thu, 08 Aug 2019 14:21:12 +0200

 

Flash is used in the project, and Nordic has a wealth of tools for operating flash, including fstorage and fds, both of which return real operation results asynchronously, where FDS relies on fstorage and is a simple and easy-to-use file system.

Before using fds, we usually need to initialize it, including using the fds_register function to register an event callback to get the result of an asynchronous operation. Then you need to use the fds_init function to initialize, after which you can do a variety of file operations.

To isolate from physical addresses, FDS uses the concept of virtual pages. Before using fds, you need to configure how much space a virtual page allocates, which must be an integral multiple of the space of a physical page (usually 4096 byte). Then you need to set how many virtual pages are used in total, and the number of virtual pages available is reduced by one because one of them is used as a GC cache.

Traceability begins here with fds_init.

In order to prevent repeated initialization, fds_init first checks whether it has been initialized and returns directly to FDS_SUCCESS if initialization has been done. It can be seen from this that the fds_init function can be called repeatedly to ensure that its module is initialized correctly when using fds.

fds relies on fstorage, and any fds operation will eventually be transformed into fstorage operation, where a handle of fstorage is needed first. _

NRF_FSTORAGE_DEF(nrf_fstorage_t m_fs) =
{
    // The flash area boundaries are set in fds_init().
    .evt_handler = fs_event_handler,
};

Where fs_event_handler is an asynchronous callback function of 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;
    }

The next operation is flash_subsystem_init, which is literally impossible to understand. Looking in, this function has two main operations, one is flash_bounds_set, and the other is nrf_fstorage_init.

Because fds relies on fstorage, the initialization of fds is necessary.

Then you can see from the function name that flash_bounds_set is to determine how to allocate the real page address and boundary of fds, and the implicit information here is that the real page allocated by FDS is a continuous area.

The main contents of flash_bounds_set function are as follows:

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 here is a word unit, occupying 4 bytes. Flash_size is the real size of flash space occupied by fds. Here, the size of flash_size is determined by the following mapping relationship:

// 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)

Then the starting address of the real page is calculated indirectly by the address at the end of flash. The flash_end_addr function is used to get the address at the end of the flash. If the flash contains bootloader, the address is the starting address of the 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);
}

The data sheet does not specify what values NRF_UICR-> NRFFW[0] holds here, but since it is the domain of uicr, it should be possible to store whatever values. But this domain can't be searched in sdk. Then don't go to bootloader's source code to search, even not! In line with the principle of breaking the casserole to the end, we should not relearnestly study the data sheet, noting that the base address of UICR is 0x100010001000, and the offset of NRFFW[0] is 0x14. In response, when we studied bootloader source code, there was a define, NRF_UICR_BOOTLOADER_START_ADDRESS, in nrf_bootloader_info.h. View Definitions:

/**
 * @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)

Ha, it's clear that NRF_UICR-> NRFFW [0] points to the starting address of bootloader, which is initialized by bootloader. As long as the burned file contains bootloader, there will be a value not less than 0xFFFFFFFF FFFFFFFFFFFF.

Next, the operation queue is initialized, and Nordic provides atfifo for everyone to use. There are many predefined macros in nordic's sdk, which is very convenient to use. In one sentence, the initialization of queues is accomplished.

Then, the last step of initialization is to initialize the page. There is a need to map virtual pages page by page to real physical pages. In the allocated virtual pages, there is a page that needs to be cached in GC.

The complete page initialization function is as follows:

// 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;
}

There's nothing to say here. Of course, initializing flash needs to be traversed daily. Note that the address of each page is (uint32_t*) m_fs. start_addr+i*1024. Note that the start_addr is converted to the pointer type of uint32_t, so for each additional 1024, the actual address increment is 1024*4.

uint32_t        const * const p_page_addr = (uint32_t*)m_fs.start_addr + (i * FDS_PAGE_SIZE);

Then, get the status of each page, and you can see that fds will mark the status of each page on its default address, just like every record of fds has a header, it can be understood that every page has a header-like thing. There is nothing to say about the page_indentify function, which is to query the tag value on a fixed cheap address corresponding to p_page_addr.

fds_page_type_t const         page_type   = page_identify(p_page_addr);

Normally, flash is erased completely before burning the program, so all pages here are in the FDS_PAGE_UNDEFINED state (0xFF). But here we consider the situation of non-fds storing data, such as writing some data in the position of non-fds markup page header. So here the page_is_erased(p_page_addr) function brutally traverses every byte of the page and only assigns this page to FDS if the entire page is completely empty. In fact, there are two markers on the face of the moon, one is the data page and the other is the exchange page. The page marked as the exchange page is used for GC caching.

At this point, the page initialization operation is completed, and some queue operations will complete the fds initialization completely. fds also has a lot of implementation details that will be updated later if there is time to write.

 

Topics: SDK less