defmt-persist 0.1.0

Persistent defmt logger that survives resets
Documentation
//! RTT (Real-Time Transfer) output channel for defmt.
//!
//! Based on defmt-rtt. The host/debugger can set MODE_BLOCK_IF_FULL in the channel flags
//! to enable blocking mode when connected.

use core::{
    cell::UnsafeCell,
    mem::MaybeUninit,
    ptr,
    sync::atomic::{AtomicU32, Ordering},
};

// BUF_SIZE is generated by build.rs from the DEFMT_RTT_BUFFER_SIZE env var.
include!(concat!(env!("OUT_DIR"), "/consts.rs"));

/// Writes bytes to the RTT up channel.
///
/// # Safety
///
/// Must be called from within a critical section to prevent concurrent access
/// to the RTT channel.
pub(crate) unsafe fn write(bytes: &[u8]) {
    // SAFETY: The caller guarantees we're in a critical section, so no other
    // code can access `up_channel` concurrently. The `UnsafeCell::get` returns
    // a valid pointer to the channel initialized at static construction.
    unsafe { &*_SEGGER_RTT.up_channel.get() }.write_all(bytes)
}

/// Flushes the RTT buffer by busy-waiting until the host reads all data.
///
/// # Safety
///
/// Must be called from within a critical section to prevent concurrent access
/// to the RTT channel.
pub(crate) unsafe fn flush() {
    // SAFETY: The caller guarantees we're in a critical section, so no other
    // code can access `up_channel` concurrently. The `UnsafeCell::get` returns
    // a valid pointer to the channel initialized at static construction.
    unsafe { &*_SEGGER_RTT.up_channel.get() }.flush()
}

/// Mask for MODE bits.
const MODE_MASK: u32 = 0b11;
/// Block the application if the RTT buffer is full, wait for the host to read data.
const MODE_BLOCK_IF_FULL: u32 = 2;
/// Don't block if the RTT buffer is full. Truncate data to output as much as fits.
const MODE_NON_BLOCKING_TRIM: u32 = 1;

// NOTE the `rtt-target` API is too permissive. It allows writing arbitrary data to any
// channel (`set_print_channel` + `rprint*`) and that can corrupt defmt log frames.
// So we declare the RTT control block here and make it impossible to use `rtt-target` together
// with this crate.
#[unsafe(no_mangle)]
static _SEGGER_RTT: RttHeader = RttHeader::new(NAME.as_ptr(), BUFFER.0.get().cast());

#[cfg_attr(target_os = "macos", unsafe(link_section = ".uninit,defmt-rtt.BUFFER"))]
#[cfg_attr(
    not(target_os = "macos"),
    unsafe(link_section = ".uninit.defmt-rtt.BUFFER")
)]
static BUFFER: UnsafeBuffer = UnsafeBuffer(UnsafeCell::new([MaybeUninit::uninit(); BUF_SIZE]));

// Place NAME in data section, so the whole RTT header can be read from RAM.
// This is useful if flash access gets disabled by the firmware at runtime.
#[unsafe(link_section = ".data")]
static NAME: [u8; 6] = *b"defmt\0";

#[repr(C)]
struct RttHeader {
    /// RTT pattern.
    id: [u8; 16],
    /// Number of up (to the host) channels.
    max_up_channels: u32,
    /// Number of down (to the device, from the host) channels.
    max_down_channels: u32,
    /// Data buffer.
    up_channel: UnsafeCell<Channel>,
}

impl RttHeader {
    const fn new(name: *const u8, buffer: *mut MaybeUninit<u8>) -> Self {
        RttHeader {
            id: *b"SEGGER RTT\0\0\0\0\0\0", // Defined by SEGGER
            max_up_channels: 1,
            max_down_channels: 0,
            up_channel: UnsafeCell::new(Channel {
                name,
                buffer,
                size: BUF_SIZE as u32,
                write: AtomicU32::new(0),
                read: AtomicU32::new(0),
                flags: AtomicU32::new(MODE_NON_BLOCKING_TRIM),
            }),
        }
    }
}

// SAFETY: RttHeader can be safely shared between threads because:
// - The `id`, `max_up_channels`, and `max_down_channels` fields are immutable after construction.
// - The `up_channel` is protected by `UnsafeCell` and only accessed via `write()` and `flush()`,
//   which require the caller to be in a critical section.
// - The RTT protocol itself handles concurrent access from the host (debugger) via atomic
//   read/write pointers with appropriate memory ordering.
unsafe impl Sync for RttHeader {}

#[repr(transparent)]
struct UnsafeBuffer(UnsafeCell<[MaybeUninit<u8>; BUF_SIZE]>);

// SAFETY: UnsafeBuffer can be safely shared between threads because:
// - It is only accessed through the Channel's buffer pointer within critical sections.
// - The RTT protocol uses atomic read/write pointers to coordinate access between
//   the target (writer) and host (reader), preventing data races.
unsafe impl Sync for UnsafeBuffer {}

/// RTT Up channel
#[repr(C)]
struct Channel {
    name: *const u8,
    /// Pointer to the RTT buffer.
    buffer: *mut MaybeUninit<u8>,
    size: u32,
    /// Written by the target.
    write: AtomicU32,
    /// Written by the host.
    read: AtomicU32,
    /// Channel properties.
    ///
    /// Currently, only the lowest 2 bits are used to set the channel mode (see constants below).
    flags: AtomicU32,
}

impl Channel {
    fn write_all(&self, mut bytes: &[u8]) {
        // The host-connection-status is only modified after RAM initialization while the device is
        // halted, so we only need to check it once before the write-loop.
        let host_connected = self.host_is_connected();
        let write_fn = match host_connected {
            true => Self::blocking_write,
            false => Self::nonblocking_write,
        };

        while !bytes.is_empty() {
            let consumed = write_fn(self, bytes);
            if consumed != 0 {
                bytes = &bytes[consumed..];
            } else if !host_connected {
                // Non-blocking mode: discard remaining data when buffer is full.
                break;
            }
            // Blocking mode: keep spinning until space becomes available.
        }
    }

    fn blocking_write(&self, bytes: &[u8]) -> usize {
        if bytes.is_empty() {
            return 0;
        }

        let read = self.read.load(Ordering::Relaxed) as usize;
        let write = self.write.load(Ordering::Acquire) as usize;
        let available = available_buffer_size(read, write);

        if available == 0 {
            return 0;
        }

        self.write_impl(bytes, write, available)
    }

    fn nonblocking_write(&self, bytes: &[u8]) -> usize {
        let read = self.read.load(Ordering::Relaxed) as usize;
        let write = self.write.load(Ordering::Acquire) as usize;

        let available = available_buffer_size(read, write);
        self.write_impl(bytes, write, available)
    }

    // Ring buffer copy safety invariants (referenced by SAFETY comments below):
    // - `buf` points to a valid buffer of size `BUF_SIZE` (initialized in `_SEGGER_RTT`).
    // - `cursor` is always < BUF_SIZE (maintained by `% BUF_SIZE` in write pointer updates).
    // - `len <= available` ensures we don't write past the read pointer (ring buffer invariant).
    // - `available_buffer_size` always leaves 1 byte gap, so `len < BUF_SIZE`.
    // - Source (`bytes`) is valid for `len` bytes by slice invariants.
    // - No overlap: `bytes` is caller-provided stack/heap data, `buf` is the static RTT buffer.
    fn write_impl(&self, bytes: &[u8], cursor: usize, available: usize) -> usize {
        let len = bytes.len().min(available);
        let buf: *mut u8 = self.buffer.cast();

        if cursor + len > BUF_SIZE {
            // Wrapping case: split into two copies.
            let pivot = BUF_SIZE - cursor;
            // SAFETY: See ring buffer copy safety invariants above.
            // First copy: bytes[0..pivot] -> buf[cursor..BUF_SIZE]
            unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), buf.add(cursor), pivot) };
            // SAFETY: See ring buffer copy safety invariants above.
            // Second copy: bytes[pivot..len] -> buf[0..len-pivot]
            unsafe { ptr::copy_nonoverlapping(bytes.as_ptr().add(pivot), buf, len - pivot) };
        } else {
            // SAFETY: See ring buffer copy safety invariants above.
            unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), buf.add(cursor), len) };
        }

        self.write.store(
            (cursor.wrapping_add(len) % BUF_SIZE) as u32,
            Ordering::Release,
        );

        len
    }

    fn flush(&self) {
        if !self.host_is_connected() {
            return;
        }

        let read = || self.read.load(Ordering::Relaxed);
        let write = || self.write.load(Ordering::Relaxed);
        while read() != write() {}
    }

    fn host_is_connected(&self) -> bool {
        // We assume that a host is connected if we are in blocking-mode.
        // This is what probe-run does.
        self.flags.load(Ordering::Relaxed) & MODE_MASK == MODE_BLOCK_IF_FULL
    }
}

/// How much space is left in the buffer?
fn available_buffer_size(read_cursor: usize, write_cursor: usize) -> usize {
    if read_cursor > write_cursor {
        read_cursor - write_cursor - 1
    } else {
        BUF_SIZE - write_cursor - 1 + read_cursor
    }
}