第二章 启动时间测量:从DWT到示波器的四把尺子

做多核启动优化,第一件事不是改代码,而是先搞清楚——你的系统到底花了多少时间在启动上?

我见过太多工程师,上来就对着代码一顿猛改,改完发现启动时间反而更长了。为什么?因为没有基准数据。你连起点和终点都没量准,怎么知道优化有没有效果?

这一章,我带你过一遍启动时间测量的四种方法。从最精准的DWT周期计数器,到最直观的示波器抓波形,每种方法都有它的适用场景。我个人习惯是:调试阶段用DWT和SYSTICK,验证阶段上逻辑分析仪,最终交付前用示波器拍个波形留档。

2.1 DWT时钟周期计数器——精度最高的“尺子”

DWT(Data Watchpoint and Trace)是Cortex-M内核自带的调试组件。它里面有一个CYCCNT寄存器,专门用来数CPU时钟周期。说白了,就是一颗从复位开始一直往上数的计数器,永不停止。

为什么我偏爱DWT?因为它精度最高,一个周期都不差。而且它不占用定时器资源,对实时系统零干扰。

核心要点:DWT_CYCCNT是32位寄存器,在168MHz主频下约25.6秒溢出一次。测量短时间启动段完全够用。

2.1.1 初始化与读取代码

/* 启用DWT时钟周期计数器 */
void DWT_Init(void)
{
    /* 使能DWT外设 */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    
    /* 复位CYCCNT计数器 */
    DWT->CYCCNT = 0;
    
    /* 使能CYCCNT计数器 */
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

/* 读取当前计数值 */
uint32_t DWT_GetCycles(void)
{
    return DWT->CYCCNT;
}

/* 测量两个时间点之间的周期数 */
uint32_t DWT_MeasureDelta(uint32_t start)
{
    return DWT_GetCycles() - start;
}

这段代码我几乎每个项目都会复用。注意一点:CoreDebug->DEMCR必须在使能CYCCNT之前配置好,否则计数器不会跑。我曾经在这个顺序上栽过跟头,查了半天才发现是初始化顺序反了。

2.1.2 测量启动时间段的典型用法

/* 在启动代码最前面记录起点 */
uint32_t start_cycles = 0;

void EarlyInit(void)
{
    DWT_Init();
    start_cycles = DWT_GetCycles();
    
    /* ... 其他初始化代码 ... */
}

/* 在进入main()或RTOS启动后记录终点 */
void MainEntry(void)
{
    uint32_t elapsed = DWT_MeasureDelta(start_cycles);
    /* 转换为微秒:elapsed_us = elapsed / (SystemCoreClock / 1000000) */
    float elapsed_us = (float)elapsed / (SystemCoreClock / 1000000.0f);
    
    /* 保存到调试变量或串口输出 */
    debug_printf("Boot time: %.2f us (%u cycles)\r\n", elapsed_us, elapsed);
}

小技巧:如果测量段跨越了时钟配置变化(比如从HSI切换到PLL),DWT的计数频率会跟着变。我建议在时钟稳定后再读取DWT,或者干脆用SYSTICK做跨时钟域的测量。

2.2 SYSTICK与TIM定时器——灵活的第二选择

DWT虽然精准,但有些场景下它不太方便。比如你想测量某个外设初始化函数的执行时间,或者想统计多个启动阶段的耗时分布。这时候,SYSTICK和通用定时器反而更好用。

2.2.1 SYSTICK测量法

SYSTICK是Cortex-M内核自带的24位递减计数器。它通常被用作操作系统的心跳,但也可以临时拿来测时间。我习惯在启动阶段用SYSTICK做分段计时,等RTOS启动后再把SYSTICK交给系统。

/* 配置SYSTICK为1ms中断模式 */
void SYSTICK_Init(void)
{
    SysTick_Config(SystemCoreClock / 1000);  /* 1ms中断一次 */
}

/* 全局时间戳变量 */
volatile uint32_t sys_tick_ms = 0;

void SysTick_Handler(void)
{
    sys_tick_ms++;
}

/* 获取当前毫秒时间戳 */
uint32_t GetTick(void)
{
    return sys_tick_ms;
}

/* 测量某段代码的执行时间 */
void MeasurePhase(void)
{
    uint32_t t0 = GetTick();
    
    /* 被测代码段 */
    HAL_SomeModule_Init();
    
    uint32_t t1 = GetTick();
    uint32_t cost_ms = t1 - t0;
    
    debug_printf("Phase cost: %u ms\r\n", cost_ms);
}

注意:SYSTICK的精度只有1ms,对于微秒级的启动段不够用。如果被测代码执行时间小于1ms,你会得到0ms的结果。这时候要么用DWT,要么把SYSTICK中断间隔改成100us。

2.2.2 TIM定时器测量法

通用定时器比SYSTICK更灵活。你可以配置一个TIM以最高频率计数(比如168MHz),然后像DWT一样读取计数值。而且TIM可以同时测量多个通道,非常适合做多核启动的并行时间分析。

/* 使用TIM2作为高精度计时器 */
void TIM2_Init(void)
{
    __HAL_RCC_TIM2_CLK_ENABLE();
    
    TIM2->PSC = 0;                    /* 不分频,直接使用内核时钟 */
    TIM2->ARR = 0xFFFFFFFF;           /* 最大自动重装值 */
    TIM2->CR1 |= TIM_CR1_CEN;        /* 使能计数器 */
}

/* 读取当前计数值 */
uint32_t TIM2_GetCounter(void)
{
    return TIM2->CNT;
}

/* 测量Core0和Core1的启动时间差 */
void MeasureDualCoreBoot(void)
{
    uint32_t t_core0_start = TIM2_GetCounter();
    /* Core0启动代码 */
    
    uint32_t t_core1_start = TIM2_GetCounter();
    /* Core1启动代码 */
    
    uint32_t t_core1_end = TIM2_GetCounter();
    
    uint32_t core0_boot = t_core1_start - t_core0_start;
    uint32_t core1_boot = t_core1_end - t_core1_start;
    uint32_t skew = (t_core1_start > t_core0_start) ? 
                    (t_core1_start - t_core0_start) : 0;
    
    debug_printf("Core0: %u, Core1: %u, Skew: %u\r\n", 
                 core0_boot, core1_boot, skew);
}
测量方法 精度 资源占用 适用场景
DWT CYCCNT 1个时钟周期 无(内核自带) 短时间、高精度测量
SYSTICK 1ms(可调) 1个中断 长时间、分段计时
TIM定时器 1个时钟周期 1个定时器 多通道、并行测量

2.3 逻辑分析仪——多信号同步抓取

软件测量有个天然缺陷:它会影响被测系统本身。你想想看,每打印一条调试信息,就要多花几十微秒。对于启动时间优化来说,这几十微秒可能就是瓶颈所在。

逻辑分析仪就没有这个问题。它只监听GPIO电平变化,对目标系统零干扰。我一般在代码的关键节点插入GPIO翻转指令,然后用逻辑分析仪同时抓取多个信号。

2.3.1 硬件连接与代码植入

/* 定义测量用的GPIO引脚 */
#define BOOT_PIN_CORE0_START   GPIO_PIN_0  /* PA0 */
#define BOOT_PIN_CORE0_READY   GPIO_PIN_1  /* PA1 */
#define BOOT_PIN_CORE1_START   GPIO_PIN_2  /* PA2 */
#define BOOT_PIN_CORE1_READY   GPIO_PIN_3  /* PA3 */

/* 初始化GPIO为推挽输出 */
void BootMeasure_GPIO_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = BOOT_PIN_CORE0_START | BOOT_PIN_CORE0_READY |
               BOOT_PIN_CORE1_START | BOOT_PIN_CORE1_READY;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &gpio);
    
    /* 初始化为低电平 */
    HAL_GPIO_WritePin(GPIOA, BOOT_PIN_CORE0_START | BOOT_PIN_CORE0_READY |
                             BOOT_PIN_CORE1_START | BOOT_PIN_CORE1_READY, 
                      GPIO_PIN_RESET);
}

/* 在启动代码关键位置插入 */
void Core0_Start(void)
{
    HAL_GPIO_WritePin(GPIOA, BOOT_PIN_CORE0_START, GPIO_PIN_SET);
    /* ... Core0初始化代码 ... */
    HAL_GPIO_WritePin(GPIOA, BOOT_PIN_CORE0_READY, GPIO_PIN_SET);
}

void Core1_Start(void)
{
    HAL_GPIO_WritePin(GPIOA, BOOT_PIN_CORE1_START, GPIO_PIN_SET);
    /* ... Core1初始化代码 ... */
    HAL_GPIO_WritePin(GPIOA, BOOT_PIN_CORE1_READY, GPIO_PIN_SET);
}

经验之谈:逻辑分析仪的采样率至少要设到被测信号最高频率的4倍。对于168MHz主频的系统,我建议用200MHz以上采样率的分析仪。便宜一点的24MHz采样率也能用,但测出来的上升沿会有抖动。

2.4 示波器实测——最直观的“铁证”

示波器是最后一道防线。当你在逻辑分析仪上看到时序异常,或者DWT报出奇怪的数据时,示波器能帮你确认信号质量。我遇到过好几次,代码逻辑完全正确,但示波器一看,GPIO上升沿有毛刺,原来是电源纹波太大导致电平抖动。

2.4.1 示波器测量要点

  • 探头补偿:测量前先用1kHz方波校准探头,确保波形不畸变
  • 接地方式:用弹簧接地夹,不要用长接地线,否则会引入噪声
  • 触发设置:用上升沿触发,触发电平设在1.65V(3.3V系统的一半)
  • 时基选择:启动时间一般在ms级别,时基设在200us/div到1ms/div之间

避坑指南:我曾经用示波器测一个低功耗项目的启动时间,结果发现每次测量结果都不一样。查了半天,原来是示波器探头的10pF电容影响了RC复位电路的时序。对于高阻抗的复位引脚,建议用1x档位或者有源探头。

2.5 四种方法对比与选择策略

说了这么多,到底该用哪种?我总结了一个选择矩阵,你可以根据实际情况来定。

场景 推荐方法 理由
单次启动总时间 DWT 精度高,零干扰
多阶段耗时分布 SYSTICK + 数组记录 可记录多个时间戳
双核启动同步性 TIM + GPIO + 逻辑分析仪 多通道并行抓取
最终验证与报告 示波器截图 直观、可信、可存档

我个人习惯是:开发阶段用DWT做精细测量,调试阶段用逻辑分析仪看整体时序,交付前用示波器拍一张波形图贴在文档里。这样既有数据支撑,又有视觉证据,客户看了也放心。

启动时间测量方法选择流程 需要测量启动时间 是否需要零干扰测量? 是 否 DWT / 逻辑分析仪 SYSTICK / TIM 是否需要多通道并行? 是 否 TIM + 逻辑分析仪 DWT / SYSTICK 开始测量并记录数据 提示:最终验证阶段建议用示波器截图留档,作为交付物的一部分

2.6 实战案例:一次启动时间优化前后的数据对比

最后分享一个真实案例。之前做的一个双核项目,客户要求启动时间必须小于100ms。第一次用DWT测量,结果是187ms,远超目标。

我用逻辑分析仪抓了四个信号:Core0启动、Core0就绪、Core1启动、Core1就绪。波形显示Core0启动后等了80ms才开始Core1的初始化——原来代码里有个不必要的延时等待。

去掉这个延时后,启动时间降到了95ms。再用示波器拍了一张波形图,贴在测试报告里,客户当场就签字通过了。

关键数据:

  • 优化前:187ms(DWT测量)
  • 优化后:95ms(DWT测量)
  • 优化幅度:49.2%
  • 验证工具:逻辑分析仪 + 示波器

你看,没有准确的测量,你根本不知道瓶颈在哪里。这四种方法,你至少得掌握两种。DWT和逻辑分析仪是我最推荐的组合,一个管精度,一个管视野,搭配起来几乎能覆盖所有启动时间测量场景。