Tonbandgerät
Tonbandgerät /ËtoËnbantÉĄÉrÉËt/ n.
- German for audio tape recorder.
- 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
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.
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:
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:
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:
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.
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 ./tools/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>
Arguments:
<INPUT>
Input file 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
[possible values: bare-metal, free-rtos]
-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, or0
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:
- 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:
- Metadata: no
- Max length (unframed): 16 bytes
Base/dropped_evt_cnt:
- Metadata: no
- Max length (unframed): 16 bytes
Base/ts_resolution_ns:
- Metadata: yes
- Max length (unframed): 11 bytes
Base/isr_name:
- Metadata: yes
- Max length (unframed): 6 bytes + varlen field
Base/isr_enter:
- Metadata: no
- Max length (unframed): 16 bytes
Base/isr_exit:
- Metadata: no
- Max length (unframed): 16 bytes
Base/evtmarker_name:
- Metadata: yes
- Max length (unframed): 6 bytes + varlen field
Base/evtmarker:
- Metadata: no
- Max length (unframed): 16 bytes + varlen field
Base/evtmarker_begin:
- Metadata: no
- Max length (unframed): 16 bytes + varlen field
Base/evtmarker_end:
- Metadata: no
- Max length (unframed): 16 bytes
Base/valmarker_name:
- Metadata: yes
- Max length (unframed): 6 bytes + varlen field
Base/valmarker:
- 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:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_to_rdy_state:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_resumed:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_resumed_from_isr:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_suspended:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/curtask_delay:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/curtask_delay_until:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_priority_set:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/task_priority_inherit:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/task_priority_disinherit:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/task_created:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_name:
- Metadata: yes
- Max length (unframed): 6 bytes + varlen field
FreeRTOS/task_is_idle_task:
- Metadata: yes
- Max length (unframed): 11 bytes
FreeRTOS/task_is_timer_task:
- Metadata: yes
- Max length (unframed): 6 bytes
FreeRTOS/task_deleted:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/queue_created:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/queue_name:
- Metadata: yes
- Max length (unframed): 6 bytes + varlen field
FreeRTOS/queue_kind:
Field Name: | id | queue_id | kind |
---|---|---|---|
Field Type: | u8 | u32 | u8 enum FrQueueKind |
Note: | 0x65 | required | required |
- Metadata: yes
- Max length (unframed): 7 bytes
FreeRTOS/queue_send:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_send_from_isr:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_overwrite:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_overwrite_from_isr:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_receive:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_receive_from_isr:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_reset:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/curtask_block_on_queue_peek:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/curtask_block_on_queue_send:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/curtask_block_on_queue_receive:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/queue_cur_length:
- Metadata: no
- Max length (unframed): 21 bytes
FreeRTOS/task_evtmarker_name:
- Metadata: yes
- Max length (unframed): 11 bytes + varlen field
FreeRTOS/task_evtmarker:
- Metadata: no
- Max length (unframed): 16 bytes + varlen field
FreeRTOS/task_evtmarker_begin:
- Metadata: no
- Max length (unframed): 16 bytes + varlen field
FreeRTOS/task_evtmarker_end:
- Metadata: no
- Max length (unframed): 16 bytes
FreeRTOS/task_valmarker_name:
- Metadata: yes
- Max length (unframed): 11 bytes + varlen field
FreeRTOS/task_valmarker:
- Metadata: no
- Max length (unframed): 26 bytes
Technologies Overview
TODO
Target
WASM
TODO
Website
Perfetto & Synthetto
TODO