圆山中庸 2025-11-02 12:35 采纳率: 98.6%
浏览 1
已采纳

Arduino循环函数中如何避免阻塞?

在Arduino开发中,`loop()`函数若包含长时间运行的操作(如延时`delay()`、串口通信等待或复杂计算),会导致程序阻塞,无法及时响应外部事件。如何在不使用`delay()`的前提下,利用`millis()`实现非阻塞多任务调度,是常见且关键的技术问题。例如:同时控制LED闪烁与读取传感器数据时,如何确保各任务独立运行、互不干扰?
  • 写回答

1条回答 默认 最新

  • kylin小鸡内裤 2025-11-02 12:42
    关注

    基于 millis() 的非阻塞多任务调度在 Arduino 中的深度实践

    1. 问题背景与核心挑战

    在传统的 Arduino 开发中,loop() 函数是程序执行的核心循环体。然而,当其中包含如 delay()、串口通信等待(如 Serial.readBytesUntil() 阻塞等待)、或耗时的数学运算时,会导致整个主循环被“冻结”。

    这种阻塞性行为会使得系统无法及时响应外部中断、传感器变化或用户输入,严重影响实时性和可靠性。

    例如:若 LED 每 1 秒闪烁一次,使用 delay(1000) 将导致在此期间无法读取温度传感器数据,造成采样延迟甚至丢失关键事件。

    2. 核心机制:millis() 的时间追踪原理

    millis() 是 Arduino 提供的一个函数,返回自板子启动以来经过的毫秒数(unsigned long 类型,约 49.7 天溢出一次)。

    通过记录“上一次执行某任务的时间”,并与当前 millis() 值比较,可判断是否达到预设的时间间隔,从而触发动作而不阻塞程序。

    • 避免使用 delay() 导致的程序停滞
    • 实现多个独立计时任务并行运行
    • 提升系统的响应速度和事件处理能力

    3. 基础实现模式:状态机 + 时间差检测

    非阻塞任务调度通常采用“状态机”结构,每个任务维护自己的最后执行时间戳和状态变量。

    以下是一个典型的模板结构:

    
    unsigned long previousMillis = 0;
    const long interval = 1000; // 1秒
    
    void loop() {
      unsigned long currentMillis = millis();
    
      if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        // 执行任务(如翻转LED)
        digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
      }
    
      // 其他非阻塞任务可在此继续检查
    }
    

    4. 多任务并行调度示例:LED闪烁 + 温度读取

    设想一个场景:需要每 500ms 闪烁一次 LED,同时每 2000ms 读取一次 DHT11 温湿度传感器数据。

    两个任务必须互不干扰,且不能因其中一个耗时而影响另一个的定时精度。

    任务名称执行周期(ms)关键变量是否阻塞
    LED 闪烁500ledPreviousTime, ledState
    读取 DHT112000dhtPreviousTime否(假设使用非阻塞DHT库)
    串口输出日志1000logPreviousTime

    5. 完整代码实现

    
    const int LED_PIN = LED_BUILTIN;
    const long LED_INTERVAL = 500;
    const long DHT_READ_INTERVAL = 2000;
    const long LOG_INTERVAL = 1000;
    
    unsigned long ledPreviousTime = 0;
    unsigned long dhtPreviousTime = 0;
    unsigned long logPreviousTime = 0;
    
    int ledState = LOW;
    
    // 模拟传感器读取(实际应替换为真实DHT库调用)
    float readTemperature() {
      return 23.5 + random(0, 10) / 10.0; // 模拟值
    }
    
    void setup() {
      pinMode(LED_PIN, OUTPUT);
      Serial.begin(9600);
      while (!Serial); // 等待串口连接(用于调试)
    }
    
    void loop() {
      unsigned long currentMillis = millis();
    
      // === LED 控制任务 ===
      if (currentMillis - ledPreviousTime >= LED_INTERVAL) {
        ledPreviousTime = currentMillis;
        ledState = !ledState;
        digitalWrite(LED_PIN, ledState);
      }
    
      // === 温度读取任务 ===
      if (currentMillis - dhtPreviousTime >= DHT_READ_INTERVAL) {
        dhtPreviousTime = currentMillis;
        float temp = readTemperature();
        // 实际项目中此处调用 DHT.readTemperature()
      }
    
      // === 日志输出任务 ===
      if (currentMillis - logPreviousTime >= LOG_INTERVAL) {
        logPreviousTime = currentMillis;
        Serial.print("System tick: ");
        Serial.print(currentMillis);
        Serial.println(" ms");
      }
    
      // 可添加更多任务...
    }
    

    6. 进阶设计:任务抽象化与调度器封装

    随着任务数量增加,直接在 loop() 中写多个 if 判断将变得难以维护。可引入“任务调度器”概念。

    定义一个任务结构体:

    
    struct Task {
      unsigned long interval;
      unsigned long lastRun;
      void (*callback)();
    };
    
    Task tasks[] = {
      { 500, 0, toggleLED },
      { 2000, 0, readSensor },
      { 1000, 0, logStatus }
    };
    const int TASK_COUNT = 3;
    
    void runScheduler() {
      unsigned long now = millis();
      for (int i = 0; i < TASK_COUNT; i++) {
        if (now - tasks[i].lastRun >= tasks[i].interval) {
          tasks[i].lastRun = now;
          tasks[i].callback();
        }
      }
    }
    

    7. 流程图:非阻塞任务调度逻辑

    graph TD A[进入 loop()] --> B{获取当前 millis()} B --> C[遍历所有任务] C --> D{当前时间 - 上次执行 >= 间隔?} D -- 是 --> E[更新上次执行时间] E --> F[执行任务回调] D -- 否 --> G[跳过该任务] F --> H[继续下一个任务] G --> H H --> I{还有任务未检查?} I -- 是 --> C I -- 否 --> J[loop 结束,下次循环]

    8. 注意事项与常见陷阱

    • 变量溢出处理millis() 在约 49.7 天后回滚为 0,但使用“差值比较法”(current - previous >= interval)天然兼容溢出,无需额外处理。
    • 避免在回调中使用 delay():即使封装了调度器,也不能在任务函数内部调用阻塞函数。
    • 长计算任务拆分:对于复杂算法,建议将其分解为多个小步骤,每次只执行一部分,利用状态标记推进进度。
    • 优先级管理缺失:上述调度器为轮询式,无优先级。高实时性任务需结合中断或更高级 RTOS。
    • 内存对齐与函数指针开销:在资源受限设备上,过多任务结构体可能占用较多 RAM。

    9. 替代方案对比分析

    方法优点缺点适用场景
    delay()简单直观完全阻塞,无法并发仅用于初始化或测试
    millis() 手动调度轻量、高效、可控需手动管理时间变量中小型项目主流做法
    TimerMs 库API 更简洁增加依赖快速开发
    FreeRTOS on ESP32支持真正多线程资源消耗大,复杂度高高性能 MCU 平台

    10. 实际工程中的扩展应用

    在工业控制、智能家居网关或物联网终端中,常需同时处理:

    1. 周期性采集多种传感器(温湿度、光照、PM2.5)
    2. 向云端上报数据(HTTP/MQTT)
    3. 响应按键或红外遥控
    4. 驱动 OLED 屏幕动态刷新
    5. 执行自动校准或故障检测逻辑
    6. 监听串口命令通道
    7. 管理 PWM 调光渐变动画
    8. 处理 SD 卡日志写入
    9. 运行 PID 控制环路
    10. 同步 RTC 时间
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

问题事件

  • 已采纳回答 11月3日
  • 创建了问题 11月2日