做延时函数,可以使用简单的循环等待,如下面这样的:
void Delay(uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
但是有个问题,就是这个nCount
值怎么取?
我们可以通过多次试验,来确定调用时使用的循环次数。但是还要考虑下,如果硬件有变化,例如外接晶振变化,或类似的主芯片替换等情况下,这个值有可能会变化。另外,编译的优化选项变化,也可能导致循环次数的变化。也就是说,这样写的延时函数,对外部的依赖项比较多,稍不注意,可能最终的延时时间不准确。更好的延时方式是使用定时器,这样能更准确的定时,并且移植性也更好一些。但是使用定时器做延时函数时,也是有一些需要注意的事情的,否则,可能会掉入坑中还茫然不知。例如我本人,就掉了几次坑,花了好长时间才爬出来…
先简单说明下我的开发环境,芯片类型是stm32F030C8,集成开发环境用的是Keil5 MDK-ARM,仿真器使用JLINK。
通常我们使用定时器来做延时函数,比较常见的例子就是这样的:
#include "delay.h"
static int8_t fac_us=0;//us
static int16_t fac_ms=0;//ms
static int flag_HCLK_Div8=1;
void delay_init()
{
if(flag_HCLK_Div8){
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//选择外部时钟 HCLK/8
fac_us=SystemCoreClock/48000000; //为系统时钟的1/8
} else {
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择外部时钟
fac_us=SystemCoreClock/1000000; //为系统时钟
}
fac_ms=(int16_t)fac_us*1000;//每个ms需要的systick时钟数
}
//延时N us
void delay_us(int32_t nus)
{
int32_t temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00;
}
//延时N ms
void delay_ms(int16_t nms)
{
int32_t temp;
SysTick->LOAD=(int32_t)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00;
}
然后我就开始使用了。
我是这么使用的:
主循环调用 delay_ms()
,中断里面调用delay_us()
,这是考虑到中断里面要尽量做少的操作,所以使用短的延时。然而,在运行过程中,发现有时候会遇到主循环有快速结束等待的情况,远远没有达到我希望的延时时间!对着代码左看右看,没看出来毛病。后来,在主循环中替换使用那种简单的循环等待的延时函数,就不再出问题了。这才确定到问题就在这个delay_*()
延时函数上。再仔细分析延时耗时,发现问题:这两个函数使用的是同一个定时器硬件:SysTick。
例如,若主循环中希望延时1000ms,调用delay_ms(1000)
,SysTick->LOAD
的值设置为1000ms了。若在这时,又进入了中断,有个延时100us的操作,调用delay_us(100)
,SysTick->LOAD
的值设置为100us了。两次设置的是同一个寄存器,显然,后一次的设置,覆盖了前一次的设置值!然后,启动定时器的倒数计时:SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;
等100us时间到了,判断循环中的结束条件,while(temp&0x01&&!(temp&(1<<16)));
符合,则延时完成,继续进行中断里面的其他操作。
等退出中断后,主循环继续执行。此时还在延时函数delay_ms()
中等待呢,
查看判断条件:while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
看temp的赋值:temp=SysTick->CTRL;
开启了倒数计时,并且SysTick倒数到0了。
这里,我们需要判断 SysTick->CTRL
中相关字段的意义:
最低位(第0位):ENABLE
,是SysTick 定时器的使能位
第16位:COUNTFLAG
,如果在上次读取本寄存器后, SysTick 已经计到了 0,则该位为 1。
再来看这两个位的现状:
考虑到delay_us()
执行完成了,也就是说,SysTick 已经计到了 0了,即 SysTick->CTRL&(1<<16)
的值置1了。并且,跳出循环后还执行了一句:SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
它的意思就是关闭SysTick定时器的使能,即 (SysTick->CTRL&0x01)
的值为0。
所以,此时,在delay_ms()
中的判断条件已经不满足了:即temp&0x01
为0
,而且,temp&(1<<16)
也是0
,所以,会立即结束循环。基本上,相当于,外面的延时1s,被里面的delay_us截断,同时也结束了。主循环的延时1s,在最坏的情况下(延时刚启动就遇到中断),可能才过了约100us,就结束了!
教训:对于同一个定时器,这样写法的延时函数,不能在主循环与中断里面同时调用!
当在主循环中处于延时等待状态下,中断里面的延时,会修改定时器的状态,从而导致主循环的延时不准确了。再从另一个更通用的角度来看,其实就是对于同一个全局变量(SysTick),在两个线程中同时访问,并且没有做访问保护。所以,产生问题,就是迟早的事情了。
解决方法,使用两个定时器,就能解决了:一个在主循环中调用,一个在中断里面调用。
进一步思考:若有多个中断,而且都存在延时的调用,就需要多个定时器吗,这可能会导致定时器都不够用呢,那又该怎么办?这其实就是涉及一个设计思路了。可以说,在中断里面调用延时函数,本身就不是一个好主意,能避免则避免。如果不能避免,就需要采用另外的一种方式来解决问题了,也并不需要多个定时器,一个定时器就可以了,我们看了下面这个问题再来说。
上面这种主循环与中断里面同时调用延时函数的问题,还有另一种表现形式:
类似于我在上一篇博客中的延时函数:
#include "timer.h"
#include "stdio.h"
#include "gpio.h"
#include "stm32f0xx_tim.h"
volatile unsigned int gTimer;
void TIM1_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 系统中TIM1用的是APB2,TIM14时钟用的是APB1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //tim1时钟使能,APB1时钟8M
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV2; //分频系数为2 //是对APB1的2倍频进行分频,分频系数为2,所以频率还是8M
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数设置 //对于TIM1是必须设置的
// 计算定时周期: t=(9+1)*1/f=2/(8M/(7+1))=10*8/8M(s)=10us
TIM_TimeBaseInitStructure.TIM_Period = 9; //定时10us //最大65536
TIM_TimeBaseInitStructure.TIM_Prescaler = 7; //时钟8M
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure);
TIM_ClearITPendingBit(TIM1,TIM_IT_Update);//清除TIM1的中断待处理位:TIM 中断源
TIM_ITConfig(TIM1,TIM_IT_Update,ENABLE); //允许定时器1更新中断
TIM_Cmd(TIM1,ENABLE); //使能定时器1
// 设置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM1_BRK_UP_TRG_COM_IRQn; //定时器1中断
NVIC_InitStructure.NVIC_IRQChannelPriority = 0; //优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1,TIM_IT_Update) != RESET) //溢出中断
{
if(gTimer>0){
gTimer--;
}
}
TIM_ClearITPendingBit(TIM1,TIM_IT_Update); //清除中断标志位
}
//毫秒的延时函数
void delay_ms_tim(uint32_t nTimer)
{
gTimer=nTimer*100;
while(gTimer);
}
//微秒的延时函数,实际以10us为最小单位
void delay_us_tim(uint32_t nTimer)
{
gTimer=nTimer/10;
while(gTimer);
}
若在中断与主循环中同时使用delay_ms_tim() , delay_us_tim()
,也会有同样的问题。
当主循环中执行了delay_ms_tim(1000)
时,设置了gTimer=1000*100=100000
;若此时中断进入,执行了delay_us_tim(100)
,则设置了gTimer=100/10=10
;这样,后一次的设置,就覆盖了前一次对gTimer的赋值,从而导致两个延时函数会同时结束,也就是说,delay_ms_tim(1000)
实际延时时间可能只比100us略多,而我们期望的是1s,差距巨大!这里的问题,我们可以看出,是因为使用了同一个全局变量gTimer
。
那么,我们就有了一个不一样的解决方法:并不需要使用多个定时器,而是使用同一个计数器,多个计数变量。
只是增加计数变量的话,不影响TIM1_Init()
函数,下面的代码就不展示它了。我们增加一个gTimer
变量,相应修改两个延时函数如下:
volatile unsigned int gTimer_ms;
volatile unsigned int gTimer_us;
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1,TIM_IT_Update) != RESET) //溢出中断
{
if(gTimer_ms>0){
gTimer_ms--;
}
if(gTimer_us>0){
gTimer_us--;
}
}
TIM_ClearITPendingBit(TIM1,TIM_IT_Update); //清除中断标志位
}
//毫秒的延时函数
void delay_ms_tim(uint32_t nTimer)
{
gTimer_ms=nTimer*100;
while(gTimer_ms);
}
//微秒的延时函数,实际以10us为最小单位
void delay_us_tim(uint32_t nTimer)
{
gTimer_us=nTimer/10;
while(gTimer_us);
}
这样,这两个延时函数就不会互相影响了。
相应的,如果需要更多独立作用的延时函数,就可以增加相应的gTimer变量即可。这样子,就实现了一个定时器多个延时函数的功能了。
另外,我还遇到过一个bug,会导致死循环:
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1,TIM_IT_Update) != RESET) //溢出中断
{
gTimer--;
}
TIM_ClearITPendingBit(TIM1,TIM_IT_Update); //清除中断标志位
}
void delay_ms_tim(uint32_t nTimer)
{
gTimer=nTimer;
while(gTimer);
}
有时,调用这个延时函数会变成死循环!为什么?
几经折腾,终于搞明白是时序的问题,在gTimer=1
时,若其它的中断里面,做了较多的事情,导致一次定时器的中断周期过去了,还没有处理完成,则
gTimer--
,变为0
;
gTimer--
,变为4G-1
;4294967295
然后的等待时间之长,几乎就可以认为是进入了死循环了。
这里的问题有两个:
1,是逻辑不完善的问题,解决方法:在 gTimer--
; 时,加一个判断即可解决:
if(gTimer>0){
gTimer--;
}
2,是软件设计问题,在那个中断里面,做的事情太多了,导致耗时超过了1ms!这是不合理的,耗时较多的操作,不应该在中断里面做!不过,这就需要调整软件中的设计了。
最后,还有一个注意的事情,就是定时的最小值。
在我的系统中,使用了默认的8M的时钟,基本上最小定时为:10us,若是再小,就不准确了。
这个定时的最小值,应该与是否使用外部晶振,以及定时器的频率设置有关系,不同的芯片,也会有不同。我没有详细研究,使用不同方案的同学们,需要关注自己的最小定时间隔。比较安全的做法,就是不要用太小的定时间隔。毕竟,追求极限是好事,但是,没有掌握好极限,出了问题,就是麻烦事了。
原文链接:https://blog.csdn.net/lintax/article/details/86771653?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-86771653-blog-132601297.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-86771653-blog-132601297.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=5