一、问题现象与背景
在STM32开发中,HAL_Delay() 是常用的毫秒级延时函数。但许多开发者发现,在中断服务函数(ISR)中调用 HAL_Delay() 时,程序可能卡死、延时时间严重偏长,甚至触发HardFault。例如:
void EXTI0_IRQHandler(void) {
HAL_Delay(10); // 期望延时10ms,实际可能卡死
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
这种现象并非偶然,而是由 HAL_Delay() 的实现机制与中断上下文冲突导致。
二、根因分析:HAL_Delay如何工作
2.1 HAL_Delay源码剖析
HAL库中 HAL_Delay() 的典型实现(以STM32CubeF1为例):
__weak void HAL_Delay(uint32_t Delay) {
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
if (wait < HAL_MAX_DELAY) {
wait += (uint32_t)(uwTickFreq);
}
while ((HAL_GetTick() - tickstart) < wait) {
// 空循环等待
}
}
其中 HAL_GetTick() 返回全局变量 uwTick,该变量在 SysTick_Handler() 中递增:
void SysTick_Handler(void) {
HAL_IncTick(); // uwTick++
}
2.2 失效根因
-
SysTick中断优先级问题:
SysTick默认优先级较低(通常为15)。若当前中断的优先级高于或等于SysTick优先级,SysTick中断无法抢占,uwTick停止递增,HAL_Delay()中的while循环将永远等待,导致死锁。 -
中断嵌套与重入:若在
SysTick中断服务函数内调用HAL_Delay(),uwTick不会更新,同样死锁。 -
实时性破坏:即使
SysTick能抢占,HAL_Delay()是忙等待,会阻塞当前中断,延长中断响应时间,影响系统实时性。
结论:HAL_Delay() 依赖中断更新计数,且为阻塞式轮询,在中断上下文中不可靠。
三、替代延时方案
3.1 方案一:DWT周期计数器忙等待(微秒/毫秒级)
Cortex-M内核自带DWT(Data Watchpoint and Trace)周期计数器,不依赖中断,适合短延时。
配置步骤:
- 使能DWT:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; - 复位并启动周期计数器:
DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
代码示例:
#include "core_cm4.h" // 根据内核调整
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
void DWT_DelayUs(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles) {
__NOP();
}
}
void DWT_DelayMs(uint32_t ms) {
while (ms--) {
DWT_DelayUs(1000);
}
}
优点:不依赖中断,可在任意上下文使用;精度高。
缺点:忙等待,长延时会浪费CPU;需内核支持DWT(Cortex-M3/M4/M7等)。
3.2 方案二:硬件定时器单次触发延时
利用一个基本定时器(如TIM6)产生单次延时,通过轮询标志位实现。
配置步骤(以TIM6为例):
- 使能TIM6时钟。
- 设置预分频器(PSC)和自动重装载寄存器(ARR)以产生所需延时。
- 启动定时器,等待更新标志位。
代码示例:
void TIM6_DelayMs(uint32_t ms) {
// 假设系统时钟84MHz,预分频8400-1,则计数频率10kHz,1ms=10计数
TIM6->PSC = 8400 - 1;
TIM6->ARR = ms * 10 - 1;
TIM6->EGR = TIM_EGR_UG; // 生成更新事件,加载寄存器
TIM6->SR = ~TIM_SR_UIF; // 清除更新标志
TIM6->CR1 |= TIM_CR1_CEN; // 启动定时器
while (!(TIM6->SR & TIM_SR_UIF)); // 等待更新标志
TIM6->SR = ~TIM_SR_UIF; // 清除标志
TIM6->CR1 &= ~TIM_CR1_CEN; // 关闭定时器
}
优点:不依赖SysTick中断,精度较高;适合毫秒级延时。
缺点:占用一个定时器资源;忙等待。
3.3 方案三:非阻塞状态机延时(推荐用于中断)
在中断中避免任何忙等待,采用状态机+系统滴答计数实现非阻塞延时。
代码示例:
volatile uint32_t delay_counter = 0;
// 在SysTick中断中递减
void SysTick_Handler(void) {
HAL_IncTick();
if (delay_counter > 0) delay_counter--;
}
// 启动非阻塞延时(单位:ms)
void StartDelay(uint32_t ms) {
delay_counter = ms;
}
// 检查延时是否结束
bool IsDelayDone(void) {
return (delay_counter == 0);
}
// 在中断中使用示例
void EXTI0_IRQHandler(void) {
static bool led_on = false;
if (IsDelayDone()) {
led_on = !led_on;
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, led_on ? GPIO_PIN_SET : GPIO_PIN_RESET);
StartDelay(500); // 下次触发间隔500ms
}
}
优点:完全非阻塞,不占用CPU;适合中断和主循环。
缺点:需要SysTick中断正常工作;延时精度受中断响应影响。
四、注意事项
- 中断优先级配置:若使用SysTick相关方案,确保SysTick优先级高于或等于调用延时的中断优先级,否则计数不更新。
- DWT计数器溢出:DWT->CYCCNT为32位,在168MHz下约25.5秒溢出,长延时需处理溢出。
- 定时器资源冲突:使用硬件定时器方案时,避免与其他功能共用定时器。
- 非阻塞方案适用性:状态机延时适合周期性任务,但无法实现“精确等待”语义。
- 调试影响:DWT在调试器暂停时可能停止计数,需注意。
- HAL_Delay的替代:在中断中绝对避免调用HAL_Delay;主循环中可继续使用,但需知悉其阻塞特性。
五、总结
HAL_Delay() 在中断上下文失效的根因是依赖SysTick中断更新计数且为忙等待。开发者应根据场景选择替代方案:短延时用DWT,毫秒级用硬件定时器,中断中推荐非阻塞状态机。理解底层机制才能写出稳定可靠的嵌入式代码。
暂无评论,来抢沙发~