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

העגלה ריקה

ניסוי הקופים כמשל לתקשורת I2C מתוך ISR


2024-08-18 10:55:33

הבעיה

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

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

אאהה, כן... אני עדיין עם הסיפור הזה שאנחנו לא עובדים נכון עם ארדואינו...

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

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

קחו למשל מקודד של מנוע DC שמספק פולסים בהתאם לכמות הסיבובים של ציר המנוע:
"מנוע DC עם מקודד"

דחיתם את הטיפול בפסיקות של קווי המקודד - פספסתם את המיקום אליו המנוע היה אמור להגיע.
בטח מישהו יקפוץ ויגיד שאין שום בעיה לקדם את המונה של המקודד בתוך ה-ISR של הפסיקה... נכון... אבל מה עם המנוע שצריך לעצור? ומה אם בקר המנוע מתקשר דרך I2C?

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

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

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

אל תלמדו מהקופים

נחזור לבעיה שנתקעתי בה. היו לי מצבים שאני מצליח לתקשר דרך I2C מתוך ISR (פונקציית טיפול בפסיקה - Interrupt Service Routine) והיו מצבים שהכל פשוט נתקע, או התבצע באיטיות מוזרה. הבעיה הופיעה אחרי שמיקרובקר מתעורר ממצב שינה עמוקה, אז לא היה לי כיוון אם הבעיה היא בתקשורת I2C, בזה שאני מנסה לבצע את זה מהפסיקה, התעוררות ממצב שינה, או משהו אחר... חשדתי בתקשורת I2C ומכיוון שלא מצאתי תשובות ברשת, החלטתי לשאול את האנשים החכמים בקבוצת ארדואינו בפייסבוק. חלק מהתשובות שמצאתי ציינו שאפשר לעשות את זה עם ספרייה אלטרנטיבית כלשהי, שהיא לא ספרית ה-Wire הרשמית של ארדואינו.

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

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

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


למה זה לא עובד?

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

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

אם נעזוב את פסיכולוגית ההמונים, טיעון הבא שחזר הרבה פעמים בפורומים שחיפשתם בהם את התשובה לבעיה, היה שספריית ה-Wire הרשמית (ספריה שמטפלת בתקשורת I2C) משתמשת בפסיקות, לכן אי אפשר לתקשר דרך I2C מתוך פונקציית ISR כי פסיקות אחרות לא יכולות להגיע, אז הכל פשוט נתקע. בחלק מהמקומות מציעים להשתמש בספריה אחרת, שלא משתמשת בפסיקות, אבל זה היה במקרה של כרטיסים שמבוססים על בקר AVR, כמו ה-UNO וה-NANO. זו היתה אחת הסיבות לשאול את השאלה בקבוצת ארדואינו הישראלית, אולי מישהו יוכל להמליץ על ספריה לא רשמית שמותאמת לבקר SAMD21, איתו אני עושה את הניסויים שלי.

בסופו של דבר עברתי על הקוד של ספריית Wire הרשמית וזה מה שמצאתי:
  • במימוש לבקרי AVR אכן משתמשים בפסיקות, כך שספריה זו לא תוכל לעבוד מתוך ISR. בפורומים מציעים להשתמש בספריה אלטרנטיבית. לא ניסיתי, וגם לא ברור אם זו ספריה שאמורה להיות יותר יציבה מהספרית ה-Wire הרשמית, או שהיא לא משתמשת בפסיקות באותה הצורה כמו Wire.
  • במימוש לבקרי SAMD21 (שזה המקרה שלי), נראה שבעבר השתמשו בפסיקות, אבל שינו את המימוש לצד ה-Master שלא ישתמש בפסיקות לפני יותר מ-10 שנים. צד ה-Slave נשאר עם מימוש שמשתמש בפסיקות, אבל למצב של ה-Slave יש את הפונקציות ()onReceive ו-()onRequest שנקראות מתוך ה-ISR המטפל לתקשורת ה-I2C, כך שאפשר לקבל את הנתונים מיד כשהם מגיעים, ואפשר גם לשלוח את הנתונים מיד כשמבקשים אותם ישירות דרך אותה ספריית Wire. כך שנראה שלפני יותר מ-10 שנים מישהו טיפל בנושא זה ואפשר להשתמש בספריית Wire הרשמית כדי לתקשר דרך I2C מתוך ה-ISR כשמשתמשים בבקר SAMD21 במצב Master של ה-I2C (ואני מנחש שגם בקרים אחרים שמבוססים על ארכיטקטורת ARM).

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

אוסיף גם לגבי הטיעון שאסור להשתמש בתקשורת טורית (UART) מתוך ה-ISR, ששם המימוש עוד יותר פשוט, הכתיבה או (הדפסה עם ()serial.print) היא פעולה שפשוט מעתיקה את הנתונים לתוך buffer פנימי והנתונים נשלחים מתוכו לחומרה, בית אחרי בית, ע"י פסיקה, שתטופל אחרי שתצאו מה-ISR שלכם, כך שהם לא אמורים להפריע אחד לשני.

אז אחרי שהפרחתי את הטיעון הזה, למה זה עדיין לא עובד לאנשים? לדעתי בגלל התנגשות / מירוץ (Race condition).

אגב, הבעיה שאני תקלתי בה לא ממש היתה קשורה ל-I2C, אלא להתעוררות ממצב שינה של הבקר, שבמסלול שחויתי בו את הבעיה פספסתי משהו, שבסופו של דבר גרם למצב בו אני מנסה להשתמש במודול חומרתי בתוך הבקר שאחראי על התקשורת I2C כשהוא לא מקבל שעון Sad emoticon

התנגשות

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

נתחיל מהבסיס. ככה כותבים לרכיב דרך ערוץ ה-I2C:
קוד: בחר הכל
Wire.beginTransmission(address);
Wire.write("Hello");
Wire.endTransmission();


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

אז בואו נוסיף את הבדיקה וגם נראה איך מבצעים קריאה מרכיב I2C, שזו כנראה פעולה שכיחה יותר בעבודה עם חיישנים:
קוד: בחר הכל
uint8_t rc;
uint8_t receivedBytes;
uint8_t value;

Wire.beginTransmission(i2cAddress);
Wire.write(regsterAddress);
rc = Wire.endTransmission(false);
if (rc != 0) {
  return rc;
}

receivedBytes = Wire.requestFrom(i2cAddress, 1);
value = Wire.read();


בהתחלה שולחים לרכיב את כתובת הרגיסטר שרוצים לקבל את הערך שלו, אחרי זה קוראים ל-()requestFrom שתבצע את העברת הנתונים מהרכיב לבקר ותשמור אותם בחוצץ פנימי (במקרה הזה ביקשנו לקרוא בית אחד - הפרמטר השני לפונקציה), וקריאה ל-()read מחזירה את הערך של הבית מתוך החוצץ. צריך לבדוק גם ש-receivedBytes מוחזר עם כמות הבתים שביקשנו (1) כי פונקציה ()requestFrom היא blocking ותחזור אחרי שהתקבלו הנתונים. יש מצבים בהם הרכיב מחזיר פחות בתים ממה שביקשתם לקרוא. שוני קטן נוסף שאתם יכולים לשים אליו לב זה שמעבירים false כפרמטר ל-()endTransmission, שאומר לא לשחרר את ערוץ ה-I2C, כי מיד אחרי זה פונים שוב לרכיב כדי לקבל ממנו נתונים.

אתם כנראה לא פונים לרכיבים בצורה כמו שתיארתי, אלא דרך ספריות מוכנות שיצרני הרכיבים מספקים. אבל בתוך הפונקציות של הספריות האלה יש בדיוק אותן פניות לערוץ ה-I2C.

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

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

אם היינו פונים לערוץ ה-I2C רק מהלולאה הרגילה (כל 10 שניות), הכל היה עובד מעולה. אם היינו פונים לערוץ ה-I2C רק מתוך ה-ISR של הפסיקה, גם הכל היה עובד מעולה. 
אבל מתישהו יגיע הרגע שהלולאה הראשית מתחילה תהליך תקשורת עם חיישן הטמפרטורה ותוך כדי מגיעה פסיקה, שמפסיקה כל מה שלולאה הרגילה עושה ומתחילה תהתיך תקשורת חדש מול הערוץ, מה שישבש את התהליך הקודם. במצב כזה הסיכוי שהערוץ לא יתקע הוא אפסי. לערוץ ה-I2C יש מכונת מצבים שמוכנה לקבל את הפקודות בדיוק בסדר כמו שתיארתי קודם. המימוש לא יודע מה לעשות כשקוראים ל-()beginTransmission פעמיים. וזה לא רק קריאות לפונקציות האלה, גם בתוך הפונקציות מתרחשים דברים בסדר מסויים, כתיבה לרגיסטרים של החומרה, העתקה של הנתונים לתוך ה-buffer למיקום מסויים... הפרעה לתהליך זה היא מתכון בטוח לאסון. אותו הדבר גם לגבי הדפסה ל-Serial.

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

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

כדי לעצור את הפסיקות בסביבת ארדואינו יש לנו את הפונקציה ()noInterrupts. כדי להחזיר את הגעת הפסיקות יש לנו את הפונקציה ()interrupts.
כלומר:
קוד: בחר הכל
noInterrupts();
קוד גישה לערוץ תקשורת
interrupts();


חשוב לא לשכוח לטפל בכל המסלולים היוצאים מהפונקציה שאנחנו רוצים להגן מפני פסיקות. סתם לשים קריאות לשתי הפונקציות המיוחדות האלה בהתחלה ובסוף של הפונקציה שלנו זה לא פתרון טוב, כי יש לנו נקודות יציאה באמצע הפונקציה שלא משחזרות את הפסיקות לקדמותן (לא להשתמש בקוד הבא, יש בו באג!):
קוד: בחר הכל
uint8_t rc;
uint8_t receivedBytes;
uint8_t value;

noInterrupts();

Wire.beginTransmission(i2cAddress);
Wire.write(regsterAddress);
rc = Wire.endTransmission(false);
if (rc != 0) {
  return rc;
}

receivedBytes = Wire.requestFrom(i2cAddress, 1);
if (receivedBytes != 0) {
  return 99;
}
value = Wire.read();

interrupts();


להוסיף קריאה ל-()interrupts בכל נקודת יציאה? אפשר, אבל יש פתרון אלגנטי יותר ע"י שימוש ב-do/while(0) בצורה הבאה:
קוד: בחר הכל
uint8_t i2cReadRegister(uint8_t i2cAddress, uint8_t registerAddress, uint8_t& value)
{
  uint8_t rc = 0;
  uint8_t receivedBytes;

  noInterrupts();

  do {
    Wire.beginTransmission(i2cAddress);
    Wire.write(regsterAddress);
    rc = Wire.endTransmission(false);
    if (rc != 0) {
      break;
    }

    receivedBytes = Wire.requestFrom(i2cAddress, 1);
    if (receivedBytes != 0) {
      rc = 99;
      break;
    }
   
    value = Wire.read();
  } while(0);

  interrupts();

  return rc;
}


כשעוטפים בלוק של קוד עם do/while(0), הוא יתבצע רק פעם אחת, אבל תוכלו להפסיק את ביצוע הפקודות בתוך הבלוק ע"י break, מבלי לצאת מהפונקציה, כך שעדיין יש את ההזדמנות להחזיר את הפסיקות לפעולה ע"י קריאה ל-()interrupts.

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

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

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

עדיין סקפטיים? תנסו ותבדקו!

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

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

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

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

    האם אנחנו מתכנתים נכון עם הארדואינו? - חלק 2

    2024-06-27 14:45:28

    מה יהיה פוסט הזה?
    אספר לכם על מעבר לכרטיס SAMD21 Mini של SparkFun, הגעה ל-22uA במצב שינה, עוד קיטורים על הספריות הקיימות, מטרות הרעיון שמאחורי כל העבודה הזו

  • בלוג ספריית ה-Event Based Framework

    ספריית ה-Event Based Framework

    2024-06-06 15:04:27

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

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

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

    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

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