Memory placement
By default the linker decides where your code and data land: constants go to non-volatile memory, initialized data and the stack go to tightly coupled memory. That is the right answer most of the time. This page is about the times it is not: a buffer that has to be reachable by a peripheral, an inner loop you want executing out of ITCM, or a working set you want in the larger, slower shared SRAM.
Placing something
Section titled “Placing something”Annotate the definition:
#include "nsx_mem.h"
NSX_MEM_SRAM uint8_t g_dma_buffer[4096];NSX_MEM_SRAM_BSS uint8_t g_scratch[16384];NSX_MEM_FAST_CODE void hot_loop(void);The macros are the portable spelling of a section attribute. Six are defined:
| Macro | What it selects |
|---|---|
NSX_MEM_NVM |
Non-volatile memory. Where const data goes already. |
NSX_MEM_FAST |
The fast default for initialized data, which is tightly coupled memory on every SoC NSX packages a board for. |
NSX_MEM_FAST_BSS |
The zero-initialized equivalent. |
NSX_MEM_SRAM |
Initialized data in shared SRAM. |
NSX_MEM_SRAM_BSS |
Zero-initialized data in shared SRAM. |
NSX_MEM_FAST_CODE |
Code, placed for fast fetch rather than left in NVM. |
Use NSX_MEM_SRAM_BSS rather than NSX_MEM_SRAM for anything large and zero-initialized.
Initialized data has to be copied out of NVM at startup, so a 16 KB initialized array
costs you 16 KB of NVM and the time to copy it, for a buffer you were going to overwrite.
NSX_SRAM_BSS and NS_PUT_IN_TCM are older spellings kept for compatibility; prefer the
NSX_MEM_ names.
What they expand to
Section titled “What they expand to”Each macro resolves per SoC. On apollo510 the mapping is:
#define NSX_MEM__SEC_SRAM ".shared"#define NSX_MEM__SEC_SRAM_BSS ".sram_bss"#define NSX_MEM__SEC_FAST_CODE ".itcm_text"A companion set of NSX_MEM__HAS_* macros says whether a region exists on the target at
all, which is what lets a portable module ask before placing something. On an SoC without
a given region, the macro falls back to the default placement rather than failing to
build, so check NSX_MEM__HAS_SRAM and friends if placement is load bearing for you.
If you need a section the macros do not name, nsx_compiler.h has the raw attribute
wrappers, which work the same across GCC, Arm Compiler for Embedded and ATfE:
#define NSX_SECTION(s) __attribute__((section(s)))#define NSX_ALIGNED(n) __attribute__((aligned(n)))#define NSX_USED __attribute__((used))#define NSX_WEAK __attribute__((weak))Pair NSX_SECTION with NSX_USED for anything the linker would otherwise garbage
collect.
The regions you are placing into
Section titled “The regions you are placing into”Region names and sizes come from the linker script the board selects, which is a per-SoC
file inside the vendored nsx-core module. For Apollo510 the ITCM variant committed in
this repository as examples/power_benchmark/linker_script_itcm.ld declares:
MCU_ITCM (rwx) : ORIGIN = 0x00000000, LENGTH = 262144MCU_MRAM (rx) : ORIGIN = 0x00410000, LENGTH = 4128768MCU_TCM (rwx) : ORIGIN = 0x20000000, LENGTH = 507904SHARED_SRAM (rwx) : ORIGIN = 0x20080000, LENGTH = 3145728Those four numbers are that file’s, for that SoC and that linker profile. Every other part has its own script with its own values, and the MRAM origin in particular differs between the bootloader and no-bootloader variants. Read your own build’s script rather than carrying these figures across parts. See Startup and linker for which script your board selects.
When you run out
Section titled “When you run out”Overflowing a region is a link-time error naming the region and the limit, for example a TCM overflow reported against that script’s 507904 bytes. The options, in the order worth trying:
- Move large zero-initialized buffers to
NSX_MEM_SRAM_BSS. - Reduce the stack or heap sizes the board’s
board.cmakepasses to the startup code. - Stop placing code in ITCM that does not need to be there.
Caches
Section titled “Caches”Placement and caching interact: a region’s access cost depends on whether the cache is on and whether the data you are looking at is coherent with what a peripheral wrote.
nsx_mem.h declares the cache surface:
uint32_t nsx_cache_enable(void);void nsx_cache_disable(void);uint32_t nsx_cache_flush(void);uint32_t nsx_cache_publish_writes(void);uint32_t nsx_cache_invalidate_observed_data(void);uint32_t nsx_cache_sync_shared_data(void);Not every part implements every operation. The header exposes capability macros, so
portable code can test before calling. Each macro is always defined, as 1 or 0, so
test it with #if rather than #ifdef:
| Macro | Meaning when 1 |
|---|---|
NSX_CACHE_HAS_PUBLISH_WRITES |
nsx_cache_publish_writes() is implemented. |
NSX_CACHE_HAS_INVALIDATE_OBSERVED |
nsx_cache_invalidate_observed_data() is implemented. |
NSX_CACHE_HAS_SYNC_SHARED |
nsx_cache_sync_shared_data() is implemented. |
NSX_CACHE_HAS_EXPLICIT_DCACHE |
The data cache can be controlled explicitly. |
An unimplemented operation returns NSX_CACHE_UNSUPPORTED rather than failing silently,
so a portable module can call and check instead of guarding every call site.
The two cases that bite
Section titled “The two cases that bite”- You wrote a buffer, a peripheral reads it. Call
nsx_cache_publish_writes()before handing the buffer over, so your writes are visible outside the CPU. - A peripheral wrote a buffer, you read it. Call
nsx_cache_invalidate_observed_data()first, so you are not reading a stale line.
nsx_cache_sync_shared_data() covers both directions for shared-SRAM buffers where
ownership moves back and forth.
Caches are enabled through system
initialization, by setting enable_cache in
nsx_system_config_t or by calling nsx_cache_enable() directly.
Measure rather than assume
Section titled “Measure rather than assume”Whether moving something changes anything depends on the part, the clock, the cache state
and the access pattern. The repository ships two examples that measure instead of
guessing: pmu_profiling reads the Cortex-M55 PMU for cycles, cache hits and branch
mispredicts, and power_benchmark measures across active, idle and deep-sleep phases. See
Examples.