嵌入式系统代码执行时间测量的4种实用方法

张开发
2026/4/6 0:17:02 15 分钟阅读

分享文章

嵌入式系统代码执行时间测量的4种实用方法
1. 嵌入式代码执行时间测量的必要性在嵌入式系统开发中精确测量代码执行时间是一项基础但至关重要的技能。作为一名嵌入式工程师我经常遇到这样的场景当你优化了一个关键算法后如何证明它确实变快了或者当你设计一个实时控制系统时如何确保控制周期能够稳定运行在1kHz甚至4kHz的频率下这些问题都指向一个核心需求我们需要准确、可靠地测量代码块的执行时间。不同于PC环境嵌入式系统特别是资源受限的MCU的测量面临着独特挑战系统资源有限很多嵌入式设备没有成熟的操作系统支持甚至运行在裸机环境时序精度要求高控制算法通常需要微秒级甚至纳秒级的测量精度测量手段受限不能像PC那样随意使用printf或复杂性能分析工具提示在嵌入式环境中不当的测量方法本身就可能显著影响被测代码的执行时间导致测量结果失真。2. 嵌入式时间测量的常见误区2.1 使用不合适的计时单位很多初学者会直接使用系统滴答(SysTick)来测量短时间代码块这是一个典型错误。例如系统滴答配置为1ms周期被测函数实际执行时间为10μs测量结果会显示0ms完全无法反映真实情况2.2 过度依赖打印输出另一个常见错误是滥用printf进行时间测量uint32_t start HAL_GetTick(); my_function(); uint32_t end HAL_GetTick(); printf(Execution time: %lu ms\n, end - start);问题在于printf本身可能耗时数毫秒在实时性要求高的场景串口输出会严重干扰系统时序频繁打印可能导致缓冲区溢出或其他副作用2.3 忽视CPU频率变化现代MCU常有动态频率调整功能运行中可能改变主频进入低功耗模式会降低或停止某些时钟外设可能使用与CPU不同的时钟域如果不考虑这些因素简单的周期数×时钟周期计算就会出错。3. 四种实用的测量方法3.1 CPU周期计数器DWT_CYCCNT3.1.1 原理与优势在Cortex-M系列处理器中调试观察点与跟踪单元(DWT)提供了一个32位周期计数器CYCCNT。这个计数器每个CPU时钟周期自增1提供最高精度的计时在72MHz主频下约14ns分辨率几乎零开销访问3.1.2 实现步骤以STM32为例启用DWT_CYCCNT的典型代码#define DEMCR_TRCENA 0x01000000 #define DWT_CTRL_CYCCNTENA (1UL 0) void cycle_counter_init(void) { /* 启用DWT模块 */ CoreDebug-DEMCR | DEMCR_TRCENA; /* 重置并启用周期计数器 */ DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA; } uint32_t cycle_counter_get(void) { return DWT-CYCCNT; }测量代码执行时间cycle_counter_init(); uint32_t start cycle_counter_get(); my_function_to_measure(); uint32_t end cycle_counter_get(); uint32_t cycles end - start; float time_us (float)cycles / (SystemCoreClock / 1000000.0f);注意使用前需确认芯片是否支持DWT模块某些低成本Cortex-M0可能没有此功能。3.1.3 优缺点分析优点最高精度单周期分辨率极低测量开销不受外设定时器配置影响缺点并非所有MCU都支持计数器可能在某些低功耗模式下停止32位计数器在低主频下可能溢出如72MHz下约59秒3.2 片上定时器测量法3.2.1 实现原理当DWT不可用时可以使用通用定时器作为替代方案。基本思路配置一个定时器为自由运行模式设置合适预分频使定时器计数频率已知如1MHz通过捕获前后计数值计算时间差3.2.2 具体实现以STM32的TIM2为例void timer_init(void) { /* 使能TIM2时钟 */ RCC-APB1ENR | RCC_APB1ENR_TIM2EN; /* 配置为向上计数无分频 */ TIM2-PSC 84 - 1; // 假设APB1时钟为84MHz目标1MHz TIM2-ARR 0xFFFFFFFF; // 32位自动重装载值 TIM2-CR1 | TIM_CR1_CEN; // 启动定时器 } uint32_t timer_get(void) { return TIM2-CNT; }测量示例timer_init(); uint32_t start timer_get(); my_function(); uint32_t end timer_get(); uint32_t delta_us end - start; // 直接得到微秒数3.2.3 注意事项定时器时钟源需稳定不受CPU主频变化影响16位定时器需处理溢出问题多个测量点可能共用一个定时器资源3.3 GPIO翻转示波器法3.3.1 方法介绍这是一种硬件辅助测量技术在代码开始处设置GPIO为高电平在代码结束处设置GPIO为低电平用示波器或逻辑分析仪测量高电平脉宽3.3.2 实现代码#define PROBE_PIN GPIO_PIN_0 #define PROBE_PORT GPIOA void probe_init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin PROBE_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(PROBE_PORT, GPIO_InitStruct); } void measure_function(void) { HAL_GPIO_WritePin(PROBE_PORT, PROBE_PIN, GPIO_PIN_SET); my_function_to_measure(); HAL_GPIO_WritePin(PROBE_PORT, PROBE_PIN, GPIO_PIN_RESET); }3.3.3 优势与局限优势测量结果直观可靠几乎不影响被测代码执行可以观察多次执行的波动情况局限需要额外硬件设备不便于自动化测试GPIO操作会引入少量额外时间通常可忽略3.4 RTOS任务运行时间统计3.4.1 FreeRTOS实现方案FreeRTOS提供了任务运行时间统计功能可以显示每个任务占用CPU时间的百分比。关键配置步骤在FreeRTOSConfig.h中启用统计功能#define configGENERATE_RUN_TIME_STATS 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1实现两个回调函数void vConfigureTimerForRunTimeStats(void) { // 初始化高精度定时器 timer_init(); // 使用前面介绍的定时器初始化 } uint32_t ulGetRunTimeCounterValue(void) { return timer_get(); // 返回当前定时器值 }在任务中打印统计信息void stats_task(void *pvParameters) { char stats_buffer[512]; while(1) { vTaskGetRunTimeStats(stats_buffer); printf(%s\n, stats_buffer); vTaskDelay(pdMS_TO_TICKS(1000)); } }3.4.2 输出示例典型输出格式Task Runtime Percentage ----------- ------- ---------- ControlTask 350000 35% CommTask 250000 25% LogTask 150000 15% Idle 250000 25%3.4.3 使用建议定时器频率建议1MHz1tick1μs统计功能会增加任务切换开销适合系统级优化不适用于函数级测量4. 方法选择与工程实践4.1 各方法对比方法精度侵入性实现复杂度适用场景DWT_CYCCNT最高低低函数级精确测量片上定时器高中中通用代码块测量GPIO示波器高低高硬件验证与调试RTOS运行时间统计中高高系统级性能分析4.2 实用宏定义在实际项目中可以定义一套测量宏方便使用#ifdef ENABLE_PROFILING #define PROF_INIT() cycle_counter_init() #define PROF_START(var) uint32_t var cycle_counter_get() #define PROF_END(var, label) \ do { \ uint32_t _end cycle_counter_get(); \ uint32_t _delta _end - (var); \ printf([PROF] %s: %lu us\n, (label), \ (unsigned long)(_delta / (SystemCoreClock / 1000000))); \ } while(0) #else #define PROF_INIT() #define PROF_START(var) #define PROF_END(var, label) #endif使用示例void control_loop(void) { PROF_START(t0); // 控制算法代码 PROF_END(t0, control_loop); }4.3 测量误差处理在实际测量中需要考虑以下误差来源测量代码本身的开销中断干扰缓存效应特别是带Cache的MCU流水线停顿建议采取以下措施多次测量取平均值关闭中断进行关键测量考虑最坏情况执行时间(WCET)5. 进阶技巧与注意事项5.1 低功耗模式下的测量当MCU进入低功耗模式时某些定时器可能停止工作CPU时钟可能被分频或关闭DWT计数器可能失效解决方案使用由独立时钟源驱动的低功耗定时器(LPTIM)在测量期间临时禁止低功耗模式校准不同模式下的时钟差异5.2 多核系统中的测量对于多核MCU如STM32H7每个核心可能有独立的DWT需要考虑核间同步问题共享资源可能引入测量偏差建议策略为每个核心单独配置测量工具使用核间通信最小化干扰考虑使用硬件性能计数器(PMU)5.3 长期运行的统计方法对于需要长时间监控的场景使用环形缓冲区记录时间样本实现滑动窗口统计记录最大值、最小值和平均值检测异常波动示例数据结构#define STAT_WINDOW_SIZE 100 typedef struct { uint32_t samples[STAT_WINDOW_SIZE]; uint32_t index; uint32_t sum; uint32_t max; uint32_t min; } time_stat_t; void stat_update(time_stat_t *stat, uint32_t new_sample) { stat-sum - stat-samples[stat-index]; stat-sum new_sample; stat-samples[stat-index] new_sample; if(new_sample stat-max) stat-max new_sample; if(new_sample stat-min) stat-min new_sample; stat-index (stat-index 1) % STAT_WINDOW_SIZE; }5.4 测量结果的可视化将测量数据导出到上位机可以更直观地分析通过串口或USB传输数据使用PythonMatplotlib绘制执行时间分布实现实时监控界面示例Python处理代码import serial import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200) times [] while True: line ser.readline().decode().strip() if line.startswith([PROF]): us int(line.split(:)[1].split()[0]) times.append(us) if len(times) 100: plt.clf() plt.plot(times[-100:]) plt.pause(0.01)通过多年实践我发现嵌入式系统中的时间测量既是一门科学也是一门艺术。选择合适的方法需要考虑精度需求、系统资源、开发阶段等多方面因素。建议在项目早期就建立完善的测量基础设施这将为后续的优化和调试节省大量时间。

更多文章