برنامه‌نویسی Arduino بدون delay؛ millis و زمان‌بندی چند کار

عکس واقعی برد Arduino Uno R3 از منبع؛ اجرای کد یا آزمون فیزیکی توسط LCENJ را نشان نمی‌دهد.
عکس واقعی برد Arduino Uno R3 از منبع؛ اجرای کد یا آزمون فیزیکی توسط LCENJ را نشان نمی‌دهد. عکس: Suyash Dwivedi؛ منبع Wikimedia Commons؛ CC BY-SA 4.0. محتوای عکس ویرایش نشده؛ نسخهٔ نمایشی ویکی‌مدیا استفاده شده است.

پاسخ کوتاه: زمان گذشته را بررسی کنید، منتظر نمانید

به‌جای آن‌که برنامه را با delay در یک نقطه نگه دارید، زمان فعلی را بخوانید و ببینید موعد هر کار رسیده است یا نه. هر کار باید کوتاه اجرا شود و کنترل را به loop برگرداند. این روش چند کار را در یک حلقه پیش می‌برد؛ به معنی اجرای هم‌زمان واقعی پردازنده یا تضمین زمان‌واقعی سخت نیست.

در مثال این مقاله، LED داخلی Uno هر ۵۰۰ ms تغییر حالت می‌دهد و هر ۱۰۰۰ ms یک گزارش سریال ثبت می‌شود. سنسور خارجی لازم نیست. خروجی، زمان کار برنامه است و دادهٔ ساختگیِ سنسور به‌عنوان اندازه‌گیری واقعی معرفی نمی‌شود.

چرا now >= last + interval انتخاب خوبی نیست؟

millis روی Uno یک شمارندهٔ بدون علامت ۳۲ بیتی است و پس از حدود ۴۹٫۷ روز دور می‌زند. مقایسه با جمع یک موعد می‌تواند نزدیک این مرز رفتار نامطلوب داشته باشد. برای بازه‌های کوتاه و با بررسی منظم حلقه، اختلاف بدون علامت را با فاصلهٔ موردنظر مقایسه کنید.

if (uint32_t(now - last) >= interval) {
    last = now;
    // one bounded operation
}

مثال عددی: اگر last برابر 0xFFFFFFF0 و now پس از سرریز برابر 0x00000010 باشد، تفاضل ۳۲ بیتی بدون علامت برابر ۳۲ ms می‌شود. برای فاصلهٔ ۳۲ ms موعد رسیده و برای ۳۳ ms هنوز نرسیده است. این روش مجوز فاصله‌های دلخواه یا متوقف‌ماندن برنامه به مدت یک دور کامل شمارنده نیست؛ در این مثال فاصله‌ها بسیار کوچک‌اند و مرتب بررسی می‌شوند.

کد کامل دو کار دوره‌ای برای Uno R3

#include <Arduino.h>
#if !defined(ARDUINO_AVR_UNO)
#error "This example is qualified for Arduino Uno R3 only."
#endif
constexpr uint32_t BLINK_MS = 500UL;
constexpr uint32_t REPORT_MS = 1000UL;
uint32_t lastBlink = 0, lastReport = 0;
bool ledOn = false;
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.begin(115200);
  lastBlink = lastReport = millis();
}
void loop() {
  const uint32_t now = millis();
  if (uint32_t(now - lastBlink) >= BLINK_MS) {
    lastBlink = now;
    ledOn = !ledOn;
    digitalWrite(LED_BUILTIN, ledOn ? HIGH : LOW);
  }
  if (uint32_t(now - lastReport) >= REPORT_MS) {
    lastReport = now;
    Serial.print(F("uptime_ms="));
    Serial.println(now);
  }
}

در setup، مبنای زمانی هر دو کار از همان لحظهٔ شروع گرفته می‌شود. در loop یک snapshot از millis داریم تا تصمیم‌ها بر مبنای زمان مشترک باشند. برنامه از Serial.printf استفاده نمی‌کند و به کتابخانهٔ اضافی نیاز ندارد. نسخهٔ دانلودی با متن همین مثال یکسان است و برای Uno R3 کامپایل شده است؛ اتصال یا اندازه‌گیری فیزیکی انجام نشده است.

نمودار آموزشی تألیفی: برنامه‌نویسی Arduino بدون delay؛ millis و زمان‌بندی چند کار
نمودار آموزشی تألیفی LCENJ؛ عکس مدار، شکل موج اندازه‌گیری‌شده یا نتیجهٔ آزمایش فیزیکی نیست. فرض‌ها و کاربرد آن در متن توضیح داده شده است.

اگر برنامه عقب افتاد، چه سیاستی دارید؟

روشرفتارکاربرد محتمل
last = nowیک اجرا؛ عقب‌افتادگی را به صف تبدیل نمی‌کندگزارش وضعیت یا چشمک آموزشی
last += intervalفاز موعدها را بهتر نگه می‌دارد؛ ممکن است چند موعد عقب‌افتاده وجود داشته باشدکار با سیاست مشخص برای نمونه‌های ازدست‌رفته
بازسازی چندباره در whileمی‌تواند مدت حلقه را طولانی کندفقط با سقف تعداد اجرا و نیاز روشن

در این مقاله، روش اول انتخاب شده است. اگر حلقه برای مدتی طولانی مشغول شود، LED برای جبران، چند بار پشت سر هم چشمک نمی‌زند. این تصمیم، زمان‌بندی دقیق و بدون لغزش تولید نمی‌کند. برای نمونه‌برداری دقیق، تایمر سخت‌افزاری، وقفه و محدودیت تأخیر را متناسب با نیاز بررسی کنید.

حذف delay همهٔ کارها را غیرمسدود نمی‌کند

خواندن طولانی سنسور، انتظار شبکه، حلقهٔ بی‌پایان و حتی چاپ زیاد روی سریال می‌تواند مسیر را متوقف کند. هر عملیات را از نظر بیشترین زمان اجرا بررسی کنید و برای خطا Timeout و تعداد تلاش محدود داشته باشید. یک while که منتظر آماده‌شدن قطعه می‌ماند، فقط شکل دیگری از انتظار است.

برای DS18B20 می‌توان آغاز تبدیل را از خواندن نتیجه جدا کرد: حالت «در حال تبدیل» را ثبت کنید و پس از زمان مناسبِ رزولوشن انتخاب‌شده، نتیجه را بخوانید. در راهنمای DS18B20 نمونهٔ این مسیر آمده است. شروع‌نکردن تبدیل تازه و چاپ دوبارهٔ عدد قبلی را با نمونهٔ تازه اشتباه نگیرید.

چگونه زمان‌بندی را آزمون کنیم؟

آزمون را فقط به یک دقیقه چشمک LED محدود نکنید. در تست نرم‌افزاری، تابع مقایسه را با زمان‌های نزدیک سرریز، یک میلی‌ثانیه پیش از موعد و دقیقاً روی موعد اجرا کنید. فایل Python همراه این حالت‌ها را بدون انتظار ۴۹ روز بررسی می‌کند. این آزمون، رفتار محاسبه را تأیید می‌کند؛ پین و کلاک واقعی برد را تأیید نمی‌کند.

روی محصول، مدت بدترین اجرای کارها و سن آخرین دادهٔ معتبر را ثبت کنید. اگر کار شبکه یا ذخیره‌سازی اضافه می‌کنید، آزمون را در زمان خطا و قطع ارتباط هم تکرار کنید. برای خروجی PWM، تداخل تایمرها را در PWM چیست؟ تفاوت فرکانس و Duty Cycle با مثال Arduino و برای Reset واقعی، چرا ESP32 ریست می‌شود؟ عیب‌یابی Brownout و تغذیه ببینید.

پرسش‌های رایج و تمرین بعدی

آیا millis تعداد کارها را محدود می‌کند؟

محدودیت اصلی، زمان اجرای هر کار، حافظه و منابع برد است. صد شرط زمان‌بندی، کار سنگین را سبک نمی‌کند.

آیا int برای ذخیرهٔ millis مناسب است؟

در این مثال خیر؛ نوع و محاسبهٔ بدون علامت ۳۲ بیتی را یکسان نگه دارید تا مقدار زود قطع یا منفی نشود.

چرا زمان LED دقیقاً روی ۵۰۰ ms نیست؟

کار در نخستین دور حلقه پس از موعد اجرا می‌شود و خود منبع زمان نیز محدودیت دارد. برای دقت بالاتر باید روش و شاهد دیگری انتخاب کنید.

تمرین: یک کار سوم برای ثبت وضعیت اضافه کنید، سپس عمداً یک کار کند ایجاد و اثر آن را بر دو کار دیگر ثبت کنید. بعد کار کند را به مرحله‌های کوتاه تقسیم کنید و فقط همین تغییر را مقایسه کنید.

منابع رسمی و فایل‌های همراه

منابع فنی در ۲۰۲۶/۱۰/۱۳ بررسی شده‌اند. نسخه و مدل قطعهٔ خود را با سند مرتبط تطبیق دهید.

دریافت برگهٔ تصمیم و ثبت آزمون (CSV)؛ بستهٔ کامل مقاله و فایل‌های همراه از بخش دانلود همین صفحه در دسترس است.

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