• 正文
  • 相关推荐
申请入驻 产业图谱

HAL_Delay(0)为什么实际还要等1ms?读懂STM32延时设计的小巧思

8小时前
29
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大伙第一次测 HAL_Delay(0),很容易得到一个反直觉的结果:函数没有立刻返回,而是等到了下一次 HAL tick,完成了1秒的等待

参数明明是 0,为什么还会等待?这不是编译器插入了什么神秘延时,而是 HAL 的实现先把等待值补成了一个时基周期。

这篇文章博主从这个现象开始,逐级追到 HAL_Delay()、HAL_GetTick()、HAL_IncTick() 和 uwTickFreq,最后再解释 ST 为什么要这样设计。

HAL_Delay(0) 不是空操作

在一份 STM32 HAL 实现中,函数核心代码是:

uint32_t tickstart = HAL_GetTick();uint32_t wait = Delay;/* Add a freq to guarantee minimum wait */if (wait < HAL_MAX_DELAY){  wait += (uint32_t)(uwTickFreq);}while ((HAL_GetTick() - tickstart) < wait){}

当调用 HAL_Delay(0) 时:

tickstart = 当前 tickwait      = 0wait      = 0 + uwTickFreq

默认 1 kHz HAL tick 下,uwTickFreq 的数值是 1。因此,循环退出条件实际上变成了:

HAL_GetTick() - tickstart >= 1

也就是说,它至少要等到 uwTick 发生一次更新。调用时距离下一次 tick 还剩多少时间,决定了这次等待可能很短,但不会在当前计数值不变时直接返回。

这是带有 uwTickFreq 补偿的 HAL 实现的行为。不同 STM32 系列、HAL 包和版本可能有差异,最终应以工程中的 stm32xxxx_hal.c 为准。

HAL_Delay() 到底在等什么

把 HAL_Delay(0) 替换成一个普通参数,代码逻辑仍然一样:先记录起点,再持续读取当前 tick。

uint32_t tickstart = HAL_GetTick();uint32_t wait = Delay;if (wait < HAL_MAX_DELAY){  wait += (uint32_t)(uwTickFreq);}while ((HAL_GetTick() - tickstart) < wait){}

函数并没有读取 CPU 的高精度计时器,也没有计算微秒差。它只比较两个离散的 tick 计数值。

以 HAL_Delay(1) 为例,如果调用发生在当前 1 ms 周期的最后 0.1 ms,tickstart 可能是 100。等到下一次 tick 更新后,计数变成 101,差值已经是 1。

如果没有补偿,循环此时就会退出,实际只等了约 0.1 ms。

因此,HAL_Delay() 等的是 “tick 计数差”,而不是从函数入口开始精确计时。问题的关键,已经从 HAL_Delay(0) 追到了它的计时来源。

一些较早或不同系列的实现,可以抽象成下面这样:

uint32_t tickstart = HAL_GetTick();while ((HAL_GetTick() - tickstart) < Delay){}

这段代码在参数为 0 时会立即退出;参数为 1 时,则只要 tick 差达到 1 就退出。它的缺口正是调用时刻的 tick 相位:第一次更新可能只剩 0.1 ms,计数却已经满足条件。

这里的“旧实现”指不带最小等待补偿的实现形态,不指向一个已经核实的统一版本号。判断具体工程是否存在这个问题,仍然要直接查看对应 HAL 源文件。

HAL_GetTick() 返回的值从哪里来?

STM32Cube FW F1 V1.8.7 软件包中的原始代码如下

__weak voidHAL_IncTick(void){  uwTick += uwTickFreq;}
__weak uint32_tHAL_GetTick(void){  return uwTick;}

通常情况下,SysTick 或工程选定的其他 HAL 时基中断会周期性调用 HAL_IncTick()。默认 1 ms 更新一次时,每次调用让 uwTick 增加 1。

所以,HAL_Delay() 的等待过程可以还原成一句话:记录当前 uwTick,然后等它增加到目标差值。

为什么补的是 uwTickFreq

看同一软件包里的频率枚举

typedefenum{  HAL_TICK_FREQ_10HZ         = 100U,  HAL_TICK_FREQ_100HZ        = 10U,  HAL_TICK_FREQ_1KHZ         = 1U,  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ} HAL_TickFreqTypeDef;

这里的数值不是“每秒调用多少次”,而是每次 tick 更新时,uwTick 增加多少毫秒。

- 1 kHz:每 1 ms 更新一次,uwTickFreq = 1;- 100 Hz:每 10 ms 更新一次,uwTickFreq = 10;- 10 Hz:每 100 ms 更新一次,uwTickFreq = 100;

因此源码写成 wait += uwTickFreq,而不是把 wait 永远加 1。它补的是一个 HAL 时基周期对应的计数量。

为什么要多等一个周期?

现在回到最初的现象。

如果 HAL_Delay(n) 只等待 tick 差达到 n,调用时刻落在当前周期末尾,就可能只等到 n-1 个完整周期加上一小段尾巴。对 HAL_Delay(1) 来说,实际等待甚至可能接近 0 ms。

补上一个 uwTickFreq 后,目标变成 n + uwTickFreq。在默认 1 ms 时基下,调用后的第一次 tick 更新只完成了“补上当前周期尾差”的一部分,函数还会继续等待下一次更新。

可以用一个简单模型表示:

不补偿:T ≈ (n - 1) × tick_period + δ补偿后:T ≈  n      × tick_period + δ

其中 δ 是从函数调用时刻到下一次 tick 更新的剩余时间。补偿的目标不是让返回时刻精确落在 n ms,而是避免在参数要求的最短时间之前返回。

这就是注释 Add a period to guaranty minimum wait 所表达的意思:增加一个时基周期,保证最小等待。

因此,HAL_Delay(0) 也会等待一个 tick,并不是一个单独的特殊分支。它只是把 Delay = 0 经过同一套最小等待补偿后,变成了“至少跨过一次时基更新”。

区分:最小等待和精确定时

这个设计解决的是“不能提前结束”,并不代表 HAL_Delay(n) 能提供精确的 `n ms` 波形。

其他中断、函数执行开销、时钟误差和调度都会让返回时间变长。如果 HAL tick 被暂停,或者在会阻止时基中断运行的上下文里调用,等待循环还可能无法退出。

同样,下面的代码也不能自动形成严格的 10 ms 采样周期:

while (1){  ReadSensor();  HAL_Delay(10);}

需要固定周期时,使用定时器触发;需要微秒级脉宽时,使用硬件定时器或其他专用时基。先区分“至少等待”“固定周期”和“精确脉宽”,再选择接口。

最后

HAL_Delay(0) 之所以可能等待一个 tick,表面上看是 wait += uwTickFreq 带来的结果,往深一层看,它体现的是设计者希望:参数不只是“希望等待多久”,还要说明“能不能提前结束”。

旧实现把 tick 差达到目标值当成完成条件,代码很简单,却把调用时刻的相位误差交给了使用者。新实现多留出一个时基周期,牺牲了一点平均等待时间,换来“至少等待参数所表达时间”的下限保证。对于复位保持、芯片启动、总线切换这类有最短时序要求的操作,这个取舍更容易预测。

当然,离散 tick 只能解决最小等待,不能替代精确定时器。真正重要的是先弄清楚需求:需要的是一个不会提前结束的延时、一个严格的周期,还是一个精确的脉宽。代码里的 +1 才不会看起来像一个莫名其妙的补丁,而是整个时基设计的一部分。

相关推荐

嵌入式开发兴趣爱好者,热衷于电子信息技术,嵌入式开发,物联网技术等领域开发。