STM32CubeWBA: Memory management

1. Introduction

An enhanced version of dynamic allocation is available with the STM32WBA: the Advanced Memory Manager (AMM). It eases dynamic allocations in a multitask/shared environment without adding much complexity.

The new features provided by AMM are the following:

  • Support of several Virtual Memories
  • Minimum size warranty per Virtual Memory (Quality Of Service - QOS)
  • Callback in case of memory allocation failure

2. Features

The Advanced Memory Manager comes in addition to a Basic Memory Manager (BMM), meaning here anything capable of providing basic memory allocation features such as Alloc and Free, and adding some new capabilities on top of it.

Advanced Memory Manager overview
Connectivity AMM Overview.png

2.1. Basic Memory Manager and Heap

The Basic Memory Manager (BMM)
Anything that is capable to allocate/free memory. For instance, standard malloc() / free() or custom static pool API, etc.
Shall at least support the capability to merge into one single memory two continuous memories that have been freed (coalescence)
Shall be provided by the user before any use of the AMM via the registration function
The Heap
Can either be a dedicated memory provided by the application, or the legacy default Heap provided by most applications at link time
Its size shall be at least equal to the sum of all Virtual Memory sizes

2.2. Virtual Memories and Shared Pool

In a concurrent execution application, dynamic allocation can encounter some issues with heap sharing. For instance, a task may allocate a major part of the available heap leading to allocation failure for all other tasks requesting memory. To avoid this kind of issue, the Advanced Memory Manager introduces the concept of virtual memories and shared pool.

AMM Heap overview
Connectivity AMM Heap.png
The Virtual memory
Dedicated user memory pool, that is, specific VM IDs.
Ensures the memory amount needed for minimal execution: Heap resource is only available for this Virtual Memory ID.
The Shared pool
Mutual memory pool, meaning that it can be used by any Virtual Memory ID.
Provides memory for an optimal execution.
Its size corresponds to the BMM pool size minus the space required for the Virtual memories.

For every operation, the user must provide a Virtual Memory ID. This helps determine its identity and allows the AMM to identify how much space is remaining in this Virtual Memory.
However, once the Virtual Memory quota is consumed (Sum of all the user allocations = Virtual Memory size), the user can still pursue with allocation requests. Those requests then take place in the Shared Pool area and are, therefore, fully dependent of the others users' sympathy, meaning that the request may or may not succeed depending on the availability of resources.

2.3. Retry callback

During program execution, users may encounter some memory allocation failure in that there is not enough memory available. Instead of setting up a polling mechanism, in an allocation request, the AMM offers the possibility to register a callback.
This callback informs the requester (in an asynchronous way) that some space has been freed, either in the shared pool or in its dedicated virtual pool, and a new allocation request can be submitted.
In some cases, the memory can be freed from a different context than the one it has been allocated from. The callback should not implement any active code and should be used only to set up a new allocation request from the main context.

2.3.1. Single callback case

Connectivity AMM Callback.png

2.3.2. Multiple callback case

Connectivity AMM MultiCallback Part1.png
Connectivity AMM MultiCallback Part2.png

3. Interface

3.1. AMM error codes

The error codes for AMM are listed below:

AMM_Function_Error_t
Error code Description
AMM_ERROR_OK No error code
AMM_ERROR_NOK Error that occurred before any check
AMM_ERROR_BAD_POINTER Bad pointer error
AMM_ERROR_BAD_POOL_CONFIG Bad pool configuration error
AMM_ERROR_BAD_VIRTUAL_CONFIG Bad virtual memory configuration error
AMM_ERROR_BAD_BMM_REGISTRATION Bad BMM registration error
AMM_ERROR_BAD_BMM_ALLOCATION Bad BMM allocation error
AMM_ERROR_NOT_ALIGNED Not aligned address error
AMM_ERROR_OUT_OF_RANGE Out of range error
AMM_ERROR_UNKNOWN_ID Unknown virtual memory manager ID error
AMM_ERROR_BAD_ALLOCATION_SIZE Bad allocation size error
AMM_ERROR_ALLOCATION_FAILED Allocation failed error
AMM_ERROR_ALREADY_INIT Already initialized AMM error
AMM_ERROR_NOT_INIT Not initialized AMM error

3.2. AMM functions

The available functions for AMM are listed below:

AMM_Init

Description

Initialize the Advanced Memory Manager Pool.
Syntax
AMM_Function_Error_t AMM_Init (const AMM_InitParameters_t * const p_InitParams);
Parameters
[in] p_InitParams
Type: const AMM_InitParameters_t * const
Description: Struct of init parameters
Return Value
AMM_Function_Error_t
AMM_DeInit
Description
DeInitialize the Advance Memory Manager Pool
Syntax
AMM_Function_Error_t AMM_DeInit (void);
Parameters
None
Return Value
AMM_Function_Error_t
AMM_Alloc
Description
Allocate a buffer from the Advance Memory Manager Pool.
Syntax
AMM_Function_Error_t AMM_Alloc (const uint8_t VirtualMemoryId,
                                const uint32_t BufferSize,
                                uint32_t ** pp_AllocBuffer,
                                AMM_VirtualMemoryCallbackFunction_t * const p_CallBackFunction);
Parameters
[in] VirtualMemoryId
Type: const uint8_t
Description: Virtual Memory Identifier - AMM_NO_VIRTUAL_ID Can be used.
[in] BufferSize
Type: const uint32_t
Description: Size of the pool with a multiple of 32bits.
ie: BufferSize = 4; TotalSize = BufferSize * 32bits = 4 * 32bits = 128 bits
[out] pp_AllocBuffer
Type: uint32_t **
Description: Pointer onto the allocated buffer
[in] p_CallBackFunction
Type: AMM_VirtualMemoryCallbackFunction_t * const
Description: Pointer onto the Callback to call in case of failure - Can be NULL.
Return Value
AMM_Function_Error_t
AMM_Free
Description
Free the allocated buffer from the Advance Memory Manager Pool
Syntax
AMM_Function_Error_t AMM_Free (uint32_t * const p_BufferAddr);
Parameters
[in] p_BufferAddr
Type: uint32_t * const
Description: Address of the buffer to free
Return Value
AMM_Function_Error_t
AMM_BackgroundProcess
Description
Background routine that aims to call registered callbacks for an allocation retry
Syntax
void AMM_BackgroundProcess (void);
Parameters
None
Return Value
None
AMM_RegisterBasicMemoryManager
Description
Register the Basic Memory Manager functions to use
Syntax
void AMM_RegisterBasicMemoryManager (AMM_BasicMemoryManagerFunctions_t * const p_BasicMemoryManagerFunctions);
Parameters
[in] p_BasicMemoryManagerFunctions
Type: AMM_BasicMemoryManagerFunctions_t * const
Description: Address of the basic memory manager functions
Return Value
None
AMM_ProcessRequest
Description
Request to application for a Background process run
Syntax
void AMM_ProcessRequest (void);
Parameters
None
Return Value
None


Info white.png Information
For more information about the Background process system, visit the STM32CubeWBA System modules article

4. How to

The following chapters explain how to configure and use the Advanced Memory Manager.

4.1. Initialize the AMM

Before use, AMM needs to be setup and there are a few steps that must be followed.

4.1.1. Basic Memory Manager selection

First, determine the Basic Memory Manager to be used.
As said above, it can be anything capable of allocation and free. The only mandatory point is coalescence.
In the following examples, the selected BMM is the Memory manager utility, which is already available in the current release under the memory manager folder: UTIL_MM_XXX ().

BMM selection
Connectivity BMM repo.png

4.1.2. Basic Memory Manager registration

Once the BMM is identified, register its functions in the AMM. To do so, create function wrappers, since the selected BMM does not always fully match the expected function of the AMM register function:

Definition:
static void AMM_WrapperInit (uint32_t * const p_PoolAddr, const uint32_t PoolSize);

static uint32_t * AMM_WrapperAllocate (const uint32_t BufferSize);

static void AMM_WrapperFree (uint32_t * const p_BufferAddr);
Implementation:
static void AMM_WrapperInit (uint32_t * const p_PoolAddr, const uint32_t PoolSize)
{
  UTIL_MM_Init ((uint8_t *)p_PoolAddr, ((size_t)PoolSize * sizeof(uint32_t)));
}

static uint32_t * AMM_WrapperAllocate (const uint32_t BufferSize)
{
  return (uint32_t *)UTIL_MM_GetBuffer (((size_t)BufferSize * sizeof(uint32_t)));
}

static void AMM_WrapperFree (uint32_t * const p_BufferAddr)
{
  UTIL_MM_ReleaseBuffer ((void *)p_BufferAddr);
}
Info white.png Information
Beware that operations with AMM are achieved on a 32 bits size reference


Once wrappers are defined, the register function is the next function to implement. Note that its definition is already done in the AMM header file. The AMM calls this function at its initialization, so it is necessary to provide the pointer to the newly declared wrappers.

Implementation:
void AMM_RegisterBasicMemoryManager (AMM_BasicMemoryManagerFunctions_t * const p_BasicMemoryManagerFunctions)
{
  /* Fulfill the function handle */
  p_BasicMemoryManagerFunctions->Init = AMM_WrapperInit;
  p_BasicMemoryManagerFunctions->Allocate = AMM_WrapperAllocate;
  p_BasicMemoryManagerFunctions->Free = AMM_WrapperFree;
}

After completing these steps, the BMM must be linked to the AMM and must be fully operational. The next step is the configuration of the AMM.

Info white.png Information
Refer to the function section for more information on the register function.

4.1.3. Configure and Initialize the AMM

The Advanced memory manager configuration is done at initialization and the parameters are the following:

typedef struct AMM_InitParameters
{
  /* Address of the pool of memory to manage */
  uint32_t * p_PoolAddr;
  /* Size of the pool with a multiple of 32bits.

     ie: PoolSize = 4; TotalSize = PoolSize * 32bits
                                 = 4 * 32bits
                                 = 128 bits */
  uint32_t PoolSize;
  /* Number of Virtual Memory to create */
  uint32_t VirtualMemoryNumber;
  /* List of the Virtual Memory configurations */
  AMM_VirtualMemoryConfig_t a_VirtualMemoryConfigList[];
}AMM_InitParameters_t;

The AMM configuration is a two step procedure:

  1. Establish the Virtual Memory + Shared pool configurations (number of VMs, size, etc.).
  2. Adapt the pool size and location of the AMM pool according to the configuration previously determined.
4.1.3.1. Virtual Memory and Shared pool configurations
The Virtual Memory and the Shared pool configurations are the backbone of the AMM configuration. Both must be determined along with their characteristics to continue with the AMM initialization.
Virtual Memories configurations
Connectivity AMM VMs.png
There are 3 characteristics to identify: the number of VMs', the proper size of each VM, and their IDs:
The number of VMs
To determine the number of needed Virtual Memories, identify the number of processes that need their own heap for nominal operating. Consider one Virtual Memory for each of these processes.
The size of each VM
The size of each Virtual Memory is determined by the process needs. Each Virtual Memory must be sized to the process nominal heap value. Note that the Virtual Memory size is set on a 32 bits basis.
The ID of each VM
For each VM, define a unique ID. This is used to access the proper Virtual Memory during memory operation.
Those characteristics can afterwards be fulfilled in the AMM initialization parameter's structure.
For the number of Virtual Memories:
  /* Number of Virtual Memory to create */
  uint32_t VirtualMemoryNumber;
For the proper configuration of each Virtual Memory, a structure like the one below must be instanced:
typedef struct AMM_VirtualMemoryConfig
{
  /* ID of the Virtual Memory */
  uint8_t Id;
  /* Size of the Virtual Memory buffer with a multiple of 32 bits.

     ie: BufferSize = 4; TotalSize = BufferSize * 32 bits
                                   = 4 * 32 bits
                                   = 128 bits */
  uint32_t BufferSize;
}AMM_VirtualMemoryConfig_t;
and provided to the AMM Initialization parameter:
  /* List of the Virtual Memory configurations */
  AMM_VirtualMemoryConfig_t a_VirtualMemoryConfigList[];
Shared Pool configuration
Connectivity AMM SharedPool.png
Shared Pool size
Regarding the Shared Pool size, its size is determined by the user. The shared pool is designed to allow an optimal execution of each process that occasionally needs some heap to perform better.
The Shared Pool configuration is not a proper parameter of AMM initialization. However, it must be defined to determine the whole AMM required pool space. This concept also eases the AMM comprehension and enhances its configuration, leading to a better optimization of the required space.
4.1.3.2. AMM pool configuration
As seen above, the AMM pool size is mostly dependent on the VM and Shared Pool configurations.
Nonetheless, there is also a management part that needs to be considered in the pool size computation.
AMM management part
For operating purposes, the AMM allocates some of the heap provided to it. For each VM, it creates an info element.
Thus, at the initialization, the user must consider more space for the pool.
Considering all these elements, the AMM pool size representation is as follows:
AMM pool size representation
Connectivity AMM PoolSize.png
Pool Size computation
With all the points listed above, compute the adequate Pool Size with the following formula:
#define CFG_AMM_POOL_SIZE = (CFG_AMM_NUMBER_OF_VM * AMM_VIRTUAL_INFO_ELEMENT_SIZE) + \
                             CFG_AMM_SHARED_POOL_SIZE + \
                             CFG_AMM_SUM_OF_VMS_SIZE
and afterward, fulfill the AMM init parameter, keeping in mind that the size is on a 32 bits basis:
  /* Size of the pool with a multiple of 32 bits.

     ie: PoolSize = 4; TotalSize = PoolSize * 32 bits
                                 = 4 * 32 bits
                                 = 128 bits */
  uint32_t PoolSize;
Start address
The start address can either be a heap located address or a static buffer used as a memory pool.
For instance, use a static allocated buffer:
static uint32_t AMM_Pool[CFG_AMM_POOL_SIZE];
Info white.png Information
Refer to the function section for more information on the initialization function.
4.1.3.3. Concrete example

A concrete example of this whole configuration can be found in any of the available STM32WBA example applications.

Definitions for AMM constants are present in the app_conf.h file:

#define CFG_MM_POOL_SIZE                          (4000)
#define CFG_AMM_VIRTUAL_MEMORY_NUMBER             (2u)
#define CFG_AMM_VIRTUAL_STACK_BLE                   (1U)
#define CFG_AMM_VIRTUAL_STACK_BLE_BUFFER_SIZE       (400U)
#define CFG_AMM_VIRTUAL_APP_BLE                   (2U)
#define CFG_AMM_VIRTUAL_APP_BLE_BUFFER_SIZE       (200U)
#define CFG_AMM_POOL_SIZE                      DIVC(CFG_MM_POOL_SIZE, sizeof (uint32_t)) \
                                               + (AMM_VIRTUAL_INFO_ELEMENT_SIZE * CFG_AMM_VIRTUAL_MEMORY_NUMBER)

The whole AMM init takes place in the app_entry.c file and under different parts:

Initialization parameters
static uint32_t AMM_Pool[CFG_AMM_POOL_SIZE];
static AMM_InitParameters_t ammInitConfig =
{
  .p_PoolAddr = AMM_Pool,
  .PoolSize = CFG_AMM_POOL_SIZE,
  .VirtualMemoryNumber = CFG_AMM_VIRTUAL_MEMORY_NUMBER,
  .a_VirtualMemoryConfigList =
  {
    /* Virtual Memory #1 */
    {
      .Id = CFG_AMM_VIRTUAL_STACK_BLE,
      .BufferSize = CFG_AMM_VIRTUAL_STACK_BLE_BUFFER_SIZE
    },
    /* Virtual Memory #2 */
    {
      .Id = CFG_AMM_VIRTUAL_APP_BLE,
      .BufferSize = CFG_AMM_VIRTUAL_APP_BLE_BUFFER_SIZE
    },
  }
};
Initialization and Background process register
uint32_t MX_APPE_Init(void *p_param)
{
  ...

  /* Initialize the Advance Memory Manager */
  AMM_Init (&ammInitConfig);

  /* Register the AMM background task */
  UTIL_SEQ_RegTask( 1U << CFG_TASK_AMM_BCKGND, UTIL_SEQ_RFU, AMM_BackgroundProcess);

  ...

}
AMM wrapper functions and BMM register function
static void AMM_WrapperInit (uint32_t * const p_PoolAddr, const uint32_t PoolSize)
{
  UTIL_MM_Init ((uint8_t *)p_PoolAddr, ((size_t)PoolSize * sizeof(uint32_t)));
}

static uint32_t * AMM_WrapperAllocate (const uint32_t BufferSize)
{
  return (uint32_t *)UTIL_MM_GetBuffer (((size_t)BufferSize * sizeof(uint32_t)));
}

static void AMM_WrapperFree (uint32_t * const p_BufferAddr)
{
  UTIL_MM_ReleaseBuffer ((void *)p_BufferAddr);
}

void AMM_RegisterBasicMemoryManager (AMM_BasicMemoryManagerFunctions_t * const p_BasicMemoryManagerFunctions)
{
  /* Fulfill the function handle */
  p_BasicMemoryManagerFunctions->Init = AMM_WrapperInit;
  p_BasicMemoryManagerFunctions->Allocate = AMM_WrapperAllocate;
  p_BasicMemoryManagerFunctions->Free = AMM_WrapperFree;
}
AMM process request implementation for Background process method
void AMM_ProcessRequest (void)
{
  /* Ask for AMM background task scheduling */
  UTIL_SEQ_SetTask(1U << CFG_TASK_AMM_BCKGND, CFG_SCH_PRIO_0);
}

4.2. Allocate memory

The allocation function differs slightly from the standard malloc. It adds:

  • A function error code that informs the user on the state of the operation.
  • The possibility to register a callback function in case of allocation failure.

The function can be called with or without a Virtual Memory ID and with or without the registration of a callback. The following examples highlight these facts.

Allocation with a Virtual Memory ID and with a callback registration
This is the typical way of calling the allocation function. In this way, the allocation takes places in the Virtual Memory, or Shared Pool if the first one is full, and the user registers the callback in case of allocation failure resulting from lack of available space. This one is invoked once a space liberation occurs.
Example:
uint32_t * p_AllocBuffer = NULL;

uint32_t funcReturn = AMM_ERROR_NOK;

VirtualMemoryId = 0x1;
BufferSize = 0xA;

funcReturn = AMM_Alloc (VirtualMemoryId,
                        BufferSize,
                        &p_AllocBuffer,
                        &CallBackElt);

if (funcReturn == AMM_ERROR_OK)
{
  /* Do stuff with the brand new buffer */
  ...
}
else if (funcReturn == AMM_XXX)
{
  /* Manage the AMM_XXX error */
  ...
}
else
{
  ...
}
with the callback parameter resembling:
static AMM_VirtualMemoryCallbackFunction_t CallBackElt = 
{
  .Header =
  {
    .next = NULL,
    .prev = NULL
  },
  .Callback = Callback
};
and a callback function implemented as:
void Callback (void)
{  
  /* Set event to notify that a new allocation can be requested */
  UTIL_SEQ_SetEvt (1u << AMM_CALLBACK_EVT_BM);
}
Allocation without a Virtual Memory ID and without a callback registration
This is most straight forward way, with no ID and no callback registration. The allocation takes place in the Shared Pool and, in case of failure, the user needs to execute a new request.
Example:
uint32_t * p_AllocBuffer = NULL;

uint32_t funcReturn = AMM_ERROR_NOK;

BufferSize = 0xA;

funcReturn = AMM_Alloc (AMM_NO_VIRTUAL_ID,
                        BufferSize,
                        &p_AllocBuffer,
                        NULL);

if (funcReturn == AMM_ERROR_OK)
{
  /* Do stuff with the brand new buffer */
  ...
}
else if (funcReturn == AMM_XXX)
{
  /* Manage the AMM_XXX error */
  ...
}
else
{
  ...
}

4.3. Free memory

The free operation is rather simple, the only difference with the one from the standard lib is that the AMM_Free has a returned error code that can be analyzed.
Example:

uint32_t funcReturn = AMM_ERROR_NOK;

funcReturn = AMM_Free (p_AllocatedBuffer);

if (funcReturn == AMM_ERROR_OK)
{
  /* Do stuff with the brand new buffer */
  ...
}
else if (funcReturn == AMM_XXX)
{
  /* Manage the AMM_XXX error */
  ...
}
else
{
  ...
}