#[timestamp]

Applications that, directly or transitively, use any of defmt logging macros may define a #[timestamp] function or include one in their dependency graph.

All logs are timestamped. The #[timestamp] function specifies how the timestamp is computed. By default (if no #[timestamp] function is provided), a timestamp of 0 will be used. This function must have signature fn() -> u64 and on each invocation should return a non-decreasing value. The function is not unsafe meaning that it must be thread-safe and interrupt-safe.

No timestamp (default behavior)

When no #[timestamp] function is used, defmt will use one equivalent to this:


#![allow(unused)]
fn main() {
extern crate defmt;
#[defmt::timestamp]
fn timestamp() -> u64 {
    0
}
}

Atomic timestamp

A simple timestamp function that does not depend on device specific features and is good enough for development is shown below:


#![allow(unused)]
fn main() {
extern crate defmt;
use std::sync::atomic::{AtomicUsize, Ordering};
// WARNING may overflow and wrap-around in long lived apps
#[defmt::timestamp]
fn timestamp() -> u64 {
    static COUNT: AtomicUsize = AtomicUsize::new(0);
    COUNT.fetch_add(1, Ordering::Relaxed) as u64
}
}

Hardware timestamp

A timestamp function that uses a device-specific monotonic timer can directly read a MMIO register. It's OK if the function returns 0 while the timer is disabled.

extern crate defmt;
fn monotonic_timer_counter_register() -> *mut u32 {
    static mut X: u32 = 0;
    unsafe { &mut X as *mut u32 }
}
// WARNING may overflow and wrap-around in long lived apps
#[defmt::timestamp]
fn timestamp() -> u64 {
    // NOTE(interrupt-safe) single instruction volatile read operation
    unsafe { monotonic_timer_counter_register().read_volatile() as u64 }
}

fn enable_monotonic_counter() {}
fn main() {
    defmt::info!(".."); // timestamp = 0
    defmt::debug!(".."); // timestamp = 0
    enable_monotonic_counter();
    defmt::info!(".."); // timestamp >= 0
    // ..
}

64-bit extension

Microcontrollers usually have only 32-bit counters. Some of them may provide functionality to make one 32-bit counter increase the count of a second 32-bit counter when the first wrap arounds. Where that functionality is not available, a 64-bit counter can be emulated using interrupts:


#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicU32, Ordering};
static OVERFLOW_COUNT: AtomicU32 = AtomicU32::new(0);

// NOTE interrupt running at highest priority
fn on_first_counter_overflow() {
    let ord = Ordering::Relaxed;
    OVERFLOW_COUNT.store(OVERFLOW_COUNT.load(ord) + 1, ord);
}
}

To read the 64-bit value in a lock-free manner the following algorithm can be used (pseudo-code):

do {
  high1 <- read_high_count()
  low <- read_low_count()
  high2 <- read_high_count()
} while (high1 != high2)
count: u64 <- (high1 << 32) | low

The loop should be kept as tight as possible and the read operations must be single-instruction operations.