קטעים נבחרים מתוך המאבק העיקש שלי נגד חומרה חלשה ונגד קומפיילר עצלן, במטרה לקצר את זמן התגובה של המערכת שבניתי בפוסט הקודם.
המערכת, למי ששכח, מבוססת על מיקרו-בקר מדגם PIC16F616, עם תוכנה בשפת C שממירה קלט בינארי בן 4 ביטים לפלט שמתאים לרכיב Seven Segment. המערכת השלימה את שינוי הפלט תוך כ-70 מיליוניות השנייה מרגע שינוי הקלט, ובשביל משימה כזו, שאמורה להיות פשוטה, זה המון. מה אפשר לעשות כדי לשפר את המצב? אם היה מדובר בפרויקט ללקוח כנראה הייתי בוחר מראש מיקרו-בקר שונה, או לפחות מחבר אליו אות שעון חיצוני ומהיר יותר. אבל בשביל האתגר נניח שחייבים להסתדר עם מה שיש.
תיאור מפורט של כל תהליך האופטימיזציה ופרטיו יצריך שניים או שלושה פוסטים נוספים, וזה בטח יהיה מתיש מדי גם בשבילי וגם בשבילכם. אז החלטתי לקצר ולתת לכם את רק ה-highlights, כמו שאומרים.
נתחיל בטבלת החיפוש. בהמרה מבינארי ל-Seven Segment אין חוקיות לוגית פשוטה, ולכן הפתרון המעשי היחיד הוא טבלה עם ערכים מוכנים מראש. לתוכנה המקורית הכנתי טבלה עם 16 ערכים, עבור הספרות 0 עד 9 והאותיות A עד F: כל מה שאפשר לייצג בארבעה ביטים. הבעיה הייתה שהביטים האלה היו קצת מפוזרים על פני פורט הקלט. אם נסמן ב-"R" את הביטים הרלוונטיים וב-"x" את השאר, פורט A נראה ככה:
xxRRxxRR
כמובן, העובדה שביטים מסוימים לא רלוונטיים לא אומרת בהכרח שהערך שלהם יהיה 0. לכן, כדי להגיע לאינדקס הנכון בטבלה, צריך לקרוא את הפורט, לאפס את כל ה-x ולרכז את כל ה-R בצד ימין. אלה הן פעולות בסיסיות אבל בכל זאת לוקחות זמן, ובמיקרו-בקר שהפעולה הכי קצרה שלו נמשכת חצי מיליונית שנייה, כל הבדל קטן הוא משמעותי.
אבל מה אם, במקום להתעסק עם הביטים האלה, נשאיר את כולם כמות שהם וניצור טבלת חיפוש גדולה הרבה יותר, עם ערכים שחוזרים על עצמם? טבלה כזו תוכל לכסות את כל 256 הערכים הגולמיים שעשויים להתקבל מפורט A, ולהגיע באופן מיידי לערך הרצוי. מצד שני, מה המשמעות המדויקת של "מיידי"? ל-616' יש רק 128 בייטים של SRAM, כך שטבלה מלאה תצטרך לשכון בזיכרון ה-FLASH שהגישה אליו איטית קצת יותר. כאן בא לעזרתנו ה-Datasheet:
הסתכלו טוב מתחת למקרא, על השורה שעוסקת בביטים 6-7: הם לא ממומשים ומה שחשוב עוד יותר, נקראים כאפס! כלומר, מובטח לנו שהערכים הגולמיים שנקרא מ-PORTA יהיו בטווח 0-63 בלבד, וטבלה בגודל מתאים יכולה להתאחסן אפילו בזיכרון ה-SRAM המוגבל.
אחרי השינוי הזה, זמן התגובה של המערכת ירד מ-70us לכ-25us, והחלק שעוסק בכתיבת הפלט לשני פורטים הפך להיות גוזל הזמן הראשי. המערכת כותבת 6 ביטים לפורט C ואחד לפורט A: בכל בייט שנשלף מהטבלה, הביטים בעצם מסודרים וירטואלית כך:
xACCCCCC
כיוון שלפורט C יש בחומרה רק שישה פינים, שמקבילים לששת הביטים הימניים, אני יכול לכתוב ישירות ל-PORTC את כל הבייט הזה, ושני הביטים העליונים פשוט לא יעשו כלום. קל ונוח. אבל פין הפלט בפורט A היה RA2, כך ששם עדיין צריך לנקות את הבייט ולהזיז את הביט הרלוונטי לפני ההשמה.
הדבר הראשון שה-Datasheet מציין לגבי ה-616' הוא ש"צריך ללמוד רק 35 פקודות", כאשר הכוונה כמובן לפקודות האסמבלי שהרכיב מכיר. זהו מן הסתם שריד לעידן פרהיסטורי, שבו אנשים עבדו באסמבלי ביומיום ופרמטר שכזה היה משמעותי מבחינתם. הגיוני שקל יותר ללמוד אסמבלי כזה מאשר, למשל, אסמבלי של AVR (בסביבות 135 פקודות, אם אינני טועה), אבל יש לו מחיר: הרבה דברים שאפשר לעשות ב-AVR בפקודת אסמבלי אחת ידרשו ב-PIC שתיים, שלוש או יותר, וזה לוקח זמן.
מעבר לזה, כשהסתכלתי על האסמבלי שהקומפיילר הפיק מקוד ה-C שלי, הזדעזעתי לגלות משהו שבדיעבד לא היה אמור להפתיע אותי: הסטה של הביטים בבייט ארבעה מקומות ימינה, כמו בפקודה
v = v >> 4;
נעשית באמצעות לולאה שמבצעת ארבע פעמים הסטה של מקום אחד. זאת אומרת, לא רק ארבע פקודות הסטה נפרדות בזו אחר זו (כפי שקומפיילר חכם היה עושה), אלא גם פקודות לאתחול, בדיקה וקפיצה בקוד שדרושות כדי לממש לולאה! גם את הבעיה הזו אפשר בעיקרון לפתור באמצעות טבלת חיפוש, אלא שכאן, כיוון שהביט הקריטי נמצא במקום גבוה, הטבלה תהיה חייבת לשבת ב-FLASH והגישה אליה תהיה איטית. אפשר גם לכתוב פקודת if שתבדוק את הביט הספציפי ותעדכן את PORTA בהתאם. אבל שוב, כמו שאמרתי, כל דבר קטן מתנפח למספר פקודות ומרחב התמרון הוא אפסי.
ואז הגיעה ההברקה. דבר ראשון, כיוון שעוד לא הלחמתי שום דבר והכול היה על מטריצה, אפשר להחליף את התפקידים של RA2 ו-RA5 כך ש-RA2 יהיה קלט (מה שגרר עדכון של טבלת החיפוש, כמובן) ואילו RA5 יהיה פלט. במצב כזה, כדי להביא את הביט ההוא למקום הנכון צריך להסיט את הבייט רק מקום אחד ימינה – פקודה אחת ויחידה, בלי שום לולאה!
דבר שני, כיוון שיש שישה פינים בפורט A ורק אחד מהם מוגדר כפלט, אני יכול לכתוב לפורט מה שבא לי ורק פין הפלט יושפע. במילים אחרות, לא צריך לאפס את הביטים האחרים בבייט שהגיע מהטבלה.
אחרי כל השיפורים הללו, הלולאה הראשית של התוכנית שלי נראתה כך (המשתנה o הוא בייט מקומי ו-LUT הוא טבלת החיפוש):
for ( ; ; ) { PORTC = o = LUT[PORTA]; PORTA = o >> 1; }
וזמן התגובה של התוכנית הזו לשינויים בקלט הוא בסביבות 10 מיליוניות השנייה – פי שבעה מהר יותר מהתוכנית המקורית. מי שמכיר היטב את האסמבלי ואת הארכיטקטורה של המיקרו-בקר יוכל בוודאי לסחוט עוד כמה מחזורי שעון, אך אני כבר מיציתי את הפרויקט הזה.
רק טריק אחד נשאר, כפינאלה: למיקרו-בקר יש הרי מתנד פנימי, ויש גם רגיסטר (בשם OSCTUNE) שאיתו אפשר לכייל את המתנד. אם ניתן ל-OSCTUNE את הערך המקסימלי, המתנד הפנימי ירוץ הכי מהר שהוא יכול… אם כי מדובר על הבדלים של אחוז או שניים במקרה הכי קיצוני.
אני סקרן מדוע האופטימיזציה הזו כל כך דרושה. אם זה אמור להפעיל תצוגת 7-segment אז מצידך שכל עידכון כזה יקח גם מילישניות שכן בני אדם לא קולטים שינויים מהירים יותר (ואפילו לא ברור שהתצוגה מצליחה להשתנות במהירות כזו).
אם כי כמובן שיש לזה ערך פדגוגי.
קודם כל, למען הסר ספק – דווקא כן, התצוגה מבוססת בסופו של דבר על לדים פשוטים ולכן יכולה בהחלט להשתנות במהירות כזו וגם הרבה יותר גבוהה 🙂
אתה צודק שאדם לא יבחין בהבדל, כך שליישום הספציפי שתיארתי אין באמת צורך באופטימיזציה וזה רק תרגיל.
מצד שני, כפי שהזכרתי איפשהו בפוסט הקודם, במקרים אחרים הפרשי זמן כאלה בין ג'וק ייעודי לבין אמולציה על ידי מיקרו-בקר יכולים להיות הרסניים. אז לפני שניגשים למימוש כזה, כדאי לחקור ולבדוק מה גבולות היכולת הסבירים של המיקרו-בקר. את הלקחים מהניסוי הקטן שערכתי כאן אפשר להעביר די בקלות למקרים קריטיים יותר, כגון serial-to-parallel, בקרת מנוע וכדומה.