ארדואינו למתחילים: מולטיטסקינג

איך גורמים לארדואינו (או לכל מיקרו-בקר, לצורך העניין) לבצע יותר מפעולה אחת בו-זמנית? למשל, איך עושים שהארדואינו גם יהבהב בנורית LED בקצב קבוע, גם יקרא נתונים בזמן אמת מחיישן מרחק וגם יגיב להם בשינוי צליל ברמקול בלי שהפעולות יפריעו זו לזו?

בתור מי שתכנת עוד בימי DOS העליזים, התשובה היתה לי כל כך מובנת מאליה, שלא שמתי לב בכלל שיש כאן שאלה. אך מכיוון שהנושא עלה באופן בלתי-תלוי מספר פעמים, ברור שזה לא באמת מובן מאליו – והפוסט הנוכחי נועד להסיר את הערפל מהעניין הבסיסי-אך-מסתורי הזה.

הדבר הראשון שחשוב להבהיר הוא שאין מולטיטסקינג אמתי בארדואינו. ל"מעבד" שלו יש רק ליבה אחת, וכשהוא מדליק נורה (לצורך העניין), זה הדבר היחיד שהוא עושה באותו רגע. עם זאת, רוב הפקודות שאנחנו מריצים בארדואינו לוקחות מעט מאד זמן: הפקודה digitalWrite, לדוגמה, נמשכת מיליוניות שניה ספורות. כלומר, אם אנחנו מחליפים מצב של נורית LED פעם בשניה בעזרת הקוד הבא –

void loop() {
  digitalWrite(LEDPin, HIGH);
  delay(1000);
  digitalWrite(LEDPin, LOW);
  delay(1000);
}

אז במשך 99.99999% מהזמן, פלוס מינוס, הארדואינו "תקוע" בפקודות ה-delay ולא יכול לעשות שום דבר מועיל. ללא ספק, אנחנו חייבים למצוא דרך טובה יותר לנצל את הזמן הזה!

שיטה 1: שאריות

נניח שיש לנו פעולה A שצריכה להתבצע פעם בשניה, ופעולה B שצריכה להתבצע ארבע פעמים בשניה. כלומר, פעולה A יכולה להתבצע בכל פעם רביעית ש-B מתבצעת. נריץ את B תוך שימוש בפקודת delay רגילה (250 אלפיות השניה), ונגדיר בנוסף משתנה שסופר את ההרצות שלה. בכל פעם שהמונה הזה יתחלק בלי שארית בארבע, הגיע הזמן לבצע את A. הנה קוד שמחליף מצב פלט בפין 8 ארבע פעמים בשניה, ואת מצב הפלט בפין 13 פעם אחת בשניה:

boolean StateA = false;
boolean StateB = false;

int counter = 0;

void setup() {
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
}

void loop() {

  // "Fast" blink
  digitalWrite(8, StateB);
  StateB = !StateB;
  counter++;
  delay(250);

  if (counter % 4 == 0) {
    // "Slow" blink
    digitalWrite(13, StateA);
    StateA = !StateA;
  }  

}

אגב, השתמשתי כאן לא בערכי HIGH ו-LOW השגרתיים אלא בערכי true ו-false של משתנים בינאריים (בוליאניים). מבחינת הארדואינו זה אותו דבר בדיוק, כי HIGH ו-LOW הם בסך הכל קבועים מספריים שמיתרגמים ל-true ול-false.

אפשר כמובן לצמצם את משך ה-delay ולהוסיף עוד בדיקות שארית, וכך להריץ פעולות מרובות על אותו "שעון" ולבצע דברים שייראו לצופה מבחוץ כאילו הם מתרחשים במקביל. עם זאת, יש כאן בעיה מהותית: אם נניח שפעולה B היא פעולה מורכבת שהרצתה לוקחת לארדואינו שתי אלפיות השניה, יווצר לנו עיכוב של 8 אלפיות השניה מדי שניה – ואם הפרויקט שלנו צריך לדייק בזמנים, העיכוב הזה יצטבר ויפריע מאד. מוטב להיפטר לגמרי מפקודת ה-delay!

שיטה 2: הפרשים

סביבת הפיתוח של הארדואינו מספקת לנו את הפונקציה השימושית millis, שאומרת כמה זמן עבר (במילישניות) מאז האתחול האחרון של הארדואינו. הערך שמוחזר הוא מטיפוס unsigned long, שגודלו 4 בייטים והוא מסוגל לייצג ערכים בין אפס לארבעה מיליארד ורבע בערך (בסביבות חמישים יום, כך שאם התוכנה שלכם אמורה לרוץ חודשים שלמים ברציפות, קחו את זה בחשבון).

בשיטת ההפרשים – וזה לא שם רשמי, רק כינוי שהמצאתי לצורך העניין – אנחנו שומרים את זמן הביצוע האחרון של הפעולה, ובודקים שוב ושוב את הפרש הזמן בינו לבין הזמן הנוכחי. אם הפרש זה גדול או שווה להפרש שהגדרנו מראש, נבצע את הפעולה ונעדכן את זמן הביצוע האחרון. כך נראית החלפת מצב של LED פעם בשניה, בשיטת ההפרשים:

#define MY_DELAY 1000
boolean LEDState = false;
unsigned long lastMillis;

void setup() {
  pinMode(13, OUTPUT);
  lastMillis = millis();
}

void loop() {

  if (millis() - lastMillis >=  MY_DELAY) {
    digitalWrite(13, LEDState);
    LEDState = !LEDState;
    lastMillis = millis();
  }   

}

אנחנו יכולים להגדיר משתנה כמו lastMillis עבור כל פעולה שנרצה, ולהוסיף פקודות if בהתאם. כך נוכל להריץ מגוון פעולות שונות בכל הפרש זמן שמתחשק, בלי להסתבך עם כפולות, ומה שטוב עוד יותר – מכיוון שהערך שמוחזר על ידי millis אינו תלוי בפעילויות האחרות של הארדואינו, הדיוק בזמנים יהיה גבוה ולא יצטברו עיכובים.

שימו לב לנקודה חשובה: ההשוואה של הפרש הזמנים לערך שנקבע מראש חייבת להיות באמצעות סימן "=<" (גדול או שווה) ולא באמצעות "==" (שווה). למה? נניח שפעולה A צריכה להתבצע פעם בשניה, ולגמרי במקרה זה יוצא בדיוק כש-millis מתחלקת בלי שארית ב-1000. נניח גם שפעולה B התחילה להתבצע כש-millis החזירה ערך 999, ונמשכה 3 אלפיות השניה. כמו שהזכרתי קודם, הארדואינו אינו מסוגל לבצע ריבוי משימות אמיתי, ולכן בדיקת ה-if הבאה עבור משימה A תתבצע רק כש-millis תחזיר 1002. אם נבדוק באמצעות "==", נקבל אי-שוויון ומשימה A תתפספס לגמרי! לעומת זאת, אם נבדוק בעזרת "=<", A תתבצע בהקדם האפשרי, ברגע שהארדואינו יסיים לבצע את B.

חשוב מאד לציין שכל הפוסט הזה מתעלם בכוונה מהנושא של פסיקות, אשר יכול לעזור לנו במקרים מסוימים בריבוי משימות – אבל הוא מורכב יותר.


נ.ב. הערה אישית לספאמרים הארורים מחו"ל: אתם חושבים שאתם כאלה חכמים, אה? יש לי עוד כמה טריקים בשרוול בשבילכם…

להרשמה
הודע לי על
12 Comments
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

אני חושב שהפוסט הזה מראה פתרון "מקומי" ומאולתר לבעיה גדולה ו
"מפספס" את פתרון האמיתי – שהוא scheduler , שמאפשר הרצה של תהליכים או חוטים (threads) ב"מקביל".

לארדואינו נכתבה תמיכה כזאת בדמות ספריות כמו Protothread ו mthread. בנוסף יש מערכות הפעלה אמיתית לארדואינו כגון DuinOS ,ardOS שעיקר השירות שהם נותנות הוא מולטי טאסקינג .

יש שיטה דיי נחמדה למולטיטסקינג שנקראת PROTO-THREADS.
מה שנחמד בשיטה הזו היא שהיא מבוססת כולה על שפת C ולא על פסיקות וכו'.

אפשר לקרוא על זה כאן:
http://dunkels.com/adam/pt/publications.html
(מומלץ להתחיל במצגת למטה)

אזהרה קלה – למרות שהשיטה דיי פשוטה, היא לא כל כך קלה להבנה בגלל המעשים המגונים שהיא עושה בשפת C (למרות שהכל עדיין לפי התקן) , וגם בגלל שהיא לא נפוצה ואין הרבה הסברים טובים באינטרנט.

שם שדה זה נחמד, בעיקר כשמתמודדים עם מישהו שלא מסתכל על הטקסט שכתוב ליד הTextBox (ותשאל את Facebook כמה זה יעיל) הפתרון הטוב ביותר הוא קאפאצ'ה בשילוב חסימת כמויות של הודעות ברצף.
יש לי חבר שבנה קאפאצ'ה בעברית, קלה למילוי ולא מעצבנת בכלל. מעניין אותך?

לא שמתי לב ללינק ה"להגיב".
נדבר בפרטי

אני מכיר אותם, מהר מאוד הם יתגברו על זה.
מספיק פעם אחת שבה מישהו אנושי מסתכל על זה והם משנים את ההגדרה. הדברים האלה דינמיים מאוד.

צריך עזרה במלחמה בספאמרים?
יש לי ידע מסוים בנושא שיכול לעזור.
צור איתי קשר

מדריך מצויין! תודה רבה !!
אני מת כבר שיגיע הארדואינו שלי ואני יתחיל באמת לנסות דברים…