Memory Management

The most common problems associated with tasks are stack overflows. This occurs, when a task tries to allocate more data than it is allowed to. A stack overflow is an application-breaking issue, and it is considerarbly difficult to analyze why stack overflow is happening, especially in large projects. This chapter gives an introduction to how the RAM and the flash memory are used on the XDK, and especially the role of FreeRTOS and the allocation of tasks.

  1. Flash Memory
    1. Full Code Example
  2. Random Access Memory

Flash Memory

The XDK’s microcontroller is equipped with 1MB (= 1024kB) of flash memory. Flash memory is not lost after restarting the XDK, which is why the flash memory is mainly used to store the bootloader and the application code.

The bootloader is the first piece of code that runs after starting the XDK. Its responsibility lies in setting the XDK into a proper state, before starting the actual application. The bootloader is writeprotected, and is the only piece of software on the XDK that shall not be modified by the XDK user.

The application code is runnable code, which had been compiled and flashed onto the XDK’s flash memory, using the XDK-Workbench.

Depending on the bootloader version, the flash memory is partitioned differently. Picture first shows the bootloader flash memory partitioning for version 0.0.11 and below, and Picture second shows the partitioning for versions 1.0.0 and 1.1.0.

Image

Image

Note that in versions 1.0.0. and 1.1.0 the space marked as reserved will be used for the application, if the application is flashed using FOTA (=Firmware Over The Air). But, if the application is flashed over USB, bootloader and application use a total of 728kB of memory size, which leaves 296kB of memory which is essentially unused.

In either case, the memory can be written on by the XDK user, which allows for data to be kept even after the XDK had been turned off.

There is an API that allows writing onto the flash memory. It is made available by including the headerfile BCDS_MCU_Flash.h, as shown in the following code-snippet

// include the BCDS_MCU_FLASH module
  #include "BCDS_MCU_Flash.h"

The function in the code below shows how to read four bytes from the flash memory, starting at the first location marked as reserved, which is 0x000B6000 in hexadecimal system, or 745472 in decimal system. The API’s function MCU_Flash_Read() requires a pointer to the address. In this case, the address is stored in a variable of type uint32_t, holding the value 0x000B6000. Furthermore, a buffer and the length to read are used as input. For visualization, the four bytes are printed to the console.

void read(void) {
    uint32_t readAddress = 0x000B600;
    uint8_t buffer[4];
    uint32_t length = sizeof(buffer);
    MCU_Flash_Read((uint8_t *) readAddress, buffer, length);
    printf("Memory: %d, %d, %d, %d\n\r", buffer[0], buffer[1], buffer[2], buffer[3]);
}

Writing is done quite similarly. The next code shows how to write four bytes to the flash memory, starting at the first location marked as reserved which is 0x000B6000 in hexadecimal system, or 745472 in decimal system. The API’s function MCU_Flash_Write() requires a pointer to the address. In this case, the address is stored in a variable of type uint32_t, holding the value 0x000B6000. Furthermore, a buffer and the length to read are used as input. For visualization, the four bytes are read from the flash memory afterwards and printed to the console.

void write(void) {
    uint32_t writeAddress = 745472;
    uint8_t buffer[4] = {1,2,4,8};
    uint32_t length = sizeof(buffer);
    MCU_Flash_Write((uint8_t *) writeAddress, buffer, length);
    memset(buffer, 0, 4);
    MCU_Flash_Read((uint8_t *) writeAddress, buffer, length);
    printf("Memory: %d, %d, %d, %d\n\r", buffer[0], buffer[1], buffer[2], buffer[3]);
}

There are a few things to keep in mind, when writing to the flash memory. The first being that the application code inside the flash memory can actually be overwritten, because only the bootloader is write-protected. Overwriting application code will lead to unexpected behavior and should be avoided at all cost. Even writing to the bootloader segment should be avoided.

Secondly, although the flash memory is persistent through restarting the XDK, it will not persist through flashing a new application. When a new application is flashed, every unused bit of the flash memory will be set to 1. Since the flashing-process does not use the reserved segment, the reserved segment will always be cleared.

Full Code Example

Note: The full code example is intended for XDK-Workbench versions 3.4.0 and higher.

/** ************************************************************************* */

/* --------------------------------------------------------------------------- |
 * INCLUDES & DEFINES ******************************************************** |
 * -------------------------------------------------------------------------- */

/* own header files */
#include "XdkAppInfo.h"
#undef BCDS_MODULE_ID  /* Module ID define before including Basics package*/
#define BCDS_MODULE_ID XDK_APP_MODULE_ID_APP_CONTROLLER

/* own header files */
#include "AppController.h"

/* system header files */
#include <stdio.h>

/* additional interface header files */
#include "BCDS_CmdProcessor.h"
#include "XDK_Utils.h"
#include "FreeRTOS.h"
#include "task.h"

// include the BCDS_MCU_FLASH module
#include "BCDS_MCU_Flash.h"

/* --------------------------------------------------------------------------- |
 * HANDLES ******************************************************************* |
 * -------------------------------------------------------------------------- */

static CmdProcessor_T * AppCmdProcessor;

/* --------------------------------------------------------------------------- |
 * VARIABLES ***************************************************************** |
 * -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------- |
 * EXECUTING FUNCTIONS ******************************************************* |
 * -------------------------------------------------------------------------- */

void read(void) {
    uint32_t readAddress = 0x000B600;
    uint8_t buffer[4];
    uint32_t length = sizeof(buffer);
    MCU_Flash_Read((uint8_t *) readAddress, buffer, length);
    printf("Memory: %d, %d, %d, %d\n\r", buffer[0], buffer[1], buffer[2], buffer[3]);
}

void write(void) {
    uint32_t writeAddress = 745472;
    uint8_t buffer[4] = {1,2,4,8};
    uint32_t length = sizeof(buffer);
    MCU_Flash_Write((uint8_t *) writeAddress, buffer, length);
    memset(buffer, 0, 4);
    MCU_Flash_Read((uint8_t *) writeAddress, buffer, length);
    printf("Memory: %d, %d, %d, %d\n\r", buffer[0], buffer[1], buffer[2], buffer[3]);
}

/* --------------------------------------------------------------------------- |
 * BOOTING- AND SETUP FUNCTIONS ********************************************** |
 * -------------------------------------------------------------------------- */

static void AppControllerEnable(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);

    vTaskDelay(UINT32_C(5000));
    read();

    vTaskDelay(UINT32_C(5000));
    write();
}

static void AppControllerSetup(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);
    Retcode_T retcode = RETCODE_OK;

    retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerEnable, NULL, UINT32_C(0));
    if (RETCODE_OK != retcode)
    {
        printf("AppControllerSetup : Failed \r\n");
        Retcode_RaiseError(retcode);
        assert(0); /* To provide LED indication for the user */
    }
}

void AppController_Init(void * cmdProcessorHandle, uint32_t param2)
{
    BCDS_UNUSED(param2);

    Retcode_T retcode = RETCODE_OK;

    if (cmdProcessorHandle == NULL)
    {
        printf("AppController_Init : Command processor handle is NULL \r\n");
        retcode = RETCODE(RETCODE_SEVERITY_ERROR, RETCODE_NULL_POINTER);
    }
    else
    {
        AppCmdProcessor = (CmdProcessor_T *) cmdProcessorHandle;
        retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerSetup, NULL, UINT32_C(0));
    }

    if (RETCODE_OK != retcode)
    {
        Retcode_RaiseError(retcode);
        assert(0);
    }
}

/** ************************************************************************* */

Random Access Memory

The XDK is equipped with 128kB of RAM, which is used by the application at runtime. It is mainly composed of the program stack, used and managed by the MCU, and the heap, used and managed by FreeRTOS. The stack has its base address at the last address of the RAM, while the heap has its base at zero. In a sense, the stack is growing downwards and the heap is growing upwards.

The key difference between the stack and the heap is that the heap is actually fixed in size, while the stack can grow dynamically. That means, allocating anything on the heap that exceeds the remaining available space of the heap is not possible. In contrast, if the stack grows too large, the stack and the heap may still collide.

The heap can be configured in the header-file FreeRTOSConfig.h located in the SDK at:

xdk110 > Common > config > AmazonFreeRTOS > FreeRTOS

The original values should not be overwritten without full knowledge of their implications. For more information, please refer to the FreeRTOS Configuration page.

Inside the header-file in line 109 configTOTAL_HEAP_SIZE is defined as 61kB. This heap-size accommodates tasks, queues, semaphores and other FreeRTOS entities, which are allocated at runtime. The FreeRTOS heap is actually preceeded by the FreeRTOS kernel, which is the core of the operating system, responsible for allocation of memory on the heap, managing tasks, etc. The FreeRTOS kernel has a fixed size, independent of the configured heap-size. If the total heap size is set too high, there will not be enough space for the stack on the RAM, and the application may crash because of a heap and stack collision after a short amount of runtime.

Tasks each have their respective stack. This stack is limited in size, and a task will only be initialized if the heap offers enough space for the chosen stack-size. Otherwise, the function xTaskCreate() will fail. Every task has its entry function, that is used as an input in xTaskCreate(), and every function that is called from within a task (including function calls within the entry function), requires space on the stack. Space is required even if no variables are used within a function. That means recursive function only calling itself over and over again will eventually lead to a stack overflow.

Note: Allocating an array that requires more bytes than the stack can hold in total will also lead to a stack overflow.

The FreeRTOS kernel will prevent any operation on the heap from overflowing its memory, which is why tasks are the safest way to design an application on the XDK.

Note: The function AppController_Init(), which is the entry point of every application, is subsequently called within the context of a task as well.

Visualization of the RAM usage

Image