>9: קלט מספרי מהסריאל מוניטור

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

הגדרת הבעיה

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

משתנה מטיפוס בייט יכול לקבל ערך בין 0 ל-255 (כולל), וקוד ASCII המפורסם הוא תקן שמקצה לכל אחד מהמספרים הללו תו יחיד. לדוגמה, בקוד ASCII התו שמספרו 48 הוא "0" (הספרה אפס), התו שמספרו 49 הוא הספרה "1", וכן הלאה עד הספרה "9" שמספרה 57. במקום 58 בטבלת ASCII כבר נמצא תו שאינו קשור למספרים – הנקודתיים (":").

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

האפשרות השניה היא לשלוח את המספר כמו שאנחנו כותבים אותו – בספרות נפרדות, "1" ואחריה "0". אלא שזה יוצר קושי חדש, והפעם בצד המקבל. איך הארדואינו יכול לדעת שהכוונה ל-10 ולא לאחד ולאפס בנפרד? או אולי זו בכלל רק ההתחלה של 103, או של 1068599?

לסיום, הקש סולמית

אם אסור לנו להניח הנחות מקלות כלשהן לגבי השידור, כגון חלון זמן מרבי או מספר התווים הצפוי, הפתרון המעשי היחיד(?) יהיה להכריח את המשתמש לציין את סיום הקלט באמצעות תו מפסיק כלשהו (Terminator). אנחנו מתחילים את הקריאה עם משתנה שערכו 0, ועם כל תו ספרה חדש שמגיע, אנחנו מכפילים אותו בעשר ומוסיפים לו את הספרה החדשה – לא לשכוח להפחית ממנה 48 אם היא מגיעה בקוד ASCII. הקריאה מסתיימת כאשר מופיע התו שנבחר כ-Terminator.

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

#define TERMINATOR '#'
unsigned long n = 0;

void setup() {
 Serial.begin(9600);
}

void loop() {

  int c;

  // Wait for input, then read it
  while (Serial.available() == 0) ;
  c = Serial.read();

  // Is it the end of a number?
  if (c == TERMINATOR) {

    // Process and display
    Serial.print(n);
    Serial.print(" - 1 = ");
    Serial.println(n - 1);
    n = 0; // Reset number

  } else n = n * 10 + (c - 48); 

}

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

פלט התוכנה לקריאת מספרים באמצעות תו סיום

פלט התוכנה לקריאת מספרים באמצעות תו סיום

אגב, הטיפוס של המשתנה c בקוד הוא  int, אף על פי שאני משתמש בו כ-char (תו). הסיבה לכך היא שהפונקציה Serial.read עשויה להחזיר במקרי קצה מסוימים ערך שגיאה של 1-, ולכן היא הוגדרה להחזרת int. הקוד הספציפי הזה היה עובד גם אם c היה מוגדר כ-char או unsigned char – פשוט כדאי להיות מודעים לעניין.

הכל בחיים זה טיימינג

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

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

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

unsigned long timeStamp;
unsigned int timeOut = 2; // ms
unsigned long n = 0;

void setup() {
 Serial.begin(9600);
}

void loop() {

  // Wait for input
  while (Serial.available() == 0) ;

  do {

    if (Serial.available()) {
      n = n * 10 + (Serial.read() - 48); 
      timeStamp = millis();
    }  

  } while (millis() - timeStamp < timeOut); 

   Serial.print(n);
   Serial.print(" - 1 = ");
   Serial.println(n - 1);
   n = 0; // Reset number

}

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

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

מעבר למוניטור

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

תוספת: בעקבות הערה בפורום ב-[עריכה: האתר לא קיים יותר], מן הראוי לציין פונקציה מובנית של ארדואינו, בשם Serial.parseInt, שמממשת למעשה את שתי הטכניקות הנ"ל גם יחד. היא מחזירה מספרים שלמים שהגיעו דרך החיבור הסריאלי, ומזהה את הסיום שלהם לפי תו שאינו ספרה או לפי מעבר של שניה שלמה (או כפי שמוגדר באמצעות הפונקציה Serial.setTimeout) ללא קלט נוסף.

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

מוזר לי שלא הזכרת את הפונקציה Serial.flush בהקשר הזה
http://arduino.cc/en/Serial/Flush

זה מה שהתיעוד שלה אומר, בפועל היא גם מחכה עד שהקלט ייגמר.
תנסה להריץ את זה (קוד שכתבתי כשעשיתי את צעדיי הראשונים בארדואינו, על uno, אני יודע שיש גרסאות שזה לא יעבוד עליהם)
void setup() {
Serial.begin(9600);
}

void loop() {
if(Serial.available()){
int n=getnum();
Serial.println(n);
}
}

long getnum(){
Serial.flush();
byte cur;
long n=0;
do{
cur=Serial.read();
if(cur='0'){
n*=10;
n+=cur-'0';
}
else{
Serial.write("iligall character: number expected");
Serial.println(cur);
return n;
}
}
while(Serial.available() );
return n;
}