באג שקרה לי לא מזמן (ועוד בפרויקט חשוב!) חזר והדגיש את החשיבות של היכרות מעמיקה עם כלל המרכיבים של המערכת – כולל מוצרי מדף שיושבים מחוץ למיקרו-בקר.
ראשית כל, הבהרה: כדי שהפוסט יהיה קצר וברור ככל האפשר השמטתי ופישטתי הרבה פרטים על הפרויקט והתהליך, והשארתי רק את מה שחיוני להבנת הפואנטה.
ובכן, הפרויקט התבסס על שני אלמנטים חומרתיים: מיקרו-בקר, ומודול קנוי שמנגן קובצי mp3 מכרטיס זיכרון Micro SD. המודול מקבל את הפקודות ("נגן קובץ מס' X מהכרטיס", "הפסק לנגן" וכו') בתקשורת UART מהמיקרו-בקר, ויש לו – למודול – גם פין פלט בשם "Busy" שמוציא מתח כל עוד המודול עסוק בניגון של קובץ.
אגב, המודול הזה לא יודע לזהות תיקיות, שמות או תאריכים של קבצים: הוא מסוגל לאתר אותם רק לפי הסדר שבו נכתבו על הכרטיס מלכתחילה (ייתכן שזהו הסדר בו הם מופיעים ב-FAT של הכרטיס, מה שמקל מאוד על השליפה שלהם, אבל זה רק ניחוש שלי).
בכל אופן, הקבצים שעל הכרטיס מתחלקים בפרויקט דנן לשתי קבוצות, נקרא להן A ו-B. בתסריט שימוש רגיל המשתמש מבצע איזו פעולה, ואז המיקרו-בקר משמיע קובץ מקבוצה A ומיד אחריו קובץ מקבוצה B. אם המשתמש מבצע פעולה אחרת בזמן שקובץ מ-A מתנגן, הנגינה נפסקת לגמרי. אבל אם אותה פעולה אחרת מתבצעת בזמן שמתנגן קובץ מ-B, לא קורה כלום: הקובץ ממשיך עד הסוף.
המיקרו-בקר מסתכל כל הזמן על המתח בפין Busy של המודול כדי לדעת מתי הוא גמר לנגן, ויש כאן קאץ' קטן: המתח לא עולה מיד כשהפקודה לנגן נשלחת, אלא רק אחרי שהמודול התחיל לנגן בפועל. זהו פרק זמן קצרצר במונחים שלנו, אבל למיקרו-בקר זה הרבה. הוא עלול לבדוק אם יש מתח, לראות שאין (עדיין אין!) ולחשוב שהמודול כבר גמר לנגן. הוספתי השהייה מסוימת, בהתבסס על קצת ניסוי וטעייה, וליתר ביטחון הגבלתי את ההשהייה הזו לפרק הזמן המכובד של 100 אלפיות השנייה: אם המודול לא הגיב עד אז, כנראה שיש איזו בעיה ולא כדאי שהמערכת תיתקע בלולאת המתנה אינסופית. בדקתי את המערכת עם הקוד הזה ועם הכרטיס וקובצי ה-mp3 שקיבלתי לניסוי, והכול עבד כמו שצריך.
מסרתי אותה ללקוח, וקיבלתי אותה במהרה בחזרה עם דיווח על באג: מדי פעם היא פשוט מדלגת על הקובץ מ-B. אחד הדברים הראשונים שצריך לעשות בדיבוג (זוכרים?) הוא לשחזר את הבאג. את זה, למרבה הפדיחה, הצלחתי לעשות כמעט מיד. אבל עשיתי עוד משהו שהלקוח לא עשה: בדקתי בתשומת לב איך משפיעות כל הפעולות השונות שהמשתמש יכול לעשות, ואז גיליתי משהו מעניין: המערכת לא באמת דילגה לגמרי על קובץ מ-B, אלא ניגנה קובץ מ-A כאילו הוא היה מ-B.
זה היה די ביזארי, כי בחנתי שוב את הקוד שלי מקרוב ולא ראיתי שום בדל אפשרות שדבר כזה יקרה – אלא אם הקומפיילר עצמו פישל באיזו נקודה, והסבירות לזה נמוכה ביותר. אבל כמובן, אנחנו נוטים להיות שופטים די גרועים של הקוד של עצמנו, ובסופו של דבר מה שראיתי או לא ראיתי בקוד לא משנה. צריך לשבת ולהמשיך לבדוק.
החומרה ומאפייני התזמון של המערכת הקשו מאוד על דיבוג עמוק (עם נקודות עצירה וכדומה), אז הגדרתי מספר פיני פלט כך שהמתח בהם ישתנה ויעזור לי לראות לפחות באיזה קטע קוד המערכת נמצאת בכל שלב. ברמה הזאת הכול עדיין נראה הגיוני ונכון… חוץ מזה שקובץ מ-A עדיין התנגן לפעמים במקום קובץ מ-B.
גם כשצותתי לתקשורת ה-UART בין המיקרו-בקר למודול, התוכן של ההודעות המשודרות היה תקין. אבל בזווית העין נדמה היה לי פתאום שיש פרק זמן מעורר חשד בין שתי פקודות. מדדתי ביתר דיוק: הוא נמשך 100 אלפיות השנייה. באותו רגע הכול התחבר: נדרשו למודול יותר ממאה אלפיות השנייה מרגע קבלת הפקודה ועד להרמת המתח בפין Busy. הקומפיילר לא עשה שום טעות. הקוד שלי המתין במשך פרק הזמן הזה, ראה ששום דבר לא קורה וחזר לנקודת ההתחלה, אבל בינתיים המודול הצליח סוף כל סוף להבין מה רוצים ממנו, התחיל לנגן את הקובץ מהסיבוב הקודם וכל התזמונים של התוכנה הלכו לעזאזל. שיניתי את פרק הזמן להמתנה מ-100 ל-1000 אלפיות שנייה, והמערכת חזרה לתפקד כמו גדולה (טוב, לפחות עד שיתגלה הבאג הבא…)
נשאלת השאלה, כמובן, למה הבאג לא הופיע קודם לכן, כשבדקתי את המערכת עם קובצי הניסוי. אין לי תשובה חד משמעית לזה, אך אני יודע שבמערכת הסופית היו יותר קבצים על כרטיס הזיכרון מאשר במערכת הניסוי. אם הגישה של המודול לקבצים היא אכן סדרתית ואיטית מספיק, ייתכן ש-100 אלפיות שנייה פשוט לא מספיקות כדי להגיע לקבצים שנמצאים רחוק יותר במורד הרשימה.
אחרי שהסיפור הזה נגמר חזרתי למפרט הטכני של המודול. בפעם הקודמת קראתי אותו די ברפרוף, בעיקר כדי להעתיק את הפורמט הנכון של הפקודות השונות (זו לא הייתה הפעם הראשונה שעבדתי עם מודול כזה). הפעם חיפשתי במיוחד מידע על השהיות, ואמנם, בתחתית של אחת הטבלאות נכתב שה-UART Response Time הטיפוסי הוא 128 אלפיות השנייה. אבל מה זה אומר בדיוק Response time – הזמן עד ביצוע הפקודה? או אולי הזמן עד לתשובה החוזרת ה-UART (שביישום הנ"ל התעלמתי ממנה כי לא היה בה צורך)? ולמה לא צוינו ערך מינימלי וערך מקסימלי? הרי אילו 128ms היה ערך מדויק וקבוע, הבאג היה מתגלה כבר בשלבי הניסוי המוקדמים שלי.
כל רכיב חומרה במערכת הוא עולם בפני עצמו, וצריך להכיר את המגבלות שלו ולהתחשב בהן בעת התכנון והתכנות. כשמדובר במוצרי מדף, ובמיוחד כשהם נפוצים ומוכרים, קל לטעות ולחשוב שהוראות ההפעלה הרגילות מספיקות כדי לעבוד איתם. אבל כפי שהתקרית לעיל הראתה, זה לא נכון, ואפילו המסמכים הרשמיים לא תמיד מספיקים. כדי להבטיח (ברמה סבירה של וודאות) שהמערכת שלנו תפעל היטב בכל התנאים, עלינו לדעת קודם כל מהם כל התנאים – במקרה שלי, בין השאר, כמה קבצים עשויים להיות לכל היותר על כרטיס הזיכרון – ואז לבדוק ולאפיין ביסודיות את התנהגות הרכיב במקרים טיפוסיים ובמקרי קצה.