一、HAL_Delay的实现原理与设计初衷

HAL_Delay是STM32 HAL库中最常用的延时函数,其核心实现依赖于SysTick定时器。典型代码如下:

__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中断中递增。
  • 函数通过忙等待(轮询)方式实现延时,期间CPU空转。
  • 设计初衷是用于非中断上下文的简单延时,如初始化外设、等待稳定时间等。

二、在中断上下文中调用HAL_Delay的隐患

2.1 优先级反转与系统死锁

若在中断服务函数(ISR)中调用HAL_Delay,而该中断的优先级高于或等于SysTick中断优先级,则SysTick中断无法抢占当前ISR,uwTick停止递增,HAL_GetTick()返回值恒定,导致while循环永不退出——系统死锁。

  • 即使SysTick优先级更高,若在ISR中关闭了全局中断(__disable_irq()),同样会导致uwTick不更新。
  • 在RTOS环境中,SysTick常被RTOS接管,HAL_Delay可能依赖RTOS的时基,行为更不可控。

2.2 破坏实时性

忙等待延时在ISR中会阻塞低优先级中断和任务,严重破坏系统实时响应能力。

2.3 可移植性差

不同HAL版本或RTOS适配层对HAL_Delay的实现可能不同,依赖它进行中断延时存在移植风险。

结论:严禁在中断上下文中调用HAL_Delay。

三、替代延时方案

3.1 方案一:DWT周期计数器(推荐用于短延时)

Cortex-M内核内置DWT(Data Watchpoint and Trace)单元,其CYCCNT寄存器记录CPU周期数,可实现高精度、无中断依赖的延时。

配置步骤:

  1. 使能DWT和CYCCNT:CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
  2. 计算延时周期数:cycles = us * (SystemCoreClock / 1000000);
  3. 轮询CYCCNT差值。

完整代码:

#include "stm32f4xx_hal.h"  // 根据实际型号调整

void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

void DWT_Delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT;
    uint32_t cycles = us * (SystemCoreClock / 1000000U);
    while ((DWT->CYCCNT - start) < cycles) { }
}

注意事项:

  • 需在系统时钟初始化后调用DWT_Init()。
  • 延时期间不可被更高优先级中断打断过久,否则实际延时偏长(但不会死锁)。
  • 适用于微秒级短延时,长延时仍建议用硬件定时器。

3.2 方案二:硬件定时器单次触发(推荐用于中长延时)

使用一个基本定时器(如TIM6/TIM7)产生单次更新中断,在中断中置标志位,主循环或ISR中轮询标志。

配置步骤(以TIM6为例):

  1. 使能TIM6时钟。
  2. 配置预分频器和自动重装载值,使定时周期等于所需延时。
  3. 使能更新中断,启动定时器。
  4. 在中断回调中置位标志。

完整代码:

volatile uint8_t tim6_done = 0;

void TIM6_Init(void)
{
    __HAL_RCC_TIM6_CLK_ENABLE();
    TIM6->PSC = 8400 - 1;          // 84MHz/8400 = 10kHz
    TIM6->ARR = 1000 - 1;          // 1000 * 0.1ms = 100ms
    TIM6->DIER |= TIM_DIER_UIE;
    HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
}

void TIM6_DAC_IRQHandler(void)
{
    if (TIM6->SR & TIM_SR_UIF) {
        TIM6->SR &= ~TIM_SR_UIF;
        tim6_done = 1;
        TIM6->CR1 &= ~TIM_CR1_CEN; // 单次模式
    }
}

void TIM6_Delay_ms(uint32_t ms)
{
    tim6_done = 0;
    TIM6->CNT = 0;
    TIM6->ARR = ms * 10 - 1;       // 10kHz计数
    TIM6->CR1 |= TIM_CR1_CEN;
    while (!tim6_done) { }
}

注意事项:

  • 该方案在ISR中轮询标志仍会阻塞,但不会死锁(定时器独立运行)。
  • 若在ISR中调用,需确保定时器中断优先级高于当前ISR,否则标志无法置位。
  • 更优雅的方式是使用状态机非阻塞设计。

3.3 方案三:RTOS延时(适用于RTOS环境)

在FreeRTOS等系统中,任务上下文使用vTaskDelay(),中断上下文使用xTaskGetTickCountFromISR()配合状态机,避免任何忙等待。

// 任务中
vTaskDelay(pdMS_TO_TICKS(100));

// 中断中记录时间戳,任务中判断
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
TickType_t xTime = xTaskGetTickCountFromISR();

四、总结与选型建议

  • 禁止在中断中使用HAL_Delay。
  • 微秒级短延时:优先选用DWT周期计数器。
  • 毫秒级中长延时:使用硬件定时器单次触发,配合标志位或状态机。
  • RTOS环境:任务用vTaskDelay,中断用时间戳+状态机。
  • 所有替代方案均需注意中断优先级配置,避免相互阻塞。

合理选择延时方案,是保证嵌入式系统实时性与稳定性的关键一步。