STM32在中断里使用SysTick_delay延时引起的一些异常死机bug分析及其解决方案

STM32中断中安全使用delay函数
本文探讨了在STM32程序中如何在中断里安全地使用delay函数,通过修改delay函数避免资源冲突,解决由此引发的各种bug,如显示屏异常、串口数据异常等问题。文中详细介绍了delay函数的修改方法,包括处理中断中多次进入delay函数的情况。

在编写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使能,使能后定时器开始倒数
	}
}

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

### 解决STM32中HAL_Delay影响TIM2中断的问题 #### 问题分析STM32 HAL库中,`HAL_Delay()`函数依赖于SysTick定时器来实现毫秒级延时[^1]。当调用`HAL_Delay()`时,程序会进入一个空循环等待,直到SysTick计数器的值达到指定的延时时间。如果在此期间发生其他高优先级中断(如TIM2中断),可能会导致中断处理被延迟或阻塞,从而影响系统的实时性。 #### 解决方案 为避免`HAL_Delay()`对TIM2中断的影响,可以采取以下几种方法: #### 方法一:调整中断优先级 通过调整SysTick和TIM2中断的优先级,确保TIM2中断的优先级高于SysTick中断。这样即使在`HAL_Delay()`执行期间,TIM2中断也能及时响应。 ```c // 设置TIM2中断优先级 HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 主优先级0,子优先级0 HAL_NVIC_EnableIRQ(TIM2_IRQn); // 设置SysTick中断优先级 HAL_NVIC_SetPriority(SysTick_IRQn, 1, 0); // 主优先级1,子优先级0 ``` #### 方法二:使用自定义非阻塞延时函数 替代`HAL_Delay()`,使用基于定时器的非阻塞延时函数。这种方法不会阻塞CPU,允许其他中断正常运行。 ```c volatile uint32_t delay_start_tick; void delay_ms(uint32_t ms) { delay_start_tick = HAL_GetTick(); } uint8_t is_delay_finished(uint32_t ms) { return (HAL_GetTick() - delay_start_tick) >= ms; } ``` 在主循环中,可以这样使用: ```c delay_ms(100); // 延时100ms while (!is_delay_finished(100)) { // 在这可以执行其他任务 } ``` #### 方法三:禁用SysTick中断 在需要精确控制中断优先级的情况下,可以临时禁用SysTick中断,以确保TIM2中断不受干扰。 ```c // 禁用SysTick中断 SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 启用SysTick中断 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; ``` 需要注意的是,禁用SysTick中断期间,`HAL_Delay()`将无法正常工作,因此应尽量缩短禁用时间。 #### 方法四:使用独立的定时器实现延时 通过配置一个独立的定时器(如TIM3)来实现延时功能,避免使用SysTick。这种方式完全独立于SysTick,不会对其它中断造成影响。 ```c // 初始化TIM3用于延时 void MX_TIM3_Init(void) { TIM_HandleTypeDef htim3; htim3.Instance = TIM3; htim3.Init.Prescaler = 7200 - 1; // 72MHz/(7200) = 10kHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0xFFFF; // 最大计数值 if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { Error_Handler(); } HAL_TIM_Base_Start(&htim3); } // 非阻塞延时函数 void delay_tim3(uint32_t ms) { __HAL_TIM_SET_COUNTER(&htim3, 0); // 清零计数器 while (__HAL_TIM_GET_COUNTER(&htim3) < (ms * 10)) { // 每tick 0.1ms // 允许其他任务运行 } } ``` #### 性能对比 - **`HAL_Delay()`**:简单易用,但可能阻塞中断。 - **自定义非阻塞延时**:灵活性高,不影响其他中断[^4]。 - **独立定时器延时**:完全独立于SysTick,适合复杂系统[^2]。 #### 注意事项 - 调整中断优先级时需谨慎,避免影响系统稳定性。 - 自定义延时函数需确保计数器溢出处理正确。 - 使用独立定时器时,需合理分配资源,避免冲突。 ### 示例代码 以下是一个完整的示例,展示如何使用TIM3实现非阻塞延时,同时确保TIM2中断正常运行。 ```c #include "stm32f1xx_hal.h" TIM_HandleTypeDef htim2, htim3; void MX_TIM2_Init(void) { // 配置TIM2为100ms中断 htim2.Instance = TIM2; htim2.Init.Prescaler = 7200 - 1; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1000 - 1; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } HAL_TIM_Base_Start_IT(&htim2); } void MX_TIM3_Init(void) { // 配置TIM3为延时定时器 htim3.Instance = TIM3; htim3.Init.Prescaler = 7200 - 1; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0xFFFF; if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { Error_Handler(); } HAL_TIM_Base_Start(&htim3); } void delay_tim3(uint32_t ms) { __HAL_TIM_SET_COUNTER(&htim3, 0); while (__HAL_TIM_GET_COUNTER(&htim3) < (ms * 10)) { // 非阻塞延时 } } int main(void) { HAL_Init(); SystemClock_Config(); MX_TIM2_Init(); MX_TIM3_Init(); while (1) { delay_tim3(100); // 延时100ms // 执行其他任务 } } ```
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值