آردوینومتوسط۷ دقیقه مطالعهArduino IDE 2.x / Uno R3

برنامه‌نویسی بدون وقفه با millis

چرا delay برنامه را قفل می‌کند؛ زمان‌بندی غیرمسدودشونده با millis و micros؛ الگوی صحیح سرریز unsigned long و اجرای هم‌زمان دو چشمک‌زن با بازه‌های مختلف.

آنچه در این درس می‌آموزید

  • توضیح دهید چرا delay کل برنامه را متوقف می‌کند و این در پروژه‌های واقعی چه مشکلی می‌سازد
  • زمان‌بندی غیرمسدودشونده را با millis و الگوی تفریق unsigned long پیاده کنید
  • دو بازهٔ زمانی مستقل را هم‌زمان در یک loop اجرا کنید
  • باگ سرریز متغیر int در ذخیرهٔ millis را بشناسید و از آن پرهیز کنید
نویسنده: تیم داکنامه · بازبینی: ۲۰۲۶-۱۰-۳۱
فهرست مطالب

تا اینجا برای زمان‌بندی از delay استفاده کرده‌اید: LED را روشن کن، صبر کن، خاموش کن. این روش تا اسکچ چشمک‌زن ساده جواب می‌دهد، اما یک هزینهٔ پنهان دارد: در تمام مدت انتظار، برد هیچ کار دیگری نمی‌تواند بکند. دکمه را نمی‌خواند، سنسور را نمی‌خواند و به هیچ رویدادی پاسخ نمی‌دهد. در این درس «زمان‌بندی غیرمسدودشونده» (Non-Blocking Timing) را با تابع millis یاد می‌گیرید؛ الگویی که ستون فقرات تقریباً همهٔ برنامه‌های حرفه‌ای آردوینو است.

مشکل delay: توقف کامل برنامه

delay فقط «صبر» نیست؛ بلکه «قفل‌کردن» است. وقتی delay(1300) اجرا می‌شود، CPU تا پایان آن هیچ خط دیگری از کد شما را اجرا نمی‌کند. این را با یک اسکچ دو LED ببینید که می‌خواهیم LED اول هر ۵۰۰ میلی‌ثانیه و LED دوم هر ۱۳۰۰ میلی‌ثانیه چشمک بزند:

two-leds-delay.ino
const int LED_A = 8;
const int LED_B = 9;

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
}

void loop() {
  digitalWrite(LED_A, HIGH);
  delay(500);
  digitalWrite(LED_A, LOW);
  delay(500);
  digitalWrite(LED_B, HIGH);
  delay(1300);
  digitalWrite(LED_B, LOW);
  delay(1300);
}

دو بازه هرگز هم‌پوشانی نمی‌کنند؛ LED دوم فقط بعد از یک چرخهٔ کامل LED اول تکانی می‌خورد. چون delay اجرای برنامه را نگه می‌دارد، هیچ دو رویداد زمانیِ موازی‌ای نمی‌توانید بسازید. بدتر از آن: اگر یک دکمه هم به برد وصل بود، در تمام ۳٫۶ ثانیهٔ هر دورِ حلقه، فشردن دکمه دیده نمی‌شد.

زمان با millis و micros

تابع millis() تعداد میلی‌ثانیه‌های گذشته از روشن‌شدن برد را برمی‌گرداند؛ مثل یک ساعت‌شمار همیشگی که پشت‌صحنه (توسط یک تایمر سخت‌افزاری) جلو می‌رود و کد شما هر لحظه می‌تواند آن را بپرسد — بدون هیچ صبری. تابع micros() همان کار را با دقت میکروثانیه انجام می‌کند.

نوع بازگشتی این دو تابع unsigned long است؛ عددی بدون علامت با ظرفیت ۴٬۲۹۴٬۹۶۷٬۲۹۵. پس millis پس از حدود ۴۹٫۷ روزِ کار پیوسته از ۴٬۲۹۴٬۹۶۷٬۲۹۵ به صفر برمی‌گردد؛ به این پدیده «سرریز» (Overflow) می‌گویند. اگر برنامه را درست بنویسید، این سرریز هیچ مشکلی نمی‌سازد — و همین «درست نوشتن» موضوع بخش بعد است.

الگوی غیرمسدودشونده

الگوی استاندارد چنین است:

cpp
unsigned long now = millis();
if (now - lastEvent >= interval) {
  lastEvent = now;
  // Do the periodic work
}

به‌جای «صبر کن تا زمان برسد»، حالا منطق «هر بار که loop می‌چرخد، بپرس آیا وقتش رسیده؟» است. نکتهٔ ظریف، عبارت now - lastEvent است: چون هر دو متغیر unsigned longاند، این تفریق حتی هنگام سرریز هم جواب درست می‌دهد. مثلاً اگر lastEvent برابر ۴٬۲۹۴٬۹۶۷٬۲۰۰ باشد و now به‌سرعت سرریز کند و ۱۰۰ شود، حاصل تفریق ۱۹۶ می‌شود — دقیقاً همان مدت واقعی گذشته. به همین دلیل الگو را هرگز با now < lastEvent + interval ننویسید؛ جمع، خودش سرریز می‌کند و شرط می‌شکند، درحالی‌که تفریق سرریز را بلعیده و درست ادامه می‌دهد.

پروژه: دو چشمک‌زن هم‌زمان با دو بازه

حالا همان هدف ابتدای درس — LED اول با بازهٔ ۵۰۰ و LED دوم با بازهٔ ۱۳۰۰ میلی‌ثانیه — به‌شکل غیرمسدودشونده:

two-leds-millis.ino
const int LED_A = 8;
const int LED_B = 9;

const unsigned long INTERVAL_A = 500;   // ms
const unsigned long INTERVAL_B = 1300;  // ms

unsigned long lastA = 0;
unsigned long lastB = 0;
bool stateA = false;
bool stateB = false;

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
}

void loop() {
  unsigned long now = millis();  // Read once at the top

  if (now - lastA >= INTERVAL_A) {
    lastA = now;               // Schedule the next toggle
    stateA = !stateA;          // Flip the state
    digitalWrite(LED_A, stateA ? HIGH : LOW);
  }

  if (now - lastB >= INTERVAL_B) {
    lastB = now;
    stateB = !stateB;
    digitalWrite(LED_B, stateB ? HIGH : LOW);
  }
}

توضیح خط‌به‌خط:

  1. چهار متغیر زمان‌بندی بیرون از توابع تعریف شده‌اند تا بین دورهای loop مقدارشان بماند.
  2. unsigned long now = millis(); در ابتدای loop یک بار خوانده می‌شود تا هر دو شرط از یک «عکس لحظه‌ای» از زمان استفاده کنند.
  3. هر بلوک if مستقل است: اگر وقت LED خودش رسیده باشد وضعیتش را برعکس می‌کند و lastX = now زمان کار بعدی را برنامه‌ریزی می‌کند.
  4. چون هیچ delayای وجود ندارد، loop هزاران بار در ثانیه می‌چرخد و دو بازه به‌طور طبیعی هم‌پوشانی می‌کنند — دقیقاً همان چیزی که با delay غیرممکن بود.
  5. متغیرهای stateA و stateB وضعیت هر LED را نگه می‌دارند، چون digitalWrite خودش نمی‌داند LED روشن است یا خاموش.

مزیت واقعی اینجا پیدا می‌شود: همین حالا می‌توانید یک if (digitalRead(2) == LOW) هم به loop اضافه کنید و دکمه در هر لحظه پاسخ‌گو خواهد بود.

خطاهای رایج

  • ذخیرهٔ millis در int — متغیر int فقط ۱۶ بیت در آردوینو Uno دارد و پس از حدود ۳۲٫۷ ثانیه سرریز می‌کند؛ زمان‌بندی شما بعد از نیم دقیقه به‌هم می‌ریزد. همیشه از unsigned long استفاده کنید.
  • delay داخل کد غیرمسدودشونده — حتی یک delay(10) کوچک در loop، دقت همهٔ بازه‌ها را خراب می‌کند و دکمه‌ها را در آن بازه کور می‌کند.
  • جایگزینی شرط تفریق با جمع — now < last + interval با سرریز می‌شکند؛ همان now - last >= interval را نگه دارید.

اشتباهات رایج و عیب‌یابی

نشانه علت رایج راه‌حل
برنامه بعد از حدود ۳۰ ثانیه رفتار عجیب می‌کند زمان در متغیر int یا long علامت‌دار ذخیره شده نوع متغیرها را به unsigned long تغییر دهید
یکی از بازه‌ها گاهی یک دور دیر می‌زند داخل loop کد کندی مثل delay یا Serial.print زیاد اجرا می‌شود کار سنگین را کوتاه کنید؛ الگوی millis خطا را جمع نمی‌کند، جبران می‌کند (lastX = now)
هر دو LED هم‌زمان می‌زنند استفاده از یک متغیر last مشترک برای هر دو برای هر رویداد زمانی متغیر last جداگانه بسازید
چشمک‌زن از اول کار نمی‌کند جا انداختن مقداردهی اولیه یا اشتباه در علامت شرط شرط را با now - last >= interval و مقادیر اولیهٔ صفر مقایسه کنید

تمرین‌ها

تمرین

تمرین ۱ — چشمک‌زن سوم

اسکچ two-leds-millis را طوری گسترش دهید که یک LED سوم روی پین ۱۰ با بازهٔ ۷۵۰ میلی‌ثانیه هم چشمک بزند؛ هر سه بازه باید مستقل و هم‌زمان کار کنند.

نمایش راه‌حل

همان الگو برای رویداد سوم کپی می‌شود؛ یک متغیر زمان و یک متغیر وضعیت جدید:

cpp
const int LED_A = 8;
const int LED_B = 9;
const int LED_C = 10;

const unsigned long INTERVAL_A = 500;
const unsigned long INTERVAL_B = 1300;
const unsigned long INTERVAL_C = 750;

unsigned long lastA = 0;
unsigned long lastB = 0;
unsigned long lastC = 0;
bool stateA = false;
bool stateB = false;
bool stateC = false;

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
  pinMode(LED_C, OUTPUT);
}

void loop() {
  unsigned long now = millis();

  if (now - lastA >= INTERVAL_A) {
    lastA = now;
    stateA = !stateA;
    digitalWrite(LED_A, stateA ? HIGH : LOW);
  }

  if (now - lastB >= INTERVAL_B) {
    lastB = now;
    stateB = !stateB;
    digitalWrite(LED_B, stateB ? HIGH : LOW);
  }

  if (now - lastC >= INTERVAL_C) {
    lastC = now;
    stateC = !stateC;
    digitalWrite(LED_C, stateC ? HIGH : LOW);
  }
}

سه رویداد مستقل زمانی، هم‌زمان، بدون یک خط delay. اگر روزی ده چشمک‌زن خواستید، همین الگو ده بلوک if می‌خواهد — یا در سطح بالاتر، آرایه و حلقه.

:::

خلاصه

  • delay برنامه را کاملاً قفل می‌کند؛ در مدت آن هیچ ورودی و رویدادی پردازش نمی‌شود.
  • millis() میلی‌ثانیه‌های گذشته از روشن‌شدن را در unsigned long می‌دهد و پس از حدود ۴۹٫۷ روز سرریز می‌کند.
  • الگوی استاندارد if (now - last >= interval) است؛ تفریق unsigned حتی هنگام سرریز درست می‌ماند.
  • هر رویداد زمانی متغیر last و وضعیت خودش را دارد و همه در یک loop بدون صبر اجرا می‌شوند.

حالا برنامهٔ شما همیشه پاسخ‌گوست؛ اما باز هم loop باید مدام بچرخد تا رویدادها را ببیند. در درس بعدی با وقفه‌ها (Interrupts) یاد می‌گیرید سخت‌افزار خودش بیاید سراغ کد شما — حتی وسط کار.

منابع و ارجاع‌ها