ארדואינו למתחילים: Debouncing

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

Debouncing - להתמודד עם דברים שקופצים
Debouncing – להתמודד עם דברים שקופצים

כפי ששמה מרמז, פעולת ה-Debouncing (יש לזה שם בעברית?) נלחמת בתופעת ה-Bouncing, שהיא חולשה מובנית כמעט בכל רכיב שמתרגם תנועה מכנית – אולי אפילו מאורע פיזיקלי כלשהו – לאות חשמלי.

הדוגמה שנציג כאן, ושהחומרה שלה מופיעה בתמונה למעלה, עושה שימוש בלחצן מהסוג הפשוט ביותר. צד אחד שלו מחובר ל-Ground של הארדואינו, והצד השני לפין קלט שהוגדר עבורו Pull-up resistor. כלומר, בתאוריה, הפין יזהה מתח (HIGH) עד שמישהו ילחץ על הלחצן, ואז – כל עוד ממשיכים ללחוץ – הקלט יהיה LOW.

התוכנה שכתבתי סופרת לחיצות, ומציגה ב-Serial Monitor גם את הזמן שעבר, במילי-שניות, מהלחיצה הקודמת ועד לזו הנוכחית. זיהוי הלחיצה עצמה נעשה באמצעות הצמדה של פסיקת חומרה לפין הקלט. פסיקה זו מריצה את הפונקציה count בכל פעם שהקלט יורד מ-HIGH ל-LOW, בהתאם למה שאמרנו קודם. למי שרוצה לראות, הנה הקוד – אם כי הפרטים הקטנים לא באמת חיוניים לנושא הפוסט:

volatile int counter = 0;
int oldCounter = counter;
unsigned long lastMillis;

void count() {
   counter++;
}

void setup() {

  Serial.begin(9600);

  pinMode(2, INPUT);
  digitalWrite(2, HIGH); // Set inner pull-up resistor
  attachInterrupt(0, count, FALLING);

  Serial.println("Ready when you are...");
  lastMillis = millis();    

}

void loop() {

 if (oldCounter != counter) {

   Serial.print(counter);
   Serial.print(" d(ms) = ");
   Serial.println(millis() - lastMillis);

   oldCounter = counter;
   lastMillis = millis();

 } 

}

והנה מה שקיבלתי אחרי לחיצה אחת בלבד. היה לי אמנם מזל – בדרך כלל לא מתקבלות תוצאות כל כך קיצוניות – אבל בכל זאת:

פלט לדוגמה של התוכנה
שמונה-עשרה לחיצות בחמש אלפיות השניה?!

מה לכל הרוחות קורה פה? אני לא באמת כזה זריז, וכמו שציינתי קודם לחצתי על הלחצן (למיטב ידיעתי) פעם אחת בלבד. למעשה, אני בספק אם יש בעולם כולו אדם שמסוגל ללחוץ על מתג כזה פעמיים ברווח של פחות ממאה מילי-שניות, שלא לדבר על שתיים ומטה! אגב, הסיבה לכך שהלחיצות 5,6,7,8,9 ו-11 עד 20 לא רשומות היא שהן אירעו תוך כדי שבריר השניה שהארדואינו היה עסוק בכתיבת התוצאות הקודמות!

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

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

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

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

if (digitalRead(buttonPin) == HIGH) {
  delay(5);
  if (digitalRead(buttonPin) == HIGH) buttonPressed = true;
}

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

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

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

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

אני תמיד אוהב לקרוא את התיעוד של הקוד. מהתיעוד למדתי:
1. על השימוש ב VOLATILE למשתנים בתוך האינטרפט
2. על שימוש לא מומלץ בפונקציה attachInterrupt ללא הפונקציה digitalPinToInterrupt

הכל בקישור הזה: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

שורה אחת מאוד סיקרנה אותי ואני לא מצליח להבין למה השתמשת בהגדרה הזו:
volatile int counter = 0
למה volatile? קראתי קצת ועדיין איני מבין את המשמעות בקוד שלך… אשמח להסבר
תודה
🙂

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

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

נושא חשוב… תודה!

מאמר מצויין!
פשוט ומסביר כמו שצריך.
למה ה"קפיצות" קורות במקרה הזה אבל במאמר על כוחות משיכה זה לא קרה?
פשוט ה"קפיצות" מהירות מדי שיראו אותן?
[https://www.idogendel.com/whitebyte/archives/354]

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

אם כבר debouncing בתוכנה, למה לא להגדיר שמתעלמים מסף מסוים ומטה? ז"א millis() – lastMillis מתחת ל-100 מילישניות, צריך להתעלם כי זו בעצם לא לחיצה