无延迟闪烁模式
无延迟闪烁模式
Section titled “无延迟闪烁模式”本节介绍使用 millis() 的非阻塞定时模式,这是ESP32开发的基本技术。通过本节学习,你将能够:
- 用非阻塞的
millis()模式替换delay() - 同时管理多个独立的定时间隔
- 理解为什么
delay()在物联网应用中存在问题 - 在基础草图架构中实现此模式
- 基础草图架构(01-11)
- 理解
setup()和loop()的工作方式
delay()的问题
Section titled “delay()的问题”delay() 函数在指定持续时间内暂停所有代码执行。在此期间:
- MQTT连接丢失:
client.loop()未被调用,导致连接超时 - 错过传感器读数:如果在延迟期间触发传感器中断,它将被忽略
- Wi-Fi断开未被检测:ESP32无法处理Wi-Fi事件
- 错过按钮按下:物理输入未被轮询
- 响应时间下降:设备显得无响应
简而言之,delay() 将一个具有多任务能力的双核ESP32变成了单任务设备。
millis()解决方案
Section titled “millis()解决方案”millis() 函数返回自ESP32启动以来的毫秒数(约50天后翻转)。通过比较当前的 millis() 与存储的”上一次”值,我们可以确定何时执行操作——而不阻塞。
模式:
unsigned long previousMillis = 0;const unsigned long INTERVAL = 1000; // 1秒
void loop() { unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= INTERVAL) { previousMillis = currentMillis; // 执行定时操作 }
// 其他代码立即运行,不被阻塞}millis()翻转
Section titled “millis()翻转”millis() 约50天后复位为0。然而,减法 currentMillis - previousMillis 即使在翻转后也能正常工作,因为结果是unsigned long:
场景:previousMillis = 4,294,967,000(接近翻转) currentMillis = 100(翻转后) currentMillis - previousMillis = 100 - 4,294,967,000 = 102(正确!)由于无符号整数算术的环绕特性,这可以正常工作——无需特殊处理。
第一步:单定时器——LED闪烁
Section titled “第一步:单定时器——LED闪烁”ESP32的经典”无延迟闪烁”示例:
const int LED_PIN = 2; // 大多数ESP32 DevKit上的内置LED
// 定时变量unsigned long previousBlinkMillis = 0;const unsigned long BLINK_INTERVAL = 1000; // 1秒
int ledState = LOW;
void setup() { pinMode(LED_PIN, OUTPUT);}
void loop() { unsigned long currentMillis = millis();
// 非阻塞闪烁检查 if (currentMillis - previousBlinkMillis >= BLINK_INTERVAL) { previousBlinkMillis = currentMillis;
// 切换LED ledState = (ledState == LOW) ? HIGH : LOW; digitalWrite(LED_PIN, ledState); }
// 其他代码可以放在这里——它立即执行! // 例如:检查MQTT、读取传感器等}第二步:多个独立定时器
Section titled “第二步:多个独立定时器”millis() 的真正威力在于管理多个具有不同间隔的并发任务:
const int LED_PIN = 2;const int STATUS_LED_PIN = 15;
// 定时器1:LED闪烁(快速)unsigned long previousBlinkMillis = 0;const unsigned long BLINK_INTERVAL = 500; // 0.5秒
// 定时器2:状态LED(慢速)unsigned long previousStatusMillis = 0;const unsigned long STATUS_INTERVAL = 3000; // 3秒
// 定时器3:串行打印unsigned long previousPrintMillis = 0;const unsigned long PRINT_INTERVAL = 10000; // 10秒
int ledState = LOW;int statusLedState = LOW;
void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); pinMode(STATUS_LED_PIN, OUTPUT);}
void loop() { unsigned long currentMillis = millis();
// 定时器1:快速LED闪烁(500ms) if (currentMillis - previousBlinkMillis >= BLINK_INTERVAL) { previousBlinkMillis = currentMillis; ledState = !ledState; digitalWrite(LED_PIN, ledState); }
// 定时器2:慢速状态LED(3000ms) if (currentMillis - previousStatusMillis >= STATUS_INTERVAL) { previousStatusMillis = currentMillis; statusLedState = !statusLedState; digitalWrite(STATUS_LED_PIN, statusLedState); }
// 定时器3:串行心跳(10000ms) if (currentMillis - previousPrintMillis >= PRINT_INTERVAL) { previousPrintMillis = currentMillis; Serial.print("Heartbeat - Uptime: "); Serial.print(currentMillis / 1000); Serial.println(" seconds"); }}第三步:集成到基础草图
Section titled “第三步:集成到基础草图”在基础草图(01-11)中,非阻塞模式用于LED闪烁和定期数据发布:
#include <Arduino.h>#include <WiFi.h>#include <PubSubClient.h>
// MQTT和Wi-Fi设置(缩写——完整代码见01-11)WiFiClient wifiClient;PubSubClient mqttClient(wifiClient);
// 发布间隔unsigned long previousPublishMillis = 0;const unsigned long PUBLISH_INTERVAL = 5000;
// LED心跳unsigned long previousHeartbeatMillis = 0;const unsigned long HEARTBEAT_INTERVAL = 1000;
// MQTT保活检查unsigned long previousMqttMillis = 0;const unsigned long MQTT_CHECK_INTERVAL = 100;
int ledState = LOW;
void setup() { Serial.begin(115200); pinMode(2, OUTPUT);
// 连接Wi-Fi和MQTT(见01-11) connectToWiFi(); connectToMQTT();}
void loop() { unsigned long currentMillis = millis();
// 1. MQTT维护(每100ms——非常频繁) if (currentMillis - previousMqttMillis >= MQTT_CHECK_INTERVAL) { previousMqttMillis = currentMillis;
if (!mqttClient.connected()) { connectToMQTT(); } mqttClient.loop(); }
// 2. LED心跳(每1000ms) if (currentMillis - previousHeartbeatMillis >= HEARTBEAT_INTERVAL) { previousHeartbeatMillis = currentMillis; ledState = !ledState; digitalWrite(2, ledState); }
// 3. 发布传感器数据(每5000ms) if (currentMillis - previousPublishMillis >= PUBLISH_INTERVAL) { previousPublishMillis = currentMillis; publishSensorData(); }}第四步:高级模式——使用结构体管理多个定时器
Section titled “第四步:高级模式——使用结构体管理多个定时器”对于复杂项目,使用结构体组织定时器:
struct Timer { unsigned long previousMillis; unsigned long interval; bool enabled;
Timer(unsigned long interval) : previousMillis(0), interval(interval), enabled(true) {}
bool isReady() { if (!enabled) return false; unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; return true; } return false; }
void setInterval(unsigned long newInterval) { interval = newInterval; }
void reset() { previousMillis = millis(); }
void disable() { enabled = false; } void enable() { enabled = true; }};
// 用法Timer blinkTimer(500);Timer sensorTimer(5000);Timer publishTimer(30000);
void loop() { if (blinkTimer.isReady()) { // 切换LED }
if (sensorTimer.isReady()) { // 读取传感器 }
if (publishTimer.isReady()) { // 发布数据 }}- LED按预期间隔闪烁,无需
delay() - 多个定时器以不同速率独立运行
- MQTT连接保持活动(无超时断开)
- 串行输出确认长时间内的定时准确性
- 基于结构体的定时器方法适用于更复杂的项目
LED闪烁快于或慢于预期
Section titled “LED闪烁快于或慢于预期”原因:
- 操作后未更新
previousMillis - 错误的数据类型(使用
int而不是unsigned long)
解决方案:
- 确保
previousMillis = currentMillis;在if块内部 - 始终使用
unsigned long存储millis()值(int约32秒后会溢出) - 验证间隔值正确(1000 = 1秒,500 = 0.5秒)
操作多次快速触发
Section titled “操作多次快速触发”原因:在下一次循环迭代复位之前,currentMillis - previousMillis >= interval 多次评估为true。
解决方案:
- 在
if块内立即更新previousMillis(在操作代码之前) - 在块开始处使用
previousMillis = currentMillis;模式可防止此问题
数小时后定时器似乎停止
Section titled “数小时后定时器似乎停止”原因:虽然 millis() 翻转由无符号算术处理,但其他格式存储的时间戳可能不行。
解决方案:标准模式 currentMillis - previousMillis >= interval 是安全的。永远不要将 millis() 转换为 signed long 或 int。
- 永远不要在主循环中使用
delay():将delay()保留给setup()使用(例如,等待Serial或传感器预热) - 始终使用
unsigned long:存储millis()值的数据类型必须是unsigned long - 使用描述性变量名:
previousBlinkMillis比prev或lastTime更清晰 - 对间隔使用
const:const unsigned long INTERVAL = 1000;防止意外修改 - 每50-100ms定期检查MQTT:确保连接稳定性而不浪费CPU
- 对于5个以上定时器的项目,考虑使用Timer结构体:它减少代码重复
delay()阻塞所有代码执行,在物联网应用中应避免使用millis()模式使用无符号长整数减法创建非阻塞定时器- 多个独立定时器可以并发运行,每个有不同的间隔
- 该模式与基础草图架构无缝集成
- 基于结构体的定时器抽象简化了许多并发定时器的管理
millis()约50天后的翻转由无符号整数算术正确处理