זה התחיל בתור פוסט כללי, על הדרך בה בחרתי להתמודד עם נושא הארדואינו בוויקי שאני כותב. תוך כדי העבודה עליו גיליתי את הפונקציה pulseInLong… ואז גיליתי את הבאג בה!
כפי שציינתי בפוסט קודם, התחלתי לכתוב מעין ויקיפדיה בעברית לחובבים בתחום ה-Embedded. מכיוון שמדובר בפרויקט בהיקף עצום לאדם אחד, החלטתי להתמקד בשלב ראשון בפינה הקטנה (יחסית!) אך פופולרית של ארדואינו. במקום לתרגם ישירות את החומרים מ-arduino.cc, בחרתי לכתוב על פונקציות הליבה המוכרות והנפוצות מנקודת מבט רחבה יותר – שמודעת למיקרו-בקר הספציפי שמריץ את הפונקציות האלה, ליכולות ולמגבלות שלו, ולעובדה שזו לא שפת תכנות או טכנולוגיה ייחודיות אלא בסך הכל קוד שמישהו כתב בצורה מסוימת, לטוב ולרע.
תוך כדי עיון בקוד המקור של ספריות הליבה גיליתי את הפונקציה pulseInLong, שהתווספה לספריות הליבה של ארדואינו לפני קצת יותר משלושה חודשים בלבד. היא דומה מאד ל-pulseIn הישנה והמוכרת, ונועדה לפתור בעיה שהייתה קיימת בה מהיום הראשון.
הבעיה עם pulseIn
כידוע, הפונקציה pulseIn אמורה למדוד את המשך, במיליוניות השניה, של פולס חיובי או שלילי בפין כלשהו. היא מחכה לסיום הפולס הנוכחי, אם יש כזה, לתחילת הפולס הבא (מעבר מהמתח ה"לא-נכון" למתח הנכון) ואז לסיום הפולס החדש, וכל זה נעשה תחת מגבלת זמן קבועה מראש של הפרמטר timeout (אותו אפשר לציין במפורש, או להסתפק בערך ברירת המחדל של שניה אחת). ההמתנה והמדידה נעשות באמצעות חזרה על לולאה קטנה בקוד הראשי. מישהו טרח ובדק כמה מחזורי שעון בדיוק נמשך כל מעבר על הלולאה, ועל סמך זה נקבע משך הפולס הנמדד.
שיטה זו גורמת לשתי בעיות. ראשית, בארדואינו פועלות כל הזמן פסיקות (בעיקר עבור פונקציות מדידת הזמן millis ו-micros), ופסיקות הרי עוצרות לרגע את הקוד הראשי – כך שספירת מחזורי השעון של הלולאה לא נותנת את התמונה המדויקת, והתוצאה תצא שגויה יותר (קצרה מדי) ככל שהפולס יתארך. שנית, כפי שגיליתי בדיעבד בפורומי הדיונים של מפתחי ארדואינו, ספירת מחזורי השעון נכונה רק לתוצרים של קומפיילר ספציפי עם הגדרות ספציפיות, וכל שינוי או עדכון של אלה יכול לשנות את המספר ולשבש את המדידות.
אפרופו, נכון לכתיבת שורות אלה, גם התיעוד הרשמי של pulseIn ב-arduino.cc שגוי, וטוען שפסיקות הארדואינו חייבות לפעול כדי שהפונקציה תפעל…
הפתרון של pulseInLong
כדי להרוג שתי ציפורים במכה אחת, מישהו החליט לכתוב פונקציה נוספת שתתאים למדידות ארוכות יחסית (מכאן ה-Long הנוסף לשם). גם פונקציה זו מחכה, במגבלת ה-timeout, לתחילת פולס ואז מודדת אותו, אלא שכאן משך הזמן מחושב בעזרת הפונקציה micros עצמה. בדיוק כפי שחובבי ארדואינו רגילים לעשות, הפונקציה שומרת את הערך שחוזר מ-micros בתחילת הפולס, קוראת את הערך החדש בסוף הפולס ומחסירה את הראשון מהשני.
הבעיה עם pulseInLong
עקרונית זה בסדר גמור, כל עוד המדידה לא אמורה להיות מדויקת לחלוטין (הרי גם micros עצמה לא מושלמת). אבל איפשהו במימוש של הפונקציה, משהו השתבש לגמרי. הסתכלו על הקוד הזה, מתחילת הפונקציה, שממתין לסיומו של פולס קודם (אם יש כזה):
unsigned long numloops = 0; unsigned long maxloops = microsecondsToClockCycles(timeout); while ((*portInputRegister(port) & bit) == stateMask) if (numloops++ == maxloops) return 0;
המשתנה maxloops מייצג את מספר מחזורי השעון של המיקרו-בקר עד לדד-ליין שהוגדר ב-timeout, והמשתנה numloops סופר את לולאות הבדיקה כדי להבטיח שאם הפולס הקודם לא נגמר ועבר הזמן המוקצב, הפונקציה תחזיר את הערך 0. ההנחה הסמויה כאן היא שכל מעבר על הלולאה לוקח בדיוק מחזור שעון אחד – וזו כמובן הנחה מופרכת ושגויה לחלוטין.
אותה טעות נעשתה בשתי הלולאות הבאות (של ההמתנה לתחילת הפולס ושל ההמתנה לסיומו), והתוצאה של כל זה היא שאם הגדרנו, נניח, timeout של שניה אחת, הפונקציה תמתין למעשה כעשרים(!) שניות עד שתתייאש, ובין לבין היא עשויה למדוד פולסים ארוכים הרבה יותר משניה אחת.
איך עליתי (בטעות) על הבאג
את הבעיה הנ"ל לא ראיתי בקריאה ראשונה של הקוד – התרכזתי בשורות שמתייחסות ל-micros ובכלל לא חשבתי שעלולה להיות תקלה במקום אחר. העניין צץ כשכתבתי תוכנית זעירה לבדיקת הדיוק היחסי של pulseIn ושל pulseInLong, על ידי מדידה של אות PWM (ב-duty cycle של 50%) שהארדואינו עצמו מוציא, עם timeout של אלפית שניה. הפונקציה pulseInLong החזירה תוצאות סבירות, אבל pulseIn החזירה תמיד 0.
כעבור רגע הבנתי איפה הטעות שלי: תדר ה-PWM בארדואינו הוא בסביבות 500 הרץ, כלומר מחזור כל שתי אלפיות שניה, כך שהגיוני מאד ש-pulseIn לא תספיק "לתפוס" את הפולס עם timeout של אלפית אחת בלבד. אבל אם זה נכון, למה pulseInLong כן הצליחה? בשלב זה חזרתי לקוד וגיליתי את הממצאים הנ"ל.
ומה הלאה?
הלקח החשוב מהסיפור הוא, כרגיל, שארדואינו הוא לא מוצר מושלם ובדוק, ואסור לסמוך עליו בעיניים עצומות. מעבר לזה, אני מנסה להעלות את הבעיה הספציפית בפורום מפתחי ארדואינו – אולי מישהו שם ייקח על עצמו את התיקון, או ייתן לי לעשות זאת. אעדכן כשיהיה במה.
עדכון: ההודעה שלי בפורום מפתחי ארדואינו הופיעה רק אחרי יממה, וזכתה לתשובה שגויה (כביכול מצאתי גרסה ישנה מאד של pulseIn) אבל בינתיים העליתי את העניין גם ב-GitHub של ארדואינו, ושם מישהו (כותב הפונקציה במקור?) הודה לי בזריזות על איתור הבאג ויישם את התיקון שהצעתי – ומישהו אחר מצא באג קטן בתיקון שלי… בקרוב אצלכם, בגרסה הבאה של סביבת הפיתוח של ארדואינו… 🙂