תסריטי פעולה (או: בלינק למתקדמים)

הדבר הראשון שעשינו עם ארדואינו זה להבהב בלד, שנייה ON ושנייה OFF במחזוריות. מאז התקדמנו קצת, ועכשיו הלקוח מבקש – במקביל לפעולות האחרות של המערכת! – שהלד "יהבהב שלוש פעמים מהר, יכבה לעשר שניות ואז יהבהב לאט פעמיים עם fade. לא, רגע, ארבע פעמים. קצת יותר מהר. אתה יודע מה, בעצם…" בואו נראה איך אפשר להתמודד ביעילות עם תסריטי פעולה כאלה, ועם שינויים תכופים שאנחנו מתבקשים לעשות בהם.

שלב ראשון: רשימת תזמונים פשוטה

נתחיל במקרה-לדוגמה פשוט יחסית, שבו מערכת מבוססת-ארדואינו צריכה להבהב את המסר S.O.S בקוד מורס, שוב ושוב בלי הפסקה. לצעירים שביניכם, זה אומר: שלושה הבהובים קצרים, רווח קצר, שלושה הבהובים ארוכים, עוד רווח קצר, שלושה קצרים, רווח ארוך – ושוב מהתחלה. בעצם, כל מה שאנחנו צריכים לעשות כאן זה להדליק ולכבות את הלד לחילופין, רק בתזמון נכון. אפשר לעשות זאת בסגנון בלינק הקלאסי, כלומר שורה ארוכה של פקודות digitalWrite ו-delay, אבל לצורת עבודה כזאת יש שני חסרונות ענקיים: ראשית, קוד מהסוג הזה נעשה בקלות ארוך, מסורבל ומאוד לא ידידותי לשינויים. שנית, זהו קוד חוסם (Blocking), שכל עוד הוא רץ המערכת שלנו לא תוכל לעשות כמעט שום דבר אחר.

נטפל בחיסרון השני, גם כי הוא קריטי יותר ברוב היישומים, וגם כי ההתמודדות איתו תעלים לנו על הדרך את החיסרון הראשון. בעזרת פסיקת טיימר או בתוך לולאה אינסופית שבודקת כל הזמן את millis, אנחנו נקרא פעם באלפית שנייה לפונקציה שנכתוב במיוחד, ושמעדכנת (אם הגיע הזמן) את מצב הלד. בשאר הזמן המערכת חופשיה לעשות דברים אחרים. הפונקציה שלנו צריכה בעצם נתון אחד בלבד: בעוד כמה אלפיות שנייה צריך לקרות השינוי הקרוב. הרי בדוגמה שלנו הלד יכול רק להאיר או לא להאיר – ואם אנחנו יודעים מה מצב הלד כרגע, אנחנו גם יודעים אוטומטית מה המצב שאליו הוא צריך לעבור.

העליתי ל-Pastebin קוד ארדואינו שעושה בדיוק את זה. הלולאה הראשית קוראת לפונקציה timedToggle פעם באלפית שנייה, והפונקציה הזו מטפלת במדידת הזמנים ובשינוי מצב הלד. כל הזמנים, של ON ושל OFF לסירוגין, מוגדרים במערך הזה שמופיע בתחילת הקוד:

קטע הקוד עם התזמונים להבהוב SOS
קטע הקוד עם התזמונים להבהוב SOS (לחצו לתמונה קצת יותר גדולה)

בתוספת הקוד של הפונקציה עצמה, מספר השורות נטו כבר קרוב אולי לזה של שרשרת digitalWrite ו-delay, אבל כאן אפשר לעשות כאמור עוד דברים בזמן שהלד מהבהב S.O.S, ומה שהכי יפה, אם פתאום נצטרך להבהב משהו אחר במורס – נניח את האותיות U.S.A – נצטרך לשנות רק את המערך הזה שבתחילת התוכנית. הרבה יותר מסודר ונוח מאשר האלטרנטיבה, ובמיוחד אם נעזרים בריווח ובהערות כמו בצילום המסך למעלה.

שימו לב לשימוש בטיפוס הנתונים uint16_t: אם הייתי משתמש ב-byte, אי אפשר היה להגדיר מספרים גדולים מ-255. בנוסף, אם מפעילים את הלד לפי המערך במחזוריות ואין בקרה נוספת, צריך לוודא שיש בו מספר זוגי של ערכים כדי שהפונקציה תחזיר את הלד למצב ההתחלתי שלו ולא "תתבלבל" בסיבוב הבא בין כיבוי להדלקה.

שלב שני: תזמונים + ערכים

נעבור למקרה מעניין קצת יותר, שבו אנחנו רוצים להשתמש בפקודה analogWrite של ארדואינו כדי לתזמן הפעלה של לד ביותר משתי עוצמות תאורה – לדוגמה "כבוי", "חלש" ו"חזק". מערך של זמנים בלבד כבר לא יספיק לנו: אנחנו צריכים גם לציין איפשהו את העוצמה הרלוונטית בכל צעד.

אפשר לעשות את זה באמצעות שני מערכים מקבילים (כלומר שני מערכים נפרדים באותו גודל, אחד עם זמנים ואחד עם עוצמות הארה) או באמצעות מערך של struct, שכל תא בו מחזיק גם את הזמן וגם את העוצמה. לדעתי הפתרון של struct יותר קריא, והרבה יותר נוח מבחינת ביצוע שינויים בתסריט. רק היזהרו אם אתם עובדים עם מיקרו-בקרים בלי כפל בחומרה

אגב, פתרון כזה יכול להיות מעולה להשמעה של מנגינה פשוטה, בדומה למה שעשיתי לפני יותר מעשר שנים באחד הפוסטים הראשונים בבלוג הזה. במקום עוצמות הארה נשמור תדרים של צלילים, ובהנחה שהגדרנו גם ערך שייתן לנו שקט, נוכל ליצור מנגינות שלא יביישו אף צעצוע סיני בשקל.

אבל גם לשיטה הזו יש מגבלות רציניות. הזכרתי בתחילת הפוסט אפקט של fade עם לד (יש גם את האפקט הקשור, "breathe"). אפקט כזה נוצר על ידי עלייה/ירידה מהירה בצעדים קטנים בערכי ה-PWM. דמיינו גם יצירת צליל של אזעקה עולה ויורדת. לכתוב ידנית מערכים עם כל התדרים השונים זה טירוף, אפילו אם זה לא יגמור לנו את הזיכרון המוגבל של המיקרו-בקר. אפשר כמובן לכתוב תוכנה במחשב, או גיליון אקסל, שיפיקו מחרוזות טקסט עם רצף המספרים הדרוש במקום להקליד אותם ידנית, אך בעיית הזיכרון עדיין קיימת, וכך גם הסרבול והקושי לבצע שינויים בהמשך. איך לדעתכם אפשר לפתור זאת?

שלב שלישי: פקודות

נשכח לשנייה מכל נושא התסריטים. בקוד ארדואינו "רגיל", איך היינו מממשים אפקט fade? בעזרת לולאה כמובן, שבכל סיבוב מגדילה איזשהו משתנה, נניח מ-0 עד 255, קוראת ל-analogWrite עם הערך הנוכחי ומוסיפה delay קטן.

התסריטים שתיארתי למעלה כוללים אך ורק נתונים: ערכי זמן וערכי פלט. נכון שזה יהיה מגניב אם נוסיף להם גם פקודות, שיאפשרו לנו להריץ לולאות ולהגדיל או להקטין ערך של משתנה מסוים? לשם כך נצטרך לכתוב קוד שיידע להבדיל, כך או אחרת, בין הפקודות לבין הנתונים, ולבצע את הפקודות. במילים אחרות, הרעיון עכשיו הוא לכתוב interpreter קטנטן, שיפענח בזמן אמת תוכנית (נתונים+פקודות) ששמורה כמספרים במערך. ככה נוכל גם ליצור אפקטים מורכבים בפקודות בודדות, וגם לשנות אותם בקלות בעתיד, לפי מצבי הרוח והדרישות של הלקוח.

"אבל זה לא הגיוני", אתם בטח חושבים לעצמכם. "התחלנו מזה שלכתוב קוד רגיל זה לא גמיש ולא נוח, אז איך יכול להיות שהפתרון הוא קוד שמריץ קוד אחר פחות ברור? זה רק להוסיף עוד קומה לבעיה, ולהפוך אותה לעוד יותר מגושמת!"

ביקורת כזו מתעלמת מנקודה קטנה אך חשובה מאוד, שהזכרתי למעלה: ה-interpreter מריץ את התסריט רק פקודה אחת כל פעם, ובין לבין, התוכנית הראשית יכולה לעשות עוד דברים. רוצים להבהב בארבעה לדים שונים, כל אחד בדפוס אחר, בו-זמנית תוך כדי שהקוד הראשי מאזין ל-UART ושולח נתוני חיישנים? אין שום בעיה, פשוט כיתבו את ארבעת התסריטים ותנו ל-interpreter לעבד את כולם. נסו לבנות כזה פרויקט בקוד רגיל, או לשנות בו את אחד מדפוסי ההבהוב!

"שפת התכנות" של התסריט ממש לא חייבת להיות מקיפה כמו שפת C. בהתאם לשימוש הרצוי, ניתן להסתפק בפחות מעשר "פקודות" בשביל רוב האפקטים הנפוצים עם לדים, רמקולים פשוטים או מנועי DC. בשביל משימות מגוונות יותר אפשר להוסיף פקודות כגון קריאה מה-EEPROM, להקצות לתסריט מצביעים לפונקציות שהוא יכול לקרוא להן (Callback), ואפילו לשכן את כל התסריט עצמו ב-SRAM או ב-EEPROM כדי לשנות אותו "מבחוץ" בזמן אמת. התוכנית השלמה לא תהיה הכי יעילה וחסכונית, ולא תצליח להגיע לדיוק של מיליוניות שנייה בתזמונים, אך ברוב המקרים זה לא מה שאנחנו, והלקוח, צריכים. קצת כמו הפלטפורמה של ארדואינו בעצמה…

כדי ליצור interpreter מתאים, אפילו ל"שפת" תסריטים פשוטה כזאת, צריך להשלים עוד פרטים רבים. למשל, אם אנחנו רוצים להגדיר פקודות ללולאה, איך הן ייראו? איך ה-interpreter יידע לחזור לתחילת הלולאה ואיפה הוא ישמור ויעדכן את מספר הסיבובים שעוד נותרו בה? אך אלה הם פרטים טכניים, שאפשר לקבוע אחרי שכבר הבנו את העיקרון של התסריט ואת התועלת שבו.

להרשמה
הודע לי על
3 תגובות
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

באופן כללי הרעיון של שימוש בפונקציות והעברת פרממטרים לצורך פעולות שחוזרות על עצמן באופנים דומים היא אחד מהכלים החזקים שיש לנו בתור מתכנתים, שמציל אותנו מ"קוד הספגטי" הידוע לשמצה.
למי שלמד במסודר תכנות זה נראה טריוויאלי, אבל למי שארדואינו הוא הניסיון הראשון שלו בתכנות הרבה פעמים לא חושב בראש הזה…

שימושי מאוד כדי לממש מורס, עשיתי את זה פעם על attiny13a.
כדאי להזהר כשמממשים דברים כאלה על מיקרובקרים פשוטים כמו שלנו – הטבלאות האלה עלולות להשמר ל-RAM אפילו אם מציינים אתם כ-const, ואם ה-RAM קטן מדי עלולות להיות בעיות מהגיהינום – לי היו 64 בתים ובסוף הטבלאות שלי דרסו את המחסנית והקוד לא רץ – ורק עם סימולציה של המעבד הצלחתי להבין שזו הסיבה שאני צורב את הקוד וכלום לא קורה 🙁
זה מוליך אותך לאיזורים אפלים כמו שימוש ב-PROGMEM ב-AVR-GCC ופקודות יעודיות לקריאת הטבלאות שהוגדרו כך – מסוג הדברים שעושים רק אם חייבים…