آموزش مولتی ‌تسکینگ در آردوینو – چگونه از دستور ()millis در کدهای آردوینو استفاده کنیم؟

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

آموزش مولتی‌تسکینگ در آردوینو

خوشبختانه آردوینو هم قابلیت مولتی‌تسکینگ دارد. در این آموزش به شما یاد می‌دهیم که چگونه از آن استفاده کنید.

اگر از قبل با آردوینو و کدنویسی آن آشنا باشید، احتمالا می‌دانید که  در آن از تابع ()delay برای انجام امور متناوب استفاده می‌کنیم. مثلا برای روشن و خاموش شدن‌ها پریودیک یک LED. اما نکته‌ای که وجود دارد این است که تابع ()delay برای لحظات مشخصی برنامه را متوقف کرده و به هیچ دستور دیگری نیز اجازه‌ی اجرا نمی‌دهد. به این ترتیب نمی‌تواند برای مولتی ‌تسکینگ کاربرد داشته باشد. در عوض تابع ()millis را داریم که می‌تواند این کار را برای ما انجام دهد و آردینو را به یک بورد با قابلیت مولتی‌ تسکینگ تبدیل کند. در ادامه قرار است با هم طرز استفاده از تابع ()millis را بیاموزیم. قبل از وارد شدن به جزئیات، خوب است از صحبت کردن در مورد خود مفهوم مولتی‌تسکینگ شروع کنیم.

مولتی‌تسکینگ چیست ؟

به زبان ساده، مولتی‌تسکینگ یعنی اجرا همزمان چند وظیفه یا برنامه. امروزه تقریبا بیشتر سیستم‌عامل‌ها این توانایی را در خود دارند و اصطلاحا به آن‌ها MOS یا (multitasking operating system) گفته می‌شود. MOSها می‌توانند روی سیستم‌های PC خانگی و یا حتی روی موبایل‌ها اجرا شوند. یک مثال خیلی ملموس از مولتی‌تسکینگ در کامپیوتر زمانی است که شما اپلیکیشن‌های خواندن ایمیل، مرورگر اینترنت، مدیا پلیر و یک بازی را همزمان باز کرده‌اید حتی در حالتی‌که همزمان به همه‌ی آن‌ها سر نمی‌زنید، باز هم چون در پس زمینه‌ باز و در حال اجرا هستند، سیستم عامل در حال مولتی‌تسکینگ است. البته از نگاه ما این‌طور است که همزمان در حال استفاده از تمام اینها هستیم، از نگاه سیستم عامل قضیه قدری متفاوت است. اما چگونه؟ برای پاسخ به این سوال ابتدا باید ببینیم سیستم عامل‌ها مولتی‌تسکینگ را چگونه مدیریت می‌کنند.

همان‌طور که در عکس بالا می‌بینید، CPU زمان را به سه قسمت مساوی تقسیم کرده و هر قسمت را به یکی از سه دستور یا برنامه‌ی درخواستی اختصاص داده است. این تکنیک در حقیقت پشت پرده‌ی مولتی‌تسکینگ در اغلب سیستم‌ها محسوب می‌شود. در مورد آردوینو هم به همین صورت است فقط نحوه‌ی تخصیص زمان‌ها قدری متفاوت است. از آنجایی که آردوینو در مقایسه با لپ‌تاپ، PC و گوشی‌های موبایل از فرکانس و RAM پایین‌تری برخوردار است، زمانی که به هر task اختصاص می‌دهد را به گونه‌ی دیگری محاسبه می‌کند که در ادامه آن را توضیح می‌دهیم. همچنین توضیح می‌دهیم که چرا استفاده از تابع ()delay را علیرغم محبوبیت فراوان آن، در هیچ پروژه‌ای توصیه نمی‌کنیم.

چرا از تابع ()delay استفاده نکنیم؟

اگر به مراجع اصلی خود بوردهای آردوینو مراجعه کنیم، می‌بینیم که در آنجا دو گونه تابع تاخیر برای آردوینو تعریف شده است؛ یکی ()delay و دیگری ()delayMicroseconds. از نظر تولید تاخیر هردوی این توابع کار یکسانی انجام می‌دهند. تنها تفاوتی که با هم دارند در واحد زمانی تولید تاخیر است. delay عددی که برای زمان به آن می‌دهیم را با واحد میلی‌ثانیه می‌خواند؛ یعنی اگر بنویسیم delay(1000)، تابع یک تاخیر 1000 میلی‌ثانیه‌ای یا به عبارتی 1 ثانیه‌ای برای ما ایجاد می‌کند. در ()delayMicroseconds این واحد میکروثانیه است. یعنی اگر بنویسیم delayMicroseconds(1000)،  یک تاخیر 1000 میکروثانیه‌ای، معادل با 1 میلی‌ثانیه، برای ما ایجاد می‌شود.

نکته‌ی مهمی که در مورد هر دوی این توابع وجود دارد، این است که دقیقا به ازای همین زمانی‌ که تاخیر تولید می‌کند، اجرا برنامه‌ها را نیز متوقف می‌کنند. یعنی تاخیر همراه با وقفه. مثلا اگر قرار باشد 1 ثانیه تاخیر داشته باشیم، از زمانی که آن 1 ثانیه شروع می‌شود تا زمانی که به اتمام برسد، پروسسور نمی‌تواند به سراغ دستور بعدی برود. مدت زمان تاخیر هرچقدر که باشد روال به همین صورت است. یعنی اگر 10 ثانیه، یا هر میزان دیگری هم تاخیر ایجاد کنیم، تا زمانی که آن مدت به اتمام نرسد پروسسور متوقف می‌ماند. واضح است که این روند موجب کند شدن سرعت و افت عملکرد می‌شود.

اجازه بدهید که عملکرد و تاثیر منفی این توابع را در یک مثال نیز با هم بررسی کنیم. فرض کنید دو LED داریم که می‌خواهیم با استفاده از دو کلید آن‌ها را به حالت چشمک‌زن در آوریم. به این ترتیب که اگر کلید اول را فشار دهیم، LED اول به مدت 2 ثانیه روشن شود و اگر کلید دوم را فشار دهیم، LED دوم به مدت 4 ثانیه روشن شود. اگر در نوشتن کد این برنامه از ()delay استفاده کرده باشیم، پس از اینکه کلید اول را فشار دادیم، اگر قبل از به پایان رسیدن آن 2 ثانیه کلید دوم را هم فشار دهیم، پروسسور آن را نادیده می‌گیرد و LED دوم را روشن نمی‌کند. چون در مرحله‌ی وقفه‌ی ناشی از فشردن کلید اول است.

مطلب پیشنهادی:  راه اندازی برد اردینو

مستندات و مراجع آردوینو این نقطه ضعف و اشکال تابع ()delay را در قسمت نکات و هشدارهای پیرامون آن (Notes and Warnings) قید کرده‌ و به صورت کامل توضیح داده‌اند. اگر دوست داشتید می‌توانید به آن‌ها مراجعه کنید و مثال‌های بیشتری را ببینید.

چرا باید از تابع ()millis استفاده کنیم؟

به عنوان یک توسعه‌دهنده‌ی حرفه‌ای، برای پرهیز از اشکالی که توابع ()delay و ()delayMicroseconds دارند، شما باید از تابع ()millis استفاده کنید. کار کردن با این تابع بسیار راحت است و کافیست به استفاده از آن عادت کنید تا به خوبی از برنامه‌هایتان نتیجه بگیرید. این تابع با بهره گرفتن از 100 درصد توانایی CPU، هم تاخیر ایجاد می‌کند و هم روند اجرای سایر دستورات و برنامه‌ها را متوقف نمی‌کند. عملکرد ()millis به این صورت است که در هر لحظه، مدت‌ زمان سپری شده از شروع اجرای هر برنامه در آردوینو را به میلی‌ثانیه برمی‌گرداند. بدون آنکه برنامه‌ها را متوقف کند. بعد از هر 50 روز، این مدت زمان سرریز کرده و دوباره به صفر تنظیم می‌شود.

همان‌طور که برای ()delayMicroseconds() ،delay را داریم، برای ()millis هم ()micros را داریم. تفاوت ()micros با ()millis این است که ()micros به جای هر 50 روز، هر 70 دقیقه یکبار سرریز کرده و صفر می‌شود. بنابراین با توجه به کاربردی که مدنظر داشته باشیم، می‌توانیم از ()millis یا ()micros استفاده کنیم.

Using millis() instead of delay():

برای استفاده از تابع ()millis در کاربردهای timing و delay، باید زمانی‌که اتفاق مورد نظر رخ می‌دهد را ضبط و ذخیره کنید. از لحظه‌ی شروع مرتب چک کنید تا ببینید بازه‌ی زمانی مورد نظر کی اتفاق می‌افتد. همان‌طور که گفتیم این زمان را در یک متغیر ذخیره کنید.

unsigned long currentMillis = millis();

به دو متغیر دیگر هم نیاز داریم که به وسیله‌ی آنها بتوانیم چک کنیم که آیا زمان مورد نظر سپری شده است یا خیر. در اینجا ما زمان حال حاضر را در متغیری به نام currentMillis نگه‌داری می‌کنیم. اما باید بدانیم که بازه‌ی زمانی مورد نظر کی شروع می‌شود و چه مدت طول می‌‌کشد. بنابراین دو متغیر Interval و previousMillis را تعریف می‌کنیم. اولی برای نگه داشتن time delay و دومی برای آنکه بدانیم آخرین باری که این اتفاق رخ داده است چه زمانی بود.

unsigned long previousMillis;
unsigned long period = 1000;

اگر کمی گیج کننده به نظر می‌رسد نگران نباشید، بیایید در یک مثال این مفاهیم را مرور کنیم. فرض کنیم پروژه‌ی ما پروژه‌ی ساده‌ی LED چشمک زن باشد. period = 1000 به ما می‌گوید که LED هر 1000 میلی‌ثانیه ، یا به عبارتی هر 1 ثانیه یک بار چشمک می‌زند.

const int ledPin =  4; // the LED pin number connected
int ledState = LOW;             // used to set the LED state
unsigned long previousMillis = 0;  //will store last time LED was blinked
const long period = 1000;         // period at which to blink in ms

void setup() {
  pinMode(ledPin, OUTPUT); // set ledpin as output
}

void loop() {
 unsigned long currentMillis = millis(); // store the current time
  if (currentMillis - previousMillis >= period) { // check if 1000ms passed
   previousMillis = currentMillis;   // save the last time you blinked the LED
   if (ledState == LOW) { // if the LED is off turn it on and vice-versa
     ledState = HIGH;
   } else {
ledState = LOW;
}
   digitalWrite(ledPin, ledState);//set LED with ledState to blink again
 }
}

در این کد جمله‌ی <if (currentMillis – previousMillis >= period)>  وظیفه دارد چک کند که آیا 1000 میلی‌ثانیه‌ی مدنظر سپری شده است یا خیر. اگر جواب مثبت باشد، LED یک بار چشمک می‌زند و دوباره به وضعیت خود برمی‌گردد. این روال به همین شکل ادامه می‌یابد. یعنی با سپری شدن هر 1000 میلی‌ثانیه شرط جمله‌ی فوق صحیح شده و LED چشمک می‌زند. خب تمام شد. به همین سادگی شما یاد گرفتید که به جای ()delay از ()millis استفاده کنید. به این ترتیب روال پروسسور هم در اجرای دستورات متوقف نمی‌شود.

مطلب پیشنهادی:  توابع ورودی/خروجی پیشرفته آردوینو

وقفه‌ها‌ (Interrupts) در آردوینو همانند میکروها عمل می‌کنند. بورد Arduino UNO دارای دو پین جداگانه برای وصل نمودن وقفه‌هاست. پین‌های 2 و 3 از GPIOها. البته در آموزش‌های مربوط به وقفه در بوردهای آردوینو، مطالب این موضوع را به طور کامل پوشش داده‌ایم و در اینجا نمی‌خواهیم از موضوع اصلی خارج شویم. فقط خواستیم یادآوری کنیم که بین میکروها و آردوینوها در موارد زیادی شباهت عملکرد وجود دارد و اگر یکی را بلد باشید، فهم و یادگیری دیگری چندان سخت نخواهد بود.

در ادامه‌ی آموزش می‌خواهیم قابلیت مولتی‌تسکینگ در آردوینو را برای اجرای همزمان سه task با هم آزمایش کنیم. به این ترتیب که می‌خواهیم دو LED چشمک زن داشته باشیم که هرکدام با تاخیر زمانی متفاوتی چشمک می‌زنند (task 1 و task 2) و یک کلید که بتواند وضعیت ON/OFF شدن LED دیگری را کنترل کند (task3).

آموزش مولتی‌تسکینگ در آردوینو

قطعات مورد نیاز

  • بورد Arduino UNO
  • سه عدد LED ( ترجیحا با رنگ‌های متفاوت)
  • مقاومت (470 و 10k)
  • برد بورد
  • سیم برد بوردی (جامپر)

شماتیک کلی مدار

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

آموزش مولتی‌تسکینگ در آردوینو

نوشتن برنامه برای Arduino UNO و اجرای مولتی‌تسکینگ در آن

تنها قسمت شاید پیچیده‌ی نوشتن این برنامه برای Arduino UNO دانستن منطق استفاده از ()millis برای مولتی‌تسکینگ است که در قسمت‌های ابتدای آموزش آن را یاد گرفتیم. اگر احساس می‌کنید که هنوز به قدر کافی بر روی آن مسلط نیستید، پیشنهاد می‌کنیم که چندین بار برنامه‌ی قبلی را که ساده‌تر بود و طرز استفاده از ()millis را در آن توضیح داده بودیم، مرور کنید تا زمانی که بتوانید خودتان هم به راحتی و با تسلط کامل از ()millis استفاده کنید.

تنها راهنمایی ‌ای که ممکن است از طرف ما برای شما مفید باشد این است که در اینجا کلید که فشار دادن آن یکی از taskها محسوب می‌شود، در واقع نوعی وقفه است و با هر بار اجرای آن وضعیت LED از ON به OFF و یا برعکس تغییر می‌کند.

مانند تمام کدها، در اینجا هم برنامه با قسمت معرفی و توصیف اتصالات پین‌ شروع می‌شود.

int led1 =  6;
int led2 =  7;
int toggleLed = 5;
int pushButton = 2;

پس از آن یک متغیر تعریف می‌کنیم که در آن وضعیت فعلی LEDها را ذخیره می‌کنیم. از این متغیر در آینده استفاده می‌کنیم.

int ledState1 = LOW;
int ledState2 = LOW;

همان‌طور که در مثال قبلی هم توضیح دادیم، به دو متغیر دیگر هم نیاز داریم تا period و previousmillis را نگه‌داری کنیم. از مقایسه‌ی این دو می‌توانیم برای LEDها تاخیر تولید کنیم. تنظیمات را طوری می‌نویسیم کهLED  اول هر 1 ثانیه و LED دوم هر 200 میلی‌ثانیه یکبار چشمک بزنند.

unsigned long previousMillis1 = 0; 
const long period1 = 1000;
unsigned long previousMillis2 = 0;
const long period2 = 200; 

حالا به یک تابع ()millis دیگر هم نیاز داریم تا به کمک آن یک تاخیر رهاسازی را برای کلید تعریف کنیم. به این ترتیب فشاردادن‌های بدون فاصله‌ی کلید روال مدار را بر هم نمی‌زنند. دقیقا مانند کاری که در بالا انجام دادیم را در اینجا هم تکرار می‌کنیم.

int debouncePeriod = 20;  
int debounceMillis = 0;

از سه متغیر برای ذخیره کردن وضعیت کلید، LED متناظر با آن و ورودی کلید به عنوان وقفه استفاده می‌کنیم.

bool buttonPushed = false;
int ledChange = LOW;  
int lastState = HIGH;

ورودی (INPUT) یا خروجی (OUTPUT) بودن هر پین را هم مشخص می‌کنیم.

  pinMode(led1, OUTPUT);             
  pinMode(led2, OUTPUT);
  pinMode(toggleLed, OUTPUT);
  pinMode(pushButton, INPUT);

حالا پین مرتبط با وقفه را با تعریف کردن ISR و interrupt Mode، تعریف می‌کنیم.

توصیه می‌کنیم زمان استفاده از تابع ()attachInterrupt، از digitalPinToInterrupt(pin_number) استفاده کنید. با این کار پین دیجیتال به طور مستقیم به یک شماره‌ی مشخص برای وقفه ترجمه می‌شود.

attachInterrupt(digitalPinToInterrupt(pushButton), pushButton_ISR, CHANGE); 

بخش interrupt subroutine تنها buttonPushed flag را تغییر می‌دهد. تلاش کنید تا حد امکان این بخش را کوتاه بنویسید. با حداقل تعداد دستورات.

void pushButton_ISR()
{
  buttonPushed = true; 
}

حلقه‌ها با ذخیره‌ی مقدار millis در متغیرهای currentMillis آغاز می‌شوند. همان‌طور که گفتیم این متغیرها مدت زمان سپری شده از آغاز حلقه را نگه‌داری می‌کنند.

unsigned long currentMillis = millis();

یکبار دیگر taskها موجود در این مولتی‌تسکینگ را مرور کنیم.

یک LED چشمک زن با فواصل زمانی 1 ثانیه

یک LED چشمک زن با فواصل زمانی 200 میلی ثانیه

و یک کلید که اگر فشرده شود LED سومی که در مدار داریم از ON به OFF یا برعکس تغییر وضعیت می‌دهد.

مطلب پیشنهادی:  آموزش شروع کار با آردوینو Due

برای نوشتن هر کدام از این taskها یک بخش اختصاص می‌دهیم.

ابتدا برای LED چشمک‌زن با فواصل 1 ثانیه‌ای.

 if (currentMillis - previousMillis1 >= period1) { 
    previousMillis1 = currentMillis;  
    if (ledState1 == LOW) { 
      ledState1 = HIGH;
    } else {
      ledState1 = LOW;
    }
    digitalWrite(led1, ledState1);   
  }

دومی هم دقیقا مانند اولی نوشته می‌شود با این تفاوت که مقایسه‌ی millisها باید اختلاف 200 میلی‌ثانیه را بدهد تا LED چشمک بزند.

  if (currentMillis - previousMillis2 >= period2) {
    previousMillis2 = currentMillis;  
    if (ledState2 == LOW) {
      ledState2 = HIGH;
    } else {
      ledState2 = LOW;
    }
    digitalWrite(led2, ledState2);
  }

و در نهایت buttonPushed flag را داریم که بعد از وقفه‌ی رهاسازی که آن را 20 میلی‌ثانیه در نظر می‌گیریم، وضعیت LED را با توجه به وقفه‌ی وارد شده تغییر می‌دهد.

  if (buttonPushed = true)    // check if ISR is called
  {
    if ((currentMillis - debounceMillis) > debouncePeriod && buttonPushed)  // generate 20ms debounce delay to avoid multiple presses
    {
      debounceMillis = currentMillis;      // save the last debounce delay time
      if (digitalRead(pushButton) == LOW && lastState == HIGH)     // change the led after push button is pressed
      {
        ledChange = ! ledChange;
        digitalWrite(toggleLed, ledChange);   
        lastState = LOW;
      }
      else if (digitalRead(pushButton) == HIGH && lastState == LOW)    
      {
        lastState = HIGH;
      }
     buttonPushed = false;
    }
  }

بسیار خب، به انتهای این جلسه رسیدیم. باز هم توصیه می‌کنیم که برای تسلط بر استفاده از تابع ()millis، باید تلاش کنید بارها و بارها آن را در مثال‌های مختلف استفاده و مرور کنید. برای اینکه مشتاق دیدن و پیدا کردن مثال‌های بیشتر شوید، به شما بگوییم که از این تابع می‌توان در برنامه‌نویسی موتورها، سروو موتورها، سنسورها و بسیاری موارد دیگر استفاده کرد. هر سوالی هم که داشتید می‌توانید همین‌جا از ما بپرسید.

ضمنا، کد را به صورت کامل و یکپارچه، و ویدئوی آموزشی همین جلسه را هم می‌توانید داشته باشید.

کد نهایی مثال آموزش مولتی ‌تسکینگ در آردوینو

/* Arduino Multitasking Author : CircuitDigest (circuitdigest.com) */ int led1 = 6; // led1 connected at pin 6 int led2 = 7; // led1 connected at pin 7 int toggleLed = 5; // push button controlled led connected at pin 5 int pushButton = 2; // push butoon connected at pin 2 which is also interrupt pin int ledState1 = LOW; // to determine the states of led1 and led2 int ledState2 = LOW; unsigned long previousMillis1 = 0; //store last time LED1 was blinked const long period1 = 1000; // period at which led1 blinks in ms unsigned long previousMillis2 = 0; //store last time LED2 was blinked const long period2 = 200; // period at which led1 blinks in ms int debouncePeriod = 20; // debounce delay of 20ms int debounceMillis = 0; // similar to previousMillis bool buttonPushed = false; // interrupt routine button status int ledChange = LOW; // to track the led status last int lastState = HIGH; // to track last button state void setup() { pinMode(led1, OUTPUT); // define pins as input or output pinMode(led2, OUTPUT); pinMode(toggleLed, OUTPUT); pinMode(pushButton, INPUT); attachInterrupt(digitalPinToInterrupt(pushButton), pushButton_ISR, CHANGE); // use interrupt pin2 } void pushButton_ISR() { buttonPushed = true; // ISR should be as short as possible } void loop() { unsigned long currentMillis = millis(); // store the current time if (currentMillis - previousMillis1 >= period1) { // check if 1000ms passed previousMillis1 = currentMillis; // save the last time you blinked the LED if (ledState1 == LOW) { // if the LED is off turn it on and vice-versa ledState1 = HIGH; //change led state for next iteration } else { ledState1 = LOW; } digitalWrite(led1, ledState1); //set LED with ledState to blink again } if (currentMillis - previousMillis2 >= period2) { // check if 1000ms passed previousMillis2 = currentMillis; // save the last time you blinked the LED if (ledState2 == LOW) { // if the LED is off turn it on and vice-versa ledState2 = HIGH; } else { ledState2 = LOW; } digitalWrite(led2, ledState2);//set LED with ledState to blink again } if (buttonPushed = true) // check if ISR is called { if ((currentMillis - debounceMillis) > debouncePeriod && buttonPushed) // generate 20ms debounce delay to avoid multiple presses { debounceMillis = currentMillis; // save the last debounce delay time if (digitalRead(pushButton) == LOW && lastState == HIGH) // change the led after push button is pressed { ledChange = ! ledChange; digitalWrite(toggleLed, ledChange); lastState = LOW; } else if (digitalRead(pushButton) == HIGH && lastState == LOW) { lastState = HIGH; } buttonPushed = false; } } }

ویدئو

 

منبع: ترجمه از سایت circuitdigest.com

همچنین لطفا اپلیکیشن اندویدی ما را هم نصب کنید.

دانلود اپلیکیشن میکرو دیزاینر الکترونیک

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *