状态机
核心原理公式: 状态机 (逻辑骨架) + 定时器中断 (时间切片/粒度) = 非阻塞的实时多任务处理
我们把这句话拆解开来,看看它们到底是怎么配合的:
一、 定时器:提供“时间切片”(Time Slice)
- 作用:它是一把**“尺子”**。
- 原理:
- 如果不作为,CPU 感觉不到时间的流逝。
- 定时器每 10ms 中断一次,相当于给系统打了一个**“节拍 (Tick)”**。
- 每一次中断,就是一个基本的时间单位(粒度)。
- 为什么重要?
- 有了这个节拍,我们就不需要用
delay_ms(1000)去死等 1 秒钟。 - 我们只需要数:“滴答”响了 100 次,就是 1 秒钟。
- 有了这个节拍,我们就不需要用
二、 状态机:提供“上下文记忆”(Context)
光有时间切片没用,CPU 每次进中断读一下 GPIO,如果不用状态机,它就是个健忘症患者。
- 场景:
- 第 1 次进中断:读到低电平。CPU:“哦,低的。”
- 第 10 次进中断:读到低电平。CPU:“哦,低的。”
- 没有状态机,CPU 不知道这“第 10 次”和“第 1 次”之间是有联系的。
- 作用:它是一个**“记事本”**。
- 它记录了**“上一次我处于什么情况”**。
- 原理:
- 状态变量 (State) 就是记忆。
- 当 CPU 发现当前是低电平,且状态是
STATE_PRESS_DOWN(按下态),它就知道:“这不是刚按下的,而是已经按下一段时间了。”
三、 它们是如何“狼狈为奸”的?(核心逻辑流)
这就是你说的“判断时间粒度”的具体执行过程。让我们看一个长按 1 秒是如何被识别出来的:
- 粒度设定:定时器设定 10ms 一次中断(这就是你的粒度)。
- 初始状态:状态机处于
IDLE(空闲),计数器cnt = 0。 - 开始计数:
- 按下按键。
- 中断来了,检测到低电平 -> 状态切到
PRESS。
- 重读与累积(核心):
- 下一个 10ms 中断来了(重读):GPIO 还是低,状态还是
PRESS->cnt++(现在是 1)。 - 再过 10ms (重读):GPIO 还是低 ->
cnt++(现在是 2)。 - ...
- CPU 每次只花几微秒做个加法,然后立马去干别的(比如刷新屏幕)。它没有被卡住!
- 下一个 10ms 中断来了(重读):GPIO 还是低,状态还是
- 阈值判断:
- 当
cnt累加到 100 时。 - 数学换算:
100次 * 10ms = 1000ms = 1秒。 - 状态机判断:
if (cnt > 100)-> 触发长按事件!
- 当
结论: 你说得对,核心就是利用定时器的周期性中断(重读),将物理时间转化为“计数值”,再由状态机根据这个“计数值”来决定是否跳转状态。
四、 资深工程师的经验总结:关于“时间粒度”的坑
既然你提到了“时间粒度”,我必须提醒你选择粒度的三个原则:
1. 粒度太细(比如 10us)—— CPU 累死
- 如果你搞个 10us 的定时器去扫按键。
- 后果:CPU 疯狂进中断,每秒进 10万次。光是进出中断的开销就把 CPU 占满了,主程序会变卡。而按键这种慢速设备根本不需要这么快的反应。
2. 粒度太粗(比如 100ms)—— 手感稀烂
- 如果你 100ms 才扫一次。
- 后果:用户快速点击(比如 50ms 内按下松开),你的定时器可能正好夹在中间那段“空隙”里,漏检了。用户会觉得按键“不灵”。
3. 黄金粒度(10ms - 20ms)
- 对于人手操作的按键,10ms 到 20ms 是最佳平衡点。
- 既能完美实现消抖(通常抖动在 5-10ms),又能保证 CPU 负载极低,同时用户感觉不到延迟。
总结
定时器是心跳(提供时间基准),状态机是大脑(处理逻辑关系)。**