feat(esp_trace): add example to demonstrate external lib integration

This commit is contained in:
Erhan Kurubas
2026-05-15 14:55:02 +02:00
parent 4c88c77f9c
commit 41d425bf05
18 changed files with 885 additions and 4 deletions

View File

@@ -227,6 +227,20 @@ The `esp_trace` component supports integration of external trace libraries throu
- **Transport Adapters**: Handle the physical transport layer (e.g., JTAG, UART)
- **Encoder Adapters**: Handle the trace encoding/formatting (e.g., SystemView, custom formats)
> ⚠️ **Reentrancy constraint for adapter runtime callbacks**
>
> Encoder and transport callbacks invoked from the hot path — `write`, `flush` / `flush_nolock`, `read`, `take_lock` / `give_lock`, `panic_handler` — run from inside FreeRTOS trace hooks (and from ISR context for `traceISR_ENTER` / `traceISR_EXIT`). They are also called while the encoder's lock is held.
>
> Do **not** call FreeRTOS / IDF APIs that themselves trigger trace hooks from these callbacks. Anything that would emit a `trace*()` macro re-enters the tracing path: it can recurse into your own encoder, deadlock on the encoder's non-recursive spinlock, or call a task-only API from ISR context.
>
> Specifically avoid:
> - Task APIs: `vTaskDelay`, `vTaskSuspend`, `xTaskNotify*`, anything that yields.
> - Queue / semaphore / mutex APIs: `xQueueSend/Receive`, `xSemaphoreTake/Give`, `xQueueSemaphoreTake`.
> - Stream / message buffer APIs.
> - Heap allocations that may take an internal mutex.
>
> Safe building blocks for adapter code: lock-free or spinlock-only primitives (e.g. `esp_trace_lock_*`, `esp_trace_rb_*`), low-level peripheral register access, atomic operations, and `esp_rom_*` helpers. Do any heavier work (FreeRTOS APIs, allocations) only at adapter `init()` time, before the trace session is in steady state.
### Creating a Transport Adapter
Transport adapters provide the physical communication layer for trace data.
@@ -408,4 +422,5 @@ For detailed usage instructions, see:
Examples demonstrating trace usage can be found in:
- `examples/system/app_trace_basic/` - Basic application tracing
- `examples/system/sysview_tracing/` - SystemView tracing example
- `examples/system/esp_trace/` - Minimal template for integrating an external trace library (encoder + FreeRTOS hooks + vtable lock)
- `examples/system/sysview_tracing_heap_log/` - SystemView heap and log tracing example

View File

@@ -21,7 +21,13 @@ typedef struct esp_trace_transport esp_trace_transport_t;
/**
* @brief Encoder Virtual Table
*
* Defines the interface for trace encoders (libraries)
* Defines the interface for trace encoders (libraries).
*
* @warning Runtime callbacks (write, flush, take_lock, give_lock, panic_handler)
* must not call FreeRTOS / IDF APIs that themselves emit trace hooks
* (e.g. vTaskDelay, xQueue*, xSemaphore*) — doing so re-enters the
* tracing path and can deadlock on the encoder lock or crash in ISR
* context.
*/
typedef struct {
/**

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -34,6 +34,11 @@ typedef enum {
*
* Defines the interface for trace transports.
*
* @warning Runtime callbacks (read, write, flush_nolock, panic_handler) must
* not call FreeRTOS / IDF APIs that themselves emit trace hooks
* (e.g. vTaskDelay, xQueue*, xSemaphore*) — they are invoked from
* inside the encoder's lock and from ISR context, so re-entering
* the tracing path can deadlock or assert.
*/
typedef struct {
/**

View File

@@ -557,9 +557,27 @@ Good instructions on how to install, configure, and visualize data in Impulse fr
If you have problems with visualization (no data is shown or strange behaviors of zoom action are observed), you can try to delete current signal hierarchy and double-click on the necessary file or port. Eclipse will ask you to create a new signal hierarchy.
Application Examples
""""""""""""""""""""
- :example:`system/sysview_tracing` demonstrates how to trace FreeRTOS task and system events using SEGGER SystemView.
- :example:`system/sysview_tracing_heap_log` demonstrates heap allocation tracing alongside SystemView events.
.. _app_trace-gcov-source-code-coverage:
Gcov (Source Code Coverage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
In ESP-IDF projects, code coverage analysis using gcov can be done with the help of `espressif/esp_gcov <https://components.espressif.com/components/espressif/esp_gcov>`_ managed component.
.. _app_trace-integrating-a-custom-trace-library:
Integrating a Custom Trace Library
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``esp_trace`` component exposes a stable extension point (``CONFIG_ESP_TRACE_LIB_EXTERNAL``) for plugging in a third-party trace recorder without patching ESP-IDF. An external component provides an encoder adapter (registered via ``ESP_TRACE_REGISTER_ENCODER()``) and a slim ``esp_trace_freertos_impl.h`` that injects the desired FreeRTOS trace hooks. The encoder vtable also offers optional ``start`` / ``stop`` / ``flush`` and ``take_lock`` / ``give_lock`` entries dispatched from the public :cpp:func:`esp_trace_start`, :cpp:func:`esp_trace_stop`, :cpp:func:`esp_trace_flush` API.
Application Examples
""""""""""""""""""""
- :example:`system/esp_trace` is a minimal copy-paste template that wires up an external encoder, demonstrates the FreeRTOS trace-hook include-chain contract, and covers cross-core serialization through the encoder lock.

View File

@@ -557,9 +557,27 @@ Start 子命令语法:
如果你在可视化方面遇到了问题未显示数据或者缩放操作异常可以尝试删除当前的信号层次结构再双击必要的文件或端口。Eclipse 会请求创建新的信号层次结构。
应用示例
""""""""
- :example:`system/sysview_tracing` 演示了如何使用 SEGGER SystemView 跟踪 FreeRTOS 任务和系统事件。
- :example:`system/sysview_tracing_heap_log` 演示了如何在 SystemView 事件的同时跟踪堆内存分配。
.. _app_trace-gcov-source-code-coverage:
Gcov源代码覆盖率
^^^^^^^^^^^^^^^^^^^^^^^^^^^
在 ESP-IDF 项目中,可以借助 `espressif/esp_gcov <https://components.espressif.com/components/espressif/esp_gcov>`_ 托管组件使用 gcov 进行代码覆盖率分析。
.. _app_trace-integrating-a-custom-trace-library:
集成自定义跟踪库
^^^^^^^^^^^^^^^^
``esp_trace`` 组件提供了稳定的扩展点 (``CONFIG_ESP_TRACE_LIB_EXTERNAL``),允许在不修改 ESP-IDF 的情况下接入第三方跟踪记录器。外部组件需提供一个编码器适配器(通过 ``ESP_TRACE_REGISTER_ENCODER()`` 注册)以及一个轻量的 ``esp_trace_freertos_impl.h``,用于注入所需的 FreeRTOS 跟踪钩子。编码器虚表还提供可选的 ``start`` / ``stop`` / ``flush````take_lock`` / ``give_lock`` 入口,由公共 API :cpp:func:`esp_trace_start`:cpp:func:`esp_trace_stop`:cpp:func:`esp_trace_flush` 调度。
应用示例
""""""""
- :example:`system/esp_trace` 是一个最简的复制粘贴模板,演示如何接入外部编码器、说明 FreeRTOS 跟踪钩子头文件的包含链约束,以及通过编码器锁实现多核序列化。

View File

@@ -63,6 +63,24 @@ examples/system/esp_timer:
depends_components:
- esp_timer
examples/system/esp_trace:
disable:
- if: SOC_USB_SERIAL_JTAG_SUPPORTED != 1
reason: example transport is USB Serial JTAG
disable_test:
- if: IDF_TARGET == "esp32h21"
temporary: true
reason: lack of runners
- if: IDF_TARGET == "esp32h4"
temporary: true
reason: lack of runners
- if: IDF_TARGET == "esp32s31"
temporary: true
reason: lack of runners
depends_components:
- esp_trace
- freertos
examples/system/eventfd:
disable:
- if: SOC_GPTIMER_SUPPORTED != 1 and (IDF_TARGET != "esp32" and (NIGHTLY_RUN != "1" or IDF_TARGET == "linux"))

View File

@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(esp_trace_example)

View File

@@ -0,0 +1,207 @@
| Supported Targets | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S3 | ESP32-S31 |
| ----------------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | --------- |
# ESP Trace External Library Integration Example
This example shows the **minimal** set of files and configuration needed to plug a third-party trace library into the [`esp_trace`](../../../components/esp_trace) component using the public `CONFIG_ESP_TRACE_LIB_EXTERNAL` extension point. It is meant as a copy-paste starting point for vendors and users who want to integrate their own trace recorder (e.g. Percepio TraceRecorder, a custom CTF emitter, a printf-style logger, …) without patching ESP-IDF itself.
The example covers:
* How to expose a custom **encoder** to `esp_trace` via `ESP_TRACE_REGISTER_ENCODER()`.
* How to provide an **`esp_trace_freertos_impl.h`** header that injects your trace hooks into FreeRTOS without breaking the `FreeRTOSConfig.h` include chain.
* How to wire everything up in **CMake** so the registration is not stripped by the linker, and so your header is visible to the FreeRTOS kernel.
* How to override the trace session parameters from the application via **`esp_trace_get_user_params()`**.
## How to Use
### Hardware Required
By default this example targets devices with built-in USB Serial JTAG (ESP32-C3/C5/C6/C61/H2/P4/S3, …). For other transports, see [Changing the Transport](#changing-the-transport).
You only need a development board and a USB cable.
### Configure the Project
```
idf.py set-target <esp_target>
idf.py menuconfig
```
The defaults in [`sdkconfig.defaults`](sdkconfig.defaults) already enable everything the example needs:
```ini
CONFIG_ESP_TRACE_ENABLE=y
CONFIG_ESP_TRACE_LIB_EXTERNAL=y # use an external encoder
CONFIG_ESP_TRACE_TRANSPORT_USB_SERIAL_JTAG=y # transport: built-in USB-Serial-JTAG
CONFIG_ESP_TRACE_TS_SOURCE_ESP_TIMER=y # timestamp source
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y # free up USB-Serial-JTAG for trace
```
`CONFIG_ESP_CONSOLE_SECONDARY_NONE=y` is required so the USB-Serial-JTAG peripheral is not claimed by the secondary console — otherwise the transport option is not selectable.
### Build, Flash, and Monitor
```
idf.py -p PORT flash monitor
```
You should see the app start up and create a task. Whatever trace bytes your encoder produces will be emitted over the configured transport — in this example, the encoder writes a short string to the transport on every task switch.
(To exit the serial monitor, type `Ctrl-]`.)
## Project Layout
```
esp_trace/
├── CMakeLists.txt
├── sdkconfig.defaults
├── main/
│ ├── CMakeLists.txt
│ └── app_main.c # overrides esp_trace_get_user_params()
└── components/
└── ext_trace_lib/ # the external trace library component
├── CMakeLists.txt # WHOLE_ARCHIVE + freertos include trick
├── include/
│ ├── esp_trace_freertos_impl.h # entry point pulled in by FreeRTOSConfig.h
│ └── trace_FreeRTOS.h # trace*() macros + forward declarations
└── src/
├── adapter_encoder_ext_trace_lib.c # vtable + ESP_TRACE_REGISTER_ENCODER()
└── trace_FreeRTOS.c # hook implementations (may include FreeRTOS.h)
```
## How the Integration Works
### 1. Selecting the external library
`CONFIG_ESP_TRACE_LIB_EXTERNAL=y` tells `esp_trace` that the encoder lives in a separate component. Internally, `CONFIG_ESP_TRACE_LIB_NAME` resolves to `"ext"`. You can either:
* register your encoder under that default name — `ESP_TRACE_REGISTER_ENCODER("ext", &vt);` — and the system picks it up automatically, **or**
* register under any name you like (this example uses `"ext_trace_lib"`) and override the session parameters at runtime via `esp_trace_get_user_params()`. See [`main/app_main.c`](main/app_main.c):
```c
esp_trace_open_params_t esp_trace_get_user_params(void)
{
esp_trace_open_params_t trace_params = {
.core_cfg = NULL,
.encoder_name = "ext_trace_lib",
.encoder_cfg = NULL,
.transport_name = "usb_serial_jtag",
.transport_cfg = NULL,
};
return trace_params;
}
```
### 2. Providing the FreeRTOS trace hooks
`esp_trace`'s public header [`esp_trace_freertos.h`](../../../components/esp_trace/include/esp_trace_freertos.h) is included from `FreeRTOSConfig.h`. When `CONFIG_ESP_TRACE_LIB_EXTERNAL=y` is set, it pulls in **your** `esp_trace_freertos_impl.h`:
```c
#if CONFIG_ESP_TRACE_LIB_EXTERNAL
#include "esp_trace_freertos_impl.h"
#endif
```
The example splits the contract into two files:
* [`esp_trace_freertos_impl.h`](components/ext_trace_lib/include/esp_trace_freertos_impl.h) — a one-line shim that pulls in `trace_FreeRTOS.h`.
* [`trace_FreeRTOS.h`](components/ext_trace_lib/include/trace_FreeRTOS.h) — defines only the `trace*()` macros this example actually hooks into, plus forward declarations of the helper functions called from them. **No FreeRTOS includes.** Anything left undefined here falls back to FreeRTOS's own empty default (every trace macro is guarded by `#ifndef traceXXX / #define traceXXX() / #endif` in `freertos/FreeRTOS.h`), so you only need to declare what you actually intercept. Trace macros are allowed to reference FreeRTOS identifiers like `pxTCB` or `xTicksToWait` by name — they are resolved later, when the macro is expanded inside the FreeRTOS kernel `.c` files where those names are already in scope.
The actual hook implementation lives in [`trace_FreeRTOS.c`](components/ext_trace_lib/src/trace_FreeRTOS.c) and is free to `#include "freertos/FreeRTOS.h"`. By the time a `.c` file is compiled, `FreeRTOSConfig.h` has been fully parsed.
### 3. Registering the encoder
[`adapter_encoder_ext_trace_lib.c`](components/ext_trace_lib/src/adapter_encoder_ext_trace_lib.c) implements the encoder vtable (`init`, `write`, `panic_handler`) and registers it at link time:
```c
ESP_TRACE_REGISTER_ENCODER("ext_trace_lib", &s_ext_trace_lib_vt);
```
The registration places a descriptor into a dedicated linker section that `esp_trace_core` scans during startup. Because nothing in the application references that descriptor directly, the linker would normally garbage-collect it — `WHOLE_ARCHIVE TRUE` in the component's `CMakeLists.txt` prevents that.
### 4. CMake setup
[`components/ext_trace_lib/CMakeLists.txt`](components/ext_trace_lib/CMakeLists.txt) shows the two pieces of CMake plumbing every external trace library needs:
```cmake
if(CONFIG_ESP_TRACE_LIB_EXTERNAL)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs}
PRIV_REQUIRES esp_trace
WHOLE_ARCHIVE TRUE) # keep ESP_TRACE_REGISTER_* symbols
# Expose esp_trace_freertos_impl.h to the freertos component
idf_component_get_property(freertos_lib freertos COMPONENT_LIB)
target_include_directories(${freertos_lib} INTERFACE ${include_dirs})
else()
idf_component_register(PRIV_REQUIRES esp_trace)
endif()
```
The second `target_include_directories(...)` call is what makes `esp_trace_freertos_impl.h` resolvable from inside the FreeRTOS kernel's translation units.
## Changing the Transport
The example defaults to USB Serial JTAG. To use a different transport, edit `sdkconfig.defaults` (or run `idf.py menuconfig` → *Component config → ESP Trace Configuration → Trace transport*):
| Transport | Config | Notes |
| --- | --- | --- |
| USB Serial JTAG | `CONFIG_ESP_TRACE_TRANSPORT_USB_SERIAL_JTAG=y` | Default. Requires `ESP_CONSOLE_SECONDARY_NONE=y`. |
| apptrace over JTAG | `CONFIG_ESP_TRACE_TRANSPORT_APPTRACE=y` + `CONFIG_APPTRACE_DEST_JTAG=y` | Needs OpenOCD on the host to drain the buffer. |
| apptrace over UART | `CONFIG_ESP_TRACE_TRANSPORT_APPTRACE=y` + `CONFIG_APPTRACE_DEST_UART=y` | Pick a UART different from the console. |
| External transport | `CONFIG_ESP_TRACE_TRANSPORT_EXTERNAL=y` | Another component must register a transport with `ESP_TRACE_REGISTER_TRANSPORT(...)`. |
| None | `CONFIG_ESP_TRACE_TRANSPORT_NONE=y` | Useful if your library streams data over its own channel and just needs the FreeRTOS hooks. |
Don't forget to update the `transport_name` field in `esp_trace_get_user_params()` to match (e.g. `"apptrace"`, `"usb_serial_jtag"`, or your custom transport's registered name).
## What the Demo Emits
`encode()` in [`trace_FreeRTOS.c`](components/ext_trace_lib/src/trace_FreeRTOS.c) writes one line per trace event in the form:
```
[+ 9933 us] ISR_IN irq=63
[+ 29 us] ISR_IN irq=57
[+ 21 us] ISR_YIELD
[+ 16 us] ISR_OUT
[+ 1234 us] Q_CREATE q=0x3fc8a210
[+ 167 us] TASK_IN producer
[+ 54 us] Q_SEND q=0x3fc8a210
[+ 32 us] TASK_IN consumer
```
The leading number is the time elapsed since the previous traced event (microseconds when `CONFIG_ESP_TRACE_TS_SOURCE_ESP_TIMER` is selected). Eight FreeRTOS hooks are wired up — see the *active hooks* block at the top of [`trace_FreeRTOS.h`](components/ext_trace_lib/include/trace_FreeRTOS.h). The rest stay as no-ops (FreeRTOS still expects every `trace*()` macro to be defined).
`ISR_OUT` vs `ISR_YIELD` reflects how FreeRTOS leaves the interrupt: `ISR_OUT` when the handler returns to the interrupted task without scheduling, `ISR_YIELD` when it calls `portYIELD_FROM_ISR()` (triggering `traceISR_EXIT_TO_SCHEDULER`). On a busy SMP target the yield path dominates; on a mostly-idle single-core target the plain `ISR_OUT` path does.
Because the transport is USB-Serial-JTAG and the console is on UART (`CONFIG_ESP_CONSOLE_SECONDARY_NONE=y`), `idf.py monitor` shows ESP-IDF logs while the trace stream is on a separate USB endpoint — open it in any serial terminal (`screen /dev/cu.usbmodem...`, picocom, etc.) to read the output above.
## Runtime Control — `esp_trace_start` / `_stop` / `_flush`
[`esp_trace.h`](../../../components/esp_trace/include/esp_trace.h) exposes three generic lifecycle calls that dispatch to the active encoder's vtable. The application uses only the public API — it never reaches into the external library:
```c
esp_trace_start(); // resume emission (also resets the delta baseline)
// ... do stuff ...
esp_trace_stop(); // pause emission
esp_trace_flush(); // drain transport buffers
```
In this example the library boots with `s_enabled = false`, so nothing is emitted until `app_main()` calls `esp_trace_start()`. The trailing pair `esp_trace_flush(); esp_trace_stop();` makes sure the last events reach the host before the trace channel goes silent. Adapter wiring lives in [`adapter_encoder_ext_trace_lib.c`](components/ext_trace_lib/src/adapter_encoder_ext_trace_lib.c) (`start` / `stop` / `flush` callbacks); flush forwards to the transport's `flush_nolock`.
## Cross-Core Serialization
`encode()` wraps its body in the encoder's `take_lock` / `give_lock` vtable entries (an `esp_trace_lock_t` allocated in the adapter's `init()`). See [`trace_FreeRTOS.c`](components/ext_trace_lib/src/trace_FreeRTOS.c) and [`adapter_encoder_ext_trace_lib.c`](components/ext_trace_lib/src/adapter_encoder_ext_trace_lib.c).
## Other `esp_trace` Helpers
Beyond what this example uses, [`esp_trace.h`](../../../components/esp_trace/include/esp_trace.h) and [`esp_trace_util.h`](../../../components/esp_trace/include/esp_trace_util.h) also expose:
* `esp_trace_is_host_connected()` — gate expensive work when no host is listening.
* `esp_trace_get_link_type()` — returns `ESP_TRACE_LINK_DEBUG_PROBE`, `_UART`, or `_USB_SERIAL_JTAG`.
* `esp_trace_rb_*()` — power-of-2, FreeRTOS-free ring buffer for trace hot paths.
* `esp_trace_tmo_init/check()` — cooperative timeouts for flush loops.
## See Also
* [`components/esp_trace/README.md`](../../../components/esp_trace/README.md) — full architecture overview and adapter API reference.
* [`examples/system/sysview_tracing`](../sysview_tracing) — a production-grade integration of SEGGER SystemView built on the same extension points.

View File

@@ -0,0 +1,23 @@
set(src_dirs
"src"
)
set(include_dirs
"include"
)
set(priv_requires
"esp_trace"
)
if(CONFIG_ESP_TRACE_LIB_EXTERNAL)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs}
PRIV_REQUIRES ${priv_requires}
WHOLE_ARCHIVE TRUE)
idf_component_get_property(freertos_lib freertos COMPONENT_LIB)
target_include_directories(${freertos_lib} INTERFACE ${include_dirs})
else()
idf_component_register(PRIV_REQUIRES ${priv_requires})
endif()

View File

@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "trace_FreeRTOS.h"

View File

@@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* External Trace Library - FreeRTOS trace hooks
*
* This header is pulled in (via esp_trace_freertos_impl.h) from FreeRTOSConfig.h
* Do NOT include any FreeRTOS header here — keep this file restricted to:
* - forward declarations of the C functions called by the macros below,
* - the trace*() macro definitions themselves.
*
* Macros are allowed to reference FreeRTOS identifiers (pxTCB, xTicksToWait, ...)
* by name — they are resolved later, when the macro is expanded inside the
* FreeRTOS kernel .c files where those names are already in scope.
*
* Only the trace*() macros this example actually hooks are defined here.
* Anything left undefined falls back to FreeRTOS's own empty default (see
* the #ifndef guards in freertos/FreeRTOS.h).
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Forward declaration so we can pass an encoder pointer from the adapter to
* init_trace_lib() without pulling in esp_trace_port_encoder.h. */
typedef struct esp_trace_encoder esp_trace_encoder_t;
void init_trace_lib(esp_trace_encoder_t *enc);
void trace_lib_start(void);
void trace_lib_stop(void);
/* Hook implementations — defined in trace_FreeRTOS.c.
* Kept void*-typed to avoid depending on FreeRTOS types in this header. */
void trace_lib_task_switched_in(void);
void trace_lib_task_create(void *pxNewTCB);
void trace_lib_isr_enter(uint32_t irq);
void trace_lib_isr_exit(void);
void trace_lib_isr_exit_to_scheduler(void);
void trace_lib_queue_send(void *pxQueue);
void trace_lib_queue_receive(void *pxQueue);
void trace_lib_queue_create(void *pxNewQueue);
#ifdef __cplusplus
}
#endif
/* ------------------------------------------------------------------ *
* Active hooks (forwarded to trace_FreeRTOS.c)
* ------------------------------------------------------------------ */
#define traceTASK_SWITCHED_IN() trace_lib_task_switched_in()
#define traceTASK_CREATE(pxNewTCB) trace_lib_task_create(pxNewTCB)
#define traceISR_ENTER(n) trace_lib_isr_enter(n)
#define traceISR_EXIT() trace_lib_isr_exit()
#define traceISR_EXIT_TO_SCHEDULER() trace_lib_isr_exit_to_scheduler()
#define traceQUEUE_SEND(pxQueue) trace_lib_queue_send(pxQueue)
#define traceQUEUE_RECEIVE(pxQueue) trace_lib_queue_receive(pxQueue)
#define traceQUEUE_CREATE(pxNewQueue) trace_lib_queue_create(pxNewQueue)
/* All other trace*() macros fall back to FreeRTOS's default empty defines
* (#ifndef ... #define ... empty in freertos/FreeRTOS.h); no need to list
* them here. Add a mapping above when you want to hook one. */

View File

@@ -0,0 +1,141 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stddef.h>
#include <stdbool.h>
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_trace_types.h"
#include "esp_trace_registry.h"
#include "esp_trace_port_encoder.h"
#include "esp_trace_port_transport.h"
#include "esp_trace_util.h"
#include "trace_FreeRTOS.h"
typedef struct {
esp_trace_lock_t lock;
} ext_trace_lib_ctx_t;
/**
* @brief Initializes ext_trace_lib encoder.
* This function is called for each core.
* Adapter implementations do NOT need their own multi-core protection. Core does it for them.
*
* @param enc Pointer to the encoder structure. Must not be NULL.
* @param enc_cfg Pointer to the encoder configuration. Can be NULL for defaults.
*
* @return ESP_OK on success, otherwise \see esp_err_t
*/
static esp_err_t init(esp_trace_encoder_t *enc, const void *enc_cfg)
{
(void)enc_cfg;
// Ensure the encoder is initialized only once unless something todo for both cores
static bool initialized = false;
if (!enc) {
return ESP_ERR_INVALID_ARG;
}
if (initialized) {
return ESP_OK;
}
ext_trace_lib_ctx_t *ctx = heap_caps_calloc(1, sizeof(*ctx),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!ctx) {
return ESP_ERR_NO_MEM;
}
esp_trace_lock_init(&ctx->lock);
enc->ctx = ctx;
init_trace_lib(enc);
initialized = true;
return ESP_OK;
}
static esp_err_t write(esp_trace_encoder_t *enc, const void *data, size_t size, uint32_t tmo)
{
if (!enc || !data || size == 0) {
return ESP_ERR_INVALID_ARG;
}
if (!enc->tp || !enc->tp->vt->write) {
return ESP_ERR_NOT_SUPPORTED;
}
return enc->tp->vt->write(enc->tp, data, size, tmo);
}
static esp_err_t start(esp_trace_encoder_t *enc)
{
(void)enc;
trace_lib_start();
return ESP_OK;
}
static esp_err_t stop(esp_trace_encoder_t *enc)
{
(void)enc;
trace_lib_stop();
return ESP_OK;
}
static esp_err_t flush(esp_trace_encoder_t *enc)
{
if (!enc || !enc->tp || !enc->tp->vt->flush_nolock) {
return ESP_ERR_NOT_SUPPORTED;
}
return enc->tp->vt->flush_nolock(enc->tp);
}
/**
* @brief Panic handler
*
* Called during system panic to finalize encoder state.
*
* @param enc Pointer to the encoder structure. Must not be NULL.
* @param info Panic information
*/
static void panic_handler(esp_trace_encoder_t *enc, const void *info)
{
(void)info;
flush(enc);
}
static unsigned int take_lock(esp_trace_encoder_t *enc, uint32_t tmo_us)
{
if (!enc || !enc->ctx) {
return 0;
}
ext_trace_lib_ctx_t *ctx = enc->ctx;
esp_trace_lock_take(&ctx->lock, tmo_us);
return ctx->lock.int_state;
}
static void give_lock(esp_trace_encoder_t *enc, unsigned int int_state)
{
if (!enc || !enc->ctx) {
return;
}
ext_trace_lib_ctx_t *ctx = enc->ctx;
ctx->lock.int_state = int_state;
esp_trace_lock_give(&ctx->lock);
}
static const esp_trace_encoder_vtable_t s_ext_trace_lib_vt = {
.init = init,
.write = write,
.panic_handler = panic_handler,
.start = start,
.stop = stop,
.flush = flush,
.take_lock = take_lock,
.give_lock = give_lock,
};
ESP_TRACE_REGISTER_ENCODER("ext_trace_lib", &s_ext_trace_lib_vt);

View File

@@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* SPDX-License-Identifier: Apache-2.0
*
* Implementations of the trace*() hooks declared in trace_FreeRTOS.h.
*
* Each hook encodes a single human-readable line that can be observed directly
* in any serial monitor — no decoder is needed:
*
* [+ 123 us] TASK_IN Task 1
* [+ 1000 us] ISR_IN irq=5
*
* The leading number is the time elapsed since the previous traced event.
*
*/
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "trace_FreeRTOS.h"
#include "esp_trace.h"
#include "esp_trace_port_encoder.h"
#include "esp_trace_util.h"
static esp_trace_handle_t s_esp_trace_handle = NULL;
static esp_trace_encoder_t *s_enc = NULL;
static uint32_t s_ts_freq_hz = 1000000; /* default assume 1 MHz */
static uint32_t s_last_ts = 0;
static volatile bool s_enabled = false;
void init_trace_lib(esp_trace_encoder_t *enc)
{
s_esp_trace_handle = esp_trace_get_active_handle();
s_enc = enc;
uint32_t freq = esp_trace_timestamp_init();
if (freq != 0) {
s_ts_freq_hz = freq;
}
s_last_ts = esp_trace_timestamp_get();
}
void trace_lib_start(void)
{
if (!s_esp_trace_handle) {
return;
}
s_last_ts = esp_trace_timestamp_get();
s_enabled = true;
}
void trace_lib_stop(void)
{
s_enabled = false;
}
/* Encode one trace line and write it through. */
static void encode(const char *type, const char *detail)
{
if (!s_enabled || !s_esp_trace_handle || !s_enc) {
return;
}
unsigned int int_state = s_enc->vt->take_lock(s_enc, ESP_TRACE_TMO_INFINITE);
uint32_t now = esp_trace_timestamp_get();
uint32_t delta = now - s_last_ts; /* uint32 modular subtraction handles wrap */
s_last_ts = now;
uint32_t delta_us = (s_ts_freq_hz == 1000000)
? delta
: (uint32_t)((uint64_t)delta * 1000000ULL / s_ts_freq_hz);
char line[96];
int n = snprintf(line, sizeof(line), "[+%7lu us] %-12s %s\n",
(unsigned long)delta_us, type, detail ? detail : "");
if (n > 0) {
if (n >= (int)sizeof(line)) {
n = (int)sizeof(line) - 1;
}
esp_trace_write(s_esp_trace_handle, line, (size_t)n, 0);
}
s_enc->vt->give_lock(s_enc, int_state);
}
void trace_lib_task_switched_in(void)
{
TaskHandle_t h = xTaskGetCurrentTaskHandle();
encode("TASK_IN", h ? pcTaskGetName(h) : "?");
}
void trace_lib_task_create(void *pxNewTCB)
{
if (!pxNewTCB) {
encode("TASK_CREATE", "(null)");
return;
}
encode("TASK_CREATE", pcTaskGetName((TaskHandle_t)pxNewTCB));
}
void trace_lib_isr_enter(uint32_t irq)
{
char d[24];
snprintf(d, sizeof(d), "irq=%lu", (unsigned long)irq);
encode("ISR_IN", d);
}
void trace_lib_isr_exit(void)
{
encode("ISR_OUT", "");
}
void trace_lib_isr_exit_to_scheduler(void)
{
encode("ISR_YIELD", "");
}
void trace_lib_queue_send(void *pxQueue)
{
char d[24];
snprintf(d, sizeof(d), "q=%p", pxQueue);
encode("Q_SEND", d);
}
void trace_lib_queue_receive(void *pxQueue)
{
char d[24];
snprintf(d, sizeof(d), "q=%p", pxQueue);
encode("Q_RECEIVE", d);
}
void trace_lib_queue_create(void *pxNewQueue)
{
char d[24];
snprintf(d, sizeof(d), "q=%p", pxNewQueue);
encode("Q_CREATE", d);
}

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "app_main.c"
REQUIRES ext_trace_lib
INCLUDE_DIRS "")

View File

@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include "sdkconfig.h"
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_trace.h"
static const char *TAG = "main";
esp_trace_open_params_t esp_trace_get_user_params(void)
{
esp_trace_open_params_t trace_params = {
.core_cfg = NULL,
.encoder_name = "ext_trace_lib",
.encoder_cfg = NULL,
.transport_name = "usb_serial_jtag",
.transport_cfg = NULL,
};
return trace_params;
}
static QueueHandle_t s_q;
/* Producer: sends a counter value to s_q every 50 ms. Generates Q_SEND
* trace events and unblocks the consumer (driving TASK_IN switches). */
static void producer(void *arg)
{
uint32_t v = 0;
while (1) {
xQueueSend(s_q, &v, portMAX_DELAY);
v++;
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
/* Consumer: blocks on s_q indefinitely. Each receive wakes this task and
* fires a TASK_IN trace event when the scheduler switches us in. */
static void consumer(void *arg)
{
uint32_t v;
while (1) {
xQueueReceive(s_q, &v, portMAX_DELAY);
}
}
void app_main(void)
{
ESP_LOGI(TAG, "Start of trace session");
// Wait some time for host to be ready
vTaskDelay(2000 / portTICK_PERIOD_MS);
esp_trace_start();
s_q = xQueueCreate(4, sizeof(uint32_t));
xTaskCreatePinnedToCore(producer, "producer", 2048, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(consumer, "consumer", 2048, NULL, 5, NULL, portNUM_PROCESSORS - 1);
// 1 second delay is enough to generate the expected number of trace events.
vTaskDelay(1000 / portTICK_PERIOD_MS);
esp_trace_stop();
esp_trace_flush();
ESP_LOGI(TAG, "End of trace session");
}

View File

@@ -0,0 +1,115 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import os.path
import re
import time
import pytest
import serial
from pytest_embedded_idf import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
# Matches lines emitted by encode() in trace_FreeRTOS.c, e.g.
# [+ 12345 us] TASK_IN producer
# [+ 12 us] Q_SEND q=0x3fc8a210
TRACE_LINE_RE = re.compile(r'^\[\+\s*\d+ us\] ([A-Z_]+)\s*(.*)$')
# Minimum number of occurrences each event type must reach within the capture window.
EXPECTED_MIN_COUNTS = {
'TASK_CREATE': 2, # producer + consumer
'Q_CREATE': 1, # xQueueCreate in app_main
'TASK_IN': 5, # producer/consumer + idle task
'Q_SEND': 5, # producer sends every 50 ms
'Q_RECEIVE': 5, # consumer receives every send
'ISR_IN': 5, # FreeRTOS systick
}
ISR_EXIT_MIN = 5
# Fraction of malformed lines we tolerate before failing.
MAX_MALFORMED_RATIO = 0.05
def _validate_trace_data(trace_log_path: str) -> None:
"""Validate the human-readable trace log produced by ext_trace_lib."""
with open(trace_log_path, encoding='utf-8', errors='replace') as f:
lines = [line.rstrip() for line in f if line.strip()]
assert lines, f'No trace data captured in {trace_log_path}'
counts: dict[str, int] = {}
create_names: set[str] = set()
malformed = 0
for line in lines:
m = TRACE_LINE_RE.match(line)
if not m:
malformed += 1
continue
evt, detail = m.group(1), m.group(2).strip()
counts[evt] = counts.get(evt, 0) + 1
if evt == 'TASK_CREATE':
create_names.add(detail)
ratio = malformed / len(lines)
assert ratio <= MAX_MALFORMED_RATIO, (
f'Too many malformed lines: {malformed}/{len(lines)} '
f'({ratio:.1%} > {MAX_MALFORMED_RATIO:.0%}). Likely USJ TX ring overflow; '
f'bump CONFIG_ESP_TRACE_USJ_TX_BUFFER_SIZE or slow down the producer.'
)
for evt, minimum in EXPECTED_MIN_COUNTS.items():
seen = counts.get(evt, 0)
assert seen >= minimum, f'Expected at least {minimum} {evt} events, got {seen}'
isr_exits = counts.get('ISR_OUT', 0) + counts.get('ISR_YIELD', 0)
assert isr_exits >= ISR_EXIT_MIN, (
f'Expected at least {ISR_EXIT_MIN} ISR_OUT+ISR_YIELD events, '
f'got {isr_exits} (ISR_OUT={counts.get("ISR_OUT", 0)}, '
f'ISR_YIELD={counts.get("ISR_YIELD", 0)})'
)
assert 'producer' in create_names, f'producer task not seen in TASK_CREATE: {create_names}'
assert 'consumer' in create_names, f'consumer task not seen in TASK_CREATE: {create_names}'
def _capture_trace(ser: serial.Serial, trace_log_path: str, capture_s: float = 5.0) -> None:
"""Capture trace output from the USB-Serial-JTAG endpoint."""
ser.reset_input_buffer()
with open(trace_log_path, 'w+b') as f:
end_time = time.time() + capture_s
while time.time() < end_time:
try:
if ser.in_waiting:
f.write(ser.read(ser.in_waiting))
except serial.SerialTimeoutException:
assert False, 'Timeout reached while reading from serial port, exiting...'
# Drain anything still in flight after the capture window.
time.sleep(0.2)
end_time = time.time() + 1.0
last_data_time = time.time()
while time.time() < end_time and (time.time() - last_data_time) <= 0.3:
try:
if ser.in_waiting:
f.write(ser.read(ser.in_waiting))
last_data_time = time.time()
except serial.SerialTimeoutException:
assert False, 'Timeout reached while reading from serial port, exiting...'
@pytest.mark.usb_serial_jtag
@idf_parametrize('target', soc_filtered_targets('SOC_USB_SERIAL_JTAG_SUPPORTED == 1'), indirect=['target'])
@pytest.mark.parametrize('config', [pytest.param('default')], indirect=True)
@pytest.mark.temp_skip_ci(targets=['esp32h4', 'esp32s31'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_trace_ext_lib_usj(dut: IdfDut) -> None:
dut.expect('Start of trace session', timeout=5)
time.sleep(1) # wait for USJ port to be ready
usj_port = '/dev/serial_ports/ttyACM-esp32'
ser = serial.Serial(usj_port, baudrate=1000000, timeout=10)
trace_log_path = os.path.join(dut.logdir, 'ext_trace.log')
_capture_trace(ser, trace_log_path)
_validate_trace_data(trace_log_path)

View File

@@ -0,0 +1,6 @@
CONFIG_ESP_TRACE_ENABLE=y
CONFIG_ESP_TRACE_LIB_EXTERNAL=y
CONFIG_ESP_TRACE_TS_SOURCE_ESP_TIMER=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
CONFIG_ESP_TRACE_TRANSPORT_USB_SERIAL_JTAG=y
CONFIG_ESP_TRACE_USJ_TX_BUFFER_SIZE=32768

View File

@@ -123,8 +123,6 @@ KNOWN_MISSING = {
'system/gcov',
'system/gdbstub',
'system/rt_mqueue',
'system/sysview_tracing',
'system/sysview_tracing_heap_log',
'system/ulp/lp_core/build_system',
'system/ulp/lp_core/build_system/main/ulp',
'system/ulp/lp_core/debugging/main/ulp',