Advanced RTT in Embedded Rust: A Guide to Multi-Channel Logging and Binary Streaming (Part 2)
Stream RP2040 sensor data over RTT and decode logs in real time. Use Python to visualize binary temperature readings and turn raw bytes into insights.
Introduction
Welcome back! In Part 1, we set up a multi-channel RTT system on the Pi Pico (RP2040), complete with structured logging, plain-text prints, and a binary data channel. But what good is a stream of raw bytes if you can’t make sense of it?
No worries, in this post, we will:
- Stream the RP2040’s internal temperature sensor over RTT’s binary channel.
- Decode
defmtlogs into human-readable text. - Build a simple Python app to parse and display the binary data in real time.
New to RTT? Make sure to check out our previous posts for context:
Implementing an Internal Temperature Sensor Stream
The RP2040 chip on the Pi Pico includes a built-in temperature sensor. While it’s not the most precise sensor in the world, it’s perfect for demonstrating how to stream binary data over RTT. Here’s how to read it and send the data to your host.
Step 1: Add Required Packages
To read the temperature sensor, we’ll use the ADC (Analog-to-Digital Converter). However, the embedded-hal crate we’ve been using doesn’t include the OneShot trait, which we need for single-shot ADC readings. Let’s add the embedded-hal-0.2 crate with the unproven feature to unlock this functionality.
Open your Cargo.toml and add:
[dependencies]
embedded_hal_0_2 = {package = "embedded-hal", version = "0.2.5", features = ["unproven"]}
Step 2: Read the Temperature Sensor
Now, let’s add the code to read the sensor and convert its output to a temperature value. The RP2040’s temperature sensor outputs a voltage that we’ll convert to degrees Celsius using the formula provided in the RP2040 datasheet.
Add the following to your main.rs:
use embedded_hal_0_2::adc::OneShot;
fn main() -> ! {
// ... your existing setup code ...
let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);
let mut temperature_sensor = adc.take_temp_sensor().unwrap();
loop {
// ... other loop code
let temp_sens_adc_counts: u16 = adc.read(&mut temperature_sensor).unwrap();
// Convert to voltage
let v_adc = temp_sens_adc_counts as f32 * (3.3 / 4095.0);
// Convert voltage → temperature
let temp_c = 27.0 - (v_adc - 0.706) / 0.001721;
}
Step 3: Stream Temperature Data Over RTT
Now that we have the temperature in Celsius, let’s package it into a binary format and send it over RTT Channel 2. We’ll define a simple struct, TempPacket, to hold the temperature value and convert it to a byte array for transmission.
#[repr(C)]
struct TempPacket {
temp: f32,
}
impl TempPacket {
fn as_bytes(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
(self as *const TempPacket) as *const u8,
core::mem::size_of::<TempPacket>(),
)
}
}
}
fn main() -> ! {
// ... existing setup and sensor reading code ...
loop {
// ... read temperature as before ...
let pkt = TempPacket {
temp: temp_c,
};
let _ = bin_out.write(pkt.as_bytes());
}
Why #[repr(C)]? This ensures the struct is laid out in memory in a predictable way (matching C’s memory layout), which is critical for correctly interpreting the bytes on the host side.
Making Logs Human Readable
We have now 3 channels, two of those send us binary encoded data, so next lets have a look how to make them reeadable
Decoding defmt Logs
defmt logs are compact and efficient, but they need to be decoded to be human-readable. Luckily, the defmt-print tool does this for us. It listens to a TCP port and decodes the logs in real time.
If you haven’t already, install it via cargo:
cargo install defmt-print
Start defmt-print with the path to your compiled firmware and the port number for the defmt channel.
$ defmt-print -e <path to our target> tcp --port 60001
Loop 119...
Loop 120...
Loop 121...
Double-check the port numbers in your GDB output. They might differ from the example above!
Viewing Binary Temperature Data
For the binary temperature data, we’ll write a simple Python script to connect to RTT Channel 2, parse the incoming bytes, and print the temperature.
Save this as rtt_temp_viewer.py:
import socket
import sys
import struct
HOST = "127.0.0.1" # The server's hostname or IP address
PORT = 60002 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
print(f"Connected to RTT Channel 2 on port {PORT}. Press Ctrl+C to exit.")
try:
while True:
data = s.recv(1024)
print(f"Received {struct.unpack('<f', data)[0]!r}")
except KeyboardInterrupt:
sys.exit(1)
How it works:
- The script connects to the RTT server on the specified port.
- It reads 4 bytes at a time (the size of an
f32). struct.unpack('<f', data)interprets the bytes as a little-endian 32-bit float.- The temperature is printed with 2 decimal places for readability.
Start the script while your firmware is running, you should see output like this:
$ python3 rtt_temp_viewer.py
Connected to RTT Channel 2 on port 60002. Press Ctrl+C to exit.
Temperature: 32.45°C
Temperature: 32.47°C
Temperature: 32.49°C
What's Next?
And there you have it! You’ve built a multi-channel RTT debugging system, This is a dance worthy achievement.
This setup is incredibly powerful, you can adapt it for almost any embedded project. Debugging a motor controller? Send PWM values to your host. The possibilities are endless.
In the next installment of the Crab Lab Series, we’ll put RTT to the side for now and dive into storing information in FLASH and using picotool to retrieve it.
Stay tuned, and happy debugging! 🦀
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.
How to Use a Raspberry Pi as a Remote OpenOCD Debugger
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.