轻量级 MCU 函数追踪与离线可视化工具。它使用 GCC/Clang
-finstrument-functions 将函数进入/退出事件写入用户提供的 RAM
环形缓冲区,冻结后通过 UART、日志、文件、蓝牙或自定义后端导出,再结合
对应 ELF 生成 Perfetto、Speedscope、KCachegrind 和火焰图数据。
Lightweight RAM-backed function tracing and offline visualization for bare-metal and RTOS firmware.
MRamTrace 面向无法长期连接调试器、没有 ETM/SWO,或者只能通过普通通信 链路取回现场数据的设备。采集阶段不依赖串口、文件系统、RTOS 或 Python; 目标侧只保存固定16字节事件,复杂分析全部在电脑端完成。
它可以回答:
- 哪条函数调用路径消耗最多时间?
- 某个函数由谁调用,又把时间花在了哪些子函数?
- 两个 RTOS 任务的调用行为是否不同?
- 某一时间点正在运行哪层调用栈?
- 环形缓冲是否覆盖、传输是否丢块、ELF 是否匹配?
| 输出 | 工具 | 主要用途 |
|---|---|---|
trace.chrome.json |
Perfetto | 精确时间线、任务切换视角、调用顺序 |
trace.speedscope.json |
Speedscope | Time Order、Left Heavy、Sandwich |
trace.callgrind |
KCachegrind | 调用关系图、Self/Inclusive cycles、调用次数 |
trace.<task>-<id>.callgrind |
KCachegrind | 单独分析某个任务/上下文 |
trace.folded |
FlameGraph | 标准折叠栈和差分火焰图输入 |
trace.flamegraph.svg |
浏览器 | 无依赖、可分享的静态火焰图 |
trace.report.* |
文本/JSON | 覆盖率、事件速率、异常与符号解析健康度 |
聚合视图同时展示函数的 Inclusive/Self cycles、调用次数、Callee Map 和 caller→callee 调用图,适合分析“成本如何沿调用链传递”。
workload_maintenance_check 仅出现6次,这个视图展示了 KCachegrind
对低频分支的调用者、被调用者和边计数分析。
Left Heavy 将相同调用路径合并,可快速看到主路径、分支权重,
并在 worker-a 和 worker-b 之间切换。
查看 Sandwich 与 Time Order 视图
Sandwich 视图展示函数 Total/Self 排行,选中函数后同时展示它的 callers 和 callees:
Time Order 保留真实进入/退出顺序,可放大到某一次具体调用:
Perfetto 按任务展示函数时间线,适合查看不同 RTOS 上下文的调用层级、 开始时间和持续时间。
三个工具读取的是同一次采集产生的不同输出格式,底层事件和时间戳相同,区别 主要在观察视角,而不是数据精度。实际分析时通常先用 Perfetto 找到异常时段, 再用 Speedscope 判断主要调用栈,最后用 KCachegrind 深挖函数之间的成本传递。
Perfetto 保留事件的先后顺序,将每个任务或上下文展示为独立时间线。它最适合 观察某次具体调用的开始时间、持续时间、嵌套层级以及不同任务在同一时段的行为。 可以连续缩放到单次函数调用,但不擅长直接回答整个采集窗口内哪个调用路径累计 最热。使用网页版即可打开,无需安装桌面软件。
Speedscope 兼顾时间顺序和聚合分析。Time Order 还原调用过程,Left Heavy 将 相同调用栈合并以突出主路径,Sandwich 则从选中函数同时查看 callers、callees、 Total 和 Self。它界面轻量、容易分享,适合快速定位热点和比较不同任务;但调用 边、调用次数和大型调用关系图不如 KCachegrind 完整。
KCachegrind 将整段采集聚合为调用图,重点展示 Inclusive/Self cycles、调用次数、 caller/callee 边和 Callee Map。它适合量化某个函数为何昂贵、成本来自哪个子函数, 也容易发现低频但单次代价很高的路径。代价是丢失单次事件的时间顺序,而且通常 需要在 Linux/WSL 图形环境中安装桌面程序。
| 工具 | 核心视角 | 时间顺序 | 聚合热点 | 调用关系与次数 | 多任务/上下文 | 最适合 |
|---|---|---|---|---|---|---|
| Perfetto | 时间线 | 强 | 弱 | 基础嵌套 | 强,同屏分轨 | 时序、延迟、某次具体调用 |
| Speedscope | 调用栈/火焰图 | 强 | 强 | 中等,Sandwich 视图 | 强,可切换 profile | 快速找热点、比较主调用路径 |
| KCachegrind | 聚合调用图 | 无 | 强 | 最强,含边和调用次数 | 可打开汇总或单任务文件 | 成本归因、调用关系、量化优化 |
如果只选一个工具:排查时序问题优先 Perfetto,做日常热点分析优先 Speedscope, 准备深入优化函数和调用关系时优先 KCachegrind。三者结合使用能覆盖“发生时间、 热点路径、成本来源”三个互补维度。
__cyg_profile_func_enter/exit
|
v
16-byte trace event
|
v
caller-owned RAM ring buffer
|
stop/freeze
|
v
registered save sink callback
UART / log / file / BLE / custom
|
v
serial log + matching ELF
|
v
Perfetto / Speedscope / KCachegrind / SVG
核心没有堆分配、没有后台任务,保存过程同步执行。应用决定缓冲区来自静态数组、 链接段、FreeRTOS heap、RT-Thread heap 或其他分配器,也决定在哪个普通任务执行 慢速 I/O。
Version 1 的每个事件固定占用 16 字节。一次被插桩函数的完整调用通常产生
一个 enter 和一个 exit,因此需要 2 个事件,即 32 字节。用户事件也各占
16 字节。传给 ram_trace_init() 的字节数会向下取整为完整事件数:
事件容量 = floor(buffer_size / 16)
完整函数调用数 ≈ 事件容量 / 2
可保留时长(秒)≈ 事件容量 / event_rate
下表中的保留时长按约 5,000 events/s 估算,接近本项目 STM32F407 示例的 实测 4,932.8 events/s。实际值取决于插桩范围和业务调用频率。
| 缓冲区 | 事件数(采样点) | 约等于完整函数调用数 | 约可保留时长 @ 5k events/s |
|---|---|---|---|
| 4 KiB | 256 | 128 | 51 ms |
| 8 KiB | 512 | 256 | 102 ms |
| 16 KiB | 1,024 | 512 | 205 ms |
| 24 KiB | 1,536 | 768 | 307 ms |
| 32 KiB | 2,048 | 1,024 | 410 ms |
| 64 KiB | 4,096 | 2,048 | 819 ms |
| 128 KiB | 8,192 | 4,096 | 1.64 s |
STM32F407 示例配置为 24 KiB,200 ms 抓取实际生成 984 个事件,没有覆盖。
除用户缓冲区外,当前 ARM GCC 示例中核心状态、service 和 6 个 sink 注册槽约占
96 字节常驻 RAM(具体值随 ABI、编译器和配置变化)。导出阶段还会使用任务栈:
默认 12 events/block 时,编码数组共 448 字节;当前示例构建中
ram_trace_export() 自身栈帧约 552 字节,尚不包含 sink 驱动及其调用链。因此
应通过链接器 map/stack-usage 文件和 RTOS 栈高水位检查最终栈余量;示例的导出
任务使用 1,536 字节栈。
第一次接入建议先分配 16–32 KiB,进行一次短时抓取。分析器会根据实测
event rate 和默认 1.25 安全系数,在 trace.report.txt/json 中自动给出
100、500、1000 ms 三档建议,也可以指定其他窗口:
python3 tools/ram_trace/ram_trace_analyze.py serial.log \
--elf firmware.elf \
--recommend-window-ms 200 \
--buffer-safety-factor 1.5计算方法为:
buffer_bytes >= event_rate × target_seconds × 16 × safety_factor
建议 safety_factor 取 1.2–1.5,并将结果向上取整到 16 字节。RAM 紧张时,优先
排除高频叶子函数,而不是盲目扩大缓冲区。RAM_TRACE_MODE_WRAP 保留停止前最新的
一段数据,适合复现故障前现场;RAM_TRACE_MODE_STOP_WHEN_FULL 保留从启动开始的
完整前缀,适合启动过程分析。
使用事件数组配置最直观,并天然满足对齐要求:
#define TRACE_EVENT_CAPACITY 1536U /* 24 KiB */
static RamTraceEvent_t trace_events[TRACE_EVENT_CAPACITY];
config.buffer = trace_events;
config.buffer_size = sizeof(trace_events);也可以从 RTOS heap 分配字节缓冲区:
#define TRACE_BUFFER_SIZE (24U * 1024U)
void *trace_buffer = pvPortMalloc(TRACE_BUFFER_SIZE);
config.buffer = trace_buffer;
config.buffer_size = TRACE_BUFFER_SIZE;需要确保分配结果非空且至少按 RamTraceEvent_t 对齐;采集期间不得释放或复用。
性能影响不能用一个与平台无关的固定百分比描述。每个已启用事件会执行一次短 临界区、读取时间戳和上下文、顺序写入 16 字节并更新计数器;一次完整函数调用 会执行两次。采集过程中没有格式化、CRC、Base64 或 UART/BLE I/O,这些操作都在 停止采集后执行,不会污染已冻结的时间线。
可用下面的公式估算目标板 CPU 开销:
CPU 开销比例 ≈ event_rate × cycles_per_event / CPU_frequency
单次函数调用附加周期 ≈ 2 × cycles_per_event
以下数据来自 LXB407ZG(STM32F407ZG,168 MHz)、ARM GCC 9.2.1、Release 构建。 Benchmark 每组执行 512 次调用、重复7轮并取中位数,测量区间关闭中断且没有 串口 I/O:
| 测量项 | 实测周期 | 约合时间 @ 168 MHz |
|---|---|---|
无插桩 plain_cycles_per_call |
17.04 cycles/call | 101 ns/call |
已插桩、未采集 stopped_cycles_per_call |
121.03 cycles/call | 720 ns/call |
已插桩、正在采集 active_cycles_per_call |
400.98 cycles/call | 2.39 μs/call |
事件写入增量 capture_cycles_per_event |
139.97 cycles/event | 833 ns/event |
同一次 FULL workload 抓取在 199.513 ms 内生成 984 个事件,即约 4,932 events/s,没有发生环形覆盖。按这个实际事件率估算:
| 开销来源 | 当前 workload 的 CPU 占用 |
|---|---|
| Hook 存在但采集关闭(stopped − plain) | 约 0.153% |
| 采集期间写入事件(active − stopped) | 约 0.411% |
| 综合软件追踪开销 | 约 0.564% |
Benchmark 同时报告 slowdown_percent=2252.52%,这是一个函数体仅约17周期的
极短测试函数从 17.04 增至 400.98 cycles/call 后的相对结果,不代表整个系统
变慢22倍。函数本身越耗时,固定插桩成本的相对占比越低;实际系统影响应使用
业务事件率按上述公式计算。该结果只代表此芯片、编译器、优化配置和插桩范围,
更换平台或构建参数后应重新测量。
STM32F407 示例默认启用 DWT 基准,在正式采集前分别测量无插桩、已插桩但未采集、 正在采集三种状态,并输出多轮测试的中位数:
RTRACE:BENCH iterations=512 trials=7 events=1024 plain_cycles_per_call=... stopped_cycles_per_call=... active_cycles_per_call=... capture_cycles_per_event=... slowdown_percent=...
其中 capture_cycles_per_event 是 active 相对 stopped 的增量,避免把禁用状态下
仍然存在的函数 hook 成本混入事件写入成本。可以通过 CMake 参数
-DRAM_TRACE_ENABLE_BENCHMARK=OFF 关闭示例基准。其他目标应使用 DWT cycle
counter 或硬件定时器做同类测量。分析器会识别 RTRACE:BENCH,并将这些实测值
写入 trace.report.txt/json。还应分别关注:
- 插桩本身的函数进入/退出调用,即使 trace 未启动也存在少量开销;
- 高频小函数的相对扰动会明显高于耗时较长的业务函数;
- 临界区会短暂增加中断延迟,硬实时 ISR 应排除插桩;
-fno-inline会额外改变性能和代码尺寸,仅建议演示调用层级时使用;- 缩小插桩范围通常比增加 RAM 更能同时降低性能扰动和事件速率。
纯裸机或自定义 RTOS:
add_subdirectory(path/to/MRamTrace/ram_trace)
target_link_libraries(your_firmware PRIVATE ram_trace)FreeRTOS:
set(RAM_TRACE_WITH_FREERTOS ON CACHE BOOL "" FORCE)
add_subdirectory(path/to/MRamTrace/ram_trace)
target_link_libraries(your_firmware PRIVATE ram_trace)只给需要观察的应用源文件开启插桩:
set_source_files_properties(src/application.c PROPERTIES
COMPILE_OPTIONS "-finstrument-functions;-fno-optimize-sibling-calls")Trace核心、平台回调、导出代码、串口/文件/BLE驱动必须使用
-fno-instrument-functions。
RamTraceConfig_t config = {
.buffer = trace_buffer,
.buffer_size = sizeof(trace_buffer),
.timestamp_frequency = CPU_CLOCK_HZ,
.mode = RAM_TRACE_MODE_WRAP,
.platform = platform_ops,
};
ram_trace_service_init(&config);
ram_trace_service_register_sink(&uart_sink);
ram_trace_service_start(1U); /* reset then start */
/* application runs */
ram_trace_service_stop();
ram_trace_service_save("uart");底层 ram_trace_init/start/stop/export API 仍可直接使用;service 只是一个轻量
统一入口。
static RamTraceResult_t log_write(void *context,
const uint8_t *data,
size_t length)
{
app_log_write(context, data, length);
return RAM_TRACE_OK;
}
static const RamTraceSink_t log_sink = {
.name = "log",
.write = log_write,
.context = NULL,
};
ram_trace_service_register_sink(&log_sink);
ram_trace_service_save("log");文件后端可在 begin 中打开文件,在 write 中写入,在 end 中关闭。蓝牙后端
使用相同接口完成 MTU 分片或发送队列适配,模块本身不绑定具体文件系统和协议栈。
将 Shell、FreeRTOS CLI 或 AT 命令解析得到的 argc/argv 转发给
ram_trace_command_execute():
trace status
trace start
trace stop
trace reset
trace sinks
trace save uart
trace save file trace.rtrace
trace save ble
python3 tools/ram_trace/ram_trace_analyze.py serial.log \
--elf firmware.elf \
--output trace-output分析器仅使用 Python 标准库。符号解析需要 arm-none-eabi-nm,也可以在主机
测试 ELF 上使用普通 nm。日志既可以是纯文本,也可以是 WSL Serial Monitor
生成的 NDJSON。
打开结果:
kcachegrind trace-output/trace.callgrind- Speedscope:访问 https://www.speedscope.app/,拖入
trace.speedscope.json。 - Perfetto:访问 https://ui.perfetto.dev/,打开
trace.chrome.json。 - SVG:直接用浏览器打开
trace.flamegraph.svg。
必须使用与烧录固件严格匹配的 ELF。分析器会验证每个数据块和完整数据流的 CRC32,并报告事件序号缺口、环形覆盖、调用栈错配和未解析地址。
examples/stm32f407-freertos/ 提供 STM32F407 + FreeRTOS 参考接入,包括:
- DWT cycle counter 时间戳;
- FreeRTOS task 上下文识别;
- UART保存后端;
- 两个工作任务;
- 25个测试函数和最长6层调用路径。
该示例不复制 FreeRTOS、CMSIS、启动文件和链接脚本,请通过 CMake 参数指向 本机已有依赖,详见示例目录 README。
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure- Version 1 面向单核、32位、小端目标。
- 函数耗时是墙钟时间;任务被抢占的时间尚未从函数耗时扣除。
- 高频叶子函数可能快速填满缓冲区,应通过编译期排除列表控制范围。
- 环形覆盖后可能从半条调用栈开始,分析器会报告并丢弃不完整前缀。
- 保存是同步操作,必须在停止采集后由普通任务执行,不能在 ISR 中调用。
- 它是低成本软件追踪方案,不替代 ETM 等无侵入硬件追踪。







