在编写STM32程序时,经常会需要在中断里进行延时,有的人会使用变量递减的方式,但是需要进行精确延时的情况,就必须要用到定时器,而内核中的滴答定时器SysTick自然就成了不二之选,也就是常用的delay_ms/delay_us函数

但是,往往在中断使用delay函数,特别是在写大工程时,却经常遇到各种奇奇怪怪的bug,比如显示屏异常,串口数据异常,WIFI蓝牙异常等等,只要是涉及到通讯且在通讯中使用了delay延时的设备,均有可能出现异常,最严重的当然就是死机

其实网上也有许许多多的人在咨询这个问题
但是得到的回答无一都是因为中断中延时占了资源,中断中不能停留太长时间等待,所以中断中一定不能使用delay
但是我给出的答案是:中断中可以用delay函数,只需要修改delay函数!


举一个最简单的例子,在外部中断中检测按键按下时使用delay函数消抖

主程序如下

int main(void)
{
	DELAY_Init();
	LED_Init();
	USART1_Init(115200);
	KEY0_Init();
	printf("start\r\n");
	while(1)
	{
		printf("runing\r\n");
		delay_ms(500);
	}
}

中断程序如下(此处使用的是STM32F0系列,其它系列同理)

void EXTI0_1_IRQHandler(void)
{
  if(EXTI_GetITStatus(EXTI_Line0) != RESET)
  {
		delay_ms(20);//消抖
		if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
		{
			SYS_LED_Toggle;
		}
  }
    /* Clear the EXTI line 0 pending bit */
    EXTI_ClearITPendingBit(EXTI_Line0);
}

delay函数如下

void delay_ms(u16 nms)
{		
	SysTick->LOAD = (u32)fac_ms * nms;//自动重装载值
	SysTick->VAL = 0x00;//清除计时器的值
	SysTick->CTRL |= (1 << 0);//SysTick使能,使能后LOAD寄存器的值就会被装载到VAL寄存器中,然后VAL开始向下递减
	while(!(SysTick->CTRL & (1 << 16)));//判断是否减到0,减到0时CTRL的第16位会置1,读取后会自动置0
	SysTick->CTRL &= ~(1 << 0);//关闭SysTick
}

补充说明,SysTick函数在stm32F0xxx ContexM0 编程手册中的寄存器定义如下(即程序中的CTRL寄存器)
不懂的同学可以先看下这篇博客:STM32F030 Nucleo-做个准确的延时SysTick
在这里插入图片描述

程序逻辑中,理论上是主程序串口一直发送运行信息,当按键按下时,LED电平翻转
但是,实际上跑在单片机里的现象是,一旦按下按键,电脑上的串口就再也收不到信息,而按键中断反转LED的功能却是正常的

这是为什么呢?

主程序几乎99%的时间都是在delay函数中的while语句,等待滴答定时器数到0。而在等待的过程中,中断触发了,中断里进入了delay函数,正常执行后,退出时将systick关闭了,使得退出中断后在主程序的delay函数里卡死在while里(systick已经关闭了,不再向下记数)


这就引发我们的深思了

其实,这个问题从本质上讲无疑就是因为systick定时器资源只有一个,而却被两个进程同时调用产生的冲突
为什么是同时调用呢,因为有了中断,如果不使用中断,那主程序中该函数只会被单次调用

所以在中断中不能使用delay?非也
我们从底层看delay函数,在主程序中跑delay时被中断再进入delay函数时会产生什么冲突呢
1是主程序中的delay计数值会被改变为中断中延时的数值,2是中断后定时器会被关掉

那么我们将delay函数修改如下

static u8 delaying_times = 0;//该次delay是否为中断中的第二次进入
void delay_ms(u16 nms)
{
	u32 last_systick_val;
	
	if(delaying_times != 0)//如果主程序在跑delay函数的过程中,发生中断并在中断中又进入了delay函数
	{
		last_systick_val = SysTick->VAL;//将上次的计数器的值保存下来以便退出中断后回去时可以从该值继续递减
	}
	delaying_times = 1;
	SysTick->LOAD = (u32)fac_ms * nms;//自动重装载值
	SysTick->VAL = 0x00;//清除计时器的值
	SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	while(!(SysTick->CTRL & (1 << 16)));//判断是否减到0,减到0时CTRL的第16位会置1,读取后会自动置0
	delaying_times = 0;
	if(delaying_times == 0)
	{
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->VAL = 0x00;//清除计时器的值(执行关闭SysTick程序时,记数器又开始了新一轮的倒数,所以关闭后记数器的值不为0)
	}
	else
	{
		/* 读取CTRL寄存器的同时,CTRL的第16位会变为0,关闭SysTick后给VAL寄存器赋值再使能的原因
		 * 1.若未关闭SysTick,且先将CTRL的第16位清零后再给VAL寄存器赋值,则在赋值的过程中计数器可能会记数到0,从而导致CTRL的第16位又被置1
		 * 2.若未关闭SysTick,且先给VAL寄存器赋值后再将CTRL的第16位清零,则在清零的过程中计数器会继续递减并且可能在CTRL的第16位完成清零前就溢出
		 * 所以必须关闭SysTick,且赋值完需要再使能使得递归回原函数的while中计数器会继续递减
		 */
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->LOAD = last_systick_val;
		SysTick->VAL = 0x00;//清除计时器的值
		SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	}
}

程序中我们加入了last_systick_val变量用来保存上一次的delay计数值
加入了delaying_times变量用来防止定时器被关闭

但这并不完美,如果在中断里,程序如果在执行其它内容时消耗了一些时间后再进入delay函数,那么就有可能出现一种情况,在进入delay函数时,计数器已经溢出,也就是CTRL的第16位已经变为0,那么此时记录下来的last_systick_val是重装载后的计数器数值(定时器的计数器是自动重装载计数器)

于是我又加入了一个变量

static u8 delaying_times = 0;//该次delay是否为中断中的第二次进入
static u16 delaying_finish = 0;//在进入该次delay时上次是否已经完成延时
void delay_ms(u16 nms)
{
	u32 last_systick_val;
	
	if(delaying_times != 0)//如果主程序在跑delay函数的过程中,发生中断并在中断中又进入了delay函数
	{
		last_systick_val = SysTick->VAL;//将上次的计数器的值保存下来以便退出中断后回去时可以从该值继续递减
		if(SysTick->CTRL & (1 << 16))delaying_finish = 1;
	}
	delaying_times = 1;
	SysTick->LOAD = (u32)fac_ms * nms;//自动重装载值
	SysTick->VAL = 0x00;//清除计时器的值
	SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	while(!(SysTick->CTRL & (1 << 16)))//判断是否减到0,减到0时CTRL的第16位会置1,读取后会自动置0
	{
		//如果在中断中计数器已经溢出,就退出while,并且对应记数完成标志清零
		if(delaying_finish == 1)
		{
			delaying_finish = 0;
			break;
		}
	}
	delaying_times = 0;
	if(delaying_times == 0)
	{
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->VAL = 0x00;//清除计时器的值(执行关闭SysTick程序时,记数器又开始了新一轮的倒数,所以关闭后记数器的值不为0)
	}
	else
	{
		/* 读取CTRL寄存器的同时,CTRL的第16位会变为0,关闭SysTick后给VAL寄存器赋值再使能的原因
		 * 1.若未关闭SysTick,且先将CTRL的第16位清零后再给VAL寄存器赋值,则在赋值的过程中计数器可能会记数到0,从而导致CTRL的第16位又被置1
		 * 2.若未关闭SysTick,且先给VAL寄存器赋值后再将CTRL的第16位清零,则在清零的过程中计数器会继续递减并且可能在CTRL的第16位完成清零前就溢出
		 * 所以必须关闭SysTick,且赋值完需要再使能使得递归回原函数的while中计数器会继续递减
		 */
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->LOAD = last_systick_val;
		SysTick->VAL = 0x00;//清除计时器的值
		SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	}
}

然而这并没有结束
上面的情况下是只考虑到只进入了一次中断的
那如果我在执行中断中的delay函数时,又发生了一次甚至n次的更高优先级中断并且进入delay函数了呢
这就涉及到一个递归的概念了,我直接摆出代码吧

static u8 delaying_times = 0;//叠加执行延时的次数
static u16 delaying_finish = 0;//记录最多16个的递归溢出事件中,每一个是否都已经记数溢出
void delay_ms(u16 nms)
{
	u32 last_systick_val;
	if(delaying_times != 0)//如果主程序在跑delay函数的过程中,发生中断并在中断中又进入了delay函数
	{
		last_systick_val = SysTick->VAL;//将上次的计数器的值保存下来以便退出中断后回去时可以从该值继续递减
		//如果上次记数已经溢出,代表着上次的delay已经记数完成,将该次溢出事件记录下来,以便出了中断回到原delay函数时,可以直接跳出while
		//delaying_finish是16位的,最多可以记录16次溢出事件,即16层的递归
		if(SysTick->CTRL & (1 << 16))delaying_finish |= (1 << (delaying_times - 1));
	}
	delaying_times ++;
	SysTick->LOAD = (u32)fac_ms * nms;//自动重装载值
	SysTick->VAL = 0x00;//清除计时器的值
	SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	while(!(SysTick->CTRL & (1 << 16)))//判断是否减到0,减到0时CTRL的第16位会置1,读取后会自动置0
	{
		//如果在中断中计数器已经溢出,就退出while,并且对应中断位清零
		if(delaying_finish & (1 << (delaying_times- 1)))
		{
			delaying_finish &= ~(1 << (delaying_times- 1));
			break;
		}
	}
	delaying_times --;
	if(delaying_times == 0)
	{
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->VAL = 0x00;//清除计时器的值(执行关闭SysTick程序时,记数器又开始了新一轮的倒数,所以关闭后记数器的值不为0)
	}
	else
	{
		/* 读取CTRL寄存器的同时,CTRL的第16位会变为0,关闭SysTick后给VAL寄存器赋值再使能的原因
		 * 1.若未关闭SysTick,且先将CTRL的第16位清零后再给VAL寄存器赋值,则在赋值的过程中计数器可能会记数到0,从而导致CTRL的第16位又被置1
		 * 2.若未关闭SysTick,且先给VAL寄存器赋值后再将CTRL的第16位清零,则在清零的过程中计数器会继续递减并且可能在CTRL的第16位完成清零前就溢出
		 * 所以必须关闭SysTick,且赋值完需要再使能使得递归回原函数的while中计数器会继续递减
		 */
		SysTick->CTRL &= ~(1 << 0);//关闭SysTick,关闭后记数器将不再倒数
		SysTick->LOAD = last_systick_val;
		SysTick->VAL = 0x00;//清除计时器的值
		SysTick->CTRL |= (1 << 0);//SysTick使能,使能后定时器开始倒数
	}
}

最后
代码部分需要读者们自己好好理解理解,作者最终的这个程序理论上是可行的,但没有经过大数据的测验,自测在一次中断的情况下是完全无问题的,如果有什么疑问欢迎在评论区留言,如果觉得好用就给该文章点个赞罢~

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐