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.
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
}
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:
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"
Including External Libraries in CMake Projects
Learn how to use CMake’s FetchContent module to automatically download and integrate libraries like CMSIS into your embedded projects, eliminating the hassle of manual copying and updates.
How to Find the Memory Address of a Symbol in an ELF File
A quick and useful tip for locating symbol memory addresses in ELF files using arm-none-eabi-nm combined with grep—perfect for embedded debugging scenarios like setting up SEGGER RTT or inspecting linker placements and runtime symbols.
Remote Debugging with OpenOCD on Raspberry Pi
Learn how to turn a Raspberry Pi into a remote debugging server for the RP2040 using OpenOCD, a complete with step-by-step setup and instructions for building OpenOCD from source to resolve hardware compatibility issues.
Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.