第一次接触STM32开发的朋友,十有八九都是从HAL_Delay()这个函数开始入门的。记得我刚开始用STM32做项目时,最常用的就是这个毫秒级延时函数。但后来在做一个工业控制项目时,突然发现设备运行49天后就会莫名其妙卡死,排查了整整一周才发现是HAL_Delay()的"有效期"问题。今天就带大家彻底搞懂这个看似简单却暗藏玄机的函数。
HAL_Delay()是STM32 HAL库提供的标准延时函数,底层依赖SysTick定时器实现。它的工作原理就像厨房里的计时器:设置一个倒计时时间,然后不断检查当前时间是否到达设定值。在STM32内部,这个"计时器"就是uwTick这个全局变量,每毫秒自动加1(默认情况下)。当uwTick从0开始计数,经过大约49.7天后会重新归零,这就是那个著名的"49天陷阱"。
让我们打开STM32 HAL库的源代码,看看这个函数的庐山真面目:
c复制__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()获取当前系统tick值(uwTick)wait += uwTickFreq这行确保最小延时为1msHAL_Delay()的精确性依赖于SysTick定时器,它就像STM32的"心跳"。在HAL_Init()中会初始化这个定时器:
c复制HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
// 配置1ms中断
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U) {
return HAL_ERROR;
}
// 设置中断优先级
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
return HAL_OK;
}
每次SysTick中断都会调用HAL_IncTick()递增uwTick值。这种硬件级定时保证了HAL_Delay()的精度。
uwTick是32位无符号整数,最大值0xFFFFFFFF对应约49.7天。超过这个时间会归零,这时如果正在执行HAL_Delay()会发生什么?
通过实验验证:
c复制uint32_t testn = 0xFFFFFFFE; // 模拟即将溢出
uint32_t tickss = 0;
while ((tickss - testn) < 100) { // 延时100ms
tickss = HAL_GetTick();
}
结果发现当tickss=98时会正常退出循环。这是因为无符号数减法的特性保证了即使溢出也能正确计算时间差。
在实际项目中,我发现当有高优先级中断频繁触发时,HAL_Delay()会出现明显误差。这是因为:
解决方法:
当传入参数为0时,由于wait += uwTickFreq这行代码,实际会产生1ms延时。这点在需要精确控制时序时要特别注意。
通过调整uwTickFreq可以改变时间基准:
c复制#define HAL_TICK_FREQ_1KHZ 1U // 1ms
#define HAL_TICK_FREQ_100HZ 10U // 10ms
#define HAL_TICK_FREQ_1HZ 1000U // 1s
在HAL_Init()前调用:
c复制HAL_SetTickFreq(HAL_TICK_FREQ_100HZ);
注意:这会同时影响所有基于HAL_Delay()和HAL_GetTick()的功能。
在STOP模式下SysTick会停止,这时可以:
示例代码:
c复制void Enter_StopMode(uint32_t timeout_ms)
{
HAL_SuspendTick(); // 暂停SysTick
// 配置RTC唤醒
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, timeout_ms, RTC_WAKEUPCLOCK_RTCCLK_DIV16);
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
HAL_ResumeTick(); // 恢复SysTick
}
对于需要更高精度的场景,可以这样优化:
c复制void precise_delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < cycles);
}
使用前需要启用DWT计数器:
c复制CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
在RTOS中使用时,建议改用系统提供的延时函数:
c复制osDelay(100); // FreeRTOS延时
这样可以避免阻塞整个系统,让低优先级任务也有执行机会。
当怀疑HAL_Delay()有问题时,可以:
Q: 延时时间比预期长很多?
A: 检查:
Q: 程序卡在HAL_Delay()不退出?
A: 可能:
| 方式 | 精度 | 功耗影响 | 适用场景 |
|---|---|---|---|
| HAL_Delay() | 1ms | 较高 | 通用延时 |
| 硬件定时器 | 1us级 | 中等 | 高精度控制 |
| 软件循环 | 不稳定 | 最高 | 简单测试 |
| RTOS延时 | 1ms | 低 | 多任务系统 |
| 低功耗定时器 | 10ms级 | 最低 | 电池供电设备 |
根据项目需求考虑:
我的经验法则是:
在实际项目中,我总结了这些黄金法则:
有个印象深刻的项目:我们开发的一款智能锁因为使用了HAL_Delay()做按键消抖,结果在电池供电时功耗居高不下。后来改用LPTIM实现延时,待机电流从500uA降到了50uA以下。这个教训让我深刻认识到延时函数选择的重要性。
最后提醒大家,虽然HAL_Delay()简单易用,但在正式产品中要谨慎使用,特别是在对功耗和实时性有要求的场合。理解底层原理后,你就能根据实际需求选择最合适的延时方案了。