Skip to content

Mbed Memory Bank Information

Jamie Smith edited this page Oct 8, 2024 · 25 revisions

This page documents a proposal to add a "memory bank information" feature to the build system of Mbed OS Community Edition.

Update Oct 2024: This proposal has been accepted and merged!

1. Goals

The Memory Bank Information feature will provide information about the available flash and RAM banks on an MCU to the application and build system. This will allow various pieces of the Mbed system to have a better picture of where & how much memory is available and do useful things with it.

1.1 Intended Users

  • C/C++ source code: The Mbed library and application code may be interested in knowing how much memory is available and what addresses it is located at. Current examples include:
    • mbed_stats.c, which collects stats about the total memory available on the device
    • FlashIAPBlockDevice, which would highly benefit from a way to know where the flash starts and ends
    • HAL layer code using DMA, such as the Ethernet MAC drivers, which needs to detect if buffers are inside a given RAM bank or not and use that to control cache handling or decide whether to copy them somewhere else
  • memap: This script currently just prints the total RAM and flash usage, but this does not include a percentage of how much of the RAM is actually used. Additionally, it sums all the memory banks together, ignoring the fact that Mbed can only use one contiguous memory bank for static data (dang GNU LD limitations). In both cases, this requires the user to remember annoying details about their specific MCU, like which memory bank Mbed primarily targets and how much space it has.
  • Linker scripts: Mbed OS is chock full of copy-pasted linker scripts. In many cases, the only real differences between them are the sizes of memory banks (if even that -- some are identical!). If the linker scripts had well-defined access to information about a target's memory banks, they could use this info to adjust themselves, to some extent, to the current target being compiled for. This, in turn, would let us significantly cut down on the number of duplicate/near duplicate linker scripts in Mbed.
  • Custom target users: In many cases, Mbed officially supports one specific variant of an MCU with X amount of memory, but someone wants to develop for another variant with Y amount of memory. Currently, this requires editing the linker script, which is a fairly high barrier to entry (I didn't really learn linker scripts until I'd been employed full time as an embedded SW engineer for over a year). Or, you could not do that, and just hope that the memory layout of your new chip is similar enough to work. With a smarter system to declare memory bank information, and a linker script that is set up to take in that information, making these kind of custom targets could be done without modifying any linker scripts!

1.2 Goals of the proposal

  1. Provide memory bank information to Mbed and its tools that conveys:
    1. Where each bank starts and ends
    2. The type of each mem bank (flash vs RAM)
    3. The user identifiable name of each bank (e.g. builtin flash, DTCM, ITCM, etc).
  2. Be compatible, within reason, with C/C++ code and JSON files designed for the Mbed CLI 1 memory bank system.
    1. Naming of #defines should not change, though new ones can be added
    2. Where it makes sense, mbed_app.json properties should work in the same way as Mbed CLI 1.

2. Prior Work

As people with experience with Mbed CLI 1 may remember, Mbed OS actually used to have a feature somewhat similar to this. Mbed CLI 1 had behavior where it would read the CMSIS MCU description file, list out the memory banks as #defines, and then add those to the build. However, this feature was... flawed, for several reasons:

  • It was not very well documented, making it a bit of a mystery where the memory bank defines came from. Like, by default, they came out of the CMSIS MCU descriptions file, but they could also be overridden via undocumented mbed_app.json/targets.json options.
  • It didn't preserve the names of memory banks, losing out on potentially useful information (e.g. which of the memory banks is DTCM and which is general RAM)
  • It was unclear to users and developers if it was supposed to declare the memory that exists on a target, or the memory that Mbed should be using. (as in, for splitting up RAM and flash to share with a bootloader or another core). In practice, this led to different people and targets using it in different ways.

2.1 Prior Behavior

As far as I can tell, the only real place that the Mbed CLI 1 behavior was documented was in the pull request [4] and the issue [3], and by the code itself (here and here).

As best I understand it, this code would:

  • look for the following properties in mbed_app.json: target.mbed_app_start, target.mbed_app_size, target.mbed_rom_start, target.mbed_rom_size, target.mbed_ram_start, target.mbed_ram_size.
  • Also read the memory banks info from the CMSIS MCU descriptions JSON (which is a cache of the data for each MCU downloaded from the CMSIS pack index)
  • Given that the target can have multiple ROM and RAM banks, use some rather shaky name-based logic to determine the "primary" bank for each
  • Override the primary banks' address and size based on the target.[rom/ram]_[start/size] properties
  • Add definitions in the from of MBED_[RAM/ROM][START/SIZE], MBED[RAM/ROM]1_[START/SIZE], MBED_[RAM/ROM]2_[START/SIZE]... for each memory bank, indicating the bank's start address and size (and throwing away the actual name of the memory bank as declared in CMSIS JSON)
  • Also add MBED_APP_[START/SIZE] if target.mbed_app_start and target.mbed_app_size were declared in JSON.

2.2 Mbed CLI 2 Behavior

Mbed CLI 2, which is the basis for Mbed CE's build system, unfortunately didn't seem to understand the nuances of this behavior. They only set it up to handle the case where the attributes are defined directly in the target JSON, not the case where the memory bank info is loaded from the CMSIS MCU description file [1] [5]. They also didn't attempt to handle mbed_app_start and mbed_app_size, which causes issues with certain target definitions that use those attributes (e.g. Arduino boards with a bootloader).

We've only gotten away with this for so long because few places in the code actually make use of the memory defines, and nothing immediately explodes if they aren't defined (e.g. linker scripts will fall back to hardcoded values). However, these defines should be restored in order to bring back functionality that's been lost with Mbed CLI 2!

3. Proposed Mbed CE Behavior.

3.1 Rationale

The Mbed CLI 1 behavior mostly made sense. However, I'd like to make three changes to it.

First of all, Mbed CLI 1 threw away the names of the memory banks, instead putting them in a relatively arbitrary order. These bank names are valuable information, as, within a target family, it's very useful to have a standardized way to know which bank is DTCM, which is ITCM, which is backup SRAM, etc. In the new system, I would like to still provide the old-style numbered definitions, but also provide named definitions, based on the name of each bank in the CMSIS MCU descriptions. This will be much more useful for target code and linker scripts that wants to identify specific banks of memory.

The second change is, the old system made it rather unclear whether attributes like mbed_rom_start/size and mbed_ram_start/size represented the entire physical memory on the device, or a specific area of RAM that the application was supposed to use. I'd like to resolve that ambiguity. In Mbed CE, we will define that mbed_rom_start/size and mbed_ram_start/size always represent the memory banks available on the device, regardless of what Mbed is configured to use. These attributes can be populated through cmsis_mcu_descriptions.json or the target JSON but should not be overridden. In exchange, we will add new targets.json/mbed_app.json attributes which can be used to override the start address and/or size of a named memory bank.

The configured bank addresses and sizes shall be exposed to the application through a new set of defines, similar to the original names but with CONFIGURED in their names. This allows application code to decide for itself whether it wants the actual physical sizes of the memories (e.g. FlashIAPBlockDevice) or the configured sizes (e.g. Mbed Stats).

Lastly, I'd like to have the configuration script dump this information to a JSON file in the build dir. This adds an easy place for other tooling, such as memap, to get information about memory banks.

3.2 Specific Behavior

  1. The configuration script will read the memory bank info from the CMSIS MCU description for the target based on the device_name property in JSON. A warning will be issued if the JSON is missing the device_name property.
  2. It will also read in additional banks from the memory_banks section in the target JSON. The memory_banks section has the same format as the CMSIS memory descriptions (see below for details). This is intended for declaring additional memory banks that are external to the MCU and therefore can't be specified as a property of the CMSIS MCU description.
  3. For each bank, compile definitions will be added, in the form of MBED_[ROM/RAM][[number]/_BANK_name]_[START/SIZE].
    1. For legacy compatibility, "number" in the above definition starts as empty string for the zeroth bank, then increments to 1, 2, etc.
    2. The first listed bank of each type in the MCU description will become bank 0. Note that code should use the bank names rather than assigning any significance to bank 0, but a fair amount of legacy code assumes that bank 0 is the "main" bank and we want to keep compatibility where possible.
  4. Overrides, given through a memory_bank_config section in the targets JSON, are also processed.
    1. Note that banks not defined in JSON are also allowed to be overridden. This is a good way to implement configuration of external memories that are addressable, but are not built into the MCU (and therefore aren't described by the CMSIS mcu descriptions file)
  5. For each bank, a second set of compile definitions will be added including the override information, in the form of MBED_CONFIGURED_[ROM/RAM][number/name]_[START/SIZE].
    1. Note that numbering will be consistent for overridden banks, e.g. MBED_CONFIGURED_RAM1_START is guaranteed to refer to the same memory bank as MBED_RAM1_START.
  6. A target_memory_banks.json file will be written out in the build directory, containing information about each memory bank and any overrides.

NOTE: Separate, but related, functionality is the ability to select which memory banks a target executes out of (e.g. specifying if data and/or stack is stored in external or internal RAM). Currently, this API only allows restricting the size that the application uses in each memory bank. Selection of which memory banks to use for what currently must be done with target-specific options that affect the linker script and/or startup code, and I don't have plans to change this at present.

4. Worked Example

Let's demonstrate how this system could be used to implement multi-core support on the RP2040 (this doesn't currently exist in Mbed, but having a working memory bank info system would make it a lot easier)!

cmsis_mcu_descriptions.json5

The RP2040 is currrently described as:

"RP2040": {
        "memories": {
            "IRAM1": {
                "access": {
                    "execute": false,
                    "peripheral": false,
                    "read": true,
                    "secure": false,
                    "write": true
                },
                "default": true,
                "size": 0x40000, // 256kiB
                "start": 0x20000000,
                "startup": false
            },


            // Scratch banks are commonly used for critical data and functions accessed only by
            // one core (when only one core is accessing the RAM bank, there is no opportunity for stalls).
            "SCRATCH_X": {
                "access": {
                    "execute": false,
                    "peripheral": false,
                    "read": true,
                    "secure": false,
                    "write": true
                },
                "default": false,
                "size": 0x1000, // 4kiB
                "start": 0x20040000,
                "startup": false
            },
            "SCRATCH_Y": {
                "access": {
                    "execute": false,
                    "peripheral": false,
                    "read": true,
                    "secure": false,
                    "write": true
                },
                "default": false,
                "size": 0x1000, // 4kiB
                "start": 0x20041000,
                "startup": false
            }
        },
        "name": "RP2040",
        "processor": {
            "Symmetric": {
                "core": "CortexM0Plus",
                "fpu": "None",
                "mpu": "Present",
                "units": 2
            }
        },
        "vendor": "Raspberry Pi:x"
    }

This doesn't need any changes and can stay as is. Note that in older versions of CMSIS pack descriptions, the memory bank names apparently had to be IRAMx/IROMx, but now they can be anything -- the access field is used to determine if it is ROM or RAM.

Adding Flash Information to RP2040

We would first have to add information about the board flash to the RP2040 target in targets.json:

"RP2040": {
    ...
    "memory_banks:" {
        "QSPI_FLASH": {
                // Mark this memory as flash.
                // Note: Meaning of these values (albeit in XML rather than JSON) is documented here:
                // https://www.keil.com/pack/doc/CMSIS_Dev/Pack/html/pdsc_family_pg.html#element_memory
                // See also here for how they get converted from XML to JSON:
                // https://github.com/pyocd/cmsis-pack-manager/blob/032a73a93e108e1b0e268ea47d92dbe573002846/rust/cmsis-pack/src/pdsc/device.rs#L466
                "access": {
                    "execute": true,
                    "peripheral": false,
                    "read": true,
                    "secure": false,
                    "write": false
                },
                "default": true,
                "startup": true,

                // Configure size and start address in memory
                "size": 0x200000, // 2MiB
                "start": 0x10000000 
            }
        }
    }
}

This bank, plus the ones defined in cmsis_mcu_descriptions.json5, would create the following definitions:

  • MBED_RAM_START=0x20000000, MBED_RAM_SIZE=0x40000, MBED_RAM_BANK_IRAM1_START=0x20000000, MBED_RAM_BANK_IRAM1_SIZE=0x40000 (from the main internal RAM bank)
  • MBED_RAM1_START=0x20040000, MBED_RAM1_SIZE=0x1000, MBED_RAM_BANK_SCRATCH_X_START=0x20040000, MBED_RAM_BANK_SCRATCH_X_SIZE=0x1000 (from scratch X)
  • MBED_RAM2_START=0x20041000, MBED_RAM2_SIZE=0x1000, MBED_RAM_BANK_SCRATCH_Y_START=0x20041000, MBED_RAM_BANK_SCRATCH_Y_SIZE=0x1000 (from scratch Y)

Then, after applying the override, we would get:

  • MBED_CONFIGURED_RAM_START=0x20000000, MBED_CONFIGURED_RAM_SIZE=0x40000, MBED_CONFIGURED_RAM_BANK_IRAM1_START=0x20000000, MBED_CONFIGURED_RAM_BANK_IRAM1_SIZE=0x40000 (from the main internal RAM bank)
  • MBED_CONFIGURED_RAM1_START=0x20040000, MBED_CONFIGURED_RAM1_SIZE=0x1000, MBED_CONFIGURED_RAM_BANK_SCRATCH_X_START=0x20040000, MBED_CONFIGURED_RAM_BANK_SCRATCH_X_SIZE=0x1000 (from scratch X)
  • MBED_CONFIGURED_RAM2_START=0x20041000, MBED_CONFIGURED_RAM2_SIZE=0x1000, MBED_CONFIGURED_RAM_BANK_SCRATCH_Y_START=0x20041000, MBED_CONFIGURED_RAM_BANK_SCRATCH_Y_SIZE=0x1000 (from scratch Y)
  • MBED_CONFIGURED_ROM_START=0x10000000, MBED_CONFIGURED_ROM_SIZE=0x200000, MBED_CONFIGURED_ROM_BANK_QSPI_FLASH_START=0x10000000, MBED_CONFIGURED_ROM_BANK_QSPI_FLASH=0x200000 (from the external QSPI flash)

Splitting the Two Cores

To set up multi-core execution, we would add two new target definitions to Mbed:

"RP2040_CORE_0": {
    "memory_bank_config:" {
        "QSPI_FLASH": {
            "start": 0x10000000,
            "size": 0x100000, // 1MiB
        },
        "IRAM1": {
            "size": 0x20000, // 128kiB
            "start": 0x20000000,
        },
    }
},
"RP2040_CORE_1": {
    "memory_bank_config:" {
        "QSPI_FLASH": {
            "start": 0x10100000,
            "size": 0x100000, // 1MiB
        },
        "IRAM1": {
            "size": 0x20000, // 128kiB
            "start": 0x20020000,
        },
    }
}

These overrides split both the RAM and the flash into two regions of equal size using memory_overrides.

Linker Script

To make use of this memory information, the linker script's definition of the memories would probably end up looking something like this. (see the current version of the file here)

MEMORY
{
    FLASH(rx) : ORIGIN = MBED_CONFIGURED_ROM_START, LENGTH = MBED_CONFIGURED_ROM_SIZE
    RAM(rwx) : ORIGIN =  MBED_CONFIGURED_RAM_START, LENGTH = MBED_CONFIGURED_RAM_SIZE

    /*
     * Store stack in one of the two scratch banks.  If building for core 0, use scratch X, otherwise use scratch Y.
     */
#if MBED_CONFIGURED_ROM_START == MBED_ROM_START
    STACK_RAM(rwx) : ORIGIN = MBED_RAM_BANK_SCRATCH_X_START, LENGTH = MBED_RAM_BANK_SCRATCH_X_SIZE
#else
    STACK_RAM(rwx) : ORIGIN = MBED_RAM_BANK_SCRATCH_Y_START, LENGTH = MBED_RAM_BANK_SCRATCH_Y_SIZE
#endif
}

This linker script uses several of the new features being added here, including the ability to refer to specific banks by name and the distinction between hardware values and configured values (here used to determine if we are building for core 0 or core 1). Functionality like this isn't possible to add to Mbed without this proposal!

5. Conclusion

I believe that adding memory bank information in this manner would be a huge step forward for Mbed OS development. It would unlock several new features, including, but not limited to, less janky multicore support, easier configuration for custom targets, MCUboot support, and better memory usage reporting. Perhaps the biggest step forward of all would be to the way we write linker scripts: Currently, they are almost always specific to one CPU part number, meaning that they are a huge pain to make changes to and can very easily get out of sync. However, with the more detailed information provided by this proposal, it should be possible to create linker scripts that can cover a wide range of devices, and can even support newly added devices without changes! I'm really looking forward to how much cleaner this will make the low levels of Mbed.

To be honest, the only real sacrifice being made with this design is backward compatibility: there isn't really a way for this design to support old style memory definitions directly in targets.json (e.g. this kind), and they will have to be updated. However, this annoyance is well worth the benefits of a much cleaner and more extensible design!

Appendix: Updating Existing Code

JSON files

mbed_app.json and targets.json entries that use the old properties to try and override memory banks will have to be updated to the new format given above.

cmsis_nvic.h files

Lots of these currently look like this:

#if !defined(MBED_RAM_START)
#define MBED_RAM_START  0x20000000
#endif

#if !defined(MBED_RAM_SIZE)
#define MBED_RAM_SIZE  (256*1024)
#endif

I believe these type of defines were meant to be a backup in case the memory bank information from the build system wasn't available. And that backup got used up until now with Mbed CE! However, once this proposal is implemented, this code will go back to being unused, and can even be removed if needed.

Linker scripts

Linker scripts will not need any updates right away, except for those that were relying on MBED_ROM_START/MBED_RAM_START/etc as a way to access overridden memory attributes from the JSON definition. These will be updated as part of the memory bank info PR.

All linker scripts should gradually be migrated to use the new style named defines (e.g. MBED_RAM_BANK_IRAM1_START) instead of hardcoding addresses. That way, even if the memory banks end up having different sizes/addresses on different devices, the linker script will still work, and linker scripts for multiple targets can be combined.

There are also lots of linker scripts that use MBED_APP_START/MBED_APP_SIZE like so:

#if !defined(MBED_APP_START)
   #define MBED_APP_START 0x00010000
#endif

#if !defined(MBED_APP_SIZE)
   #define MBED_APP_SIZE 0x00070000
#endif

MEMORY
{
    FLASH (rx) : ORIGIN = MBED_APP_START, LENGTH = MBED_APP_SIZE
    RAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00028000
}

These defines used to be defined by the old code when the bootloader was in use to relocate the application in flash. However, MBED_APP_START is not defined by the new configuration system, and in most cases the MBED_CONFIGURED_ROM_BANK_xxx_START/SIZE defines should be used instead.

Linker scripts that are set up like won't break immediately, but it will prevent the flash and RAM size of the target from being configured. Code like the above should be changed to something like

MEMORY
{
    FLASH (rx) : ORIGIN = MBED_CONFIGURED_ROM_BANK_IROM1_START, LENGTH = MBED_CONFIGURED_ROM_BANK_IROM1_SIZE
    RAM  (rwx) : ORIGIN = MBED_CONFIGURED_RAM_BANK_IRAM1_START, LENGTH = MBED_CONFIGURED_RAM_BANK_IRAM1_SIZE
}

Worth noting that the decision on whether to use the configured addresses and sizes or the physical ones is basically up to the linker script author. In most cases, one should try to use the configured addrs and sizes, especially for the main flash and RAM banks. This will provide the best experience when using the bootloader, which basically works by installing itself at the start of the main flash bank, then shifting the application backward and reducing its size to compensate. However, there are times when it doesn't really make sense to make the size of a memory bank configurable, e.g. when there is a boot header that needs to live at a specific address or when there's a 3rd party blob that needs to be copied into a specific section (such as Nordic SoftDevice). So, it is up to the script author's judgement here, and I have no doubt that we will refine this guidance further as we move closer to implementing bootloader support and other more advanced functionality.

References

Clone this wiki locally