הלו טייני #5: לאלף את הכלב

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

התפקיד הקלאסי של ה-WD

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

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

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

הפעלת ה-WD

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

המערכת שנבנה היא מערכת Blink משולבת עם לחצן ו"באג" מתוכנן בקוד: ברגע שלוחצים על הלחצן, התוכנה נכנסת ללולאה אינסופית. נחבר לפין PB0 (פין מס' 5) נורית LED עם נגד מתאים, ול-PB1 (פין 6) נחבר את הלחצן, שצדו השני מחובר לאדמה (הקוד יפעיל את נגד ה-Pull-up הפנימי). הנה הקוד הראשוני, עבור טייני עם הגדרות שעון שתואמות ל-1MHz:

#define F_CPU 1000000

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

int main(void) {
  DDRB  = 1; // PB0 is Output
  PORTB = 2; // Internal Pull-up on PB1

  while(1) {
    PINB = 1; // Toggle PB0 output
    _delay_ms(500);
    if (!(PINB & 2)) while (1) ;
  }
}

ה-LED מהבהב כצפוי, ואם הלחצן לחוץ כאשר ה-delay מסתיים, הלולאה האינסופית מתחילה וה-LED נשאר לצמיתות באותו מצב.

מערכת בסיסית להדגמת תפקוד ה-Watchdog

מערכת בסיסית להדגמת תפקוד ה-Watchdog

כדי להכניס לפעולה את ה-WD, עלינו לכתוב את הערכים המתאימים לרגיסטר הבקרה שלו, WDTCR. כרגע מעניינים אותנו רק 5 ביטים מתוך השמונה שבו: ביט מס' 3 (זיכרו שהספירה מתחילה מ-0) שנקרא WDE, והביטים 5-2-1-0 שנקראים WDP0 עד WDP3. כתיבה של 1 ל-WDE מפעילה את ה-Watchdog, ואילו על ארבעת הביטים של WDP נכתוב מספר בינארי בין 0 ל-9. אם נחשב 2 בחזקת [המספר הזה + 1], נכפיל באלף ונחלק בתדר של טיימר ה-WD שהוא כזכור 128KHz (בלי קשר למהירות השעון של הטייני!), נקבל את משך הזמן, בשניות, של הספירה לאחור עד לאתחול.

פירוט הביטים לשימוש בסיסי ברגיסטר WDTCR
פירוט הביטים לשימוש בסיסי ברגיסטר WDTCR

לדוגמה, אם נכתוב ל-WDTCR את הערך הבינארי 00101001, זה יפעיל את ה-WD עם ערך השהיה 9. שתיים בחזקת תשע-ועוד-אחד זה 1024, כפול אלף וחלקי 128,000 נותן 8 => השהיה של כ-8 שניות, המקסימום האפשרי בטייני.

כמובן, אם נכתוב רק את הפקודה הזו, הטייני שלנו יתאפס בכל 8 שניות בלי קשר למה שקורה בקוד, וזה לא טוב. אנחנו רוצים שזה יקרה רק כשהוא תקוע בלולאה האינסופית, ולכן נשים מחוץ לה פקודה לאיפוס הטיימר של WD. לשם כך, מסתבר, לא נוגעים בשום רגיסטר. הסיבה, כנראה, היא שהרגיסטרים נמצאים למעשה במרחב הזיכרון של הטייני, ולכן מצביע תועה בקוד עלול לשבש אותם, ואנחנו הרי לא רוצים שתקלת קוד תוכל – אפילו בתיאוריה – לפגוע בתפקוד של ה-WD שנועד לשמור עלינו בדיוק מדברים כאלה… בקיצור, עלינו להפעיל פקודת אסמבלי ייעודית בשם WDR. כדי לכתוב פקודת אסמבלי באמצע קוד C עלינו להשתמש במילת המפתח asm, וברוב המקרים, כדי למנוע מהקומפיילר להתחכם ולעשות אופטימיזציות לא רצויות, נרצה להוסיף אחריה גם את מילת המפתח volatile.

כל זה מופיע בשתי שורות חדשות בלבד בפונקציית ה-main שהוצגה למעלה. הנה הקוד החדש:

int main(void) {
  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");
    PINB = 1; // Toggle PB0 output
    _delay_ms(500);
    if (!(PINB & 2)) while (1) ;
  }
}

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

מי שלא רוצה להתעסק עם פקודות אסמבלי או לחשב ערכים בינאריים בראש יכול להשתמש בפקודות מהספריה avr/wdt.h . חוסר החיבה שלי לספריות ידוע ומוכר, אך במקרה הספציפי הזה ייתכן שזהו דווקא הפתרון המומלץ, שכן הפקודות wdt_reset, wdt_enable ואחרות הן בסך הכל מאקרו, עטיפות נוחות ומסודרות של הדברים שביצענו "ידנית" קודם, ואין סכנה של התנגשויות או בלגנים אחרים שמאפיינים את ספריות הארדואינו.

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

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

תודה רבה פוסט מצויין
אתה יכול להרחיב בבקשה על התפקיד התחבירי של >> בשורה:
WDTCR = 1 << WDE | 1 << WDP3 | 1 << WDP0;
ראיתי את התחביר הזה בכמה מקומות ולא ממש הבנתי מה זה אומר