最近一工程师向我反馈了一个问题,他使用ARM Cortex-M0+的MCU,在使用延时函数std_delayms延时1s时,如果勾选了KEIL中的Use MicroLIB会导致延时有5%的计时偏差,不勾选的话误差只有1%。
首先进行问题的复现,在程序中while(1)里调用std_delayms(1000),通过串口发送一个字符,在上位机上进行接收,可以清楚的看到勾选微库之后误差确实明显增大,每次偏差大约为50ms。
这个现象看着比较奇怪,刚开始时也困扰了我,那么到底是什么原因呢?
首先要看延时函数是如何实现的,
std_delayms函数是通过systick定时器来实现,采用的是阻塞的方式,实现代码如下:
/**
* @brief us级延时函数(阻塞模式)
* @param count 计数周期
* @note 延时函数最大值受限于SysTick重载值寄存器的最大值0xFFFFFF(16777216)
* @note 该函数为weak函数,用户可选择其他定时器重新定义实现该函数
* @retval 无
*/
__weak void std_delayus(uint32_t count)
{
count = STD_DELAY_US * count;
count = count > 16777216 ? 16777216 : count;
SysTick->LOAD = count - 1;
SysTick->VAL = 0;
while(!((SysTick->CTRL >> 16) & 0x1));
}
/**
* @brief us级延时函数(阻塞模式)
* @param count 计数周期
* @note 延时函数最大值受限于SysTick重载值寄存器的最大值0xFFFFFF(16777216)
* @note 该函数为weak函数,用户可选择其他定时器重新定义实现该函数
* @retval 无
*/
__weak void std_delayus(uint32_t count)
{
count = STD_DELAY_US * count;
count = count > 16777216 ? 16777216 : count;
SysTick->LOAD = count - 1;
SysTick->VAL = 0;
while(!((SysTick->CTRL >> 16) & 0x1));
}
#define STD_DELAY_US (SystemCoreClock/1000000) /**< STD中用于计时的基础值,与计时周期有关 */
看起来似乎没什么问题,就是调用1000次std_delayus(1000)函数,std_delayus(1000)一次延时1ms,累计实现1s的延时。
勾选MicroLIB与否肯定不会影响systick计数器本身的计时,那么唯一能影响的就是其它为数不多的几条赋值语句了。
最后定位到是SystemCoreClock/1000000 这个除法运算导致的,使用微库后会让除法运算执行的更慢。
我们测一下执行一次除法运算需要的时间。本次测试中,系统时钟为8Mhz,通过GPIO翻转的方式来测
LED1_OFF();
divide_test(1);
LED1_ON();
int divide_test(uint32_t loop )
{
uint32_t count;
while(loop--)
{
count = SystemCoreClock/1000000;
}
return count;
}
可以看到勾选微库后,执行时间为49us
不勾选微库,执行时间为9us
两者足足相差了好几倍,勾选微库后,执行1000次除法运算时间大约就是50ms,正好和之前的现象就对应上了。
如果想让定时更加准确,可以不使用除法,这样误差会更小,实测只有几个ms的偏差。
原始代码,每次调用std_delayus函数都进行一次除法运算是为了保证系统主频会发生变化时依然能正确计时,实际使用中在已知特定主频的情况下,可以不用每次都进行除法运算,只在主频变化的情况下更新一次即可。
MicroLib 和 standard C library的主要区别有:
来源于:[MicroLib MDK-ARM Library (keil.com)](https://www.keil.com/arm/microlib.asp#:~:text=The primary differences between MicroLib,using the ARM standard library.)
MicroLIB更加详细的介绍,大家可以在KEIL的Help里查看:
上面提到MicroLIB虽然减少code size,但是有些函数会执行的更慢,这次遇到的问题恰恰就对应这一条。这其实对应的用时间换空间的概念,对于某些运算,MicroLIB执行时间变长了,换回的好处是空间占用更小。实际使用中需要在空间和时间上做一定的取舍。
ARM Cortex-M0+没有硬件除法器,除法是相对复杂的操作,相比加法、减法和乘法,需要更多的CPU周期才能完成。
测试一段代码所需要的CPU周期,这里墙裂推荐傻孩子的超级嵌入式系统“性能/时间”工具箱perf_counter,在Github上已开源https://github.com/GorgonMeducer/perf_counter.git ,我在这里用上了,可以很方便的测试。具体使用大家可以参考他的【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱
比如测试这个函数的CPU周期数,直接这么调用就行
__cycleof__() {
divide_test(1);
}
使用微库时,可以看到占用了414个CPU周期,对应时间414/8000000=51.7us
而不用微库时,可以看到占用了96个CPU周期,对应时间96/8000000=12us
所以就是这个问题的完整分析,希望对大家有所帮助。