为什么需要无侵入式耗时统计
在嵌入式开发中,定位性能瓶颈、验证优化效果都离不开函数耗时数据。常见做法有两种:
- GPIO翻转 + 示波器:需要额外硬件,且翻转本身有几十纳秒开销。
- 通用定时器打点:读取定时器寄存器有总线访问延迟,分辨率也受定时器时钟限制。
Cortex-M内核自带一个 DWT(Data Watchpoint and Trace) 单元,其中的 CYCCNT 寄存器是一个随内核时钟自增的32位周期计数器。读取它只需一条 MRS 指令,开销约1~2个周期,对被测代码几乎无干扰,分辨率达到单周期级别。
DWT周期计数器原理
DWT是ARM CoreSight调试组件的一部分,CYCCNT 在每个处理器时钟周期加1。以168MHz主频为例,计数器每约5.95ns加1,32位计满约25.6秒回绕。
使用前需要满足三个条件:
- 使能DWT(
DEMCR的TRCENA位)。 - 清零并使能
CYCCNT(DWT_CTRL的CYCCNTENA位)。 - 部分芯片需要先解锁调试寄存器(如STM32需写
0xC5ACCE55到LAR)。
注意:CYCCNT 依赖调试时钟域,在深度睡眠模式下会停止计数,因此只适合测量运行态代码。
配置步骤
步骤1:使能DWT
#include "core_cm4.h" /* 按内核型号选择 core_cm3/4/7.h */
static void DWT_Init(void)
{
/* 1. 解锁ITM/DWT访问(部分Cortex-M3/M4需要) */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
/* 2. 清零周期计数器 */
DWT->CYCCNT = 0;
/* 3. 使能周期计数器 */
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
若使用STM32且上述代码无效,可先执行 ITM->LAR = 0xC5ACCE55; 解锁。
步骤2:封装读取与差值宏
#define DWT_CYCCNT (DWT->CYCCNT)
/* 读取当前周期计数 */
static inline uint32_t DWT_GetCycles(void)
{
return DWT_CYCCNT;
}
/* 计算两次采样之间的周期数,自动处理32位回绕 */
static inline uint32_t DWT_Elapsed(uint32_t start, uint32_t end)
{
return end - start; /* 无符号减法天然处理回绕 */
}
步骤3:换算为时间
/* 周期数转微秒,SystemCoreClock为内核主频 */
static inline float DWT_CyclesToUs(uint32_t cycles)
{
return (float)cycles * 1000000.0f / (float)SystemCoreClock;
}
完整示例:统计一个函数的耗时
#include <stdio.h>
#include "core_cm4.h"
/* 被测函数:软件CRC32,模拟耗时业务 */
static uint32_t crc32_sw(const uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int b = 0; b < 8; b++)
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
return ~crc;
}
int main(void)
{
SystemInit();
DWT_Init();
static uint8_t buf[1024];
for (int i = 0; i < sizeof(buf); i++) buf[i] = (uint8_t)i;
/* 测量开始 */
uint32_t t0 = DWT_GetCycles();
volatile uint32_t result = crc32_sw(buf, sizeof(buf));
uint32_t t1 = DWT_GetCycles();
uint32_t cycles = DWT_Elapsed(t0, t1);
printf("CRC32 result=%08X, cycles=%lu, time=%.2f us\r\n",
result, cycles, DWT_CyclesToUs(cycles));
while (1) { }
}
实测在168MHz的STM32F4上,1024字节软件CRC32约消耗数十万周期,换算后可直接对比硬件CRC外设的加速比。
实战注意事项
-
防止编译器优化掉被测代码:用
volatile接收返回值,或把结果写入全局变量,否则空函数可能被整体优化。 - 中断干扰:测量期间若发生中断,中断服务程序的时间会被计入。对短函数建议关中断测量,或多次采样取最小值。
- 计数器回绕:32位计数器约25秒回绕一次,单次测量远小于该值,无符号减法即可正确处理;但不要跨回绕点做多次累加。
-
多核/多任务:
CYCCNT是全局的,RTOS下测量会被任务切换污染,建议在临界区或单任务基准测试中使用。 - 低功耗模式:睡眠时计数器停走,测得的“时间”是纯运行周期,反而适合评估CPU占用。
- 调试器影响:连接调试器单步时计数行为可能异常,性能数据应在全速运行下采集。
小结
DWT周期计数器把函数耗时测量简化为“读两次寄存器、做一次减法”,开销低、分辨率高、无需外设资源,是Cortex-M平台上做性能剖析的利器。配合宏封装,可以进一步做成 PROFILE_BEGIN/PROFILE_END 的即插即用工具,在优化热点函数、对比算法实现时非常实用。
暂无评论,来抢沙发~