This blog is to introduce a method how to specify a memory section on the Zephyr RTOS Project.

In this page, I refer to multiple passages as below URL and summarize all necessary information together.

Also, I use the example from the zephyr project and show how to verify the memory address by reading the memory map file.

The concept is very similar to the scatter-loading features.

On the ARM architecture framework, it provides the Scatter loading, which enables you to partition an executable image into regions that can be positioned independently in memory.

In a simple embedded computer system, memory is divided into ROM and RAM. The image produced by the linker is divided into the “Read-Only” segment, which contains the code and read-only data, and the “Read-Write” segment, which contains the initialized and non-initialized or zero-initialized (ZI) data. Usually, the “Read-Only” segment is placed in ROM and the “Read-Write” segment is copied from ROM to RAM before execution begins.

Embedded systems often use a more complex memory map, which can consist of ROM, SRAM, DRAM, FLASH and so on. The scatter loading mechanism lets you place various parts of the image in these distinct memory areas.

Scatter loading enables you to partition your program image into several regions of code and data which can be placed separately in the memory map. Each region is placed in a contiguous chunk of memory space. The location of a region can differ between load time and execute time, which the application copying code and data from its load address to its execution address.


Keil has provided the documentation how to use the scatter-loading images with a simple memory map.

For images with a simple memory map, you can specify the memory map using only linker command-line options, or with a scatter file.If an image has a simple memory map, you can either:

  • Specify the memory map using only linker command-line options.
  • Use a scatter file.

For example, you have the scatter-loading description as below that loads the segments from the object into memory:

LOAD_ROM 0x0000 0x8000       ; Name of load region (LOAD_ROM),
                             ; Start address for load region (0x0000),
                             ; Maximum size of load region (0x8000)
    EXEC_ROM 0x0000 0x8000   ; Name of first exec region (EXEC_ROM),
                             ; Start address for exec region (0x0000),
                             ; Maximum size of first exec region (0x8000)
        * (+RO)              ; Place all code and RO data into
                             ; this exec region
    SRAM 0x10000 0x6000      ; Name of second exec region (SRAM),
                             ; Start address of second exec region (0x10000),
                             ; Maximum size of second exec region (0x6000)
        * (+RW, +ZI)         ; Place all RW and ZI data into
                             ; this exec region

The maximum size specifications for the regions are optional. However, if you include them, they enable the linker to check that a region does not overflow its boundary. Apart from the limit checking, you can achieve the same result with the following linker command-line:

armlink --ro_base 0x0 --rw_base 0x10000

Methods of placing functions and data at specific addresses

There are various methods available to place functions and data at specific addresses.Where they are required, the compiler normally produces RO, RW, ZI, and XO sections from a single source file. These sections contain all the code and data from the source file. To place a single function or data item at a fixed address, you must enable the linker to process the function or data separately from the rest of the input files.The linker has two methods that enable you to place a section at a specific address:

  • You can create a scatter file that defines an execution region at the required address with a section description that selects only one section.
  • For a specially-named section the linker can get the placement address from the section name. These specially-named sections are called __at sections.

To place a function or variable at a specific address it must be placed in its own section. There are several ways to do this:

  • Place the function or data item in its own source file.
  • Use __attribute__((at(address))) to place variables in a separate section at a specific address.
  • Use __attribute__((section("name"))) to place functions and variables in a named section.
  • Use the AREA directive from assembly language. In assembly code, the smallest locatable unit is an AREA.
  • Use the --split_sections compiler option to generate one ELF section for each function in the source file. This option results in a small increase in code size for some functions because it reduces the potential for sharing addresses, data, and string literals between functions. However, this can help to reduce the final image size overall by enabling the linker to remove unused functions when you specify armlink --remove.

Example of how to place a variable at a specific address without scatter-loading

To place code and data at specific addresses without a scatter file:

  • Create the source file main.c containing the following code:
#include <stdio.h>
extern int sqr(int n1);
int gSquared __attribute__((at(0x5000)));  // Place at 0x5000
int main()
    printf("Value squared is: %d\n", gSquared);
  • Create the source file function.c containing the following code:
int sqr(int n1)
    return n1*n1;
  • Compile and link the sources: 
armcc -c -g function.c armcc -c -g main.c armlink --map function.o main.o -o squared.axf
  • The --map option displays the memory map of the image. Also, --autoat is the default.

In this example, __attribute__((at(0x5000))) specifies that the global variable gSquared is to be placed at the absolute address 0x5000gSquared is placed in the execution region ER$$.ARM.__at_0x00005000 and load region LR$$.ARM.__at_0x00005000.


Although the address is specified as 0x5000 in the source file, the region names and section name addresses are normalized to eight hexadecimal digits.

The memory map shows:

  Load Region LR$$.ARM.__at_0x00005000 (Base: 0x00005000, Size: 0x00000000, Max: 0x00000004, ABSOLUTE)

    Execution Region ER$$.ARM.__at_0x00005000 (Base: 0x00005000, Size: 0x00000004, Max: 0x00000004, ABSOLUTE, UNINIT)

    Base Addr    Size         Type   Attr      Idx    E Section Name        Object

    0x00005000   0x00000004   Zero   RW           13    .ARM.__at_0x00005000  main.o

Zephyr / nRF Connect SDK

Zephyr has provided the python script to configure the .text, .rodata, .data, and .bss sections from the required files. It can help to place them in the required memory region. The memory region and file are given to the scripts/ script in the form of a string. This script is always invoked from inside cmake.

For example,

# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.13.1)
find_package(Zephyr HINTS $ENV{ZEPHYR_BASE})

target_sources(app PRIVATE src/main.c)
zephyr_linker_sources(SECTIONS shared_ram.ld)
    SECTION_PROLOGUE (shared_ram, 0x20007000 (NOLOAD),)
        __shared_ram_start = .;
        __shared_ram_end = .;


 * Copyright (c) 2016 Intel Corporation
 * SPDX-License-Identifier: Apache-2.0

#include <zephyr.h>
#include <device.h>
#include <devicetree.h>
#include <drivers/gpio.h>
#include <drivers/sensor.h>
#include <drivers/ipm.h>

/* Empty symbol from linker script. 
 * Its only purpose is to give us the shared RAM address space.
extern uint32_t __shared_ram_start;
#define SHARED_RAM_ADDRESS &__shared_ram_start

/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS   1000

static struct sensor_value val;
static const struct device *ipm_tx_handle;
static const struct device *ipm_rx_handle;
struct sensor_value *p_val;

static bool data_requested = true;

void ipm_tx_callback(const struct device *dev, void *context,
		       uint32_t id, volatile void *data)
	printk("TX ID %d\n", id);

void ipm_rx_callback(const struct device *dev, void *context,
		       uint32_t id, volatile void *data)
	printk("RX ID %d\n", id);

	p_val = (struct sensor_value *)SHARED_RAM_ADDRESS;
	p_val->val1 = val.val1;
	p_val->val2 = val.val2;
	uint32_t ret = ipm_send(ipm_tx_handle, 1, 0, NULL, 0);

void main(void)
	const struct device *temp_dev;
	printk("Network core temp sample. Fetches from shared RAM address: %p\n", SHARED_RAM_ADDRESS);
	int ret;
	temp_dev = device_get_binding("TEMP_0");
	if (!temp_dev) {
		printk("Didn't find the temp driver!\n");

	/* IPM setup */
	ipm_rx_handle = device_get_binding("IPM_0");
	if (!ipm_rx_handle) {
		printk("Could not get TX IPM device handle");
	ipm_tx_handle = device_get_binding("IPM_1");
	if (!ipm_tx_handle) {
		printk("Could not get TX IPM device handle");

	ipm_register_callback(ipm_tx_handle, ipm_tx_callback, NULL);
	ipm_register_callback(ipm_rx_handle, ipm_rx_callback, NULL);
	ipm_set_enabled(ipm_tx_handle, 1);
	ipm_set_enabled(ipm_rx_handle, 1);
	while (1) {

		ret = sensor_sample_fetch(temp_dev);

		if (ret) {
			printk("Failed to sample temp sensor!\n");

		ret = sensor_channel_get(temp_dev, SENSOR_CHAN_DIE_TEMP, &val);

		if (ret) {
			printk("failed to get temp sensor data!\n");
		printk("Temp %d.%d\n", val.val1, val.val2);

After compile, you can have a look on the to verify. The value of __shared_ram_start is located at 0x20007000 as below.

The example code can be found at

Welcome to give any feedback to me.