עקיצה ב-6 שניות

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

כשכתבתי את הקוד לשעון הקוּקוּ שלי, שמבוסס על המיקרו-בקר הצנוע ATtiny13A, הגדרתי פסיקת טיימר שתרוץ פעם באלפית שניה בדיוק. באופן שמזכיר קצת את השימוש בפונקציה millis של ארדואינו, פונקציית הפסיקה שלי פשוט מוסיפה 1 למשתנה גלובלי בשם millis שמייצג את אלפיות השנייה שחלפו מאז שהקוד התחיל לרוץ. כיוון שמדובר במשתנה שגם הקוד הראשי וגם קוד הפסיקה ניגשים אליו, זכרתי כמובן להגדיר אותו כ-volatile. הנה קטעי הקוד הרלוונטיים:

volatile uint16_t millis = 0;

/...
// The timer interrupt function
ISR(TIM0_COMPA_vect) {
  ++millis;
}

כדי למדוד את הזמן בין תנועה אחת של מחוג השניות לבאה אחריה, כתבתי משהו דומה לזה:

void resetTimer(void) {

  // Disable interrupts
  cli();
  // Reset hardware timer counter
  TCNT0 = 0; 
  millis = 0;
  // Enable interrupts
  sei();

}

// In the main loop:

//...

// Wait exactly 1 second
resetTimer();
while (millis < 1000) {}

//...

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

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

התחלתי לשחק עם ערכים שונים, כדי לנסות לגלות דפוסים שיעזרו באיתור הבאג. החלפתי את המספר 1000 ב-2000, ואז כל תקתוק שני התקצר ל-1.7 שניות במקום 2 שניות. ערכים אחרים נתנו תוצאות אחרות ולא-סדירות לכאורה, פרט לכך שערכים מתחת ל-256 דווקא הפיקו תקתוקים מדויקים. זה אמור היה להיות רמז מספיק גדול לפתרון, אבל אני כבר הייתי בשוונג של שינויים בקוד ולכן ניסיתי עוד משהו: החלפתי את הבדיקה ">" (קטן מ-) בבדיקת אי-שוויון:

while (millis != 1000) {}

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

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

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

הנה הערך העשרוני 1000 בייצוג בינארי, כשהוא מופרד לשני הבייטים שמרכיבים אותו.

00000011:11101000

נניח שהערך (הנכון) של millis כרגע הוא 767, או בבינארי,

00000010:11111111

הקוד שלי הגיע לתנאי הלולאה, ושם צריך לקרוא את הערך הזה. הקריאה מתחילה מהבייט הנמוך יותר. אז הוא קורא 11111111 לתוך אוגר זמני… ובדיוק באותו רגע הפסיקה מתעוררת. בקוד הפסיקה, millis מקבל את הערך החדש 768:

00000011:00000000

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

00000011:11111111

שהוא 1023 בבסיס עשרוני – גבוה יותר מ-1000 שבתנאי הלולאה, אף על פי שהערך האמתי של millis הוא רק 768… והנה התנדפה לה רבע השנייה המסתורית מקודם!

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

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

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *