用DWT周期计数器在Cortex-M上做函数级耗时统计的零侵入实现
1. 为什么需要零侵入耗时统计
在嵌入式开发中,定位性能热点常需统计函数执行时间。传统方法如手动插入定时器读写、使用GPIO翻转配合示波器,均需修改源码,侵入性强且难以覆盖所有函数。Cortex-M内核提供的DWT(Data Watchpoint and Trace)单元包含一个32位周期计数器,能以CPU周期精度测量代码段耗时,且无需外设定时器。结合链接器脚本与函数钩子,可实现完全零侵入的函数级耗时统计。
2. DWT周期计数器原理
DWT是ARM CoreSight调试组件的一部分,其CYCCNT寄存器在使能后每个内核时钟周期递增1。关键寄存器:
-
DWT->CTRL:使能CYCCNT(位0) -
DWT->CYCCNT:32位周期计数 -
CoreDebug->DEMCR:使能TRCENA(位24)以访问DWT
测量原理:在函数入口和出口分别读取CYCCNT,差值即为周期数,除以系统主频得到时间。由于CYCCNT为32位,在168MHz下约25.5秒溢出,需注意长函数或中断干扰。
3. 零侵入实现方案
核心思路:利用编译器-finstrument-functions选项,在函数入口和出口自动插入__cyg_profile_func_enter和__cyg_profile_func_exit钩子。在钩子中读取DWT计数并记录,无需修改业务代码。
3.1 配置步骤
- 使能DWT:在系统初始化中调用
DWT_Init()。 - 编译选项:为需要统计的源文件添加
-finstrument-functions,避免全局开启影响中断等。 - 实现钩子函数:在钩子中维护调用栈,记录每个函数的进入周期和累计耗时。
- 输出结果:通过串口或调试器定期打印统计表。
3.2 完整代码示例
#include "stm32f4xx.h"
#include <stdio.h>
#define MAX_FUNC_NUM 64
typedef struct {
uint32_t addr; // 函数地址
uint32_t entry_cycle;
uint32_t total_cycles;
uint32_t call_count;
} func_stat_t;
static func_stat_t stats[MAX_FUNC_NUM];
static uint32_t stack[32]; // 调用栈,记录进入周期
static int stack_top = 0;
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
static func_stat_t* find_stat(uint32_t addr) {
for (int i = 0; i < MAX_FUNC_NUM; i++) {
if (stats[i].addr == addr) return &stats[i];
if (stats[i].addr == 0) {
stats[i].addr = addr;
return &stats[i];
}
}
return NULL;
}
void __cyg_profile_func_enter(void *this_fn, void *call_site) {
if (stack_top >= 32) return;
stack[stack_top++] = DWT->CYCCNT;
func_stat_t *s = find_stat((uint32_t)this_fn);
if (s) s->entry_cycle = DWT->CYCCNT;
}
void __cyg_profile_func_exit(void *this_fn, void *call_site) {
if (stack_top == 0) return;
uint32_t entry = stack[--stack_top];
uint32_t cycles = DWT->CYCCNT - entry;
func_stat_t *s = find_stat((uint32_t)this_fn);
if (s) {
s->total_cycles += cycles;
s->call_count++;
}
}
void print_stats(void) {
printf("FuncAddr\tCalls\tTotalCycles\tAvgCycles\n");
for (int i = 0; i < MAX_FUNC_NUM; i++) {
if (stats[i].addr && stats[i].call_count) {
printf("0x%08X\t%lu\t%lu\t%lu\n",
stats[i].addr, stats[i].call_count,
stats[i].total_cycles,
stats[i].total_cycles / stats[i].call_count);
}
}
}
3.3 使用示例
void delay_loop(volatile int n) {
while (n--);
}
int main(void) {
DWT_Init();
delay_loop(100000);
print_stats();
while (1);
}
编译时对delay_loop所在文件添加-finstrument-functions,即可在串口看到该函数的调用次数与平均周期数。
4. 注意事项
- 中断干扰:钩子函数本身可能被中断打断,导致统计偏大。建议在统计期间关闭全局中断,或仅统计非中断上下文函数。
- 递归与嵌套:调用栈深度有限,递归过深会丢失数据,可增大栈或使用环形缓冲。
-
编译器优化:
-O2可能内联函数,导致钩子不触发。对需统计的函数使用__attribute__((noinline))。 - CYCCNT溢出:32位计数器在高速主频下溢出较快,长函数需处理溢出(如记录溢出次数)。
- 性能开销:每个函数调用增加约数十周期开销,对高频小函数影响显著,建议仅用于性能分析阶段。
- 地址映射:输出为函数地址,需结合map文件或调试器解析为函数名。
5. 总结
DWT周期计数器为Cortex-M提供了高精度、低开销的时间测量手段。结合-finstrument-functions钩子,可在不修改业务代码的前提下实现函数级耗时统计。该方法适用于STM32等Cortex-M3/M4/M7平台,是性能调优的利器。实际使用时注意中断、溢出与优化影响,即可快速定位热点函数。
暂无评论,来抢沙发~