נא להפריע: פסיקות (Interrupts) בארדואינו

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

בפוסט זה ניצור Interrupt מהסוג הבסיסי ביותר, ונראה מה היתרונות שלו לעומת בדיקה חוזרת ונשנית בפונקציית ה-loop של הארדואינו. מוכנים?

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

void setup() {
   pinMode(13, OUTPUT);
   digitalWrite(13, LOW);
}

void loop() {
  delay(1000);
  digitalWrite(13, HIGH);
  delay(5); // So we can see the blink
  digitalWrite(13, LOW);
}
Interrupt Demo Setup
חיבור לוחות הארדואינו לצורך הפוסט. הלוח הימני יוצר את הסיגנל פעם בשניה, והשמאלי מונה לולאות וכותב את התוצאות לחיבור הטורי. שתי נקודות למי שיודע למה יש שני חוטים ביניהם, במקום רק אחד

הדרך הפרימיטיבית

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

unsigned long counter = 0;

void setup() {
   pinMode(2, INPUT);
   Serial.begin(9600);
}

void loop() {
  counter++;
  // Check if a signal arrived
  if (digitalRead(2) == HIGH) {
    Serial.println(counter);
    counter = 0;
    // Now wait until the signal ends...
    while (digitalRead(2) == HIGH);
  }
}

זה עובד, ובניסויי ההרצה שערכתי מתקבל מדי שניה, במוניטור של החיבור הטורי, ערך של 128812 או 128813. עקביות מרשימה! פירוש הדבר שלמעט אותם מקרים נדירים בהם מופיע סיגנל, הלולאה הראשית של התוכנית הזו רצה מאה עשרים ושמונה אלף, שמונה-מאות ושתים-עשרה (או שלוש-עשרה) פעמים בשניה. כל ריצה כזו כוללת את הגדלת המונה, את פעולת הקריאה מחיבור מס' 2, ואת הבדיקה אם תוצאת הקריאה היא HIGH. לפני שנמשיך, סתם בשביל הכיף, בואו נראה מה קורה אם נוציא את הטיפול בסיגנל לפונקציה חיצונית:

void report() {
    Serial.println(counter);
    counter = 0;
    // Now wait until the signal ends...
    while (digitalRead(2) == HIGH);
}

void loop() {
  // ...
  if (digitalRead(2) == HIGH) report();
}

התוצאה? בדיוק אותו דבר…

הדרך החכמה

ועכשיו נדבר על פסיקות, כשאנו מתמקדים כאן אך ורק בסוג הפשוט ביותר – פסיקות חומרה. בלוחות הארדואינו הרגילים, Arduino Duemilanove/Uno, חיבורים דיגיטליים מס' 2 ו-3 יכולים לשמש לפסיקות חומרה, שמתעוררות "מעצמן" כשהמתח בחיבור עולה, יורד או משתנה. אנחנו יכולים להצמיד לכל אחד משני החיבורים פונקציה משלנו לטיפול בפסיקה שלו. הדבר מתבצע באמצעות פונקציה שנקראת attachInterrupt, אשר מקבלת שלושה פרמטרים: מספר הפסיקה (0 עבור חיבור מס' 2 או 1 עבור חיבור מס' 3, סתם כדי לבלבל אותנו כנראה), שם הפונקציה שכתבנו במטרה לטפל בפסיקה, וסוג הסיגנל שיעורר את הפסיקה. פרמטר סוג הסיגנל יכול לקבל אחד מארבעה ערכים שונים: LOW, CHANGE, RISING, FALLING. אין צורך להסביר מה הם אומרים, נכון?

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

בקיצור, הנה הקוד החדש:

volatile unsigned long counter = 0;

void setup() {
   pinMode(2, INPUT);
   Serial.begin(9600);
   attachInterrupt(0, report, RISING);
}

void report() {
    Serial.println(counter);
    counter = 0;
}

void loop() {
  counter++;
}

למי שהתרגל לעבוד בדרך הישנה, הקוד הזה נראה משונה ביותר – אין שום פלט או גישה לפלט בלולאה הראשית! אבל הפונקציה report מוצמדת לפסיקת החומרה RISING בחיבור דיגיטלי מס' 2, ולכן בכל פעם שהמתח שם עולה, הארדואינו עוצר הכל ורץ לעשות מה שכתוב בה. שימו לב גם שלא היה צורך להמתין לירידת הסיגנל בחזרה, כי הפסיקה מטפלת בזה: היא מתעוררת רק כשהוא עובר ממצב נמוך לגבוה, לא כשהוא נשאר גבוה או חוזר בחזרה לנמוך.

ומה בתכל'ס? מה המספר שמדווח מדי שניה עם שימוש בפסיקות? מסיבות שאני לא לגמרי מבין עדיין, התוצאות קצת פחות סדירות – אך המספר הנפוץ ביותר, שהוא גם הנמוך ביותר שמופיע לי כבר דקות ארוכות, הוא 258768. כמעט בדיוק פי שניים!

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

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

למה כתבת את הפקודה digitalWrite(13, LOW); ב VOID SETUP ?
בדרך כלל פקודה כזאת צריך ב – LOOP ?

לפי מה שאני יודע: ישנן שתי (-או שני) סוגי פסיקות: יש פסיקות חומרה ויש פסיקות תוכנה: פסיקות חומרה: מתבצעות ללא כל קשר לתוכנה, והבקר לא מבזבז זמן כדי לבדוק האם היו כאלו. פסיקות תוכנה: כל פעם מתבצעת בדיקה האם יש תנאי לביצוע הפסיקה, אם כן מתבצעת אם לא, אז לא 🙂 לדוגמה פסיקת חומרה: אם תקצר את האנדרואיד שלך הוא ישר מכבה את עצמו ולא חשוב מה ולמה זו מן הסתם הגנה הגיונית למניעת נזקים (לא לנסות אומנם עובד אבל עדין…) או התחממות, אני לא יודע כאן אבל במחשב הביתי אם הוא מתחמם יותר מידי הוא מכבה את עצמו אוטומטית… לקרוא עוד »

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

אני לא יכול לסיים מבלי לשבח אותך על אתר מעולה ++
תודה רבה

מעניין לדעת מה ה'עלות' של ה volatile .
אם התדר של המעבד הוא 16MHZ אז תאורתית אפשר לקבל 16,000,000=COUNTER אחרי שניה.
אם קיבלת רק ~260,000, זה אומר שכל COUNTER++ לוקח 61 מחזורי שעון

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

1) מסכים

2) כן ולא 🙂
אפשר לעשות LOOP UNROLLING

(while(1
{
counter++;
" "
counter++
}

בשיטה הזו יש לנו הרבה ++counter על כל JMP וה'ביזבוז' של ה JMP יורד בהרבה כך שאפשר להגיע קרוב מאוד ל ++ בכל מחזור שעון.

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

(אם התגובה הזו חפרנית מדי לטעמך – תרגיש חופשי לא לפרסם אותה)

מצויין שהרחבת על הנושא הזה. מסייע מאוד. תודה. (וחג שני שמח…)