跳转到内容

无延迟闪烁模式

本节介绍使用 millis() 的非阻塞定时模式,这是ESP32开发的基本技术。通过本节学习,你将能够:

  • 用非阻塞的 millis() 模式替换 delay()
  • 同时管理多个独立的定时间隔
  • 理解为什么 delay() 在物联网应用中存在问题
  • 在基础草图架构中实现此模式
  • 基础草图架构(01-11)
  • 理解 setup()loop() 的工作方式

delay() 函数在指定持续时间内暂停所有代码执行。在此期间:

  • MQTT连接丢失client.loop() 未被调用,导致连接超时
  • 错过传感器读数:如果在延迟期间触发传感器中断,它将被忽略
  • Wi-Fi断开未被检测:ESP32无法处理Wi-Fi事件
  • 错过按钮按下:物理输入未被轮询
  • 响应时间下降:设备显得无响应

简而言之,delay() 将一个具有多任务能力的双核ESP32变成了单任务设备。

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() 约50天后复位为0。然而,减法 currentMillis - previousMillis 即使在翻转后也能正常工作,因为结果是unsigned long:

场景:previousMillis = 4,294,967,000(接近翻转)
currentMillis = 100(翻转后)
currentMillis - previousMillis = 100 - 4,294,967,000 = 102(正确!)

由于无符号整数算术的环绕特性,这可以正常工作——无需特殊处理。

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、读取传感器等
}

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");
}
}

在基础草图(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连接保持活动(无超时断开)
  • 串行输出确认长时间内的定时准确性
  • 基于结构体的定时器方法适用于更复杂的项目

原因

  • 操作后未更新 previousMillis
  • 错误的数据类型(使用 int 而不是 unsigned long

解决方案

  1. 确保 previousMillis = currentMillis;if 块内部
  2. 始终使用 unsigned long 存储millis()值(int 约32秒后会溢出)
  3. 验证间隔值正确(1000 = 1秒,500 = 0.5秒)

原因:在下一次循环迭代复位之前,currentMillis - previousMillis >= interval 多次评估为true。

解决方案

  1. if 块内立即更新 previousMillis(在操作代码之前)
  2. 在块开始处使用 previousMillis = currentMillis; 模式可防止此问题

原因:虽然 millis() 翻转由无符号算术处理,但其他格式存储的时间戳可能不行。

解决方案:标准模式 currentMillis - previousMillis >= interval 是安全的。永远不要将 millis() 转换为 signed longint

  • 永远不要在主循环中使用 delay():将 delay() 保留给 setup() 使用(例如,等待Serial或传感器预热)
  • 始终使用 unsigned long:存储 millis() 值的数据类型必须是 unsigned long
  • 使用描述性变量名previousBlinkMillisprevlastTime 更清晰
  • 对间隔使用 constconst unsigned long INTERVAL = 1000; 防止意外修改
  • 每50-100ms定期检查MQTT:确保连接稳定性而不浪费CPU
  • 对于5个以上定时器的项目,考虑使用Timer结构体:它减少代码重复
  1. delay() 阻塞所有代码执行,在物联网应用中应避免使用
  2. millis() 模式使用无符号长整数减法创建非阻塞定时器
  3. 多个独立定时器可以并发运行,每个有不同的间隔
  4. 该模式与基础草图架构无缝集成
  5. 基于结构体的定时器抽象简化了许多并发定时器的管理
  6. millis() 约50天后的翻转由无符号整数算术正确处理