ניסיון לתפעול ישיר של טיימר ב-ATmega328P דרך סביבת הפיתוח של ארדואינו חשף סיבוך נסתר, שגרם להתנהגות מוזרה מאוד של המיקרו-בקר.
במסגרת פרויקט קטן, שעליו אספר בפוסט קרוב, הייתי צריך לגרום ללוח ארדואינו Uno להוציא מאחד הפינים גל ריבועי בתדר 38KHz. הדרך הכי יעילה לעשות זאת היא להתאים ידנית אחד משלושת הטיימרים של המיקרו-בקר, שמשמשים בדרך כלל להוצאת אותות PWM בפקודה analogWrite.
כפי שמוסבר בוויקי שלי לפונקציות ארדואינו, פונקציית התזמון האלמנטרית millis מסתמכת על טיימר מס' 0, אז עדיף לא לגעת בו אם לא חייבים. מבין הנותרים, טיימר 2 הוא הפשוט יותר (המונה שלו הוא בגודל 8 ביט, לעומת 16 ביט בטיימר 1) ולכן בחרתי בו. התאמה של טיימר להוצאת גל ריבועי היא משימה בסיסית למדי (עשיתי את זה כאן ב-ATtiny85 לפני יותר מארבע שנים), ולכן הופתעתי ביותר כשהפונקציות שכתבתי, שנראו כך:
// Set up timer2 to 38KHz CTC mode, OFF
void T2_38KHzSetup() {
OCR2A = 209;
TCCR2A = (1 << COM2A0) | (1 << WGM21);
TCCR2B = 0;
}
void T2_SetActivity(const bool isON) {
if (isON) {
TCCR2B |= (1 << CS20);
} else {
TCCR2B &= ~(1 << CS20);
}
}
גרמו לפלט של גל ריבועי, אמנם, אבל בתדר קצת יותר גבוה מהצפוי – 8MHz במקום 38KHz. מה לכל הרוחות….? זה מתסכל במיוחד כי יש כאן בסך הכול ארבעה פרמטרים קלים שצריך להגדיר, הכול בדיוק לפי ה-Datasheet של שבב ידוע ומוכר, העסק אמור להיות פשוט וחד-משמעי, אז למה הטיימר מתעקש להתנהג כאילו הגדרתי סף עליון (הרגיסטר OCR2A) כ-0 במקום כ-209?
ההסבר, כך גיליתי בסופו של דבר, מסתתר במקום אחר ב-Datasheet, שמסביר באופן כללי על טיבו של רגיסטר הסף העליון:
The OCR2x Register is double buffered when using any of the Pulse Width Modulation (PWM) modes. For the Normal and Clear Timer on Compare (CTC) modes of operation, the double buffering is disabled. The double buffering synchronizes the update of the OCR2x Compare Register to either top or bottom of the counting
sequence. The synchronization prevents the occurrence of odd-length, non-symmetrical PWM pulses, thereby making the output glitch-free.
בכוונה כתבתי שההסבר מסתתר. גם כשקוראים את הפסקה הזו התשובה לא מובנת מאליה, וצריך לקרוא בין השורות. בגדול, מה שנאמר כאן זה שכאשר הטיימר נמצא במצב PWM, ואנחנו מנסים לעדכן את הערך של OCR2A או OCR2B (הסף השני שלא מעניין אותנו בפרויקט הנוכחי), אז העדכון מתבצע רק בסיומו של מחזור ה-PWM הנוכחי כדי למנוע גליצ'ים בפלט. הקאץ' הוא שספציפית בסביבת הפיתוח של ארדואינו, הקוד האוטומטי שמופק ורץ עוד לפני הפונקציה setup שם את כל הטיימרים במצב PWM, וזאת כדי שהמשתמשים יוכלו לכתוב פקודות analogWrite בלי להתעסק בהגדרות.
הפונקציה שכתבתי למעלה מנסה, דבר ראשון, לכתוב ל-OCR2A את הערך 209. הפקודה הזו רצה כשהטיימר עדיין במצב PWM, כיוון שזה מה שסביבת הפיתוח קבעה, ולכן עדכון הערך הוא לא מיידי. מתי הוא יקרה? בסוף מחזור ה-PWM כאמור. אבל מיד לאחר מכן, בפקודה הבאה, אני מעביר את הטיימר למצב אחר, CTC. מחזור ה-PWM נקטע באמצע ולא מושלם, ולכן OCR2A לא מקבל את הערך החדש!
וכאן העסק נעשה עוד יותר אכזרי: אם נקרא את OCR2A, למשל כדי להדפיס אותו דרך Serial, דווקא כן נקבל את הערך החדש שכתבנו (209 במקרה הנוכחי)… אבל הטיימר עצמו לא רואה את זה ככה ולא מתייחס לערך הנכון. ייתכן שבעת המעבר למצב CTC הטיימר קורא את הערך מתוך באפר כלשהו ולא ישירות מהרגיסטר. עוד לא הצלחתי להבין מה בדיוק קורה בעומק החומרה של המיקרו-בקר – בכל אופן יש כאן קאץ' קטלני. לשמחתנו הפתרון פשוט: לכתוב את הערך החדש ל-OCR2A (או כל סף אחר) אחרי שמעבירים את הטיימר למצב החדש, או בקוד למעלה, לשים את השורה
OCR2A = 209;
בסוף הפונקציה ולא בתחילתה.
זהו, הגל הריבועי יוצא עכשיו בתדר הנכון ובלי חוכמות, אפשר להמשיך בפרויקט. כמו שאמר פורסט גאמפ, "החיים הם כמו קופסת שוקולד, אתה אף פעם לא יודע מה אתה הולך לקבל" – רק שבמקום החיים מדובר כאן על תכנות מיקרו-בקרים, ובמקום שוקולד יש לנו באגים.