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.

Andre Leppik

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 defmt logs 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());
    }
Note

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...
Tip

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! 🦀

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.