Tonbandgerät

Tonbandgerät /ˈtoːnbantɡərɛːt/ n.

  1. German for audio tape recorder.
  2. An electrical device for recording sound on magnetic tape for playback at a later time.

A small embedded systems tracer with support for bare-metal and FreeRTOS-based targets.

Philipp Schilk, 2024


Overview

A sample trace

Tonbandgerät’s core is a small, portable, trace event generator and handler written in C and designed for embedded systems. It requires minimal porting and configuration, and features multiple backends for gathering and transmitting traces.

It can be used both with an RTOS, or in bare-metal environments to instrument user code and track hardware events by tracing interrupts. Full tracing of FreeRTOS tasks and resources is also supported out-of-the-box.

Tonbandgerät is based on a simple custom binary trace format designed to be fairly fast to encode and keep traces as small as possible. Recorded traces can be viewed in Google’s in-browser perfetto after conversion with the provided CLI tool or in-browser converter.

Documentation

The documentation for Tonbandgerät can be found in the docs/ folder and compiled for viewing with mdbook by running mdbook build in docs/. The latest version of the documentation can also be viewed online here.

Trace Converter + Viewing

The trace converter is written in rust, can be found here. For convenience, there is also a WASM version with web frontend, which runs in the browser and can be found here.

Licensing

The target tracer sources and documentation are released under the MIT License. All conversion and analysis tools, such as the decoder and converter, the CLI, and the web converter are released under the GNU GPL3 License.

Status

🚧 Note

Tonbandgerät is in early development and by no means considered stable. Everything - including the binary trace format - is subject to change.

Please report any issues here.

Completed:

Tonbandgerät:

  • Trace encoder.
  • Streaming backend.
  • Snapshot backend.
  • Metadata buffer.
  • Initial FreeRTOS support.

Conversion tools:

  • CLI converter.
  • In-browser converter.

Other:

  • STM32 + FreeRTOS example project.

Work-In-Progress:

Tonbandgerät:

  • Support for multicore tracing, including FreeRTOS SMP: Implemented and theoretically (almost?) done, but completely untested. Currently this is limited to cores that share a single, monotonic, time stamp timer.

  • Full FreeRTOS support, including some PRs: PRs are in a draft state/being reviewed. Certain FreeRTOS (rare) are not yet traced correctly due to insufficient tracing hooks. Tracing of streambuffers, direct-to-task notification, timers, and event groups are not yet Implemented.

Other:

  • This documentation.

Planned:

Tonbandgerät:

  • Post-mortem backend.
  • Task stack utilization tracing.
  • Multi-core for cores without common timebase.

Other:

  • More examples, including a bare-metal project, RTT-backed project, and RP2040 SMP project.
  • More example ports.

Ideas:

Support Other RTOSes:

  • All FreeRTOS-specific functionality has already been seperated into its own “module”, and more RTOSes could be added as separate modules.

Custom UI:

  • Perfetto, while incredibly powerful and simple to target, is not a perfect match for Tonbandgerät. It cannot display arbitrary tracks, forcing them to be global or part of a process/thread hierarchy. This could be improved by generating synthetic linux ftrace events, but that is a can of worms I don’t quite want to open. Furthermore, as it stands at the time of writing, a perfetto-based trace will never be able to display trace events in real time.

  • A custom UI would enable real-time viewing an be the first step towards a more “integrated” desktop tool that could also handle trace recording (building on probe.rs for RTT communication). A gui built on tauri and/or egui could continue to enable the current in-browser experience.

  • This would require a better in-memory model of trace data. Likely something like an in-memory DB? Apache Arrow?

Getting Started

Installation

Add all source files from tband/src to your project and place all headers from tband/inc inside a folder that is recognized as an include path.

If you are using FreeRTOS, include tband.h at the end of the FreeRTOSConfig.h header.

To use the tracer, only include tband.h in your code. Do not directly include any other Tonbandgerät headers. Note that Tonbandgerät is written using C11. Older versions of C are not tested.

Porting

Provide a tband_port.h header that implements all required porting macros. Example implementations can be found here.

Configuration

Provide a tband_config.h header, and configure Tonbandgerät using the configuration macros. Note that you have to provide this file even if you are keeping all settings at their default value or are providing configuration through compiler flags.

Trace Handling

Pick a trace handling backend and implement some mechanism for transmitting trace data to the computer.

Porting

Tonbandgerät invokes the macros below for all platform-specific operations. They must all be implemented for Tonbandgerät to function properly!

tband_portTIMESTAMP()

  • Required: YES
  • Return type: uint64_t

Get current value of the 64bit unsigned monotonic timestamp timer. The timer should have a resolution/frequency of tband_portTIMESTAMP_RESOLUTION_NS (see below).

Note that the timer must be shared between all cores. Smaller timers are allowed, but the return value of this macro should be uint64_t.

Example:

uint64_t platform_ts(void);
#define tband_portTIMESTAMP() platform_ts()

tband_portTIMESTAMP_RESOLUTION_NS

  • Required: YES
  • Value type: uint64_t

Resolution of the timestamp timer, in nanoseconds.

Example:

// Timestamp counter increases every 10ns:
#define tband_portTIMESTAMP_RESOLUTION_NS (10)

tband_portENTER_CRITICAL_FROM_ANY()

  • Required: YES

Enter a critical section from any context. For precise details of what properties a critical section must have, see here.

Example:

// FreeRTOS, ARM CM4F:
#define tband_portENTER_CRITICAL_FROM_ANY()            \
  bool tband_port_in_irq = xPortIsInsideInterrupt();   \
  BaseType_t tband_port_key = 0;                       \
  if (tband_port_in_irq) {                             \
    tband_port_key = taskENTER_CRITICAL_FROM_ISR();    \
  } else {                                             \
    taskENTER_CRITICAL();                              \
    (void)tband_port_key;                              \
  }

tband_portEXIT_CRITICAL_FROM_ANY()

  • Required: YES

See tband_portENTER_CRITICAL_FROM_ANY() above.

Example:

// FreeRTOS, ARM CM4F:
#define tband_portEXIT_CRITICAL_FROM_ANY()             \
  if (tband_port_in_irq) {                             \
    taskEXIT_CRITICAL_FROM_ISR(tband_port_key);        \
  } else {                                             \
    taskEXIT_CRITICAL();                               \
  }

tband_portNUMBER_OF_CORES

  • Required: YES
  • Return type: uint32_t

Number of cores on which the tracer is running. See Multicore Support for more details.

Example:

// Single core:
#define tband_portNUMBER_OF_CORES (1)

tband_portGET_CORE_ID()

  • Required: YES
  • Return type: uint32_t

Detect on which core the current execution context is running. See Multicore Support for more details. This macro must return a value between 0 and tband_portNUMBER_OF_CORES - 1 inclusive.

Example:

// Single core, always running on core 0:
#define tband_portGET_CORE_ID (0)

Streaming Backend Porting

tband_portBACKEND_STREAM_DATA(buf, len)

  • Required: YES (if streaming backend is enabled)
  • Return type: bool
  • Arg. 1 type: const uin8_t*
  • Arg. 2 type: size_t

Required if the streaming backend is used. Called by Tonbandgerät to submit data that is to be streamed. Return value of true indicates that data could not be streamed and was dropped. Return value of false indicates that data was not dropped and succesfully streamed.

Example:

bool stream_data(const uin8_t* buf, size_t buf_len);
#define tband_portBACKEND_STREAM_DATA(buf, len) stream_data(buf, len)

Snapshot Backend Porting

tband_portBACKEND_SNAPSHOT_BUF_FULL_CALLBACK()

  • Required: NO
  • Return type: void

If the snapshot backend is active and stops because of a full snapshot buffer, this callback is called.

Note that this macro is called from within the tracing hook of the first event that could not be stored in the buffer, and therefor may be called from any context (interrupts, RTOS tasks, RTOS scheduler, …).

Because the callback is always called from within a Tonbandgerät critical section and while certain internal spin locks are held, no Tonbandgerät APIs may be called from inside this callback.

Even in a multicore setp, this callback is called only once on the one core that first filled its buffer.

Furthermore, note that this callback is called once the (first) buffer is full, but it may some moments for the snapshot backend to finish, especially on all cores.

Example:

extern volatile bool snapshot_full;
#define tband_portBACKEND_SNAPSHOT_BUF_FULL_CALLBACK() snapshot_full = true

Critical Sections

Motivation

Tonbandgerät tracing hooks can be called from any context - including interrupts. This means Tonbandgerät requires some mechanism for preventing a higher-priority context from accessing internal state or submitting trace events, ensuring there are no race conditions and corrupted trace events.

Implementation Requirements

All sections of Tonbandgerät code that may not be interrupted are wrapped in tband_portENTER_CRITICAL_FROM_ANY() and tband_portEXIT_CRITICAL_FROM_ANY() guards, whose platform-specific implementation must be provided by the user as part of the Tonbandgerät port.

A port must ensure that no tracing events are generated or Tonbandgerät APIs are called while a critical section is active. Precisely how this is to be achieved depends on the overall software design and tracer implementation. A few example scenarios are given below.

Note that critical sections can occur in any context that tracing occurs. If you call tracing APIs in interrupts or are using an RTOS, this may include interrupts! Furthermore, note that Tonbandgerät critical sections do not need to be able to nest, but if used with an RTOS, may be placed inside an RTOS critical section.

Bare-metal Context. No tracing is done in any interrupts.

The critical section does not have to do anything. Since interrupts don’t interact with the tracer, it does not matter if one occurs during a critical section.

Bare-metal Context. Tracing is done in interrupts.

The critical section must disable interrupts. If only a known subset of interrupts generate tracing events or call tracer APIs, the critical section may also selectively disable these interrupts by adjusting interrupt masks or priorities.

RTOS.

RTOS tracing will almost always generate tracing events from interrupts. A critical section must, in this case, disable interrupts and prevent any mechanism for context switching. This is best done by using the critical section API provided by the RTOS. Note that these APIs may have calling semantics that differ from how Tonbandgerät’s critical sections work. For example, FreeRTOS provides seperate APIs for critical sections within and outside interrupts, and can only be used if the port supports detecting if execution is currently inside an interrupt. Using the FreeRTOS ARM Cortex M4 port, this may be done as follows:

#include "FreeRTOS.h"
#include "task.h"

#define tband_portENTER_CRITICAL_FROM_ANY()          \
  bool tband_port_in_irq = xPortIsInsideInterrupt(); \
  BaseType_t tband_port_key = 0;                     \
  if (tband_port_in_irq) {                           \
    tband_port_key = taskENTER_CRITICAL_FROM_ISR();  \
  } else {                                           \
    taskENTER_CRITICAL();                            \
    (void)tband_port_key;                            \
  }

#define tband_portEXIT_CRITICAL_FROM_ANY()           \
  if (tband_port_in_irq) {                           \
    taskEXIT_CRITICAL_FROM_ISR(tband_port_key);      \
  } else {                                           \
    taskEXIT_CRITICAL();                             \
  }

Critical Sections & Mulit-core Support

No adaption of a critical section implementation is necessary when moving to a multi-core configuration, as Tonbandgerät protects all static resources that are accessed from multiple cores with spinlocks.

Configuration

Tonbandgerät requires a user-provided tband_config.h header file, where the following configuration macros can be set:

tband_configENABLE:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable Tonbandgerät. If disabled, all code is excluded to save space.

Example:

// tband_config.h:
#define tband_configENABLE 1 

tband_configMAX_STR_LEN;

  • Possible Values: 1+
  • Default: 20

Maximum string length that Tonbandgerät will serialize and trace. Serves as a safeguard against incorrectly terminated strings, and helps providing a static upper bound on worst-case trace hook execution time.

tband_configTRACE_DROP_CNT_EVERY:

  • Possible Values: 0+
  • Default: 50

In addition to sending the dropped event counter after an event was dropped, Tonbandgerät will also serialize and trace the number of dropped events after ever tband_configTRACE_DROP_CNT_EVERY normal tracing events. Set to zero to disable periode dropped event tracing.

tband_configMARKER_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 1

Set to 0 to disable serialization and tracing of calls to event markers and value markers functions. Can be disabled to reduce the number of generated events.

tband_configISR_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 1

Set to 0 to disable serialization and tracing of interrupts. Can be disabled to reduce the number of generated events.


Metadata Buffer Config:

tband_configUSE_METADATA_BUF:

  • Possible Values: 0, 1
  • Default: 1

Enables the metadata buffer if set to 1.

tband_configMETADATA_BUF_SIZE:

  • Possible Values: 1+
  • Default: 256

Size of the metadata buffer in bytes, if enabled.


Streaming Backend Config:

tband_configUSE_BACKEND_STREAMING:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable the streaming backend. Note that exactly one backend must be enabled!


Snapshot Backend Config:

tband_configUSE_BACKEND_SNAPSHOT:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable the snapshot backend. Note that exactly one backend must be enabled!

tband_configBACKEND_SNAPSHOT_BUF_SIZE:

  • Possible Values: 1+
  • Default: 32768

Size of the per-core snapshot buffer in bytes, if enabled.


Post-Mortem Backend Config:

tband_configUSE_BACKEND_POST_MORTEM:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable the post-mortem backend. Note that exactly one backend must be enabled!

NOT YET IMPLEMENTED


External Backend Config:

tband_configUSE_BACKEND_EXTERNAL:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable the external backend. Note that exactly one backend must be enabled!


FreeRTOS Tracing Config:

tband_configFREERTOS_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 0

Set to 1 to enable FreeRTOS tracing.

tband_configFREERTOS_TASK_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 1

Set to 0 to disable serialization and tracing of FreeRTOS task scheduling and execution. Can be disabled to reduce the number of generated events.

tband_configFREERTOS_QUEUE_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 1

Set to 0 to disable serialization and tracing of FreeRTOS queue operatons. Can be disabled to reduce the number of generated events.

tband_configFREERTOS_STREAM_BUFFER_TRACE_ENABLE:

  • Possible Values: 0, 1
  • Default: 1

Set to 0 to disable serialization and tracing of FreeRTOS stream buffer operatons. Can be disabled to reduce the number of generated events.

NOT YET IMPLEMENTED

Instrumenting Your Code

After Tonbandgerät has been installed, configured, and ported, you can start to collect trace events by instrumenting your code.

These tracing event are split into two main groups. Firstly, there are base tracing events that can be used in any firmware project. These consist of:

Then there are FreeRTOS-specific tracing events:

Event Markers

Event markers are the most basic type of instrumentation. They allow you to quickly and easily trace both instantaneous and span events. Each marker is uniquely identified by a 32-bit, user-selectable ID. Note that markers are global, and can be started/stopped/triggered from any context.

A marker does not have to be initialized to be used, but can optionally be named. They support (strictly) nested span events.

Any instant or (begin of a) span event can feature a string message. Note that long messages require significant tracing resources and are best avoided if not strictly needed.

Example

Consider the following (fictional) firmware. An external sensor triggers, once ready, an interrupt. Inside the handler, a call to the sensor_do_work() function is scheduled, which in turn fetches the data from the sensor and does some DSP processing involving an FFT.

To gain some insight in the timing and operation of this code, it is instrumented with Tonbandgerät event markers. First, during setup, the event markers with IDs 0 and 1 are named. In the interrupt, an instant event is traced. In the callback, the fetching of sensor data and DSP calculation are wrapped in span event markers.

#include "tband.h"

#define MARKER_SENSOR  0
#define MARKER_DSP     1

// Setup:
void setup(void) {
    tband_evtmarker_name(MARKER_SENSOR, "sensor");
    tband_evtmarker_name(MARKER_DSP, "dsp");
}

// Sensor Ready ISR:
void isr(void) {
    tband_evtmarker(MARKER_SENSOR, "rdy");
    schedule_work(sensor_do_work);
}

// Sensor data readout/processing:
void sensor_do_work(void) {
    tband_evtmarker_begin(MARKER_SENSOR, "acq");
    collect_data_from_sensor();
    tband_evtmarker_end(MARKER_SENSOR);

    tband_evtmarker_begin(MARKER_DSP, "");
    dsp1();
    tband_evtmarker_begin(MARKER_DSP, "fft");
    fft();
    tband_evtmarker_end(MARKER_DSP);
    dsp2();
    tband_evtmarker_end(MARKER_DSP);
}

This code above would produce a trace similar to the following: Eventmarker Example Trace.

Configuration

Markers are only traced if the config option tband_configMARKER_TRACE_ENABLE is enabled.

API

tband_evtmarker_name:

void tband_evtmarker_name(uint32_t id, const char *name);

Name the event marker with id id. This is a metadata event. If the metadata buffer is enabled, it can be emitted at any time before or during the tracing session.

tband_evtmarker:

void tband_evtmarker(uint32_t id, const char *msg);

Trace an instant event.

tband_evtmarker_begin:

void tband_evtmarker_begin(uint32_t id, const char *msg);

Trace the beginning of a span event.

tband_evtmarker_end:

void tband_evtmarker_end(uint32_t id);

Trace the end of a span event.

Value Markers

Value markers allow you to track how a single numeric int64_t value changes over time. Each marker is uniquely identified by a 32-bit, user-selectable ID. Note that markers are global and can be updated from any context.

A marker does not have to be initialized to be used, but can optionally be named.

💡 Note

Internally, Tonbandgerät uses a varlen encoding scheme for numeric values, so don’t be appalled by the idea of using a 64-bit trace message to track your 8 bit value. Small values will only require a few bytes.

Example

Consider the following firmware snippet that implements some for of buffer. The functions to add and remove data from the buffer are instrumented with value markers to track how much data the buffer contains at any given time.

#include "tband.h"

#define MARKER_BUF  0

// Setup:
void setup(void) {
    tband_valmarker_name(MARKER_BUF, "buf");
}

// Add bytes to buffer:
void add_to_buf(struct buf *b, const uint8_t *data, size_t len) {
    tband_valmarker(MARKER_BUF, buf.len + len);
    // ...
}

// Read & remove bytes from buffer:
void remove_from_buf(struct buf *b, uint8_t *data, size_t len) {
    tband_valmarker(MARKER_BUF, buf.len - len);
    // ...
}

A trace of this imaginary fimware would like this: Valmarker Example Trace.

Configuration

Markers are only traced if the config option tband_configMARKER_TRACE_ENABLE is enabled.

API

tband_valmarker_name:

void tband_valmarker_name(uint32_t id, const char *name);

Name the value marker with id id. This is a metadata event. If the metadata buffer is enabled, it can be emitted at any time before or during the tracing session.

tband_valmarker:

void tband_valmarker(uint32_t id, int64_t val);

Trace a new value.

Interrupts

Interrupts can be traced with an API that is rather similar to normal general purpose value markers. They are also identified by a 32-bit ID, and can optionally be named but don’t otherwise require any kind of configuration.

Note that unlike event markers, they are local to one core. If two cores share an interrupt, you will have to name it on both cores.

💡 Note

Tracing of frequent interrupts may require significant resources.

Example

In the example below, an interrupt routine is being traced:

#include "tband.h"

#define TICK_ISR_ID

// Setup:
void setup(void) {
    tband_isr_name(TICK_ISR_ID, "tick");
}

void systick_isr(void) {
    tband_isr_enter(TICK_ISR_ID);
    // ...
    tband_isr_exit(TICK_ISR_ID);
}

ISRs are visualized next to the core they are associated with. The example below shows a trace of the SysTick interrupt while FreeRTOS is running:

Interrupt Example Trace.

Configuration

Interrupts are only traced if the config option tband_configISR_TRACE_ENABLE is enabled.

API

tband_isr_name:

void tband_isr_name(uint32_t id, const char *name);

Name the interrupt with id id. This is a metadata event. If the metadata buffer is enabled, it can be emitted at any time before or during the tracing session.

tband_isr_enter:

void tband_isr_enter(uint32_t id);

Trace the beginning of an interrupt.

tband_isr_exit:

void tband_isr_exit(uint32_t id);

Trace the end of an interrupt.

FreeRTOS Tracing

todo

FreeRTOS Task Tracing

todo

FreeRTOS Resource Tracing

todo

FreeRTOS Task-local markers

TODO

Trace Backends

TODO

The Metadata Buffer

TODO

The STREAMING Backend

TODO

The SNAPSHOT Backend

TODO

The POST_MORTEM Backend

🚧 Work-In-Progress

The Post-mortem backend has not been implemented yet.

Handling of dropped events

TODO

Multi-core Support

TODO

Viewing Traces

The core trace decoder & interpreter is written in rust and can be found here. It emits the native protobuffer format of Google’s perfetto, an in-browser trace viewer made for traces much bigger and denser than what this tool will ever generate.

To use this converter, two UIs are provided:

  • The tband-cli command-line tool, and
  • the web converter, an in-browser converter which embeds the converter cross-compiled to WASM.

A screenshot of perfetto, with a Tonbandgerät trace loaded

CLI Trace Converter: tband-cli

Compile & Install

The trace converter and CLI is written in rust 🦀. To compile and run tband-cli locally, first you need to download and install rust if you don’t already have it.

Then, open the ./conv/tband-cli folder.

Now you have two options. To compile & install tband-cli, run:

> cargo install --path .

This will compile the tool, and place the finished executable in your local cargo binary direction. Where that is depends on your system. Most likely you will have to add it to your PATH.

To build and run the CLI tool directly from the repository, type:

> cargo run --

Any command line arguments you want to provide need to go after the -- separator.

Commands

The tool features 5 main commands:

> tband-cli --help
Usage: tband-cli [OPTIONS] <COMMAND>

Commands:
  conv        Convert trace recording
  serve       Serve trace file for perfetto
  completion  Print completion script for specified shell
  dump        Dump trace recording
  help        Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose...  
  -h, --help        Print help
  -V, --version     Print version

conv

The conv command is where most of the action is. It takes one or more trace files, decodes them, and converts them to the perfetto format:

> tband-cli conv --help
Convert trace recording

Usage: tband-cli conv [OPTIONS] [INPUT]...

Arguments:
  [INPUT]...
          Input files with optional core id.
          
          For split multi-core recording, append core id to file name as such: filename@core_id

Options:
  -f, --format <FORMAT>
          Input format
          
          [default: bin]
          [possible values: hex, bin]

  -m, --mode <MODE>
          TraceMode
          
          [default: free-rtos]
          [possible values: bare-metal, free-rtos]

  -c, --core-count <CORE_COUNT>
          Number of cores of target
          
          [default: 1]

  -o, --output <OUTPUT>
          Location to store converted trace

      --open
          Open converted trace in perfetto

      --serve
          Serve converted trace for perfetto

  -h, --help
          Print help (see a summary with '-h')

It supports both binary and hex trace files, and both bare-metal and FreeRTOS traces. After conversion, the tool can save the result to a file (--output), open it directly in perfetto (--open), or provide a link and host a local server to provide the trace to perfetto (--serve).

The input files must be given last. If converting a multi-core trace split into seperate files, append the core id to each file as follows:

> tband-cli conv --format=bin --core-count=2 --open core0_trace.bin@0 core1_trace.bin@1

dump

The dump command takes a single trace file, decodes it, and dumps its content in human-readable form to stdout.

> tband-cli dump --help
Dump trace recording

Usage: tband-cli dump [OPTIONS] --mode <MODE> --input <INPUT>

Options:
  -f, --format <FORMAT>
          Input format
          
          [default: bin]
          [possible values: hex, bin]

  -m, --mode <MODE>
          TraceMode
          
          [possible values: bare-metal, free-rtos]

  -i, --input <INPUT>
          Input file with optional core id.
          
          For split multi-core recording, append core id to file name as such: filename@core_id

  -h, --help
          Print help (see a summary with '-h')

completion

The completion command can generate shell completion scripts for most common shells. How you can install a completion script depends on your shell and system configuration.

> tband-cli completion --help
Print completion script for specified shell

Usage: tband-cli completion <SHELL>

Arguments:
  <SHELL>  Style of completion script to generate [possible values: bash, elvish, fish, powershell, zsh]

Options:
  -h, --help  Print help

Web Trace Converter

The web converter can be found at:

https://schilk.co/Tonbandgeraet/

Overview

Traces can be added via file upload, or pasted as a hex/base64 string. Single-file traces can be uploaded, converted, and opened in using the 1-CLICK-CONVERT button. For multi-file traces, upload and add all traces, then press CONVERT, followed by OPEN to open the trace in perfetto or DOWNLOAD to download the perfetto-ready trace.

💡 Note

The first time you use the web converter to re-direct to perfetto, you will get a popup asking if you trust schilk.co. If you do trust me, the trace will open. If you don’t, you will manually have to download the trace from the converter and upload it to perfetto.

The Tonbandgerät Binary Trace Format

To store trace events in a compact binary format, Tonbandgerät uses zero-delimited COBS frames each containing a single trace event. The set of trace event types (each identified by an 8 bit id) and their respective structure is fixed.

Trace Event Structure

Inside the COBS frame, each event type is structure as follows:

  • An 8-bit ID, which identifies the type of trace event and hence internal structure of the fields to follow.
  • Zero or more required known-length fields, which are always present in every event instance of this type.
  • One of the following:
    • Zero or more optional known-length fields, which (under some restrictions) are not required to be present in every event instance of this type.
    • One variable-length field.
    • Nothing

Some examples of valid event structures follow, where each field is denoted as name:type with optional fields enclosed in square braces ([]), and variable-length fields appended with an elipses (...):

id:u8
id:u8 field1:u64
id:u8 field1:u64 field2:u8
id:u8 field1:u64 (opt:s64)
id:u8 field1:u64 (opt:s64) (abc:u8)
id:u8 (opt:s64)  (abc:u8)
id:u8 field1:64  name:str...
id:u8 name:str...

See here for a description of the specific field types.

Known-length fields

Known-length fields are fields whose encoded length is either fixed (such as u8 fields, which are always 1 byte) or can be determined from the encoded data (such as u32 fields, whose varlen encoding scheme identifies the last byte of a field by having its MSB be zero, see field types).

Required fields

Required fields are straight forward to encode and decode: The encoder serialises all fields and inserts them sequentially into the event frame. Since they are all required to be known-length fields, and the decoder is aware of the set of required fields present, it can simply identify and decode them.

Optional fields

Optional fields must not necessarily be included in an event. If they are present, they are encoded as ususal. If they are not present, they are simply omitted. Importantly however, they must always appear in the order in which they are defined and cannot be excluded and included freely without restriction:

If an event type contains \(N\) optional fields, an event instance may only exclude the last \(0 \leq n \leq N\) fields. In other words, if an event type contains the optional fields \(A\), \(B\), and \(C\), valid event instances could only take on one of the following layouts:

  • \(\emptyset\)
  • \(A\)
  • \(A\), \(B\)
  • \(A\), \(B\), \(C\)

This restriction comes from the fact that, to save space, there is no field identification mechanism in the trace format. Instead, while decoding optional events, the decoder will simply continue to decode until it reaches the end of the frame, and mark all events that were not seen as not present.

Variable-length fields

Variable length fields (such as strings) are encoded without any delimiter. Since they must appear as the last field in an event, the decoder assumes that all bytes that follow the last required field are a part of the variable-length field.

COBS Framing

The tracer uses COBS (Consistent Overhead Byte Stuffing, wikipedia) framing to separate binary trace events that have been stored or transmitted together. Specifically, the COBS algorithm removes all zeroes from a binary message in a reversible fashion, with only minimal overhead. Zeroes are then used to delimit individual trace messages.

Specifically, after COBS framing, an \( N \neq 0 \) byte message will be at most \( 1 + \left\lceil \frac{N}{254} \right\rceil + N \) bytes long, including a trailing zero for delimination.

Please read the above article for a more precise specification, but roughly speaking this is done by replacing all zeroes with a pointer to the next zero:

Original values:          0x01  0x02  0x00  0x04  0x00  0x05

                      +-----> +3 -----+  +-> +2 --+  +-> +2 --+
                      |               |  |        |  |        |
COBS framed:       0x03   0x01  0x02  0x02  0x04  0x02  0x05  0x00

                   Start  Data  Data  Zero  Data  Zero  Data  Delim.

Special care has to be given to a run of 254 or more consecutive non-zero bytes, as an 8-bit pointer is not sufficient to point beyond such a run. In this case, an additional pointer byte is added that does not correspond to a zero in the original data:

Original values:          0x00  0x01  0x02 ... 0xFD  0xFE         0xFF

                      +->-+  +----------> +255 ------------+  +--> +2 --+
                      |   |  |                             |  |         |
COBS framed:       0x01   0xFF  0x01  0x02 ... 0xFD  0xFE  0x02   0xFF  0x00

                  Start   Zero  Data  Zero ... Data  Zero  Extra  Data  Delim.

When decoding, the value pointed at by a 0xFF pointer must therefor not be decoded to a zero but only be interpreted as a another pointer.

Because trace events are usually very short, this means that most can be framed with only two bytes of overhead.

Varlen Encoding

Many trace events feature large fields that often contain only a very small value. For example, all interrupt-related events feature a 32-bit interrupt ID, but most microcontrollers will at most have a few hundred interrupts. This would mean that most bytes of most numeric fields are zero most of the time, wasting trace storage capacity or transfer bandwidth.

To combat this, Tonbandgerät uses the same specific form of variable-length (varlen) encoding that is also by UTF-8 for most numeric values:

Values are split into 7-bit septets, and are encoding starting with the least significant septet. Each septet is encoded as an 8-bit value, consisting of the septet in the lower bits, and a control bit in the most significant bit position that is set to 1 if there are more septets to follow, or 0 if this is the last septet and all following bits should be assumed to be zero.

For example, consider the 32-bit value 0x5. With the scheme above, it is encoded as a single byte:

        +---> First 7 bits
     ___|___
    00000101
    |
    +--> No more bits to follow.

The value 0xFF requires more than seven bits and therefor is split into two bytes:

        +---> First 7 bits          +---> Next 7 bits
     ___|___                     ___|___
    11111111                    00000001
    |                           |
    +--> More bits to follow.   +--> No more bits to follow.

This system trades a much improved average message length for a longer worst-case message size.

Trace Event Fields

The following field types are supported in trace packages:

u8

An unsigned, 8-bit value. Encoded as-is.

u32

An unsigned, 32-bit value. Encoded as a varlen unsigned value.

u64

An unsigned, 64-bit value. Encoded as a varlen unsigned value.

s64

A signed 64-bit value. Encoded in sign-magnitude form as a varlen value. When encoded, the least significant bit indicates the sign of the value, with a 1 marking the value as negative. All other bits (once shifted to the right by one) give the magnitude of the value. The only exception is INT64_MIN, whose magnitude would overflow a 63 bit representation. It is instead encoded as negative zero (0x01).

This is done to efficiently encode small negative integers, which would otherwise always require 10 bytes after varlen encoding due to the set MSB in the twos-complement representation of negative values.

str

A varlen string. Encoded as-is. Must be the final value in the frame. Length given by end of frame.

Code Generation

The set of possible tracing events and their fields is defined in a simple python code generation script here. Based on this, the c event encoder, an event event decoder test file, the rust event decoder, and the event index documentation is generated.

Example Output

Consider the following isr_name event as an example:

Field Name:idisr_idname
Field Type:u8u32str
Note:0x03requiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

C Encoder

First, the code generator will emit a macro that specifies if the event is a metadata event (meaning it should be appended to the metadata buffer) and a macro that gives the maximum framed length of the event so that a buffer can be pre-allocated. Then, a function is generated that takes the event fields and encodes them into a buffer, returning the actual encoded length of the message (including a trailing zero termination).

#define EVT_ISR_NAME_IS_METADATA (1)
#define EVT_ISR_NAME_MAXLEN (COBS_MAXLEN((6 + tband_configMAX_STR_LEN)))
static inline size_t encode_isr_name(uint8_t buf[EVT_ISR_NAME_MAXLEN], uint32_t isr_id, const char *name) {/* .. */}

Rust Decoder

The rust code generator emits a struct for each field, and a decoder function that attempts to reconstruct the event from a given buffer:

#[derive(Debug, Clone, Serialize)]
pub struct BaseIsrNameEvt {
    pub isr_id: u32,
    pub name: String,
}

impl BaseIsrNameEvt {
    fn decode(buf: &[u8], current_idx: &mut usize) -> anyhow::Result<RawEvt> { /* .. */ }
}

Trace Events Index

Index of all Tonbandgerät tracing events.

💡 Note

This file is automatically generated. See code generation documentation for details.

Base:

Base/core_id:

Field Name:idtscore_id
Field Type:u8u64u32
Note:0x00requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

Base/dropped_evt_cnt:

Field Name:idtscnt
Field Type:u8u64u32
Note:0x01requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

Base/ts_resolution_ns:

Field Name:idns_per_ts
Field Type:u8u64
Note:0x02required
  • Metadata: yes
  • Max length (unframed): 11 bytes

Base/isr_name:

Field Name:idisr_idname
Field Type:u8u32str
Note:0x03requiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

Base/isr_enter:

Field Name:idtsisr_id
Field Type:u8u64u32
Note:0x04requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

Base/isr_exit:

Field Name:idtsisr_id
Field Type:u8u64u32
Note:0x05requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

Base/evtmarker_name:

Field Name:idevtmarker_idname
Field Type:u8u32str
Note:0x06requiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

Base/evtmarker:

Field Name:idtsevtmarker_idmsg
Field Type:u8u64u32str
Note:0x07requiredrequiredvarlen
  • Metadata: no
  • Max length (unframed): 16 bytes + varlen field

Base/evtmarker_begin:

Field Name:idtsevtmarker_idmsg
Field Type:u8u64u32str
Note:0x08requiredrequiredvarlen
  • Metadata: no
  • Max length (unframed): 16 bytes + varlen field

Base/evtmarker_end:

Field Name:idtsevtmarker_id
Field Type:u8u64u32
Note:0x09requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

Base/valmarker_name:

Field Name:idvalmarker_idname
Field Type:u8u32str
Note:0x0Arequiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

Base/valmarker:

Field Name:idtsvalmarker_idval
Field Type:u8u64u32s64
Note:0x0Brequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 26 bytes

FreeRTOS:

FreeRTOS Enums:

FrQueueKind:

  • 0x00: FRQK_QUEUE
  • 0x01: FRQK_COUNTING_SEMPHR
  • 0x02: FRQK_BINARY_SEMPHR
  • 0x03: FRQK_MUTEX
  • 0x04: FRQK_RECURSIVE_MUTEX
  • 0x05: FRQK_QUEUE_SET

FrStreamBufferKind:

  • 0x00: FRSBK_STREAM_BUFFER
  • 0x01: FRSBK_MESSAGE_BUFFER

FreeRTOS/task_switched_in:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x54requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_to_rdy_state:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x55requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_resumed:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x56requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_resumed_from_isr:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x57requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_suspended:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x58requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/curtask_delay:

Field Name:idtsticks
Field Type:u8u64u32
Note:0x59requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/curtask_delay_until:

Field Name:idtstime_to_wake
Field Type:u8u64u32
Note:0x5Arequiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_priority_set:

Field Name:idtstask_idpriority
Field Type:u8u64u32u32
Note:0x5Brequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/task_priority_inherit:

Field Name:idtstask_idpriority
Field Type:u8u64u32u32
Note:0x5Crequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/task_priority_disinherit:

Field Name:idtstask_idpriority
Field Type:u8u64u32u32
Note:0x5Drequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/task_created:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x5Erequiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_name:

Field Name:idtask_idname
Field Type:u8u32str
Note:0x5Frequiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

FreeRTOS/task_is_idle_task:

Field Name:idtask_idcore_id
Field Type:u8u32u32
Note:0x60requiredrequired
  • Metadata: yes
  • Max length (unframed): 11 bytes

FreeRTOS/task_is_timer_task:

Field Name:idtask_id
Field Type:u8u32
Note:0x61required
  • Metadata: yes
  • Max length (unframed): 6 bytes

FreeRTOS/task_deleted:

Field Name:idtstask_id
Field Type:u8u64u32
Note:0x62requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/queue_created:

Field Name:idtsqueue_id
Field Type:u8u64u32
Note:0x63requiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/queue_name:

Field Name:idqueue_idname
Field Type:u8u32str
Note:0x64requiredvarlen
  • Metadata: yes
  • Max length (unframed): 6 bytes + varlen field

FreeRTOS/queue_kind:

Field Name:idqueue_idkind
Field Type:u8u32u8 enum FrQueueKind
Note:0x65requiredrequired
  • Metadata: yes
  • Max length (unframed): 7 bytes

FreeRTOS/queue_send:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x66requiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_send_from_isr:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x67requiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_overwrite:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x68requiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_overwrite_from_isr:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x69requiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_receive:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x6Arequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_receive_from_isr:

Field Name:idtsqueue_idlen_after
Field Type:u8u64u32u32
Note:0x6Brequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/queue_reset:

Field Name:idtsqueue_id
Field Type:u8u64u32
Note:0x6Crequiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/curtask_block_on_queue_peek:

Field Name:idtsqueue_idticks_to_wait
Field Type:u8u64u32u32
Note:0x6Drequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/curtask_block_on_queue_send:

Field Name:idtsqueue_idticks_to_wait
Field Type:u8u64u32u32
Note:0x6Erequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/curtask_block_on_queue_receive:

Field Name:idtsqueue_idticks_to_wait
Field Type:u8u64u32u32
Note:0x6Frequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 21 bytes

FreeRTOS/task_evtmarker_name:

Field Name:idevtmarker_idtask_idname
Field Type:u8u32u32str
Note:0x7Arequiredrequiredvarlen
  • Metadata: yes
  • Max length (unframed): 11 bytes + varlen field

FreeRTOS/task_evtmarker:

Field Name:idtsevtmarker_idmsg
Field Type:u8u64u32str
Note:0x7Brequiredrequiredvarlen
  • Metadata: no
  • Max length (unframed): 16 bytes + varlen field

FreeRTOS/task_evtmarker_begin:

Field Name:idtsevtmarker_idmsg
Field Type:u8u64u32str
Note:0x7Crequiredrequiredvarlen
  • Metadata: no
  • Max length (unframed): 16 bytes + varlen field

FreeRTOS/task_evtmarker_end:

Field Name:idtsevtmarker_id
Field Type:u8u64u32
Note:0x7Drequiredrequired
  • Metadata: no
  • Max length (unframed): 16 bytes

FreeRTOS/task_valmarker_name:

Field Name:idvalmarker_idtask_idname
Field Type:u8u32u32str
Note:0x7Erequiredrequiredvarlen
  • Metadata: yes
  • Max length (unframed): 11 bytes + varlen field

FreeRTOS/task_valmarker:

Field Name:idtsvalmarker_idval
Field Type:u8u64u32s64
Note:0x7Frequiredrequiredrequired
  • Metadata: no
  • Max length (unframed): 26 bytes

Technologies Overview

TODO

Target

WASM

TODO

Website

Perfetto & Synthetto

TODO