1. HAL_Delay() 的实现原理
实现机制
HAL_Delay() 是 STM32 HAL 库提供的阻塞式延时函数,其核心实现依赖 SysTick 定时器,具体流程如下:
// HAL 库关键代码片段 __weak void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); while ((HAL_GetTick() - tickstart) < Delay) { // 空循环等待 } } // 系统滴答计时器(SysTick)的中断服务函数 void SysTick_Handler(void) { HAL_IncTick(); // 全局变量 uwTick 自增 } // 获取当前系统时间戳 __weak uint32_t HAL_GetTick(void) { return uwTick; }
关键点解析
-
SysTick 配置:默认使用 1ms 中断周期(通过
HAL_SYSTICK_Config()
设置) -
阻塞式设计:通过
while
循环持续检查时间差,占用 CPU 资源 -
弱函数特性:
__weak
修饰符允许用户重写实现
2. HAL_Delay() 的缺陷
(1) 实时性杀手
// 在中断服务函数中使用 HAL_Delay() 会导致灾难性后果 void USART_IRQHandler(void) { HAL_Delay(100); // 将阻塞所有低优先级中断 }
(2) 精度局限
-
受 SysTick 中断响应延迟影响
-
最小延时单位 = 1ms(默认配置)
-
无法实现微秒级精确延时
(3) 资源浪费
-
在
while
循环期间,CPU 无法执行其他任务 -
多任务系统中可能引发优先级反转问题
3. 替代方案与实现方法
方案一:非阻塞式延时(推荐)
用结构体实现
// 定义非阻塞延时结构体(检查过程不会阻塞程序) typedef struct { uint32_t start_time;//记录当前时间 uint32_t delay_ms;//延时总时长 } NonBlockingDelay; // 初始化延时 void Delay_Start(NonBlockingDelay* d, uint32_t ms) { d->start_time = HAL_GetTick(); d->delay_ms = ms; } // 检查是否超时 uint8_t Delay_IsTimeout(NonBlockingDelay* d) { return (HAL_GetTick() - d->start_time) >= d->delay_ms; } // 使用示例 NonBlockingDelay led_delay; Delay_Start(&led_delay, 500); if (Delay_IsTimeout(&led_delay)) { // 执行周期任务 Delay_Start(&led_delay, 500); // 重置计时 }
简化版
// 非阻塞延时简化版,仅处理一个延时 //结构体繁琐,可以用 静态变量+函数 简化 节省内存资源(适合单一延时场景) uint8_t isDelayDone(uint32_t ms) { static uint32_t start = 0; //静态变量 if (HAL_GetTick() - start >= ms) { start = HAL_GetTick(); // 自动重置 return 1; } return 0; } // 使用示例 if (isDelayDone(500)) { // 每500ms执行一次 }
方案二:硬件定时器精确延时
// 使用 TIM2 实现微秒级延时(预分频配置为 84MHz/84 = 1MHz) void Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim2, 0); HAL_TIM_Base_Start(&htim2); while (__HAL_TIM_GET_COUNTER(&htim2) < us); HAL_TIM_Base_Stop(&htim2); } // 实现原理: // TIM2 时钟源 = APB1 Timer clocks (默认 84MHz) // 预分频值 Prescaler = 83 → 计数器频率 = 1MHz (1us/计数)
方案三:RTOS 任务调度
// FreeRTOS 的精确延时(需要使能 vTaskDelayUntil) void TaskFunction(void const * argument) { TickType_t xLastWakeTime = xTaskGetTickCount(); while(1) { // 每 500ms 精确执行(不受任务执行时间影响) vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500)); // 执行周期性任务 } }
方案四:低功耗模式结合
// 使用 STOP 模式实现超低功耗延时 void Enter_StopMode(uint32_t ms) { RTC->CR |= RTC_CR_WUTE; // 使能 RTC 唤醒 // 配置 RTC 唤醒间隔 HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, ms, RTC_WAKEUPCLOCK_CK_SPRE_16BITS); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config(); // 唤醒后需重新配置时钟 }
三、与其他延时方案的区别
方案 | 原理 | 阻塞? | 适用场景 |
---|---|---|---|
HAL_Delay() | 依赖SysTick中断,死循环等待直到时间到 | 阻塞 | 简单任务,无需多线程 |
SysTick寄存器轮询 | 直接读取SysTick寄存器计算时间差,无需中断 | 非阻塞 | 需要高精度或避免中断的场景 |
状态机定时检查 | 在状态机中结合时间条件切换状态,依赖非阻塞计时 | 非阻塞 | 复杂逻辑(如按键消抖、协议解析) |
本文的非阻塞结构体 | 封装开始时间和时长,通过时间差检查超时 | 非阻塞 | 多任务并行、周期性操作 |
四、关键差异详解
-
HAL_Delay()
-
❌ CPU空转:调用后卡在原地,无法处理其他逻辑。
-
❌ 中断依赖:若中断被禁用,计时会失效。
-
✅ 简单粗暴:适合初始化、单任务等简单场景。
-
-
SysTick寄存器轮询
-
✅ 更高精度:直接读寄存器,避免中断延迟。
-
❌ 需处理溢出:需自行处理32位计数器的溢出问题
-
(如用
(current - start) < 0x7FFFFFFF
)。
-
-
状态机定时检查
-
✅ 逻辑解耦:将时间条件嵌入状态迁移,适合复杂流程。
-
✅ 资源复用:多个状态可共享计时逻辑,节省内存。
-
-
非阻塞结构体
-
✅ 模块化:结构体封装多个独立计时器,方便管理多任务。
-
✅ 即插即用:通过
Delay_Start
和Delay_IsTimeout
快速实现周期任务。
-
五、实战场景对比
1. 控制LED闪烁
-
阻塞写法(HAL_Delay):
while(1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(500); // CPU卡在此处,无法做其他事 }
-
非阻塞写法(结构体):
NonBlockingDelay led_delay; Delay_Start(&led_delay, 500); while(1) { if (Delay_IsTimeout(&led_delay)) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); Delay_Start(&led_delay, 500); // 重置计时 } // 此处可插入其他任务(如按键扫描、通信) }
2. 状态机中的超时处理
typedef enum { STATE_IDLE, STATE_WAIT } State; State current_state = STATE_IDLE; NonBlockingDelay timeout; void state_machine() { switch(current_state) { case STATE_IDLE: if (触发条件) { Delay_Start(&timeout, 1000); current_state = STATE_WAIT; } break; case STATE_WAIT: if (事件达成) { current_state = STATE_IDLE; } else if (Delay_IsTimeout(&timeout)) { // 超时处理 current_state = STATE_IDLE; } break; } }
六、如何选择?
-
简单任务:用
HAL_Delay
快速实现。 -
多任务/复杂逻辑:非阻塞结构体或状态机。
-
高精度需求:SysTick寄存器轮询。
-
资源受限:状态机+静态变量节省内存。