ניסיון תמים להגדיר טיימר של מיקרו-בקר PIC לתזמונים מדויקים מוכיח – שוב – שהסיבות לבאגים הכי מוזרים יכולות להסתתר ממש מתחת לאף. הנה ההסבר המפורט, שעשוי לחסוך לכם הרבה זמן… תרתי משמע.
סיפורנו הפעם מתחיל בפרויקט קטן, שבמסגרתו רציתי לממש שידור סריאלי (כמו זה של ה-Serial בארדואינו) עם מיקרו-בקר מדגם PIC12F675 הוותיק והחביב. מכיוון שלדגם הזה אין מודול UART בחומרה שיטפל בדברים כאלה, התזמון והתפעול של העסק צריכים להתבצע "ידנית" בתוכנה. לא נורא, בדיוק בשביל דברים כאלה המציאו טיימרים. נכון?
התיאוריה
כדי שאוכל לשלוח את הביטים של הנתונים בקצב נכון, אני צריך טיימר שיעורר פסיקות בתזמון מדויק. הקצב שבחרתי הוא 9600 ביטים בשניה הסטנדרטי, שזה כ-104 מיליוניות השניה לכל ביט. איך גורמים לטיימר לעורר פסיקה במרווחים כאלה?
הטיימר Timer0 של ה-PIC הזה מבוסס על מונה בגודל 8 ביט, והוא מסוגל לעורר פסיקה רק כשהמונה גולש מהערך המקסימלי, 255, בחזרה ל-0. זאת אומרת שהפסיקה תתעורר בפעם ב-256 מחזורי שעון (או בכפולות מסוימות של מספר זה, אם משתמשים ב-Prescaler). אלא מה, שעון המערכת שמבוסס על המתנד הפנימי הוא בתדר 1MHz בלבד, ו-256 מחזורי שעון נמשכים 256 מיליוניות השניה – יותר מפי שניים ממה שאני צריך.
עם זאת, המונה של הטיימר הוא בעצמו רגיסטר (בשם TMR0), ואנחנו יכולים לקרוא אותו וגם לכתוב אליו. כלומר, אם ברגע שהפסיקה מתעוררת אני אכתוב ל-TMR0 את הערך 152, הספירה תמשיך משם והפעם הבאה שהפסיקה תתעורר תהיה אחרי 104 מיליוניות השניה. בינגו! טוב, למען האמת זה לא לגמרי מדויק – ה-Datasheet מציין שאחרי כתיבה ל-TMR0, הספירה מתעכבת למשך שני מחזורים, כך שהערך צריך להיות 154. אבל כפי שנראה בהמשך, זה לא קשור לבאג שהופיע.
הבאג
והבאג היה כזה: כדי לבדוק שלא טעיתי, כתבתי קוד זמני בפסיקה שמחליף את מצב הפלט של פין מסוים – וגיליתי שהפלט מתחלף לא כל 104 מיליוניות השניה כצפוי, אלא דווקא כל 125 מיליוניות השניה.
בדקתי את הקוד הקצרצר כולו שוב ושוב, ולא הצלחתי למצוא שום טעות. כל המספרים והחישובים נראו הגיוניים. ואז חשבתי: רגע, אולי המתנד הפנימי עצמו לא מכוון טוב? על ידי שינוי של אחת ההגדרות גרמתי למיקרו-בקר להוציא כפלט את אות השעון של עצמו, ומדדתי את התדר הזה: הוא היה גבוה בכמה אחוזים מ-1MHz – בדיוק בכיוון ההפוך מכיוון הבאג! למען הסדר הטוב, שיניתי את הערך של הרגיסטר OSCCAL לכוונון המתנד כך שאקבל תדר שעון קרוב ככל האפשר ל-1MHz. כמובן שזה לא פתר את הבעיה.
התחלתי לחפש בגוגל, לראות אם מישהו אחר נתקל בתופעה משונה כזו, ובמקביל נכנסתי לכל מיני "מחשבוני טיימרים" כדי לראות איזה קוד הם יפיקו עבור הפרמטרים הדרושים. לא מצאתי אף שותף לצרה, והמחשבונים הסכימו לגמרי עם הקוד שאני כתבתי. אז מה לעזאזל…
הפתרון
ואז הבנתי. הפסיקה מתעוררת כאשר TMR0 גולש בחזרה ל-0, ובתוך פונקציית הפסיקה אני נותן לו את הערך 154; אבל בין התעוררות הפסיקה לבין השמת הערך החדש עובר זמן. עד שהמיקרו-בקר נכנס לפונקציית הפסיקה ומשלים את הצבת הערך החדש ב-TMR0 עוברות כמה וכמה מיליוניות שניה (לא לשכוח שקצב השעון עצמו הוא בסך הכל 1MHz), והספירה של 104 מיליוניות מתחילה רק אחריהן. כדי לקבל תזמון נכון, אני צריך לקזז את ההפרש ולתת ל-TMR0 ערך קרוב יותר ל-255!
עכשיו הקוד עבד כמו שצריך, אבל עדיין הייתי מבולבל. הרי לא יכול להיות שאני הראשון שנתקל בדבר הזה, אז איך ייתכן שלא מצאתי שום אזכור שלו ברשת? איך ייתכן שכל מחשבוני הטיימרים, כולל MPLAB Code Configurator הרשמי של Microchip, מתעלמים מהנקודה הזו? שאלתי בפורום בחו"ל, והסתבר שצדקתי: הבעיה אמתית, ומתכנתים מקצועיים נתקלו בה ופתרו אותה באותה דרך. פשוט, עד היום אף אחד כנראה לא טרח לכתוב את זה ברשת בצורה מסודרת… אז הנה לכם הסקופ הבלעדי! 🙂
ב-AVR זה לא היה קורה
כל הסיפור הזה נובע מאופן הפעולה של הטיימר בארכיטקטורה הספציפית של PIC. אמנם גם במיקרו-בקרים אחרים עלולה לצוץ אותה בעיה אם מנסים לעשות את אותו הדבר בדיוק, אבל לעתים לא יהיה בכך שום צורך: ב-AVR למשל אפשר להגדיר סף עבור מונה הטיימר, שגם יעורר פסיקה וגם יאפס את המונה – והכל בחומרה, בלי שום עיכובים.
עידו שלום רב , עידו אתה יכול להסביר בקצרה מה זה אומר מהירות שעון בקצב של 1Hz קראתי שיש מן סוג של שעון בתוך הג'וקים ולא כל כך הבנתי מה תפקידם.
תודה רבה 🙂
בלי להיכנס לפרטים הטכניים, לכל מערכת דיגיטלית יש איזשהו "שעון" פנימי או חיצוני שמכתיב את הקצב שבו מתבצעות פעולות (עד לגבול שמוכתב על ידי היכולות של החומרה). השעון הוא בעצם רכיב, או מעגל, שנותן פולסים של מתח בקצב קבוע. למיקרו-בקר שהזכרתי בפוסט יש מעגל פנימי שנותן פולסים ב-4MHz (ארבעה מגהרץ), כלומר ארבעה מיליוני פעמים בשניה. התדר הזה מחולק אחר כך בארבע, כך ש"שעון המערכת" בפועל הוא 1MHz – מיליון פעמים בשניה, או מיליונית שניה לכל פעולה בסיסית. אגב, לא כל פעולה היא בסיסית – למשל, הכפלה של שני מספרים גדולים יכולה להיות בעצם סדרה של עשרות פעולות בסיסיות. ברמה הגבוהה,… לקרוא עוד »
נראה לי שהפרקטיקה הנכונה היא לבדוק את ערך הטיימר בפסיקה לפני שאתה מכניס לתוכו ערך, או בעצם להוסיף לו ערך ולא לעשות השמה.
לך תדע, אולי בעתיד תוסיף איזה קוד קריטי שימסך את הפסיקות, מה שידפוק לך שוב את כל התזמון.
זה נכון, ההצעה הכי טובה שקיבלתי ממומחים היא *להוסיף* ערך ל-TMR0 במקום סתם לכתוב אותו. בצורה הזו, גם שינויים (סבירים) בקוד לא ישנו את הדיוק של התזמונים, ולא יהיה צריך למדוד מחדש ולקזז את הטעות.
כמובן שניטרול הפסיקות עלול "ליפול" על חלון הזמן הקריטי ולפגוע בתזמון, אבל זה נכון תמיד, בלי קשר לבעיה הזו ולפתרון שלה.