در قسمت قبل آموزش آردوینو با نمایشگرهای گرافیکی TFT و OLED آشنا شدیم و نحوه نمایش متن، رسم اشکال و ایجاد تصاویر متحرک را بررسی کردیم. در این قسمت به یکی دیگر از قابلیتهای مهم آردوینو، یعنی مدیریت زمان میپردازیم.
قسمت 64 آموزش برنامه نویسی آردوینو به بررسی روشهای اندازهگیری زمان، ایجاد تأخیر در اجرای برنامه، اندازهگیری عرض پالسهای دیجیتال و استفاده از آردوینو بهعنوان یک ساعت دیجیتال اختصاص دارد. در این آموزش با توابع مهمی مانند millis()، delay() و pulseIn() آشنا میشویم و در ادامه با استفاده از کتابخانه Time یک ساعت دیجیتال میسازیم.
زمان یکی از پارامترهای مهم در بسیاری از پروژههای الکترونیکی و سیستمهای تعبیهشده است. در یک پروژه ممکن است لازم باشد مدتزمان روشن بودن یک دستگاه را محاسبه کنیم، یک عملیات را در فواصل زمانی مشخص انجام دهیم یا ساعت و تاریخ را در اختیار برنامه قرار دهیم. برای مدیریت زمان در آردوینو چند ابزار مختلف وجود دارد. برخی از آنها برای اندازهگیری زمان سپریشده از شروع اجرای برنامه استفاده میشوند و برخی دیگر امکان کار با ساعت واقعی و تاریخ را فراهم میکنند.
در این فصل چهار موضوع اصلی را بررسی میکنیم:
millis()delay() و delayMicroseconds()pulseIn()گاهی لازم است بدانیم یک رویداد چه مدتزمانی طول کشیده است. برای مثال، فرض کنید میخواهیم مدتزمانی را که کاربر یک کلید را نگه داشته است اندازهگیری کنیم. برای این کار میتوان از تابع millis() استفاده کرد. این تابع تعداد میلیثانیههای سپریشده از زمان شروع اجرای برنامه آردوینو را برمیگرداند. بنابراین کافی است زمان شروع یک رویداد را ذخیره کنیم و هنگام پایان آن، مقدار فعلی millis() را از زمان شروع کم کنیم.
فرمول محاسبه مدتزمان:
| 1 | duration = endTime - startTime; |
در مثال زیر، مدتزمان فشرده نگهداشتن یک کلید اندازهگیری میشود. کلید به پایه شماره 2 متصل شده و از مقاومت Pull-up داخلی آردوینو استفاده میکنیم.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /* * Measure button press duration */ const int buttonPin = 2; bool previousButtonState = HIGH; unsigned long pressStartTime = 0; void setup() { pinMode(buttonPin, INPUT_PULLUP); Serial.begin(9600); } void loop() { bool currentButtonState = digitalRead(buttonPin); // Detect button press if(previousButtonState == HIGH && currentButtonState == LOW) { pressStartTime = millis(); } // Detect button release if(previousButtonState == LOW && currentButtonState == HIGH) { unsigned long duration = millis() - pressStartTime; Serial.print("Button pressed for: "); Serial.print(duration); Serial.println(" ms"); } previousButtonState = currentButtonState; } |
در ابتدای برنامه، پایه شماره 2 بهعنوان ورودی تعریف میشود. با فعالکردن مقاومت Pull-up داخلی، در حالت عادی مقدار این پایه HIGH است و هنگام فشردن کلید به LOW تغییر میکند.
در لحظه فشردن کلید، مقدار فعلی millis() در متغیر pressStartTime ذخیره میشود. پس از رها کردن کلید، زمان فعلی دوباره خوانده شده و اختلاف آن با زمان شروع محاسبه میشود. نتیجه محاسبه در Serial Monitor نمایش داده میشود. برای مثال اگر کلید به مدت دو ثانیه نگه داشته شده باشد، مقدار تقریبی 2000 میلیثانیه نمایش داده خواهد شد.
خروجی تابع millis() از نوع unsigned long است. این شمارنده پس از حدود 50 روز به انتهای محدوده خود میرسد و دوباره از صفر شروع میکند.
به همین دلیل بهتر است برای محاسبه زمان سپریشده از تفریق زمان شروع از زمان فعلی استفاده کنیم، نه اینکه صرفاً منتظر رسیدن شمارنده به یک مقدار مشخص باشیم. این روش برای اندازهگیری بازههای زمانی کوتاهتر از محدوده شمارنده قابلاعتمادتر است.
| 1 | unsigned long elapsedTime = millis() - startTime; |
در بسیاری از پروژهها لازم است اجرای برنامه برای مدت مشخصی متوقف شود. برای مثال ممکن است بخواهیم یک LED برای یک ثانیه روشن بماند یا بین دو مرحله از اجرای یک برنامه چند دقیقه فاصله ایجاد کنیم.
تابع delay() برای همین منظور استفاده میشود. پارامتر این تابع برحسب میلیثانیه است. هر ثانیه برابر با 1000 میلیثانیه است؛ بنابراین برای ایجاد تأخیرهای طولانیتر میتوان زمان موردنظر را بهصورت ترکیبی محاسبه کرد.
| 1 2 3 4 5 6 7 8 9 10 | const unsigned long oneSecond = 1000UL; const unsigned long oneMinute = oneSecond * 60UL; const unsigned long oneHour = oneMinute * 60UL; const unsigned long oneDay = oneHour * 24UL; |
با استفاده از این متغیرها میتوان بهسادگی تأخیرهای مختلف ایجاد کرد:
| 1 2 3 4 5 6 7 8 9 | delay(1); // 1 millisecond delay(oneSecond); // 1 second delay(oneMinute); // 1 minute delay(oneHour); // 1 hour delay(oneDay); // 1 day |
اگر به تأخیرهای بسیار کوتاه نیاز داشته باشیم، میتوانیم از تابع delayMicroseconds() استفاده کنیم. در این تابع زمان برحسب میکروثانیه مشخص میشود. هر میلیثانیه برابر با 1000 میکروثانیه و هر ثانیه برابر با یک میلیون میکروثانیه است.
| 1 | delayMicroseconds(10); |
دستور بالا تقریباً 10 میکروثانیه مکث ایجاد میکند. محدوده این تابع از یک میکروثانیه تا حدود 16 میلیثانیه است. برای تأخیرهای طولانیتر بهتر است از delay() استفاده کنیم.
یکی از مشکلات اصلی تابع delay() این است که در طول مدت تأخیر، اجرای عادی کد اصلی متوقف میشود. در نتیجه آردوینو نمیتواند دستورات بعدی برنامه را اجرا کند. برای مثال، اگر برنامه شامل چشمکزدن LED با تأخیر یکثانیهای باشد، در طول این یک ثانیه فرصت اجرای سایر دستورات در حلقه اصلی وجود نخواهد داشت.
برای حل این مشکل میتوان از ترکیب تابع millis() و یک متغیر برای ذخیره زمان آخرین اجرای عملیات استفاده کرد. این روش در مثال معروف Blink Without Delay آردوینو نیز استفاده شده است.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /* * Blink Without Delay */ const int ledPin = LED_BUILTIN; const unsigned long interval = 1000UL; unsigned long previousMillis = 0; int ledState = LOW; void setup() { pinMode(ledPin, OUTPUT); } void loop() { unsigned long currentMillis = millis(); if(currentMillis - previousMillis >= interval) { previousMillis = currentMillis; ledState = !ledState; digitalWrite(ledPin, ledState); } // Other tasks can run here. } |
در این برنامه، برخلاف روش استفاده از delay()، حلقه اصلی متوقف نمیشود. آردوینو مرتب زمان فعلی را بررسی میکند و هرگاه فاصله زمانی موردنظر سپری شده باشد، وضعیت LED را تغییر میدهد. در نتیجه میتوان همزمان با چشمکزدن LED، وضعیت کلیدها را بررسی کرد، اطلاعات سنسورها را خواند یا اطلاعاتی را از طریق پورت سریال دریافت کرد.
در پروژههای بزرگتر ممکن است چند عملیات مختلف داشته باشیم که هرکدام باید در بازه زمانی مشخصی اجرا شوند. برای مثال یک LED هر یک ثانیه تغییر وضعیت دهد و LED دیگری هر نیم ثانیه چشمک بزند. میتوان برای هر عملیات از یک شمارنده زمانی مستقل استفاده کرد یا از کتابخانههای زمانبندی مانند Tasker کمک گرفت. این کتابخانه امکان تعریف وظایف دورهای را فراهم میکند و اجرای آنها از طریق حلقه اصلی برنامه مدیریت میشود. در هر صورت، اصل مهم این است که برنامه را به شکلی طراحی کنیم که انجام یک عملیات زمانبر، اجرای سایر بخشهای موردنیاز را متوقف نکند.
در مدارهای دیجیتال گاهی لازم است مدتزمانی را که یک سیگنال در وضعیت HIGH یا LOW قرار دارد، با دقت میکروثانیه اندازهگیری کنیم. به این مدتزمان، عرض پالس یا Pulse Width گفته میشود. تابع pulseIn() برای اندازهگیری مدتزمان یک پالس دیجیتال استفاده میشود. این تابع میتواند مدتزمان HIGH یا LOW بودن سیگنال را برحسب میکروثانیه برگرداند. ساختار کلی تابع به شکل زیر است:
| 1 2 3 | pulseIn(pin, state); pulseIn(pin, state, timeout); |
پارامترهای تابع عبارتاند از:
اگر پارامتر سوم مشخص نشود، زمان انتظار پیشفرض یک ثانیه خواهد بود. در صورت مشخصکردن Timeout، تابع تا زمان تعیینشده منتظر شروع پالس میماند. اگر پالس شروع نشود، مقدار صفر برگردانده میشود.
در مثال زیر از تابع analogWrite() برای تولید سیگنال PWM روی پایه شماره 3 استفاده میکنیم و سپس مدتزمان بخش HIGH و LOW آن را با pulseIn() اندازه میگیریم.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /* * Pulse Width Measurement */ const int signalPin = 3; unsigned long highWidth; unsigned long lowWidth; void setup() { Serial.begin(9600); } void loop() { analogWrite(signalPin, 128); measurePulseWidth(); analogWrite(signalPin, 220); measurePulseWidth(); delay(2000); } void measurePulseWidth() { highWidth = pulseIn(signalPin, HIGH, 100000UL); lowWidth = pulseIn(signalPin, LOW, 100000UL); Serial.print("High Pulse: "); Serial.print(highWidth); Serial.print(" us"); Serial.print(" | Low Pulse: "); Serial.print(lowWidth); Serial.println(" us"); } |
در ابتدای حلقه، مقدار 128 به تابع analogWrite() ارسال میشود. این مقدار میزان Duty Cycle سیگنال PWM را تعیین میکند.
سپس تابع measurePulseWidth() اجرا میشود و عرض پالس HIGH و LOW را اندازهگیری میکند. در مرحله بعد مقدار PWM به 220 تغییر میکند و اندازهگیری دوباره انجام میشود. با تغییر مقدار PWM، نسبت زمان HIGH به LOW نیز تغییر میکند. خروجی برنامه در Serial Monitor نمایش داده میشود. برای مثال، در یک اندازهگیری ممکن است خروجی مشابه زیر مشاهده شود:
| 1 2 3 | High Pulse: 989 us | Low Pulse: 997 us High Pulse: 1977 us | Low Pulse: 8 us |
مقادیر واقعی ممکن است با توجه به برد، فرکانس PWM و زمانبندی اجرای برنامه کمی متفاوت باشند.
تابع pulseIn() ابتدا منتظر شروع پالس میماند و پس از تشخیص آن، مدتزمان ادامه پالس را اندازهگیری میکند. این تابع میتواند پالسهایی با مدتزمان حدود 10 میکروثانیه تا چند دقیقه را اندازهگیری کند؛ اما برای پالسهای بسیار طولانی دقت اندازهگیری کاهش پیدا میکند. از این تابع میتوان در پروژههایی مانند اندازهگیری پالس سنسورهای اولتراسونیک، بررسی سیگنالهای دیجیتال و تحلیل زمانبندی سیگنالهای کنترلی استفاده کرد.
تا اینجا با روشهای اندازهگیری زمان سپریشده آشنا شدیم. اما اگر بخواهیم ساعت فعلی، دقیقه، ثانیه و تاریخ را در اختیار برنامه قرار دهیم، به روش دیگری نیاز داریم. برای این کار میتوان از کتابخانه Time استفاده کرد. این کتابخانه مجموعهای از توابع برای نگهداری و مدیریت زمان و تاریخ در اختیار برنامهنویس قرار میدهد. برای نصب این کتابخانه میتوان وارد Arduino IDE شد و از مسیر Library Manager کتابخانه Time را جستوجو و نصب کرد. در صورت دشوار بودن پیدا کردن آن، عبارت timekeeping نیز میتواند برای جستوجو مفید باشد.
در اولین مثال، ساعت را روی یک مقدار ثابت تنظیم میکنیم و سپس هر ثانیه زمان و تاریخ را در Serial Monitor نمایش میدهیم.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /* * Arduino Digital Clock */ #include <TimeLib.h> void setup() { Serial.begin(9600); // Set time: 12:00:00, January 1, 2020 setTime(12, 0, 0, 1, 1, 2020); } void loop() { digitalClockDisplay(); delay(1000); } void printTwoDigits(int value) { if(value < 10) { Serial.print('0'); } Serial.print(value); } void digitalClockDisplay() { printTwoDigits(hour()); Serial.print(':'); printTwoDigits(minute()); Serial.print(':'); printTwoDigits(second()); Serial.println(); Serial.print(year()); Serial.print('-'); printTwoDigits(month()); Serial.print('-'); printTwoDigits(day()); Serial.println(); } |
در این برنامه ابتدا کتابخانه TimeLib.h اضافه میشود. سپس با استفاده از تابع setTime() زمان اولیه ساعت تعیین میشود. ساختار این تابع به شکل زیر است:
| 1 | setTime(hour, minute, second, day, month, year); |
برای دریافت اجزای مختلف زمان نیز میتوان از توابع زیر استفاده کرد:
hour() برای دریافت ساعتminute() برای دریافت دقیقهsecond() برای دریافت ثانیهday() برای دریافت روز ماهmonth() برای دریافت ماهyear() برای دریافت سالتابع printTwoDigits() نیز برای نمایش اعداد یکرقمی با صفر ابتدایی استفاده شده است. به این ترتیب بهجای نمایش 9:5:3، زمان به شکل 09:05:03 نمایش داده میشود.
کتابخانه Time زمان را با استفاده از شمارش گذر زمان مدیریت میکند. بسیاری از بردهای آردوینو از کریستال کوارتز برای ایجاد مرجع زمانی استفاده میکنند و این روش میتواند در طول یک روز چند ثانیه خطا داشته باشد. اما یک محدودیت مهم وجود دارد: این ساعت نرمافزاری بهتنهایی حافظه پشتیبان باتریدار برای نگهداری زمان هنگام قطع برق ندارد. بنابراین پس از راهاندازی مجدد برنامه، باید زمان دوباره تنظیم شود.
کتابخانه Time از استانداردی به نام Unix Time یا Epoch Time استفاده میکند. در این روش زمان بهصورت تعداد ثانیههای سپریشده از اول ژانویه سال 1970 ذخیره میشود. برای مثال، مقدار زیر یک زمان یونیکس را نشان میدهد:
| 1 | 1569888000 |
یکی از مزایای این روش آن است که میتوان زمان را بهصورت یک عدد به آردوینو ارسال کرد و کتابخانه Time آن را به ساعت، دقیقه، ثانیه و تاریخ تبدیل کند.
در مثال قبلی زمان همیشه روی یک مقدار ثابت تنظیم میشد. برای استفاده عملیتر میتوان زمان فعلی را از طریق پورت سریال به آردوینو ارسال کرد. در این روش یک کاراکتر مشخص بهعنوان علامت شروع پیام در نظر میگیریم. برای مثال حرف T را به ابتدای عدد زمان یونیکس اضافه میکنیم. نمونه پیام ارسالی:
| 1 | T1569888000 |
برنامه کاراکتر T را شناسایی میکند، عدد بعد از آن را دریافت کرده و با تابع setTime() ساعت داخلی کتابخانه را تنظیم میکند.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | /* * Set Arduino Clock from Serial */ #include <TimeLib.h> #define TIME_HEADER 'T' void setup() { Serial.begin(9600); Serial.println("Waiting for time sync message"); } void loop() { if(Serial.find(TIME_HEADER)) { time_t receivedTime = Serial.parseInt(); setTime(receivedTime); Serial.println("Clock Updated"); } if(timeStatus() != timeNotSet) { digitalClockDisplay(); } delay(1000); } void printTwoDigits(int value) { if(value < 10) { Serial.print('0'); } Serial.print(value); } void digitalClockDisplay() { printTwoDigits(hour()); Serial.print(':'); printTwoDigits(minute()); Serial.print(':'); printTwoDigits(second()); Serial.println(); Serial.print(year()); Serial.print('-'); printTwoDigits(month()); Serial.print('-'); printTwoDigits(day()); Serial.println(); } |
در این برنامه تابع Serial.find() منتظر دریافت کاراکتر مشخصشده میماند. پس از پیدا شدن کاراکتر T، تابع Serial.parseInt() عدد بعد از آن را دریافت میکند. سپس مقدار دریافتی با استفاده از setTime() بهعنوان زمان اولیه تنظیم میشود. تابع timeStatus() نیز مشخص میکند که آیا ساعت مقداردهی شده است یا خیر.
setTime() ارسال شود. این نکته را هنگام استفاده از مبدلهای آنلاین زمان یونیکس در نظر بگیرید.راه دیگر تنظیم ساعت، استفاده از برنامه Processing به نام SyncArduinoClock است که همراه مثالهای کتابخانه Time ارائه میشود. این برنامه میتواند زمان سیستم کامپیوتر را از طریق ارتباط سریال به آردوینو ارسال کند.
در بعضی پروژهها ممکن است بخواهیم بدون اتصال کامپیوتر، ساعت را بهصورت دستی تنظیم کنیم. برای این کار میتوان از دو کلید استفاده کرد. در این مدار، یک کلید برای جلو بردن زمان و کلید دیگر برای عقب بردن زمان در نظر گرفته میشود. با نگه داشتن کلید، مقدار زمان بهتدریج تغییر میکند.

| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /* * Adjust Clock with Two Buttons */ #include <TimeLib.h> const int btnForward = 2; const int btnBack = 3; void setup() { pinMode(btnForward, INPUT_PULLUP); pinMode(btnBack, INPUT_PULLUP); Serial.begin(9600); setTime(12, 0, 0, 1, 1, 2020); } void loop() { if(digitalRead(btnForward) == LOW) { adjustTime(1); delay(150); } if(digitalRead(btnBack) == LOW) { adjustTime(-1); delay(150); } digitalClockDisplay(); delay(500); } void printTwoDigits(int value) { if(value < 10) { Serial.print('0'); } Serial.print(value); } void digitalClockDisplay() { printTwoDigits(hour()); Serial.print(':'); printTwoDigits(minute()); Serial.print(':'); printTwoDigits(second()); Serial.println(); } |
تابع adjustTime() برای جلو یا عقب بردن ساعت استفاده میشود. مقدار مثبت، زمان را جلو میبرد و مقدار منفی، آن را به عقب برمیگرداند. در این مثال با هر بار تشخیص فشرده شدن کلید، یک ثانیه به زمان اضافه یا از آن کم میشود.
روش دیگر برای تنظیم ساعت استفاده از یک پتانسیومتر به همراه یک کلید است. در این مدار، کلید برای فعالکردن حالت تنظیم زمان استفاده میشود و پتانسیومتر جهت و سرعت تغییر زمان را مشخص میکند. پتانسیومتر به پایه آنالوگ A0 متصل میشود و مقدار آن با استفاده از تابع analogRead() خوانده خواهد شد.

برای تبدیل مقدار خواندهشده از پتانسیومتر به یک مقدار مثبت یا منفی میتوان از تابع map() استفاده کرد.
| 1 2 3 | int value = analogRead(A0); int step = map(value, 0, 1023, 10, -10); |
در این مثال، با تغییر موقعیت پتانسیومتر مقدار step بین محدوده مثبت و منفی تغییر میکند. مقادیر مثبت باعث جلو رفتن زمان و مقادیر منفی باعث عقب رفتن آن میشوند. اگر کلید تنظیم فشرده شده باشد، مقدار محاسبهشده به تابع adjustTime() ارسال میشود:
| 1 2 3 4 | if(step != 0) { adjustTime(step); } |
به این ترتیب میتوان با یک کلید و یک پتانسیومتر، هم جهت تغییر زمان و هم سرعت آن را کنترل کرد.
کتابخانه Time علاوه بر توابع دریافت ساعت و تاریخ، امکانات دیگری نیز برای کار با بازههای زمانی در اختیار برنامهنویس قرار میدهد. برای مثال میتوان از توابع زیر استفاده کرد:
| 1 2 3 4 5 6 7 | dayOfWeek(now()); elapsedSecsToday(now()); nextMidnight(now()); elapsedSecsThisWeek(now()); |
dayOfWeek() روز هفته را مشخص میکند. در این کتابخانه یکشنبه با عدد 1 نمایش داده میشود.elapsedSecsToday() تعداد ثانیههای سپریشده از ابتدای روز را برمیگرداند.nextMidnight() زمان باقیمانده تا پایان روز را محاسبه میکند.elapsedSecsThisWeek() مدتزمان سپریشده از ابتدای هفته را برمیگرداند.در صورت نیاز میتوان بهجای نمایش عدد روز هفته یا ماه، نام آنها را نیز نمایش داد. کتابخانه Time توابعی برای دریافت نام روز و نام کوتاه ماه دارد.
| 1 2 3 4 5 6 | String dateString = String(dayStr(weekday())) + ", " + String(monthShortStr(month())) + " " + String(year()); Serial.println(dateString); |
با استفاده از این توابع میتوان اطلاعات تاریخ را به شکل خواناتری در خروجی نمایش داد.
جمعبندی
در قسمت 63 آموزش آردوینو با روشهای مختلف مدیریت زمان آشنا شدیم. ابتدا یاد گرفتیم چگونه با تابع millis() مدتزمان سپریشده از یک رویداد را محاسبه کنیم. سپس روش ایجاد تأخیر با توابع delay() و delayMicroseconds() را بررسی کردیم و دیدیم چرا استفاده بیش از حد از تأخیرهای مسدودکننده میتواند عملکرد برنامه را محدود کند.
در ادامه با تابع pulseIn() برای اندازهگیری عرض پالسهای HIGH و LOW آشنا شدیم. در بخش پایانی نیز با کتابخانه Time یک ساعت دیجیتال ساختیم و روشهای تنظیم ساعت از طریق پورت سریال، دو کلید و پتانسیومتر را بررسی کردیم.
این قابلیتها در پروژههایی مانند ساعت دیجیتال، ثبت زمان رویدادها، کنترل تجهیزات در بازههای مشخص، اندازهگیری سیگنالهای دیجیتال و ساخت سیستمهای کنترلی زمانبندیشده کاربرد دارند. در قسمت بعدی آموزش آردوینو به ادامه مباحث کتاب و روشهای پیشرفتهتر کار با زمان و اجرای عملیات زمانبندیشده خواهیم پرداخت.
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.