How to Move RTT to a Custom RAM Section in Embedded Rust

Learn how to place RTT buffers and the control block into a fixed RAM section in embedded Rust. This guide covers linker script changes, custom RTT initialization, and setting up a reliable RTT print channel.

Andre Leppik

Introduction

In the previous post, we explored how to integrate Real-Time Transfer (RTT) into an embedded Rust project. While RTT is a powerful tool for debugging and logging, one thing bothered me in that setup. Namely, the default behavior of placing the control block and buffers at non-fixed addresses somewhere in RAM. This can cause issues when our debugger or tooling expects them at a specific location. Lets have a look how we could remove this nuisance.

Step-by-Step Solution

In short, to force RTT into a known memory region, we must first allocate a memory area for the buffers and control block. We then have to place those blocks into the custom section we created. Finally, we need tp reconfigure our print channels.

Now that we have a overview, let’s go through each step in more detail.

Step 1: Update the Linker Script

To force RTT into a known memory region, start by allocating a dedicated area in your linker script. Define a new memory region and allocate a 2KB region at the start of RAM for RTT:

MEMORY {
    BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
    FLASH : ORIGIN = 0x10000100, LENGTH = 2048K
    RTT   : ORIGIN = 0x20000000, LENGTH = 2K
    RAM   : ORIGIN = 0x20000800, LENGTH = 254K
}
Note

Take extra care resizing any memory area, the start addresse and size must reflect the changes.

Next, define a section in the RTT memory area to hold the control block and buffers

SECTIONS {
    .rtt_data : ALIGN(4)
    {
        KEEP(*(.rtt_data));
        /* Keep this block a nice round size */
        . = ALIGN(4);
    } > RTT
}

If we compile the code now, RTT will not yet be placed into the new section, because the Rust code still uses the default RTT initialization. The default behavior is configured by rtt_init_print!(). We can verify where the control block currently ends up:

$ arm-none-eabi-nm ./target/thumbv6m-none-eabi/debug/rust-pico-rtt | grep SEGGER_RTT
20000800 B _SEGGER_RTT

The output should show an address outside our new RTT region, still placed in the RAM area. If we have a look at the memory dump at that address, we will see something like this:

Memory view of RAM where RTT is placed

Step 2: Initialize RTT with Custom Setup

For a custom setup, we first need to replace the default rtt_init_print!() with a custom initialization to place the control block and buffers in the .rtt_data section.

Start by importing what we need from the rtt_target crate.

use rtt_target::{set_print_channel, rprintln, rtt_init, ChannelMode};

Define the buffers and their memory sections:

let channels = rtt_init! {s
    up: {
        0: {
            size: 1024,
            mode: ChannelMode::NoBlockSkip,
            name: "Terminal",
            section: ".rtt_data"
        }
    }
    down: {
        0: {
            size: 16,
            name: "Terminal",
            section: ".rtt_data"
        }
    }
    section_cb: ".rtt_data"
};

This configuration explicitly places the up buffer and control block in the .rtt_data section.

Step 3: Set the Print Channel

Since we’re using a custom initialization, we need to manually set the default print channel which was previously handled by the rtt_init_print!() macro:

set_print_channel(channels.up.0);

Now, rprintln! will work as expected. Rebuild and check the RTT placement:

$ arm-none-eabi-nm.exe ./target/thumbv6m-none-eabi/debug/rust-pico-rtt | grep SEGGER_RTT
20000000 B _SEGGER_RTT

You should see the control block at 0x20000000, exactly where we intended.

Testing the Solution

Add a simple print statement to confirm everything works:

rprintln!("RTT initialized and working from custom memory area!");

Start your debug session and connect to the RTT server (e.g., via telnet). You should see the message appear, confirming that RTT is functioning correctly from the custom memory region.

What´s Next?

In the next part, we’ll look at creating multiple up/down RTT channels so you can separate:

  • human-readable logs (channel 0)
  • binary data streams (channel 1)
  • or any custom setup you need

Previous post: "How to Use RTT in Embedded Rust: Setup and Logging"

Need help with embedded systems development?

Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.