本人是初学者,非常愿意与各位多交流。
最近在看到定时器时,觉得还是先弄个简单版的描述,看起来比较有概念。
想象一下你在做饭:
-
你的大脑(ESP32的主CPU):正在忙着切菜、看菜谱。
-
厨房计时器(硬件定时器):你设定好“10分钟后提醒我关火”,然后就不用管它了,专心切菜。
-
时间到了(定时器中断):计时器“叮!”的一声响,提醒你该去关火了。你暂停切菜(当前任务),去关火(执行定时任务),关完火再回来继续切菜。
ESP32 的定时器工作原理几乎一模一样!
ESP32 定时器的核心概念
-
数量: ESP32 内部有 4 个独立的硬件定时器。想象成你有 4 个独立的厨房计时器(Timer0, Timer1, Timer2, Timer3)。你可以同时使用它们做不同的事情。
-
功能: 这些定时器主要有两大本领:
-
计时/定时中断: 就像厨房计时器。你设定一个时间间隔(比如 1 秒),时间一到,它就“叮!”(产生一个中断),告诉 CPU:“嘿!时间到了!该干点啥了!” CPU 会暂时停下手头的工作,去执行你预先设定好的任务(比如改变一个LED灯的状态),执行完再回来继续原来的工作。
-
计数: 它可以数外部发生的事情的次数(比如数一个按钮按了多少下),或者自己数内部的“嘀嗒”声。这更像是计数器功能,我们这里主要讲定时。
-
-
为啥不用
delay()
? 初学者常用delay(1000)
让程序停 1 秒。这就像你关火时,必须站在炉子前死死盯着 1 分钟,啥也不能干(CPU 被完全占用,不能做其他任务)。而定时器是后台计时,CPU 在等待期间可以做其他事情(比如切菜、读取传感器),效率高得多! -
核心部件:
-
时钟源: 就像计时器的电池。ESP32 定时器通常使用系统主时钟(一般是 80MHz,也就是每秒震动 8 千万次!)。
-
预分频器: 这个很重要!80MHz 太快了。预分频器就像给这个超快速度“减速”。比如设置分频系数为 80,那么定时器实际“嘀嗒”一次的速度就变成了
80,000,000 Hz / 80 = 1,000,000 Hz
(1MHz,每秒 100 万次)。 -
计数器: 这是一个可以向上或向下数的数字。它每“嘀嗒”一次(经过分频后的速度)就加 1 或减 1。
-
比较寄存器/自动重装载寄存器: 这是你设定的“目标值”。当计数器数到这个值(向上计数时)或者数到 0(向下计数时),定时器就“到时间了”,触发中断,并且计数器通常会自动重置回初始值,开始下一轮计时。
-
一个例子:用定时器让 LED 闪烁(不卡顿!)
目标: 让一个 LED 灯每秒闪烁一次(亮 0.5 秒,灭 0.5 秒),同时让 ESP32 的串口每秒打印一次“我在干活呢!”,证明主程序没有被闪烁 LED 的等待卡住。
如何用定时器实现?
-
选一个计时器: 比如我们用
Timer0
。 -
设定时间间隔: 我们想要 LED 每 0.5 秒改变一次状态(亮->灭 或 灭->亮)。所以定时器间隔 = 0.5 秒 (500 毫秒)。
-
配置定时器:
-
时钟源: 默认 80MHz (80,000,000 Hz)。
-
预分频器: 设为 80。这样定时器计数频率 = 80,000,000 / 80 = 1,000,000 Hz (每微秒计数 1 次)。
-
目标值: 我们需要定时器每 0.5 秒中断一次。0.5 秒 = 500,000 微秒。因为计数频率是 1MHz (1次/微秒),所以计数器数到 500,000 次就是 0.5 秒。这个
500000
就是我们要设置的目标值(自动重装载值)。
-
-
中断处理函数: 写一个小函数,专门处理定时器“叮!”的事件。这个函数要尽量短快!它只做一件事:设置一个标志位
timerFlag = true
,告诉主程序“时间到了,该改变 LED 了!”(实际改变 LED 的操作放在主循环里,避免在中断里做耗时操作)。 -
启动定时器: 告诉
Timer0
:“开始按我设定的参数计时吧!” -
主程序循环:
-
检查
timerFlag
是否为true
。 -
如果是
true
,说明定时器中断发生了:-
把
timerFlag
设回false
(准备下次)。 -
改变 LED 的状态 (如果灯是亮的就关掉,如果是灭的就打开)。
-
-
不管
timerFlag
是什么,每秒打印一次消息(用millis()
或另一个定时器实现,这里简化说明)。 -
主循环可以做其他事情(比如读取传感器数据、处理网络连接等),它不会被
delay()
卡住。
-
伪代码:
#include // 包含必要的库
// 定义LED引脚和状态变量
const int ledPin = 2; // 假设LED在GPIO2
bool ledState = false; // LED初始状态(灭)
volatile bool timerFlag = false; // 定时器中断标志
// 中断处理函数 (要简短!)
void IRAM_ATTR onTimer() {
timerFlag = true; // 只是设置一个标志
}
void setup() {
pinMode(ledPin, OUTPUT); // 设置LED为输出
digitalWrite(ledPin, ledState); // 初始关闭LED
// 1. 选择定时器 (Timer0)
hw_timer_t *timer = timerBegin(0, 80, true); // 分频系数80, 向上计数
// 2. 绑定中断处理函数
timerAttachInterrupt(timer, &onTimer, true); // 边沿触发中断
// 3. 设置目标值 (500ms)
timerAlarmWrite(timer, 500000, true); // 目标值500000 (0.5秒), 自动重装载
// 4. 启动定时器
timerAlarmEnable(timer);
Serial.begin(115200);
}
void loop() {
// 检查定时器标志
if (timerFlag) {
timerFlag = false; // 清除标志
ledState = !ledState; // 翻转LED状态
digitalWrite(ledPin, ledState); // 更新LED
}
// 主循环可以自由地做其他事情,不会被闪烁LED卡住
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= 1000) {
lastPrint = millis();
Serial.println("我在干活呢!主循环没卡住!");
// 这里还可以添加读取传感器、处理网络等代码...
}
// ... 其他任务代码 ...
}
关键优势总结
-
不卡顿: CPU 在等待定时器到期时可以继续执行
loop()
中的其他任务(打印消息、读传感器、连WiFi等),程序响应更灵敏。 -
精准: 硬件定时比软件循环 (
millis()
) 或delay()
更精确稳定(尤其间隔很短时)。 -
多任务: 4 个定时器可以独立工作,实现多个不同周期的定时任务(比如一个 LED 每秒闪,另一个每 2 秒闪,一个传感器每 5 秒读一次)。
-
效率高: 让硬件去做计时的脏活累活,解放 CPU。
其他用途
除了让 LED 闪,定时器还能干很多事:
-
产生精确的 PWM 信号控制电机速度或 LED 亮度。
-
测量外部脉冲的宽度(比如超声波测距)。
-
作为看门狗定时器,防止程序跑飞。
-
实现软件串口。
-
周期性采集传感器数据。
初学者小结:
-
从简单例子开始: 先理解并实现上面的 LED 闪烁例子。
-
理解分频和目标值: 计算时间间隔 (
间隔(秒) = (分频系数 * 目标值) / 时钟源频率(Hz)
) 是核心。 -
中断处理要短快! 在中断函数里只做最简单的事情(设置标志位),复杂的操作(开关设备、打印日志)放到
loop()
里根据标志位去做。 -
善用
volatile
: 在中断里修改、在主循环里读取的变量(如timerFlag
),一定要用volatile
声明,告诉编译器这个变量可能随时被意外修改(由中断)。 -
查官方文档和库示例: Arduino core for ESP32 提供了
hw_timer.h
等库,有很多示例可以参考。