פסיקה (או בשמה המוכר 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); }
הדרך הפרימיטיבית
נעבור לתוכנית הראשונה, שבודקת בלולאה הראשית אם הגיע סיגנל. אנחנו צריכים משתנה שישמש לנו מונה, וחשוב שהוא יהיה מטיפוס שמסוגל להכיל ערכים גדולים, כי המספרים יצטברו במהירות. כמו כן, אנחנו צריכים לבדוק בכל "סיבוב" אם הגיע קלט שמעיד שחלפה שניה, ואם כן – לכתוב את הערך. ככה נראה קוד התוכנית:
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, ועלולה להפריע גם לקריאה של מידע מחיבור טורי. למרות זאת, מדובר בכלי עבודה בסיסי, פשוט, יעיל ורב עוצמה ביותר.
למה כתבת את הפקודה digitalWrite(13, LOW); ב VOID SETUP ?
בדרך כלל פקודה כזאת צריך ב – LOOP ?
פקודה צריכה להופיע איפה שצריך אותה, זה לא קשור ל-setup או loop.
במקרה הזה שמתי אותה כדי להבטיח ערך התחלתי LOW בפין 13. ייתכן שהפקודה מיותרת כי LOW אמור להיות ברירת המחדל, אבל כשעובדים עם כל מיני מיקרו-בקרים וסביבות פיתוח, עדיף להתרגל לציין דברים כאלה במפורש ולא לסמוך על המזל 🙂
לפי מה שאני יודע: ישנן שתי (-או שני) סוגי פסיקות: יש פסיקות חומרה ויש פסיקות תוכנה: פסיקות חומרה: מתבצעות ללא כל קשר לתוכנה, והבקר לא מבזבז זמן כדי לבדוק האם היו כאלו. פסיקות תוכנה: כל פעם מתבצעת בדיקה האם יש תנאי לביצוע הפסיקה, אם כן מתבצעת אם לא, אז לא 🙂 לדוגמה פסיקת חומרה: אם תקצר את האנדרואיד שלך הוא ישר מכבה את עצמו ולא חשוב מה ולמה זו מן הסתם הגנה הגיונית למניעת נזקים (לא לנסות אומנם עובד אבל עדין…) או התחממות, אני לא יודע כאן אבל במחשב הביתי אם הוא מתחמם יותר מידי הוא מכבה את עצמו אוטומטית… לקרוא עוד »
כן, ציינתי בפוסט שאני מדבר בינתיים על פסיקות חומרה בלבד – וליתר דיוק, רק על פסיקות חומרה שאנחנו יכולים להיעזר בהן כמתכנתים. ברוב הפרויקטים, הכיבוי העצמי ההגנתי הוא לא כלי עבודה מי-יודע-מה שימושי… 🙂
ברמה של מיקרו-בקרים, למיטב ידיעתי, "פסיקות תוכנה" הן ספציפיות לטיימרים – כל בדיקה אחרת היא בדיקה רגילה ולא פסיקה.
שאלה קצת אחרת
אולי זה קשור ואולי לא, מעניין אותי לדעת האם ניתן לעשות שימוש בפסיקות בכדי "להעיר" את הארדואינו (אם בכלל הוא יודע ללכת לישון 🙂 ). לצורך הדוגמה אני רוצה שהארדואינו יהיה זמין לבצע משימה מסויימת נגיד להדליק אור למשך שתי דקות
נראה לי שזה בעייתי להשאיר את המעבד עובד בלי סוף במשך חודשים עד אשר מישהו יבקש להדליק את האור הנחשק
מה הדרך הנכונה להשאירו זמין לביצוע משימות ?
אני לא יכול לסיים מבלי לשבח אותך על אתר מעולה ++
תודה רבה
תודה! לשאלתך: קודם כל, למשימות בסיסיות כמו להדליק אור לשתי דקות לא צריך ארדואינו שלם – עם קצת אלקטרוניקה אפשר למצוא פתרונות זולים וחסכוניים הרבה יותר. זה תחום שאני מתכוון לחקור בעתיד הנראה לעין, אבל לא בימים הקרובים. עם זאת, המעבד של הארדואינו בהחלט מסוגל לעבור למצב "שינה" ולצאת ממנו כשמגיעה פסיקה. לא ניסיתי את זה עדיין – מצאתי מידע שנראה רציני כאן: http://www.nongnu.org/avr-libc/user-manual/group__avr__sleep.html עוד נקודה חשובה שצצה בזמן חיפוש המידע היא שלוח הארדואינו השלם כולל מווסת מתח בעל צריכת חשמל משלו, כך שאם אתה מתכוון להפעיל פרויקט בעזרת סוללת כפתור לאורך חודשים, אתה צריך לעבוד ישירות עם המיקרו-בקר ולא… לקרוא עוד »
מעניין לדעת מה ה'עלות' של ה volatile .
אם התדר של המעבד הוא 16MHZ אז תאורתית אפשר לקבל 16,000,000=COUNTER אחרי שניה.
אם קיבלת רק ~260,000, זה אומר שכל COUNTER++ לוקח 61 מחזורי שעון
נראה לי שאפשר לשפר את התוכנית אם נפטרים מה VOLATILE וגורמים לפונקצית האינטרפט להוציא את הערך של COUNTER ישירות מהמחסנית (בקריאה לאינטרפט, רוב/כל הרגיסטרים ישמרו למחסנית).
ה"שיפור" הזה לא ממש 'נחמד' מבחינת C, אבל הוא יתן תוצאה הרבה יותר טובה.
נקודה מעניינת מאד, אם כי לא הייתי הולך דווקא על ה-volatile כחשוד העיקרי. קודם כל, בוא נזכור שמדובר במעבד 8 ביט, והמשתנה הוא מסוג unsigned long, כלומר 32 ביט, כך שגם הקוד הכי אופטימלי לא יוכל להוסיף לו 1 בפקודה יחידה. שנית, קוד האסמבלי המינימלי ללולאה של הוספת 1 לרגיסטר הוא של שתי פקודות (ADD ו-JMP), לא אחת. שלישית, ברור שהיכולת לזהות פסיקות היא לא קסם. אם הבנתי נכון מה שקראתי, המעבד מבצע לסירוגין בדיקה של מצב הכניסות לצורך הפסיקה והרצה של פקודה מהתוכנה ה"רגילה", וברור שיש לזה עלות בזמן שעון. החיסכון הושג מפני שהבדיקה הזו מתבצעת בכל מקרה, וכאן… לקרוא עוד »
1) מסכים
2) כן ולא 🙂
אפשר לעשות LOOP UNROLLING
(while(1
{
counter++;
" "
counter++
}
בשיטה הזו יש לנו הרבה ++counter על כל JMP וה'ביזבוז' של ה JMP יורד בהרבה כך שאפשר להגיע קרוב מאוד ל ++ בכל מחזור שעון.
3) אני לא מכיר את המעבד הספציפי הזה, אבל במעבדים רגילים (אני מאמין שגם פה) זיהוי הפסיקה מתבצע בחומרה. כל עוד אין פסיקה, המעבד לא מבזבז עליה מחזורי שעון.
(אם התגובה הזו חפרנית מדי לטעמך – תרגיש חופשי לא לפרסם אותה)
מה פתאום חפרנית? זה נושא חשוב ומעניין לא פחות מהפסיקות עצמן…
את הבדיקה של ה-unrolling אני אנסה לבצע בהקדם האפשרי – אוסיף עוד counter++ ללולאה הראשית, וככה נראה בדיוק כמה מחזורי שעון נוספים זה לוקח.
בנוגע לזיהוי הפסיקה, אני באמת לא יודע מה בדיוק הולך שם בפנים – זה שווה בדיקה נפרדת. ברפרוף ראשוני לא מצאתי מידע רלוונטי במפרט הטכני של ה-Atmega328, אני אמשיך לחפש.
מצויין שהרחבת על הנושא הזה. מסייע מאוד. תודה. (וחג שני שמח…)