facebook pixel בלוג: ספריית ה-Event Based Framework - www.4project.co.il
Main logo www.4project.co.il
כל הרכיבים לפרוייקט שלכם
עגלת קניות

העגלה ריקה

ספריית ה-Event Based Framework


2024-06-06 15:04:27
זהו המשך לפוסט הקודם שלי לגבי תוכנית מסגרת (Framework) שתאפשר לכתוב תוכניות ארדואינו בצורה יעילה (והגיונית) יותר, כמו בסביבות Embedded מקצועיות שאני רגיל.

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

לאלה שרוצים לקצר, זהו קישור לספריית ה-EBF ב-GitHub.

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

יצרתי ספריית EBF - Event Based Framework ש"עוטפת" את הפונקציונליות של כרטיס ארדואינו ע"י מחלקות (Classes) בשפת ++C כדי שאפשר יהיה לנהל כל משאב בצורה מסודרת.

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

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

ל"נוחות" של כתיבה בצורה כזו יש מחיר בכמות הזכרון הדרוש בגלל תוספת הקוד וזמן ריצה כדי לטפל בכל הקוד שנוסף. זמן ריצה פחות מדאיג אותי, זוכרים שרוב הדוגמאות של ארדואינו עושים ()delay כדי לעכב את הביצוע? אז כנראה שזמן הוא פקטור פחות חשוב למתחילים. אבל גודל הקוד כן מעסיק אותי ואני סקרן לראות אם כמות הזכרון של Arduino UNO תספיק למימוש של רובוט כזה.

שכבה הפונקציונלית

בסופו של דבר גם הפונקציות כמו ()digitalRead ו-()digitalWrite הן מעטפת גישה לחומרה של הבקר בעזרת פונקציה בשפת C, במקום גישה ישירה לרגיסטרים. בתכנות קוראים לזה רמת ה-HAL - Hardware Abstraction Layer. בדרך כלל פונקציות שעוטפות את חומרה ברמה הנמוכה ביותר.

אז מה שה-EBF מציע זה בעצם HAL עטוף במחלקות (Classes) בשפת ++C שמספקות קריאה לפונקציה של משתמש כשקורה משהו (כמו שינוי על הקווים, או שיש מידע בערוץ תקשורת שאפשר לקרוא). במקום לדגום את הקווים בקוד שלכם (לעשות Polling), ה-EBF עושה את זה בשבילכם וקורא לפונקציה שלכם כדי שתגיבו לאירוע.

צורת הכתיבה עם EBF היא קודם להגדיר את כל האובייקטים שיהיו בשימוש, בצורה גלובלית (כדי שיהיה מופע (instance) של האובייקט בזמן אתחול של EBF). דוגמה של כפתור שמדליק לד:
קוד: בחר הכל
// EBF objects creation, should be global
EBF_Core EBF;
EBF_DigitalInput button;
EBF_DigitalOutput led;


בפונקציית ()setup מאתחלים את האובייקטים עם המאפיינים הרצויים:
קוד: בחר הכל

void setup()
{
  // EBF is the first thing that should be initialized
  EBF.Init();

  // Initialize digital input button on line 2, onButtonChange function will be called on change
  button.Init(2, onButtonChange);

  // Initialize built-in LED (generally line 13)
  led.Init(LED_BUILTIN);
}


ומעבירים את הפיקוד ל-EBF כדי שיבצע את כל מה שצריך:
קוד: בחר הכל

void loop()
{
  // Let EBF to do all the processing
  // Your logic should be done in the callback functions
  EBF.Process();
}


כל מה שנשאר זה לממש את הלוגיקה שאתם רוצים שתקרה כשכניסה הדיגיטלית שהכפתור מחובר אליה תשתנה (במקרה זה להדליק לד בהתאם למצב הכפתור):
קוד: בחר הכל

// Button change callback function
void onButtonChange()
{
  // Read the button state
  uint8_t buttonState = button.GetValue();

  // Set the LED based on the button state
  led.SetValue(buttonState);
}


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

אפשר גם לשלוט על קצב הדגימה אם לא נדרשת תגובה מהירה:
קוד: בחר הכל
// Limit polling of that analog input to once in 50 mSec
analongInput.SetPollInterval(50);


אם מישהו עדיין תוהה למה זה יותר טוב משימוש ב-()delay... תחשבו שיש לכם שתי כניסות שאתם רוצים לדגום בקצב שונה, נניח כניסה אחת כל 10 מילישניות והשניה כל 25 מילי.
די ברור שאי אפשר לשים (10)delay אחרי קריאה ראשונה ו-(25)delay אחרי השניה, כי ביצוע של שניהם בצורה טורית יאפשר לדגום את שתי הכניסות כל 35 מילישניות. אז יהיו כאלה שיעשו (5)delay ויספרו 2 מחזורים בין קריאה מכניסה ראשונה ו-5 מחזורים בין קריאה של כניסה השניה... פתרון אפרי והוא יעבוד במקרה הזה...

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

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

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

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

זו פונקציה שמטפלת בשינויים על קו דיגיטלי עם התוספת שתצטרכו להוסיף כדי לטפל בפסיקה חומרתית:
קוד: בחר הכל

void onButtonChange()
{
  uint8_t buttonState;

  // If the function is called from an interrupt (ISR mode)
  if (EBF.InInterrupt()) {
    // Pass the processing back to EBF
    EBF.ProcessInterrupt(button);
    // And return from that function
    return;
  }

  // The function will be called again, as a normal run and not from the interrupt

  // We use GetLastValue instead of the GetValue, to use the value that triggered
  // the callback. The digital input might change again since then
  buttonState = button.GetLastValue();

  // Use the LED and serial printouts as a visualization when the change was detected
  led.SetValue(buttonState);
  serial.print("Button changed to: ");
  serial.println(buttonState);
}


את הדוגמה המלאה לשימוש בפסיקות תוכלו לראות כאן.

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

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

שכבת המוצר

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

מעבר לזה, לאובייקטים בסביבת EBF יש מודעות פנימית לזמן, כך שמוצר מסוג לד יכול לבצע אפקטים כמו הבהוב (Blink) בצורה עצמאית בלי שצריך לנהל טיימר בתוכנית (ראו דוגמה Products-LedBlink), או אפילו לעשות fade-in ו-fade-out כדי לייצר אפקט של לד נושם (ראו דוגמה Products-LedFading).

שכבה זו מפשטת עוד יותר את החיים של המתחילים. במקום לדבר על PWM או "נדנוד יציאה דיגיטלית" בקצב שונה כדי לייצר אפקט של דהיה (Fading) או שינוי מהירות המנוע, מדברים על הפעולה הטבעית של המוצר, "הבהוב לד", "מנוע מופעל קדימה בעוצה של 50%" וכדומה.
בכל מקרה כל אחד כנראה יכתוב פונקציה של הפעלת המנוע שתפעיל את הקווים הרלוונטיים, או ישתמש בפונקציית ספריה כלשהי שעושה את זה. אז במקרה של EBF הכל יהיה מרוכז במקום אחד, בתוך היישות הרלוונטית.

עוד נקודה, החלטתי להשתמש באחוזים כדי לייצג ערכים "אנלוגיים" או כאלה שיש להם טווח (כמו ה-PWM). אובייקטים שמעורבים בזה (AnalogRead ו-PwmOutput למשל) יחזירו או יקבלו בפונקציות שלהם ערך float שמציין ערך מ-0% עד 100%. ככה אפשר להחביא שוני בין הבקרים השונים. ב-ATMega328 הרזולוציה של ADC (ממיר אנלוגי לדיגיטלי) היא 10bit, כלומר ערכים מ-0 עד 1023. בבקר אחר הרזולוציה יכולה להיות שונה והערכים המוחזרים מ-()analogRead יהיו שונים. מה שאומר שקוד שאתם כותבים צריך להשתנות כתלות בבקר שבחרתם להשתמש בו ואי אפשר לכתוב קוד אחיד (אלא עם define# שבוחר בין פלטפורמות, שזה כבר למתקדמים). הופתעתי לגלות שבסביבת ארדואינו אין פונקציה שמחזירה את הרזולוציה של ה-ADC, או הערך המקסימלי שאפשר לקבל כדי לכתוב קוד אחיד שיתאים לכל סוג בקר. עוד אכזבה מארדואינו...

יצרתי Issue ב-GitHub שלהם. נראה כמה מהר הם מגיבים שם, אם בכלל.

בטח יהיו כאלה שירימו גבה למה לבזבז זמן להמרת ערכים שלמים ל-float שיקר יחסית מבחינת זמן ביצוע בהשוואה למספרים שלמים (integers). 
התשובה שלי היא שלמתחילים הזמן המבוזבז הזה לא רלוונטי. במקרה הזה לדעתי חשוב יותר לפשט ולהנגיש. פשוט יותר לחשוב על מנוע DC מופעל ב-50% או מנוע סרוו מסתובב 30% לכיוון אחד ו-70% לכיוון השני מאשר ערכי PWM בין 0 ל-255, או שערכים שמוחזרים מ-()analogRead פעם בין 0 ל-1023 ופעם בין 0 ל-4095.

למי שרוצה לחסוך את ההמרה יוכל להשתמש בפונקציות "Raw" של אותו האובייקט, למשל:
קוד: בחר הכל
analogInput.GetValue();
analogInput.GetRawValue();

potentiometer.GetValue();
potentiometer.GetRawValue();

// The brightness should be specified in percents (0 - 100%)
led.SetBrightness(70);


רוצים לנסות? תוכלו להוריד את ספריית ה-EBF ב-GitHub.

אשמח לתגובתכם לפוסט בפייסבוק.

הודעות נוספות:

  • בלוג האם אנחנו מתכנתים נכון עם הארדואינו?

    האם אנחנו מתכנתים נכון עם הארדואינו?

    2024-05-27 13:41:19

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

  • בלוג כמה עולה לייצר כרטיס Arduino UNO?

    כמה עולה לייצר כרטיס Arduino UNO?

    2024-05-05 12:27:26

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

  • בלוג שיפור משמעותי באתר - סינון מוצרים

    שיפור משמעותי באתר - סינון מוצרים

    2024-03-15 14:43:06

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

  • בלוג תוכניות למערכת בית חכם DIY

    תוכניות למערכת בית חכם DIY

    2023-12-17 16:16:30

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

  • בלוג dual boot - הצלחה

    dual boot - הצלחה

    2023-10-05 18:17:07

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