用DWT周期计数器给中断服务程序做微秒级耗时测量与热点定位

为什么需要测量ISR耗时?

中断服务程序(ISR)应尽可能短小,否则会阻塞低优先级中断、增加任务延迟。但实际项目中,ISR里常包含协议解析、数据拷贝甚至浮点运算,耗时可能从几微秒到几百微秒不等。没有测量就没有优化——你需要知道:

  • 每个ISR的实际执行时间是多少?
  • 哪个ISR是“耗时大户”?
  • ISR内部哪一段代码最耗时?

传统方法是用GPIO翻转配合示波器,但需要占用引脚、增加硬件成本,且无法定位ISR内部热点。本文介绍一种纯软件、零外设依赖的方法:Cortex-M内核的DWT周期计数器。

DWT周期计数器原理

DWT(Data Watchpoint and Trace)是ARM Cortex-M内核的调试组件之一,其中CYCCNT寄存器是一个32位自由运行的周期计数器。当使能后,每个内核时钟周期自动加1。

  • 计数频率 = 内核主频(如STM32F407为168MHz)
  • 分辨率 = 1/168MHz ≈ 5.95ns
  • 溢出周期 = 2^32 / 168MHz ≈ 25.6秒(足够覆盖单次ISR测量)

关键优势:

  • 无需任何外部引脚或外设
  • 读取仅需一条指令(MRS),开销极小
  • 精度远高于通用定时器

配置步骤

1. 使能DWT

DWT属于调试组件,需先使能跟踪和计数器:

// 使能DWT周期计数器
void DWT_Init(void)
{
    // 使能TRCENA(调试跟踪使能)
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    
    // 清零并启动周期计数器
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

2. 读取周期计数

// 获取当前周期计数(内联函数减少开销)
static inline uint32_t DWT_GetCycles(void)
{
    return DWT->CYCCNT;
}

// 将周期数转换为微秒(假设SystemCoreClock为内核频率)
static inline float DWT_CyclesToUs(uint32_t cycles)
{
    return (float)cycles * 1000000.0f / (float)SystemCoreClock;
}

测量框架:分段打点定位热点

设计思路

在ISR入口记录起始周期,在关键代码段之间插入“打点”记录,退出时计算总耗时和各段耗时。使用一个全局结构体保存测量结果,避免在ISR内做浮点运算或打印。

typedef struct {
    uint32_t total_cycles;      // 总周期数
    uint32_t seg_cycles[4];     // 各段周期数(最多4段)
    uint32_t call_count;        // 调用次数
} ISR_Profile_t;

volatile ISR_Profile_t g_isr_profile;

ISR测量代码示例

以某串口接收中断为例,假设ISR包含:帧头判断、数据拷贝、校验计算三个主要段。

void USART1_IRQHandler(void)
{
    uint32_t t_start = DWT_GetCycles();
    uint32_t t_mark1, t_mark2, t_end;
    
    // 段1:帧头判断与状态机
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t data = USART1->DR;
        // ... 帧头判断逻辑 ...
    }
    t_mark1 = DWT_GetCycles();
    
    // 段2:数据拷贝到缓冲区
    // ... memcpy或循环拷贝 ...
    t_mark2 = DWT_GetCycles();
    
    // 段3:校验计算
    // ... CRC或累加和 ...
    t_end = DWT_GetCycles();
    
    // 更新测量结果(无浮点、无打印)
    g_isr_profile.seg_cycles[0] = t_mark1 - t_start;
    g_isr_profile.seg_cycles[1] = t_mark2 - t_mark1;
    g_isr_profile.seg_cycles[2] = t_end - t_mark2;
    g_isr_profile.total_cycles = t_end - t_start;
    g_isr_profile.call_count++;
}

主循环中输出结果

在main的while(1)中定期打印(注意关中断读取快照,避免数据竞争):

void PrintProfile(void)
{
    ISR_Profile_t snapshot;
    __disable_irq();
    snapshot = g_isr_profile;  // 结构体拷贝
    __enable_irq();
    
    printf("ISR calls: %lu\n", snapshot.call_count);
    printf("Total: %.2f us\n", DWT_CyclesToUs(snapshot.total_cycles));
    printf("Seg1: %.2f us\n", DWT_CyclesToUs(snapshot.seg_cycles[0]));
    printf("Seg2: %.2f us\n", DWT_CyclesToUs(snapshot.seg_cycles[1]));
    printf("Seg3: %.2f us\n", DWT_CyclesToUs(snapshot.seg_cycles[2]));
}

实战注意事项

  • DWT在低功耗模式下可能停止计数:进入Sleep/Stop模式后CYCCNT可能暂停,测量低功耗唤醒相关ISR时需注意。
  • 中断嵌套影响:若ISR被更高优先级中断打断,测量值会包含嵌套时间。建议测量时暂时关闭更高优先级中断,或使用“最小/最大/平均”统计法。
  • 编译器优化:DWT_GetCycles应声明为static inline并确保不被优化掉;打点变量用volatile防止重排。
  • 多核/多主频场景:若动态调频,需在计算时使用当前实际频率,而非固定SystemCoreClock。
  • 调试器连接影响:部分调试器连接时会占用DWT,建议在Release版本或脱机运行时测量。
  • 计数器溢出:单次ISR耗时远小于25秒,无需处理溢出;但若测量长周期任务,需做溢出补偿。

总结

DWT周期计数器为嵌入式开发者提供了一种“无侵入、高精度、零外设”的ISR耗时测量手段。通过分段打点,不仅能知道ISR总耗时,还能快速定位内部热点,指导优化方向(如将耗时计算移出ISR、改用DMA等)。建议在项目早期就加入该测量框架,形成性能基线,避免后期“盲调”。