مولتی تسکینگ یا چند وظیفگی (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 یا برعکس تغییر وضعیت میدهد.
برای نوشتن هر کدام از این 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
اگر این نوشته برایتان مفید بود لطفا کامنت بنویسید.
سلام
خسته نباشید، خواستم یه خداقوت بگم
مطالبتون واقعا بی نظیر اند
از اینکه به صورت رایگان اند خیلی خوشحالم و کمال تشکر را دارم
با تشکر
سلام من سنسور dht 22رو با اردوینو unoراه اندازی کردم و برای اینکه اطلاعات درست بده delay دو ثانیه ای بهش دادم اطلاعات روی السیدی نمایش داده میشه . حالا میخام برنامه کیپد رو هم بریزم روی برد که ماکزیموم دما مینیوم دما رو بگیره و اگه دما بیشتر مثلا۳۰ درجه شد رله فلان وصل بشه طوری که به طور همزمان بشه از جفتشون کار گرفت و همون تاخیر ۲ثانیه ای خیلی اذیت میکنه امیدوارم منظورم رو گرفته باشید خیلی ممنون
سلام محمدجان سعی کن از وقفه ها استفاده کنی برای آپدیت ال سی دی و خوندن کیبرد اینطوری اکثر مشکلات حل مبشه. بررسی وضعیت را هم در وقفه انجام بده مثلا خر 1 ثانیه یکبار شرط را داخل روتین وقفه چک کن.
بسیار عالی. درود خداوند برشما
سلام.ممنون از آموزش های ارزشمندتون.
من یه برنامه ای نوشتم که با تابع وقفه فن رو خاموش و روشن میکنه ولی میخوام بدون اینکه delay بذارم خاموش شدن فن رو به تاخیر بندازم.میشه در این مورد راهنماییم کنید.
واقعا عالی ممنون این مطلب خیلی به من کمک کرد
سلام
خسته نباشید
خدا خیرتون بده
بسیار عالی بود
به نظر بنده حقیر در بین اموزش ها بی نظیره و پر محتواست و مثل اکثر سایت ها تکراری نیست و بین اموزش ها هم ….
موفق و پیروز باشید
حداکثر زمان استفاده شده در تابع milis چقده مثلا میتونه 15 دقیقه باشه؟
سلام من برای کنترل رله می خوام این کار را انجام بدم این کد رو نوشتم
if (currentMillis – previousMillis >= period) {
previousMillis = currentMillis;
digitalWrite(motor,HIGH);
delay(10000);
digitalWrite(motor,LOW);
}
ده ثانیه تاخیر داره ولی مشکلی توی برنامم ایجاد نمیکنه و قابل چشم پوشی هست اگه امکانش باشه بگید درست هست یا نه؟
با سلام
ضمن تشكر از مطالب با ارزشي كه ارائه نموديد . آيا امكان توليد دو وقفه متوالي با اين تكنيك وجود دارد .مثلا يك LED به مدت 2 ثانيه روشن باشد و يك ثانيه خاموش شود و اين عمل تكرار شود.؟
سلام
وقت بخیر
ضمن تشکر بابت مطالب بالا یک مشکل دارن راهنمایی کنید
با اردوینو نور یک ledرو به کمک پتانسیومتر کنترل کردم
حالا میخواهم اگر پتانسیومتر با هر سرعتی تغییر کرد نور کم کم تغییر کنه
یا پتانسیومتر دومی برای کنترل سرعت 0تا 255 نور داشته باشم