This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

nRF52840 RAM power off - How to know which banks/sections are in use

Hello -

We are working with the nRF52840 and SDK v17.0.0 and tuning our power consumption during System ON sleep. We understand how to wakeup the device using the RTC and also how to selectively power off RAM banks and sections. We know that powered off RAM sections lose their contents.

In order to reduce power consumption in System ON sleep, we'd like to power off unused RAM. What we don't understand is how to know at runtime what areas of RAM are currently in use and hence should not be powered off across System ON sleep. We use malloc() for all our dynamic allocations. The SoftDevice and SDK also allocate memory. The applications we support vary in complexity, so it is likely that we'll need some type of general solution. We are also using FreeRTOS with tickless idle enabled, so the System ON sleep will be entered from within the tickless idle function.

At any given time during execution, how can we know which RAM sections can be safely powered off? Are there memory allocators in the SDK and/or linker script features (aside from a .noinit section) that can be leveraged?

Regards,

Brian

  • Hi Brian,

    To control RAM power with section granularity, you need an allocator that is able to work with section-sized RAM blocks. Take a look at balloc library in SDK - it does (almost) exactly what you're looking for.

    Create a linker section that starts at RAM section boundary, then change NRF_BALLOC_DBG_DEF macro in balloc.h to allocate _nrf_balloc_pool_mem items in that section. You also will need a small intermediate layer that will create a map of allocated blocks. Note that in debug mode each block will have a debug header, thus it will not be section-sized, so you need to disable RAM power control in debug mode.

    SoftDevice uses a static linear memory region from the start of RAM. When SoftDevice is disabled, you can safely turn its memory off, except of first section (0x20000000).

  • Dmitry -

    Thank you much for the quick follow-up. I've read through the nrf_balloc source and header files and I understand how such an allocator can help to ensure that allocations are kept in known RAM areas. I do have a few follow-up questions for you before diving in here:

    1) I know how to create linker sections, but it isn't clear to me how to modify the NRF_BALLOC_DBG_DEF macro to allocate "_nrf_balloc_pool_mem" items there. I think that the .p_stack_base needs to point at the RAM section boundary. Any code snippets could surely help if possible. FWIW, we are building with GCC.

    2) Should we be concerned about any parts of the SDK calling malloc() directly and hence bypassing this mechanism? I noticed for example that some of the crypto functions can be configured to use one of several allocators.

    3) You had mentioned the need for a small intermediate layer to map allocated blocks. I assume this refers to the sub-allocations we will need to manage? For example, multiple calls to malloc(10) call can be satisfied by a single block.

    4) Considering other potentially simpler options, I am wondering if the built-in malloc() implementation allocates always by starting on one side of RAM. If that is the case, and if we know a particular app won't allocate more than X bytes, we could potentially power off the RAM sections on the other side. Not perfect though, depending on how the built-in malloc() coalesces blocks that are freed.

    Regards,

    Brian

  • 1) I know how to create linker sections, but it isn't clear to me how to modify the NRF_BALLOC_DBG_DEF macro to allocate "_nrf_balloc_pool_mem" items there. I think that the .p_stack_base needs to point at the RAM section boundary. Any code snippets could surely help if possible. FWIW, we are building with GCC.

    Just  add an attribute to a pool definition:

    static uint32_t CONCAT_2(_name,_nrf_balloc_pool_mem) \
    
     [NRF_BALLOC_BLOCK_SIZE(_element_size, _debug_flags) * (_pool_size) / sizeof(uint32_t)] __attribute__((section(“myPowerOffPool”)));

    Concerning stack – it’s not necessary to place its base at section boundary, because you’ll probably enter WFE with not an empty stack. If you wish to save power also for unused stack space, keep in mind that CPU will wake up by interrupt, and an interrupt handler will fill the stack space before you power on RAM.  As you’re using RTOS with different stacks for each task, I think it’s not worth an effort.

    2) Should we be concerned about any parts of the SDK calling malloc() directly and hence bypassing this mechanism? I noticed for example that some of the crypto functions can be configured to use one of several allocators.

    This method is good for application code that allocates large blocks of memory (for example, I/O buffers). The use of sub-allocators within these blocks is also possible in a scenario like “allocate memory – work with it –release it completely”. For ordinary malloc’s called from SDK, leave them in main heap in retained memory – there is too much work to save 100-200 nA.

    3) You had mentioned the need for a small intermediate layer to map allocated blocks. I assume this refers to the sub-allocations we will need to manage? For example, multiple calls to malloc(10) call can be satisfied by a single block

    What I mean – you need a map of allocated blocks to quickly find out what memory can be powered off. Sure, you can take this info from balloc internals, but this is not a good way.

    4) Considering other potentially simpler options, I am wondering if the built-in malloc() implementation allocates always by starting on one side of RAM. If that is the case, and if we know a particular app won't allocate more than X bytes, we could potentially power off the RAM sections on the other side. Not perfect though, depending on how the built-in malloc() coalesces blocks that are freed

    Most allocators start from the low side of memory. The problem is that memory will always be fragmented – an allocated block cannot be moved because an application may have pointers to the memory inside a block that allocator is not aware of. Of course, you can take some simple open-source allocator and modify it to keep the start address of unused memory - a better than nothing solution.

  • Dmitry -

    Thank you again for your detailed answers and clarifications. It makes sense that balloc is used for I/O applications that require same sized buffer pools. I see that some of the SDK uses balloc for these types of scenarios.

    If we use balloc for application allocations, with linker section boundaries, we know exactly where that memory is being allocated. That is good. I'd like to confirm that the linker attribute section also prevent ordinary calls to malloc() from being satisfied from that section. I've copied our main linker script MEMORY definition below. I assume that the balloc section(s) would appear where RAM currently resides, and RAM would be pushed lower? This also means that we'd need to define how much dynamic allocation memory we require at build time I presume:

    MEMORY
    {
      FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xcb000
      PREFERENCE (rx) : ORIGIN = 0xF2000, LENGTH = 0x2000
      RAM (rwx) :  ORIGIN = 0x20008000, LENGTH = 0x20040000-0x20008000
      NOINIT (rwx) :  ORIGIN = 0x2003fb3c, LENGTH = 0x100
    }
    

    Considering the potentially simpler approach, since our allocations tend to be more arbitrary both in size and use, we can override our malloc() function to track the start address of unused memory. But wouldn't we still have the issue where we don't know where  in the main heap SDK malloc() and balloc() calls land and hence we could potentially power off memory in use? I suppose patching the nrf_balloc.h file does handle the SDK cases, assuming none of the pre-built libraries call balloc/malloc.

    Regards,

    Brian

  • GCC malloc uses memory between __HeapBase and __HeapLimit defined in startup file, it will not use any other memory. For third-party malloc implementations, the memory is usually defined as an array inside malloc source, or as direct address and size of memory area.

    But wouldn't we still have the issue where we don't know where  in the main heap SDK malloc() and balloc() calls land and hence we could potentially power off memory in use?

    You can expose your functions with tracking instead of original API, then SDK will use your layer.

Related