1.1 背景
在单片机的固件开发过程中,有的时候需要评估固件代码的执行性能,会对部分关键程序代码的执行时间进行测量。通常会用到的测量程序执行时间的方法是使用示波器进行测量。一般步骤是借助单片机的某一个GPIO口,假设默认情况下GPIO口置1;在需要测量的程序代码开始处将GPIO口清0,然后执行程序代码段,在代码段的终止处将GPIO口重新置1;示波器设置成边沿触发方式,抓取GPIO口从清0到重新置1的这段波形,然后用示波器卡出GPIO口下降沿到上升沿的这段时间,也就是程序代码段的执行时间。
以上方法的不足之处在于需要用到示波器,而且需要借用MCU的一个GPIO进行辅助测量,灵活性也欠佳,实际使用不是太方便。那有没有更简便的测量方法呢?答案是肯定的,那就是使用MCU的定时器进行程序执行时间的测量。当然,为了提高时间的测量精度,MCU需要使用外部晶振来为其提供工作主频。下面就对该方法进行详细讲解。该方法结合下面提到的开发板,可以达到10ns以内的测量分辨率和1us以内的测量精度。
1.2 测试平台
这里使用的开发环境和相关硬件如下。
操作系统:Ubuntu 20.04.2 LTS x86_64(使用uname -a命令查看)
集成开发环境(IDE):Eclipse IDE for Embedded C/C++ Developers,Version: 2021-06 (4.20.0)
硬件开发板:STM32F429I-DISCO
本文对应的例程代码链接如下。
https://download.csdn.net/download/goodrenze/85162425
1.3 使用STM32定时器测量程序执行时间的方法详解
这里就结合开发板STM32F429I-DISCO上的STM32F429ZI单片机来演示使用SysTick系统定时器测量程序代码段执行时间的实现方法。
使用SysTick系统定时器测量程序执行时间之前,必须先确认定时器的以下参数。
定时器的时钟源频率。
定时器的定时周期。
定时器的计数方向。
这里的代码基于STM32F429I-DISCO开发板,该开发板的MCU外接8MHz的石英晶振,代码使用该外部晶振经内部PLL倍频后,产生168MHz的主频供MCU使用。这里的SysTick系统定时器的时钟源直接来自168MHz的主频,对该频率进行计数,所以每过1000 / 168 = 5.95238ns时间,定时器计数值就会加1。这里将SysTick定时器的定时周期设置成1ms,即每过1ms,SysTick定时器就会产生一次定时器中断。另外,SysTick定时器是倒计数定时器,即其计数值是递减的,当计数值减到为0时,继续减1时会重新加载重装载值并继续计时,同时产生定时器溢出中断。
确定了以上参数之后,后面的代码实现就非常简单了,只需要实现以下的几个功能函数皆可。
1)SysTick系统定时器初始化函数和中断处理函数。用于配置该定时器的定时周期为1ms,打开定时器中断并启动定时,同时实现对应的中断处理函数使定时器计数值累加。程序代码如下。
// 该函数为STM32的官方代码
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
return HAL_ERROR;
}
/* Return function status */
return HAL_OK;
}
// 该函数为STM32的官方代码,调用的SysTick_Config()函数在“core_cm4.h”头文件中有现成的实现
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
// SysTick系统定时器中断入口函数
void SysTick_Handler(void)
{
HAL_IncTick();
}
// SysTick系统定时器中断处理函数,对uwTick值进行累加
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
2)获取起始时间的函数。该函数用于获取SysTick系统定时器当前的毫秒计数值,以及当前的定时器计数值。程序代码如下。参数p_pdwStartMs为获取到的起始毫秒计数值,p_pdwStartNsTicks为获取到的起始定时器计数值。
void vGetStartTime(uint32_t* p_pdwStartMs, uint32_t* p_pdwStartNsTicks)
{
*p_pdwStartMs = HAL_GetTick();
*p_pdwStartNsTicks = SysTick->VAL;
}
3)获取时间间隔的函数。该函数用于获取当前时间相对于起始时间的时间间隔。程序代码如下。参数p_dwStartMs为起始毫秒计数值,p_dwStartNsTicks为起始定时器计数值,p_pdwIntervalMs为当前时间相对于p_dwStartMs的毫秒时间间隔,p_pdwIntervalNsTicks为当前时间相对于p_dwStartNsTicks的定时器计数间隔。
void vGetIntervalTime(uint32_t p_dwStartMs, uint32_t p_dwStartNsTicks, uint32_t* p_pdwIntervalMs, uint32_t* p_pdwIntervalNsTicks)
{
uint32_t l_dwCurMs = HAL_GetTick();
uint32_t l_dwCurNsTicks = SysTick->VAL;
uint32_t l_dwReloadValue = SysTick->LOAD;
// STM32F429ZI的定时器为倒数定时器。
// 如果当前的定时器计数值比起始计数值要小,SysTick未发生相对起始时刻不足1ms的定时器中断,所以ms计数无需额外减1
if(l_dwCurNsTicks <= p_dwStartNsTicks)
{
if(l_dwCurMs >= p_dwStartMs)
{
*p_pdwIntervalMs = l_dwCurMs - p_dwStartMs;
}
else
{
*p_pdwIntervalMs = ~(p_dwStartMs - l_dwCurMs) + 1;
}
*p_pdwIntervalNsTicks = p_dwStartNsTicks - l_dwCurNsTicks;
}
// 如果当前的定时器计数值比起始计数值要大,SysTick发生了相对起始时刻不足1ms的定时器中断,所以ms计数需要额外减1
else
{
if(l_dwCurMs >= p_dwStartMs)
{
*p_pdwIntervalMs = l_dwCurMs - p_dwStartMs - 1;
}
else
{
*p_pdwIntervalMs = ~(p_dwStartMs - l_dwCurMs);
}
*p_pdwIntervalNsTicks = p_dwStartNsTicks + (l_dwReloadValue - l_dwCurNsTicks) + 1;
}
}
4)获取程序代码段执行时间的演示例程。用于演示如何使用以上提到的相关函数来测量程序代码段的执行时间。
int main(void)
{
uint32_t count = 0;
uint32_t l_dwStartMs, l_dwIntervalMs;
uint32_t l_dwStartNsTicks, l_dwIntervalNsTicks;
float l_fUs; // 微秒时间
HAL_Init();
/* Configure the system clock to 168 MHz */
SystemClock_Config();
BSP_LED_Init(LED3);
BSP_LED_Init(LED4);
vGetStartTime(&l_dwStartMs, &l_dwStartNsTicks);
#if 1
while (1)
{
if (count == 0x3fffff)
{
BSP_LED_Toggle(LED3);
BSP_LED_Toggle(LED4);
count = 0;
break;
}
count++;
}
#else
vDelayUs(1000);
#endif
vGetIntervalTime(l_dwStartMs, l_dwStartNsTicks, &l_dwIntervalMs, &l_dwIntervalNsTicks);
l_fUs = l_dwIntervalMs * 1000 + l_dwIntervalNsTicks * NS_PER_SYS_TICK / 1000.0f;
while(1);
}
图1 以上演示例程代码段的执行时间
1.4 结语
通过以上提到的相关函数,可以很方便地实现程序执行时间的测量,而且可以在几乎任何地方使用(中断内部使用需注意中断优先级的影响)。另外,如果结合串口打印调试信息的功能,可以直接将测量到的执行时间直接打印输出,方便查看。本文提到的执行时间测量方法无需使用示波器,也不需要借用MCU的GPIO口进行辅助测量,使用起来非常方便。