A lightweight C++ library that lets you capture traces on bare‑metal or embedded systems and emit them in the CTF 1.8 format – ready for consumption by tools such as babeltrace2.
The library is intentionally minimal so it can be dropped into any RTOS, bootloader, or no‑OS project.
Why CTF? The Linux
perfsubsystem and many modern trace analyzers rely on the CTF format. By emitting CTF from your firmware you get instant access to a rich ecosystem of visualizers, filters, and exporters without having to write custom parsers.
- Introduction
- Features
- Quick Start
- API Reference
traceCollectorTrace<T>– typed trace wrapper- Platform abstraction (
tracePlatform)
- Transferring Packets
- Processing & Analysis
- Advanced Usage
- Custom synchronization
- Multi‑stream support
- FAQ / Troubleshooting
- License
Embedded and bare‑metal firmware is getting increasingly complex. To debug performance issues, race conditions or to understand system behaviour you often need a trace collector that:
- Works without an OS.
- Emits data in a standard format.
- Is easy to drop into any codebase.
This library fulfills those needs by:
| Feature | Description |
|---|---|
| CTF 1.8 output | Portable binary traces consumable by babeltrace2, tracing‑tools, and custom parsers. |
| Zero runtime cost | All trace types are generated at compile time – no dynamic allocation. |
| Header‑only API | No external dependencies beyond the standard library and CMake. |
| YAML configuration | Define traces, parameters, IDs once in a human‑readable file. |
# traces.yaml
- traces:
- name: loopCount
id: 1
params:
- name: count
type: uint8_tTip – Keep the file in your project root or a dedicated
docs/folder.
Add the following to your top‑level CMakeLists.txt:
# Path to the YAML description
set(TRACE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/traces.yaml")
# Where generated files should go
set(TRACE_GENERATED_OUT_DIR "${CMAKE_BINARY_DIR}/generated")
add_subdirectory(path/to/trace-capture-lib)
# Expose the header for your application
target_include_directories(my_app PRIVATE ${TRACE_GENERATED_OUT_DIR})When you run CMake, it will:
- Parse
traces.yaml. - Generate
trace_types.hppcontaining typed structs (e.g.,loopCount_t) and helper macros. - Create a metadata file (
metadata.json) for babeltrace.
#include "trace_types.hpp" // generated header
#include "trace_collector.h"
int main() {
/* ---------- Init Collector ---------- */
auto& tc = traceCollector::getInstance();
// Provide a platform implementation (see below)
extern tracePlatform g_platform; // defined elsewhere
tc.setStreamId(42); // any uint32_t stream ID
tc.setPlatformIntf(&g_platform);
/* ---------- Emit an trace ---------- */
Trace<loopCount_t> evt;
auto* p = evt.getParam(); // get the underlying struct
p->count = 7; // fill parameters
tc.pushTrace(&evt); // send to collector
/* ---------- Transfer packet ---------- */
auto pkt = tc.getSendPacket();
if ( pkt.has_value() ) {
auto data = pkt.value();
// e.g., write to UART, file or socket
my_transmit( reinterpret_cast<const char *>( data.data() ), data.size() );
tc.sendPacketCompleted();
}
return 0;
}Singleton pattern.
| Method | Description |
|---|---|
static traceCollector& getInstance() |
Retrieve the global instance. |
void setStreamId(uint32_t id) |
Set CTF stream ID (must be unique per trace). |
void setPlatformIntf(tracePlatform* plt) |
Provide platform‑specific timestamp & lock functions. |
bool pushTrace(const traceBase* ev) |
Queue an trace for the next packet. |
const uint8_t* getSendPacket() |
Get pointer to ready‑to‑send CTF packet. |
size_t getPacketLength() |
Length of the packet in bytes. |
void sendPacketCompleted() |
Release buffer; collector can reuse it. |
A lightweight wrapper around a generated trace type.
template<typename T>
class Trace {
public:
// Access underlying CTF struct
T* getParam();
// Optional: access metadata, timestamp, etc.
};The actual data structure is defined in trace_types.hpp, e.g.:
struct loopCount_t {
uint8_t count;
};You must provide an implementation that the collector uses for:
| Function | Purpose |
|---|---|
uint64_t getTimestamp() |
Return monotonic timestamp (e.g., from a high‑resolution timer). |
bool traceTryLock() |
Acquire exclusive lock while emitting traces. |
void traceUnlock() |
Release the lock. |
void packetLock() |
Acquire exclusive lock while emitting traces. |
void packetUnlock() |
Release the lock. |
class MyPlatform : public tracePlatform {
public:
uint64_t getTimestamp() override { return hw_get_time(); }
bool traceTryLock() override { return critical_try_section_enter(); }
void traceUnlock() override { critical_section_exit(); }
void packetLock() override { critical_section_enter(); }
void packetUnlock() override { critical_section_exit(); }
};Instantiate it once and register with the collector.
The collector batches traces into CTF packets. Typical flow:
auto pkt = inst->getSendPacket();
auto data = pkt.value();
// Example: write to file
std::ofstream out("trace.bin", std::ios::binary | std::ios::app);
out.write(reinterpret_cast<const char*>(data.data()), data.size());
// Or send over UART / network...After the packet is transmitted, call:
tc.sendPacketCompleted();This signals that the buffer can be reused for subsequent traces.
- Collect packets – Store them in a single binary file or separate
_X.binfiles. - Place metadata – The generator creates
metadata.json. Keep it next to your data:
traces/
├─ metadata.json
└─ stream.bin # concatenated packet stream
- Run Babeltrace:
babeltrace -f binary traces/stream.bin > trace.txt
# or view in the GUI:
babeltrace --gui traces/stream.binYou can also write custom Python scripts using babeltrace2’s Python bindings to filter or aggregate traces.
If your platform does not provide a global lock, you can skip it by providing no‑op functions. Just make sure the collector is thread‑safe for your use case.
void traceTryLock() override { /* nothing */ }
void traceUnlock() override { /* nothing */ }The library supports multiple CTF streams per trace.
Create separate traceCollector instances (or re‑use the singleton with different IDs) and tag each packet accordingly.
| Question | Answer |
|---|---|
| Where does the generated header go? | In ${TRACE_GENERATED_OUT_DIR} – you must add this to your include path. |
| Why are my packets empty? | Ensure setStreamId() and setPlatformIntf() are called before any pushTrace(). |
| How large can a packet be? | The collector preallocates a buffer (default 1 MiB). Increase with TRACE_PACKET_SIZE CMake variable if needed. |
| Can I use this on an RTOS? | Yes – just provide the platform callbacks. Avoid dynamic memory allocation; the library is header‑only. |
| How do I add new trace types? | Edit traces.yaml, rerun CMake, and rebuild. |
MIT © 2025 Rohit Patil