Tracing & Logging with the TRICE Library (Interrupts too!) | Interrupt

What I’m doing is placing the strings into an info section in the .elf (not included in the binary), and the log contains only the address of the string and a binary representation of the log arguments (packed struct generated using the log macro at compile time).

Using some tricks I can add more information to the string like file/line information.

Then the host software (written in rust) finds the strings in the .elf, and uses information that’s embedded in the string and debug information (DWARF) to parse the log’s arguments and pretty print everything.

The logger supports native types and structs with all of rust’s formatting options and some extra ones, like finding a variable name from a pointer address.

Originally I got the idea from here, but I had to make many adjustments to what’s possible to do in C and GCC, so the idea is similar but the mechanisms are quite different.

A log looks something like:

LOG_INFO("Hi, this is a log with multiple arguments. positional: {0} {2} {1:#02x}, and non positional: {} {:p} {:?}", var1, var2, var3);

It’s expanded into something like: (N is unique in the compilation unit, generated using __COUNTER__)

do{
    static char const log_message_N[] __attribute__((used, section(".logs"))) =
        "N"
        ":"
        __FILE__
        ":"
        __LINE__
        ":"
        "INF"
        ":"
        "Hi, this is a log with multiple arguments. positional: {0} {2} {1:#02x}, and non positional: {} {:p} {:?}"
        ";"
    ;

    struct __PACKED {
        __typeof__(var1) var1;
        __typeof__(var2) var2;
        __typeof__(var3) var3;
    } log_args_N; // The host parser finds this variable by name and parses it's DWARF to know how to correctly parse the arguments from the binary data.

    // This is usually optimized away:
    memcpy(&log_args_N.var1, &var1, sizeof(var1));
    memcpy(&log_args_N.var2, &var2, sizeof(var1));
    memcpy(&log_args_N.var3, &var3, sizeof(var1));

    static_assert(
        sizeof(log_args_N) <= (MAX_ARGS_LEN),
        "Log arguments don't fit into log buffer!"
    );

    _logger_send((size_t)&(log_message_N), &(log_args_N), sizeof(log_args_N));
} while(0);

and the binary data that’s logged is only:

// log address    
sizeof(uint32_t) + sizeof(var1) + sizeof(var2) + sizeof(var3)

Then _logger_send can be implemented however you like, add a timestamp, add cobs, whatever.

The Host side parser needs only the information that’s passed to _logger_send.

The best part about this is that it doesn’t require any extra tools at compilation, except the compiler itself (GCC).