הלו טייני #6: כלב טוב!

אנחנו ממשיכים בסקירה של מנגנון ה-Watchdog הרב-שימושי של הטייני, והפעם אסביר איך לכבות אותו, איך לגלות אם הוא פעל – וגם איך ולמה להשתמש בפונקציונליות הפסיקה (interrupt) המובנית שלו.

כיבוי ה-WD והסיבוך של WDRF

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

כזכור, כדי להפעיל את ה-WD היינו צריכים בעיקר לכתוב "1" לביט 3 של הרגיסטר WDTCR. אלא שכדי לכבות אותו, ממש לא מספיק לכתוב שם "0". כפי שהסברתי, מנגנון ה-WD חשוב מכדי שאפשר יהיה לנטרל אותו כל כך בקלות – כי דבר כזה עלול לקרות גם בטעות. במקום זאת, הכיבוי נעשה ברצף של שלושה שלבים, שאין כמעט סיכוי שיקרו בלי כוונה (אלא אם המתכנת רשלני ביותר).

  1. לכתוב "0" לביט WDRF (ביט 3) ברגיסטר MCUSR; ביט זה מעיד על איפוס קודם שבוצע על ידי ה-WD, ואם במקרה ערכו "1", הוא למעשה עוקף את WDE וגורם לנו צרות. אז ליתר ביטחון, נאפס אותו.
  2. לכתוב "1" בו-זמנית לביט WDE וגם לביט WDCE (ביט 4, "Change Enable")
  3. בזריזות, לפני שעוברים ארבעה מחזורי שעון, לכתוב "0" לביט WDE.
פירוט הביטים ברגיסטר WDTCR
פירוט הביטים ברגיסטר WDTCR (הסברים נוספים בהמשך)

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

#define F_CPU 1000000

#include <avr/io.h>
#include <util/delay.h>

int main(void) {

  uint8_t WDkill = 10;

  DDRB  = 1; // PB0 is Output
  PORTB = 2; // Internal Pull-up on PB1
  // Enable watchdog for reset, 8s
  WDTCR = 1 << WDE | 1 << WDP3 | 1 << WDP0;

  while(1) {

    asm volatile ("WDR"); // Reset WD timer
    PINB = 1; // Toggle PB0 output

    if (WDkill) {
      WDkill--;
      if (!WDkill) {
        // Watchdog disable sequence
	MCUSR &= ~(1 << WDRF);
	WDTCR = (1 << WDCE) | (1 << WDE);
	WDTCR = 0;	
      } // if
    } // if

    _delay_ms(500);
    if (!(PINB & 2)) while (1) ;

  } // while

} // main

כל עוד נלחץ על הלחצן תוך 10 הדלקות/כיבויים מרגע ההפעלה או האתחול האחרון, כפי שהם נספרים במשתנה WDkill, ה-WD עדיין יפעל ויחלץ אותנו מהלולאה האינסופית. אם נלחץ מאוחר יותר, ה-WD כבר יהיה מושבת והתוכנה תיתקע לגמרי.

אז מה הסיפור הזה עם WDRF?

הצורך לאפס את הביט WDRF של MCUSR צנח עלינו די בהפתעה, והקשר שלו לתפקוד של ה-WD לא מובן מאליו. כדי להבין אותו, נתרחק לרגע ונסתכל על התמונה הכוללת.

קודם כל, נניח שבנינו מערכת עם WD והשארנו אותה בלי השגחה יומיים. חזרנו ונראה שהיא פועלת… אבל האם היא באמת עבדה ברציפות, או שמא אותחלה בזמן הזה 5,000 פעם על ידי ה-WD? בכל פעם שה-WD מבצע אתחול, הביט WDRF מקבל את הערך "1", וזה מאפשר לנו (אם נרצה) לכתוב קוד שיידע לתת מענה למצבים כאלה.

מעבר לעניין הזה, יש גם את עניין הבטיחות של כל מנגנון ה-WD. ברגע שה-WD ביצע אתחול, סימן שמשהו רע מאד קרה, וגם אם אני כמתכנת שכחתי להתייחס לזה במפורש, קטסטרופה דומה עלולה לקרות שוב ולדפוק את כל המערכת. לכן, כשביט WDRF ברגיסטר MCUSR דולק, ה-WD מופעל ושומר מפני היתקעויות אפילו אם ביט WDE כבוי. על כן, ההמלצה הרשמית היא לאפס את WDRF לא רק בזמן השבתת ה-WD, אלא למעשה בתחילת כל קוד!

אגב, הביטים ברגיסטר MCUSR נועדו לספק מידע על גורם האתחול האחרון. זה יכול להיות WD, חיבור לחשמל של מערכת שהיתה מנותקת (הביט PORF), איפוס בעקבות ירידת מתח אל מעבר למינימום ("Brown out" – הביט BORF) או, כמובן, הורדת המתח בפין ה-Reset של המיקרו-בקר (הביט EXTRF).

כלב נובח אינו מאתחל

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

אנחנו אומרים ל-WD להריץ פסיקה באמצעות כתיבת "1" לביט WDIE (קיצור של Watchdog Interrupt Enable) ברגיסטר WDTCR. זה יפעל גם אם ניגע וגם אם לא ניגע ב-WDE, אבל אם ה-WDE כן יהיה דלוק, אז מטעמי בטיחות המיקרו-בקר יאפס את הביט WDIE אחרי שהפסיקה תרוץ, כדי להבטיח אתחול בפעם הבאה. אז כדי למנוע מצב זה, נצטרך לחזור על הכתיבה ל-WDIE כמה שיותר מהר – עדיף בתוך קוד הפסיקה עצמה.

את קוד הפסיקה נכתוב בפונקציית שירות לפסיקה – ISR – Interrupt Service Routine. המהדר (קומפיילר) יזהה אותה בזכות מילת המפתח ISR ואחריה, בסוגריים, שם הפסיקה הרלוונטית – שהוא WDT_vect. מילות המפתח האלה מוגדרות בספריה avr/interrupt.h שחייבת להיכלל בתוכנית.

איך כל זה נראה בפועל? הנה קוד Blink שמשתמש בפסיקת WD כדי להבהב, במקביל, בנורית LED שניה. שתי הנוריות אמורות לשנות מצב פעם בחצי שניה, וזה מאפשר לנו להיווכח גם בחוסר הדיוק של טיימר ה-WD לעומת הטיימר הרגיל שמשמש ל-delay. שימו לב לפקודה sei – זו הפונקציה שמאפשרת לפסיקות לפעול, ובלעדיה העסק פשוט לא יעבוד!

#define F_CPU 1000000

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

int main(void) {

  MCUSR = 0;
  DDRB  = 0x11; // PB0,4 are Output
  // Enable watchdog for interrupt, 0.5s
  WDTCR = 1 << WDIE | 1 << WDP2 | 1 << WDP0;
  // Enable interrupts
  sei();

  while(1) {
    PINB = 0x01; // Toggle PB0 output
    _delay_ms(500);
  } // while

} // main

ISR(WDT_vect) {
  PINB = 0x10; // Toggle PB4 output
}

הנה סרטון שמדגים את הקוד הזה בפעולה:

בהמשך נדבר על שימוש מוצלח עוד יותר לפסיקה הזו: חיסכון דרסטי בחשמל עבור מערכות שעובדות על סוללות, בעזרת מצבי שינה!

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

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