用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 配置步骤

  1. 使能DWT:在系统初始化中调用DWT_Init()。
  2. 编译选项:为需要统计的源文件添加-finstrument-functions,避免全局开启影响中断等。
  3. 实现钩子函数:在钩子中维护调用栈,记录每个函数的进入周期和累计耗时。
  4. 输出结果:通过串口或调试器定期打印统计表。

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平台,是性能调优的利器。实际使用时注意中断、溢出与优化影响,即可快速定位热点函数。