引言在做MCU开发时衡量性能和寻找函数热点往往是一个比较麻烦的事情在代码量没有很大的情况下尚还能通过正向分析找到热点函数但当工程规模达到一定程度之后这件事就没有那么容易了。有没有一个更直接一点的办法去做性能分析呢答案是有的arm架构的MCU通常提供了一些调试组件这些调试组件拥有跟踪指令流的功能只要配合调试工具把指令流抓出来就可以对指令流进行分析从而得到热点函数甚至整个工程精确到指令的执行时序。典型的跟踪组件是ETM但由于追踪指令流需要极大的带宽因此ETM通常配合trace口使用这会占用比较多的IO并且需要专门的设备(比如j-tracetrace32等)这类trace设备价格昂贵对于轻量一点的场景(比如只是个人DIY)来讲成本过高。很多相对低端点的MCU会把ETM裁剪掉这等于在很多MCU上使用ETM做trace并不能行得通。好在对于profile需求还有另外一个轻量的解决方案也就是这篇文档要讲的东西。ITMDWTTPIUITMDWTTPIU都是调试系统的一部分以stm32f103为例整个调试系统结构如下ITM不具备精确指令追踪的功能但配合DWT可以实现定频的PC采样也就是可以定频的去采样当前CPU正在执行的指令地址当采样频率足够高采样时间足够长的时候通过统计PC命中的区域就可以做一定的函数热点分析或者简单的执行时序分析。采样信息通过TPIU在SWO引脚上输出我们抓SWO的输出就可以获取到PC的采样值了。SWO上的采样可以通过逻辑分析仪(理论上Jlink也可以采样但没有找到jlink获取SWO的PC采样的简单方案)而现在一个400M的逻辑分析仪价格也就300不到相比动辄上万的trace设备性价比就高多了。一个实例以stm32f103rc为例代码内需要初始化ITMDWTTPIU以支持SWO输出代码如下#include stm32f10x.h #define CORE_DEBUG_DEMCR_TRACE_EN BIT(24) #define ITM_LSR_ADDR (*(volatile unsigned int *)0xE0000FB0) #define ITM_LSR_UNLOCK_KEY 0xC5ACCE55 #define TPIU_SPPR_MODE_TRACE 0 #define TPIU_SPPR_MODE_SWO_MANCHESTER 1 #define TPIU_SPPR_MODE_SWO_NRZ 2 #define ITM_TCR_TRACE_BUS_ID(x) (((x) 0x7F) 16) #define ITM_TCR_SWO_EN BIT(4) /// 异步时间戳使能 #define ITM_TCR_TX_EN BIT(3) /// DWT到ITM输出使能 #define ITM_TCR_SYNCE_EN BIT(2) /// TPIU同步包输出 #define ITM_TCR_TS_EN BIT(1) /// 时间戳输出使能 #define ITM_TCR_ITM_EN BIT(0) /// ITM使能 #define DWT_CTRL_CYCCCNT_EN BIT(0) /// 启用cyccnt #define DWT_CTRL_POST_RESET(x) (((x) 0xf) 1) /// POST CNT重装载值 #define DWT_CTRL_POST_INIT(x) (((x) 0xf) 5) /// POST CNT初始值 #define DWT_CTRL_CYCTAP BIT(9) /// POSTCNT频率 1-主频/1024 0 主频/64 #define DWT_CTRL_SYNCTAP(x) (((x) 0x3) 10) /// 选择同步包计数器在CYCCNT计数器上的位置。这决定了同步包速率 /// 00 失效。没有同步包。 /// 01 CYCCNT的同步计数器分流器[24] /// 10 CYCCNT 的同步计数器分流器[26] /// 11 CYCCNT的同步计数器分流器[28] #define DWT_CTRL_PCSAMPL_EN BIT(12) /// 启用PC采样采样频率由POSTCNT频率决定 #define DWT_CTRL_EXCTRC_EN BIT(16) /// 启用异常追踪生成也就是追踪中断 #define DWT_CTRL_CPIEVT_EN BIT(17) /// 启用生成CPI计数溢出事件 #define DWT_CTRL_EXCEVT_EN BIT(18) /// 启用生成异常开销计数器溢出事件 #define DWT_CTRL_SLEEPEVT_EN BIT(19) /// 启用生成睡眠计数器溢出事件 #define DWT_CTRL_LSUEVT_EN BIT(20) /// 启用生成LSU计数器溢出事件 #define DWT_CTRL_FOLDEVT_EN BIT(21) /// 启用折叠指令计数器溢出事件的生成 #define DWT_CTRL_CYCEVT_EN BIT(22) /// 启用POSTCNT下溢事件计数器包 typedef struct { uint8_t itm_port; uint32_t main_fr; uint32_t swo_fr; } itm_config_t; itm_config_t itm_config { .itm_port 0, .main_fr 72 * 1000 * 1000, .swo_fr 24 * 1000 * 1000, }; void itm_swo_init(void) { itm_config_t *config itm_config; // 使能trace CoreDebug-DEMCR | CORE_DEBUG_DEMCR_TRACE_EN; // 解锁ITM ITM_LSR_ADDR ITM_LSR_UNLOCK_KEY; // 关闭ITM端口 ITM-TER ~BIT(config-itm_port); ITM-TCR 0; // SWO NRZ模式发送 TPI-SPPR TPIU_SPPR_MODE_SWO_NRZ; // 16bit 分频值-1即从主频分频出SWO的频率一般SWO频率不要超过16M需要注意信号质量 TPI-ACPR config-main_fr / config-swo_fr - 1; DWT-CTRL DWT_CTRL_CYCCCNT_EN | DWT_CTRL_POST_RESET(0x2) | DWT_CTRL_POST_INIT(0x2) | DWT_CTRL_PCSAMPL_EN; // 使能ITM端口 ITM-TPR 0; // ITM-TCR 0x1000D; ITM-TCR ITM_TCR_TRACE_BUS_ID(1); ITM-TCR | (ITM_TCR_TX_EN | ITM_TCR_SYNCE_EN | ITM_TCR_ITM_EN); // ITM-TCR | ITM_TCR_TS_EN; // 中断统计的时候这个有用 ITM-TER | BIT(config-itm_port); }代码中可以调整SWO的输出频率需要注意如果下调SWO的频率需要配合增大DWT_CTRL_POST_RESET的值从而能正确的获取到采样PC采样由DWT_CTRL_POST_RESET的值分频SWO带宽太低的情况下可能因为PC采样太快而导致发送异常。代码内调用itm_swo_init后就可以准备上位机环境了不过要注意不要把PB3复用成其他功能PB3默认是SWO输出。上位机使用的是Ozone配合jlink使用。Ozone连接后选择trace setting按照下图配置配置完成后打开terminal并在terminal内右键选择capture SWO此时应该可以在console内看到如下提示这就代表SWO已经开启输出这时候使用逻辑分析仪连接PB3就可以抓取到SWO的输出此处使用DSVIEW进行抓取如果没有异常应该可以抓取到输出的PC值抓取到所有的PC之后就可以导出数据做分析了在解码器处选择保存保存为txt并只保存PC的值之后就可以获取到一个pcsample.txt文件文件内容长这样之后使用python做后处理以获得可视化的结果毕竟直接看这玩意很难看得懂伟大的AI帮忙写了两个脚本用于将PC采样值可视化。代码1分析函数热点及覆盖率#!/usr/bin/env python3 import argparse import re from collections import defaultdict, Counter from elftools.elf.elffile import ELFFile def parse_pc_sample(filepath): pc_values [] with open(filepath, r, encodingutf-8) as f: for line in f: match re.search(rPC:\s(0x[0-9a-fA-F]), line) if match: pc_values.append(int(match.group(1), 16)) return pc_values def parse_elf_functions(axf_path): functions [] with open(axf_path, rb) as f: elf ELFFile(f) symtab elf.get_section_by_name(.symtab) if symtab is None: print(Warning: No .symtab section found) return functions for sym in symtab.iter_symbols(): if sym[st_info][type] STT_FUNC: name sym.name addr sym[st_value] - 1 size sym[st_size] if name and addr 0 and size 0: functions.append({ name: name, start: addr, end: addr size }) functions.sort(keylambda x: x[start]) return functions def map_pc_to_function(pc, functions): for func in functions: if func[start] pc func[end]: return func[name] return None def analyze_coverage(pc_values, functions): func_hits Counter() unique_addresses set(pc_values) for pc in pc_values: func_name map_pc_to_function(pc, functions) if func_name: func_hits[func_name] 1 total_unique len(unique_addresses) covered_funcs len(func_hits) total_funcs len(functions) return func_hits, total_unique, covered_funcs, total_funcs def generate_html(pc_values, functions, func_hits, total_unique, covered_funcs, total_funcs): coverage_rate (covered_funcs / total_funcs * 100) if total_funcs 0 else 0 func_load defaultdict(int) for pc in pc_values: func_name map_pc_to_function(pc, functions) if func_name: func_load[func_name] 1 total_samples len(pc_values) func_load_sorted sorted(func_load.items(), keylambda x: x[1], reverseTrue) load_data [] func_addr_map {f[name]: f[start] for f in functions} for name, count in func_load_sorted[:20]: percentage (count / total_samples * 100) if total_samples 0 else 0 addr func_addr_map.get(name, 0) load_data.append({name: name, addr: addr, count: count, percentage: percentage}) valid_functions [f for f in functions if f[start] 0 and f[end] f[start]] valid_func_names {f[name] for f in valid_functions} filtered_func_hits {k: v for k, v in func_hits.items() if k in valid_func_names} filtered_covered_funcs len(filtered_func_hits) filtered_total_funcs len(valid_functions) filtered_coverage_rate (filtered_covered_funcs / filtered_total_funcs * 100) if filtered_total_funcs 0 else 0 html f!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title代码覆盖率与负载分布分析报告/title style body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }} .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }} h1 {{ color: #333; text-align: center; }} .stats {{ display: flex; justify-content: space-around; margin: 20px 0; flex-wrap: wrap; }} .stat-box {{ text-align: center; padding: 20px; background: #f0f0f0; border-radius: 8px; min-width: 200px; margin: 10px; }} .stat-box .value {{ font-size: 32px; font-weight: bold; color: #2196F3; }} .stat-box .label {{ color: #666; margin-top: 10px; }} h2 {{ color: #444; border-bottom: 2px solid #2196F3; padding-bottom: 10px; }} table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }} th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }} th {{ background: #2196F3; color: white; }} tr:hover {{ background: #f5f5f5; }} .progress-bar {{ width: 100%; background: #e0e0e0; border-radius: 4px; height: 20px; }} .progress-fill {{ height: 100%; border-radius: 4px; }} .bar-low {{ background: #f44336; }} .bar-medium {{ background: #ff9800; }} .bar-high {{ background: #4caf50; }} .not-covered {{ color: #f44336; }} .covered {{ color: #4caf50; }} /style /head body div classcontainer h1代码覆盖率与负载分布分析报告/h1 div classstats div classstat-box div classvalue{total_samples}/div div classlabel总采样点数/div /div div classstat-box div classvalue{total_unique}/div div classlabel唯一PC地址数/div /div div classstat-box div classvalue{filtered_covered_funcs}/{filtered_total_funcs}/div div classlabel有效函数覆盖数/div /div div classstat-box div classvalue{filtered_coverage_rate:.1f}%/div div classlabel有效函数覆盖率/div /div /div h2负载分布 (Top 20)/h2 table thead tr th排名/th th函数名/th th基地址/th th调用次数/th th占比/th th分布条/th /tr /thead tbody max_count load_data[0][count] if load_data else 1 for i, item in enumerate(load_data, 1): bar_width (item[count] / max_count * 100) bar_class bar-high if item[percentage] 20 else (bar-medium if item[percentage] 5 else bar-low) html f tr td{i}/td td{item[name]}/td td0x{item[addr]:08x}/td td{item[count]}/td td{item[percentage]:.2f}%/td td div classprogress-bar div classprogress-fill {bar_class} stylewidth: {bar_width}%/div /div /td /tr html /tbody /table h2所有有效函数覆盖情况 (按命中次数排序)/h2 table thead tr th函数名/th th起始地址/th th结束地址/th th命中次数/th th状态/th /tr /thead tbody all_func_data [] for func in valid_functions: hits filtered_func_hits.get(func[name], 0) all_func_data.append({ name: func[name], start: func[start], end: func[end], hits: hits }) all_func_data.sort(keylambda x: x[hits], reverseTrue) for func in all_func_data: status_class covered if func[hits] 0 else not-covered status 已覆盖 if func[hits] 0 else 未覆盖 html f tr td{func[name]}/td td0x{func[start]:08x}/td td0x{func[end]:08x}/td td{func[hits]}/td td class{status_class}{status}/td /tr html /tbody /table /div /body /html return html if __name__ __main__: parser argparse.ArgumentParser(descriptionPC采样数据分析 - 生成代码覆盖率与负载分布报告) parser.add_argument(pc_file, helpPC采样数据文件路径) parser.add_argument(axf_file, helpAXF/ELF可执行文件路径) parser.add_argument(-o, --output, defaultNone, help输出HTML报告路径 (默认: coverage_report.html)) args parser.parse_args() pc_file args.pc_file axf_file args.axf_file output_file args.output or coverage_report.html print(正在解析PC采样数据...) pc_values parse_pc_sample(pc_file) print(f解析到 {len(pc_values)} 个采样点) print(正在解析ELF文件提取函数信息...) functions parse_elf_functions(axf_file) print(f提取到 {len(functions)} 个函数) print(正在分析覆盖率和负载分布...) func_hits, total_unique, covered_funcs, total_funcs analyze_coverage(pc_values, functions) print(正在生成HTML报告...) html generate_html(pc_values, functions, func_hits, total_unique, covered_funcs, total_funcs) with open(output_file, w, encodingutf-8) as f: f.write(html) print(f报告已生成: {output_file})代码2绘制PC的timeline用于分析代码执行流。由于PC是定频采样的话只能看个大概并不是精确到指令级的#!/usr/bin/env python3 import argparse import re import json from collections import Counter from elftools.elf.elffile import ELFFile def parse_pc_sample(filepath): samples [] with open(filepath, r, encodingutf-8) as f: for line in f: if line.startswith(Id,): continue match re.search(r(\d),([\d.]),PC:\s(0x[0-9a-fA-F]), line) if match: samples.append({time: float(match.group(2)), pc: int(match.group(3), 16)}) return samples def parse_elf_functions(axf_path): functions [] with open(axf_path, rb) as f: elf ELFFile(f) symtab elf.get_section_by_name(.symtab) if symtab is None: return functions for sym in symtab.iter_symbols(): if sym[st_info][type] STT_FUNC: name sym.name addr sym[st_value] - 1 size sym[st_size] if name and addr 0 and size 0: functions.append({name: name, start: addr, end: addr size}) functions.sort(keylambda x: x[start]) return functions def map_pc_to_function(pc, functions): for func in functions: if func[start] pc func[end]: return func[name] return None def analyze(samples, functions): func_visits Counter() time_start samples[0][time] for sample in samples: func_name map_pc_to_function(sample[pc], functions) if func_name: func_visits[func_name] 1 if len(samples) 1: intervals [samples[i][time] - samples[i-1][time] for i in range(1, min(100, len(samples)))] avg_interval sum(intervals) / len(intervals) / 1000000 else: avg_interval 3.56 time_data [] for i in range(len(samples)): func_name map_pc_to_function(samples[i][pc], functions) if func_name: time_us int((samples[i][time] - time_start) / 1000) time_data.append([i, time_us, func_name]) sorted_funcs sorted(func_visits.items(), keylambda x: x[1], reverseTrue) func_to_row {f[0]: i for i, f in enumerate(sorted_funcs)} row_data [] if len(samples) 0: current_func None current_start None current_end None for i in range(len(samples) - 1): func_name map_pc_to_function(samples[i][pc], functions) if func_name and func_name in func_to_row: start_ms (samples[i][time] - time_start) / 1000000 end_ms (samples[i1][time] - time_start) / 1000000 if current_func func_name: current_end end_ms else: if current_func is not None: row_data.append([func_to_row[current_func], current_start, current_end, current_func]) current_func func_name current_start start_ms current_end end_ms if current_func is not None: row_data.append([func_to_row[current_func], current_start, current_end, current_func]) return func_visits, avg_interval, time_start, time_data, sorted_funcs, row_data def generate_html(samples, functions, func_visits, avg_interval, time_start, time_data, sorted_funcs, row_data): total_samples len(samples) duration samples[-1][time] - time_start func_colors {} palette [#e74c3c, #3498db, #2ecc71, #f39c12, #9b59b6, #1abc9c, #e67e22, #34495e, #16a085, #c0392b, #27ae60, #2980b9, #8e44ad, #f1c40f, #2c3e50] for i, func in enumerate(sorted_funcs): func_colors[func[0]] palette[i % len(palette)] json_data json.dumps([[d[0], d[1], d[2], d[3]] for d in row_data]) json_colors json.dumps(func_colors) duration_sec duration / 1000000000 duration_min duration_sec / 60 maxT_ms int(duration / 1000000) html f!DOCTYPE html html head meta charsetUTF-8 titlePC采样执行时间线/title style body{{font-family:Segoe UI,sans-serif;margin:10px;padding:10px;background:#0d1117;color:#c9d1d9}} h1{{color:#58a6ff;text-align:center;margin:10px 0;font-size:18px}} .stats{{display:flex;justify-content:center;gap:15px;margin-bottom:10px}} .stat{{background:#161b22;padding:8px 15px;border-radius:4px;border:1px solid #30363d;text-align:center}} .stat .v{{font-size:16px;font-weight:bold;color:#58a6ff}} .stat .l{{font-size:10px;color:#8b949e}} .legend{{display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap;background:#161b22;padding:8px;border-radius:4px;border:1px solid #30363d;max-height:80px;overflow-y:auto}} .legend .item{{display:flex;align-items:center;gap:3px;font-size:9px}} .legend .color{{width:8px;height:8px;border-radius:2px}} #timeline{{width:100%;overflow:hidden;border:1px solid #30363d;border-radius:4px;background:#0d1117;position:relative;user-select:none}} #timeline canvas{{display:block;width:100%;height:auto;cursor:grab}} /style /head body h1PC采样执行时间线 (每行一个函数)/h1 div classstats div classstatdiv classv{total_samples}/divdiv classl采样点/div/div div classstatdiv classv{len(sorted_funcs)}/divdiv classl函数/div/div div classstatdiv classv{duration_min:.3f}/divdiv classl时长(min)/div/div div classstatdiv classv{avg_interval:g}/divdiv classl间隔(ms)/div/div /div div classlegend idlegend/div div idtimelinecanvas idcanvas/canvas/div script var data {json_data}; var colors {json_colors}; var numRows {len(sorted_funcs)}; var maxT {maxT_ms}; var M{{t:30,r:20,b:30,l:100}}; var cwdocument.getElementById(timeline).clientWidth||1200,chnumRows*20; var hM.tM.bch; var scale1,translateX0; var canvasdocument.getElementById(canvas); var ctxcanvas.getContext(2d); function resize() {{ var rectcanvas.getBoundingClientRect(); if(rect.width 0 rect.height 0) {{ canvas.widthrect.width; canvas.heightrect.height; }} else {{ canvas.width1200; canvas.heightnumRows*2060; }} render(); }} window.addEventListener(resize,resize); setTimeout(resize,100); function render(){{ var iwcanvas.width; var visibleTmaxT/scale; var startT-translateX; if(startT 0) startT 0; if(startT visibleT maxT) startT maxT - visibleT; var xScaleiw/visibleT; var rowHeightcanvas.height/numRows; ctx.fillStyle#161b22; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.save(); ctx.translate(M.l,M.t); for(var r0;rnumRows;r){{ ctx.strokeStyle#30363d; ctx.lineWidth0.5; ctx.beginPath(); ctx.moveTo(0,r*rowHeight); ctx.lineTo(iw,r*rowHeight); ctx.stroke(); }} for(var i0;idata.length;i){{ var ddata[i]; if(d[2] startT || d[1] startTvisibleT) continue; var x(d[1]-startT)*xScale; var bwMath.max(1,(d[2]-d[1])*xScale); ctx.fillStylecolors[d[3]]; ctx.fillRect(x,d[0]*rowHeight1,bw,rowHeight-2); }} ctx.restore(); ctx.fillStyle#8b949e; ctx.font10px sans-serif; ctx.textAligncenter; for(var tstartT;tstartTvisibleT;tvisibleT/10){{ var xM.l(t-startT)*xScale; if(xM.l || xM.liw) continue; ctx.fillText(Math.round(t)ms,x,canvas.height-8); }} var funcNamesObject.keys(colors); ctx.textAlignright; ctx.fillStyle#c9d1d9; for(var i0;inumRows;i){{ ctx.fillText(funcNames[i]||,M.l-8,i*rowHeightrowHeight/23); }} }} function getMouseX(event){{ var rectcanvas.getBoundingClientRect(); return event.clientX-rect.left; }} canvas.addEventListener(wheel,function(event){{ event.preventDefault(); var mouseXgetMouseX(event); var oldVisibleTmaxT/scale; var oldStartTMath.max(0,-translateX); var mouseToldStartT(mouseX-M.l)/(canvas.width/oldVisibleT); var oldScalescale; scaleevent.deltaY0?scale*0.9:scale*1.1; scaleMath.max(1,Math.min(500,scale)); var newVisibleTmaxT/scale; var newStartTmouseT-(mouseX-M.l)/(canvas.width/newVisibleT); if(newStartT0) newStartT0; if(newStartTnewVisibleTmaxT) newStartTmaxT-newVisibleT; translateX-newStartT; render(); }}); var isDraggingfalse,lastX0; canvas.addEventListener(mousedown,function(e){{ isDraggingtrue; lastXe.clientX; canvas.style.cursorgrabbing; e.stopPropagation(); }}); window.addEventListener(mouseup,function(){{ isDraggingfalse; canvas.style.cursordefault; }}); window.addEventListener(mousemove,function(e){{ if(!isDragging) return; var dxe.clientX-lastX; var visibleTmaxT/scale; translateX(dx/canvas.width)*visibleT*2; var maxTranslate-(maxT-visibleT); translateXMath.max(maxTranslate,Math.min(0,translateX)); lastXe.clientX; render(); }}); Object.keys(colors).forEach(function(f){{ var divdocument.createElement(div); div.classNameitem; var cntdata.filter(function(d){{return d[3]f;}}).length; div.innerHTMLdiv classcolor stylebackground:colors[f]/divspanf (cnt)/span; document.getElementById(legend).appendChild(div); }}); resize(); /script /body /html return html if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(pc_file) parser.add_argument(axf_file) parser.add_argument(-o, --output, defaultflowgraph.html) args parser.parse_args() print(解析PC采样...) samples parse_pc_sample(args.pc_file) print(f采样点: {len(samples)}) print(解析ELF...) functions parse_elf_functions(args.axf_file) print(f函数: {len(functions)}) print(分析...) func_visits, avg_interval, time_start, time_data, sorted_funcs, row_data analyze(samples, functions) print(f间隔: {avg_interval:.2f}ms) print(生成HTML...) html generate_html(samples, functions, func_visits, avg_interval, time_start, time_data, sorted_funcs, row_data) with open(args.output, w, encodingutf-8) as f: f.write(html) print(f完成: {args.output})如何使用这两个脚本命令行运行类似的指令需要传入PC采样文件对应的axf文件以及指定输出的html文件。运行正常的话将会获得两个html用浏览器打开会得到类似的结果结语ITM提供了一种非侵入式的trace手段(不用像RTT配合systemview一样在代码里面插桩)可以在使用较少的引脚和较低的设备成本的情况下进行热点函数分析函数运行覆盖度检查简单的执行流分析等操作。当然ITM本身还提供了一些额外的功能比如追踪exception并且可以在事件上附加时间戳这就可以精确测量中断执行时间了就不展开详细说明了。有了热点函数等信息之后优化等后续事项就可以更加有的放矢了。