STM32定时中断原理与精准延时技巧
连续:就像真实流淌的时间,每时每刻都在变化,你可以有 1.1ms、1.11ms 这样的概念。离散:就像每隔 1ms 拍下的照片,tick 值只在快门按下的那一瞬间才变化,其余时间它都“凝固”着。tick 值是离散的,意味着你只能在一个个固定的时间点(比如 1ms、2ms、3ms)去“观测”时间,而无法得知两个观测点之间精确的、连续的变化。这就是“离散性”和“边界误差”的根源。tickstart:拿
-
STM32F103 主频 72 MHz(每秒 72,000,000 个时钟周期)
-
你想每 1ms 中断一次
计算:
text
1ms = 0.001 秒 每个时钟周期 = 1/72,000,000 秒 ≈ 13.89 ns 1ms 内有多少个时钟周期? 0.001 / (1/72,000,000) = 72,000
所以把 SysTick 的初始值设成 72000,它就会每 1ms 减到 0,触发一次中断。
你去等公交车:
-
公交车每 10 分钟一班(T=10)
-
你想等 1 班车的时间(wait=1,即等 10 分钟)
如果你不加 1(只等 1 班):
-
可能你刚到车站,车刚走
-
等 10 分钟(实际只等了一班车的时间),下一班还没来
-
因为你是从车刚走开始等的,10 分钟后下一班刚好到
但如果你加 1(等 2 班车的时间):
-
无论你什么时候到车站
-
你一定能等到至少一班车
这就是为什么不管你想等几班车,都要多等一班——因为你的“开始时刻”是随机的。
为什么所有 wait 都要 +1
因为无论 wait 是 1、2、5 还是 100,都有可能遇到这种“刚启动就 tick”的最坏情况。
+1 的作用是:
把“等 wait 个 tick”变成“等 wait+1 个 tick”,保证在最坏情况下,也能等到至少 wait 个 tick 的真实时间。
如果开始时刻不是 tick 刚加完,而是差一点点就 tick呢?
你在 t=0.999ms 时开始等,此时 tick 值还是 0:
| 真实时间 | tick 值 | 差值 |
|---|---|---|
| 0.999ms 开始 | 0 | 0 |
| 1.000ms | tick 中断,tick=1 | 1 → 达到 wait,退出 |
实际等了 0.001ms,就退出了。
你想等 1ms,结果只等了 0.001ms——因为 tick 在你刚启动后 0.001ms 就更新了,差值瞬间从 0 跳到 1。
| 前缀 | 含义 | 位数(在STM32中) | 例子 |
|---|---|---|---|
uw |
Unsigned Word | 32 位 | uwTickFreq |
u16 或 hs |
Unsigned half-word / short | 16 位 | u16Counter |
u8 或 c |
Unsigned char / byte | 8 位 | u8Data |
p |
Pointer | 32 位 | pBuffer |
所以,当你看到 uwTick 时,就知道它是一个 32 位的无符号整数
-
uwTickFreq:这个名字是“Tick Frequency”的缩写。在 HAL 库里,它通常是一个表示 “一个 tick 对应多少毫秒” 的常量。最常见的情况是,uwTickFreq = 1,表示 1 个 tick 就是 1 毫秒。
真实时间是连续的,像“拍视频”
想象你在拍一个小朋友从滑梯上滑下来的过程,用的是手机录像。
-
真实世界:小朋友的位置是每时每刻都在变化的。从滑梯顶端到底部,是一个平滑、连续的运动轨迹。这就是连续的时间:1.1秒、1.11秒、1.111秒……任何一个瞬间,他都有一个确定的位置。
-
录像:你的手机其实是以每秒30帧(30fps)的速度,快速抓拍一张张静态的照片,然后连续播放出来。
真实时间(连续) 就像这个平滑的运动本身。
tick 值(离散) 就像你录下的那一张张照片。
二、tick 值是“离散的”,就像那一张张照片
现在,我们说“tick 值是离散的”,意思就是:
我们不是在每一个连续的瞬间都能读到 tick,而只是在特定的、间隔开的时刻才能读到它。
-
录像:你只在第 0 帧、第 1 帧、第 2 帧……这些“离散”的时间点,记录下了小朋友的位置。在两个帧之间(比如第 1 帧和第 2 帧之间),你其实不知道他具体在哪。
-
tick 值:它只在每次 SysTick 中断发生时(比如每 1ms)才更新一次。在两个 tick 之间(比如 1.1ms 和 1.2ms),
uwTick这个变量的值是固定不变的。
所以,tick 值是 1,它代表的不是一个瞬间,而是一整段时间区间,比如从 1.000ms 到 1.999ms 这一整段“时间块”。
三、“离散” vs “连续”导致的误差
现在回到你的代码:
你调用 HAL_GetTick(),它返回的是那个每隔 1ms 才变化一次的“照片编号”(离散值),而不是真实的、连续的物理时间。
-
你想等 5ms(连续时间)。
-
你的检查方式:
当前照片编号 - 开始时的照片编号 >= 5。
最坏的情况就像我们之前画的:
-
你在 t = 0.999ms 时开始等。这时刚拍完第 0 张照片,
start = 0。 -
0.001ms 后(t = 1.000ms),第 1 张照片拍下,tick 值变成 1。
-
你的代码一检查:
1 - 0 = 1,还差得远,继续等。 -
时间流逝,当拍到第 5 张照片时(t = 5.000ms),你的代码检查:
5 - 0 = 5,条件满足,退出。 -
但实际上,从你开始等(t=0.999ms)到退出(t=5.000ms),只过了 4.001ms。
你用一张张静止的照片(离散值),去判断一个连续的运动(真实时间),就必然会漏掉两张照片之间的那一点点微小变化,从而产生这个“差一点”的误差。
四、总结
-
连续:就像真实流淌的时间,每时每刻都在变化,你可以有 1.1ms、1.11ms 这样的概念。
-
离散:就像每隔 1ms 拍下的照片,tick 值只在快门按下的那一瞬间才变化,其余时间它都“凝固”着。
tick 值是离散的,意味着你只能在一个个固定的时间点(比如 1ms、2ms、3ms)去“观测”时间,而无法得知两个观测点之间精确的、连续的变化。这就是“离散性”和“边界误差”的根源。
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
-
tickstart:拿到开始等待时的系统 tick 值(比如 1000)。 -
wait:把用户想等的毫秒数(比如 50)存下来,后面可能会用到。
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while ((HAL_GetTick() - tickstart) < wait)
{
}
-
这是一个死循环,条件不满足就一直在里面转。
-
每次循环,都重新计算
(现在的 tick - 开始的 tick),这就是已经过去的时间。 -
当这个差值 ≥
wait时,循环退出。
__weak 是什么意思?
你可能会注意到函数名前有个 __weak。这是一个编译器指令,意思是 “弱定义”。
它告诉编译器:“这是我提供的 HAL_Delay 函数,你可以直接用。但如果你在别的地方自己写了一个同样名字的函数,那就用你自己的,不用我的。”
这给了开发者极大的自由。比如,如果你想让单片机在延时的时候进入低功耗模式,就可以自己写一个 HAL_Delay,把原来的覆盖掉。__weak 让这种替换变得非常干净。
为什么要有 __weak
因为 HAL 库不知道你的具体需求:
-
有人想在延时的时候让 CPU 睡觉省电。
-
有人想在延时的时候顺便喂狗。
-
有人想用定时器来做精确延时,而不是靠 SysTick。
__weak 让 ST 提供一个 “能用但不一定最好” 的默认实现,同时给你一个优雅的入口去替换它,而不需要去修改 HAL 库源码。
更多推荐



所有评论(0)