同名微信公众号“固件工人”同步发布的文章,欢迎同时关注,及时获取最新文章。
在单片机的程序开发中,延时一般都会用到,在对延时精度要求不高的场合,一般使用软件延时实现,就是在一个循环中执行NOP空语句。如果要实现相对高精度的延时,可以考虑使用定时器。
这里以STM32F429I-DISCO为基础硬件,来讲解如何使用ARM单片机内核System Tick定时器实现相对高精度的微秒和毫秒延时。
STM32F429I-DISCO开发板的官方例程默认配置并开启System Tick定时器,默认的定时周期是1ms。STM32F429I-DISCO开发板使用8MHz的外部晶振,这里将开发板的单片机主频设置成168MHz,System Tick定时器的时钟源来自单片机主频,所以该定时器计数值每变化1,对应的时间为1/168微秒。这样定时器的计数值每变化168,对应时间即为1微秒。我们可以将要延时的微秒数t转成定时器的计数值n,当定时器的计数值变化n时,说明对应的延时时间已到。
这里需要注意的地方是定时器的定时周期是1ms,即1000us,到了1000us之后,定时器会自动重载并重新开始计数,所以为了尽量规避定时器溢出对延时精度可能造成的影响,这里以定时器周期的一半,即500us为一个延时周期p,来设计微秒延时函数vDelayUs()。
微秒延时函数的设计思路如下。
(1)根据要延时的微秒数t,计算500us延时周期的个数n,再得出剩余的微秒数r。
(2)先延时n个500us的延时周期,完成之后,再延时剩余的微秒数r。
程序代码实现如下。该延时函数使用时有以下一些注意事项。
如果需要将代码移植到其他的单片机,需要根据单片机定时器的实际设置情况,对应修改最上面的相关宏定义即可。
延时函数的一些初始化代码也会占用一定的执行时间,所以这里的延时也会有一定的误差,但是微秒延时的理论误差应该可以控制在1us以内。
在实际使用时,进行延时时尽量只调用该函数一次,尽量不要使用循环的方式多次调用该函数实现一些长延时,因为这样会累加函数本身初始化代码的执行时间,造成延时误差变大。
该函数也会被中断打断,所以中断的执行时间要尽量短,否则会加大这里的延时误差。
由于该函数只对定时器的VAL计数值寄存器进行读取,所以并不影响定时器本身的1ms中断定时功能,可以充分利用该定时器资源来实现较为精确的延时功能。
#include "stm32f4xx.h" /* Cortex-M4 processor and core peripherals */
#define US_TICKS 168 // 每微秒的定时器计数值,需根据MCU工作主频修改,这里主频是168MHz
#define MAX_US_PER_PERIOD (1000 / 2) // 1个周期的最大微秒数,需根据System Tick周期修改,设置成System Tick周期的一半,防止微秒延时溢出造成不准确
#define MAX_TICKS_PER_PERIOD (MAX_US_PER_PERIOD * US_TICKS) // 1个周期的最大微秒数对应的定时器计数值
#define MAX_US_DELAY 4294967295 // uint32_t类型最大值
void vDelayUs(uint32_t p_dwUs)
{
uint32_t l_dwReloadValue = SysTick->LOAD;
uint32_t l_dwUsPeriodNum = p_dwUs / MAX_US_PER_PERIOD;
uint32_t l_dwUsRemainTicks = (p_dwUs % MAX_US_PER_PERIOD) * US_TICKS;
uint32_t l_dwDeltTicks = 0;
uint32_t l_dwCurTicks, l_dwPreTicks, l_dwIntervalTicks, i;
l_dwPreTicks = SysTick->VAL;
for(i = 0; i < l_dwUsPeriodNum; i++)
{
while(1)
{
l_dwCurTicks = SysTick->VAL;
// Cortex-M4的System Tick是向下计数的
if(l_dwCurTicks <= l_dwPreTicks)
{
l_dwIntervalTicks = l_dwPreTicks - l_dwCurTicks + l_dwDeltTicks;
}
else
{
l_dwIntervalTicks = l_dwPreTicks + (l_dwReloadValue - l_dwCurTicks + 1) + l_dwDeltTicks;
}
if(MAX_TICKS_PER_PERIOD <= l_dwIntervalTicks)
{
l_dwPreTicks = SysTick->VAL;
l_dwDeltTicks = l_dwIntervalTicks - MAX_TICKS_PER_PERIOD;
break;
}
}
}
if(0 < l_dwUsRemainTicks)
{
while(1)
{
l_dwCurTicks = SysTick->VAL;
if(l_dwCurTicks <= l_dwPreTicks)
{
l_dwIntervalTicks = l_dwPreTicks - l_dwCurTicks + l_dwDeltTicks;
}
else
{
l_dwIntervalTicks = l_dwPreTicks + (l_dwReloadValue - l_dwCurTicks + 1) + l_dwDeltTicks;
}
if(l_dwUsRemainTicks <= l_dwIntervalTicks)
{
break;
}
}
}
}
在上面的微秒延时函数的基础上,可以进一步设计毫秒延时函数。按照前面尽量减少微秒延时函数调用次数的原则,这里以最大微秒延时时间为基本单元,进行毫秒延时函数vDelayMs()的设计。设计思路如下。
(1)将要延时的毫秒数t转成微秒数,计算最大微秒延时时间的个数n,以及剩余的微秒延时数r。
(2)调用vDelayUs()函数先完成n个最大微秒延时,再调用vDelayUs()完成剩余的微秒延时数r。
同样,为了尽量提高延时精度,毫秒延时函数vDelayMs()在进行延时时也要尽量只进行一次调用。
毫秒延时函数vDelayMs()的程序代码实现如下。
void vDelayMs(uint32_t p_dwMs)
{
uint64_t p_llUs = (uint64_t)p_dwMs * 1000;
uint32_t p_dwMaxUsNum = p_llUs / MAX_US_DELAY;
uint32_t p_dwRemainUs = p_llUs % MAX_US_DELAY;
uint32_t i;
for(i = 0; i < p_dwMaxUsNum; i++)
{
vDelayUs(MAX_US_DELAY);
}
if(0 < p_dwRemainUs)
{
vDelayUs(p_dwRemainUs);
}
}
如果想进一步提高延时精度,可以实测延时函数开头的一些变量的初始化时间ti,然后在延时中对这些额外的执行时间进行补偿即可。也可以将C代码转成汇编代码来评估代码的实际执行时间。
从C代码转成汇编代码的方法可以参考以下链接。
arm-none-eabi交叉编译工具常用的一些指令:
https://blog.csdn.net/a13526758473/article/details/54982817
这里将延时函数放在了delay.c文件中,可以通过以下命令生成对应的汇编代码文件。
arm-none-eabi-objdump -d -S delay.o >delay1.txt
需要注意的是,要在汇编代码中嵌入C代码,需要在编译阶段使用-g参数来使汇编代码和C代码对应。即编译时使用以下命令。
arm-none-eabi-gcc -g ...