מבואות ספרתיים: חיבור חיישן ספרתי לבקר
חיישן ספרתי מוסר לבקר דבר אחד בלבד: כן או לא. לחצן לחוץ או משוחרר, עצם מול החיישן או לא, דלת פתוחה או סגורה. זה נשמע פשוט, אבל מאחורי הפונקציה digitalRead() מסתתרות שאלות חשמליות של ממש: איזה מתח נקרא 1? מה קורא הדק שאינו מחובר? מדוע התוכנית סופרת עשר לחיצות במקום לחיצה אחת? ואיך מחברים חיישן שעובד במתח שונה ממתח הבקר בלי לגרום נזק? בפרק זה נענה על השאלות האלה, ולכל מקרה נכתוב מעגל חיבור וסקיצה מלאה.
- נקבע את רמות המתח ש-Arduino Uno ו-ESP32 קוראים כ-HIGH וכ-LOW, ואת התחום הלא מוגדר שביניהן.
- נסביר את בעיית המבוא הצף ונפתור אותה בנגד משיכה למעלה או למטה, חיצוני או פנימי, ונחשב את הזרם בו.
- נספור לחיצות בעזרת זיהוי קצה, ונסנן קפיצות מגע בתוכנה בעזרת הפונקציה millis().
- נתכנן מחלק מתח כדי לקרוא חיישן שעובד ב-5 V בבקר שעובד ב-3.3 V.
- נבודד אות תעשייתי של 24 V בעזרת מצמד אופטי ונחשב את שני הנגדים במעגל.
- נספור פולסים מהירים בעזרת פסיקה חיצונית attachInterrupt().
רמות לוגיות במבוא ספרתי (Logic Levels)
מבוא ספרתי אינו מודד מתח כמו וולטמטר. הוא משווה את מתח ההדק לשני ספים, ומחזיר לתוכנית אחד משני ערכים: HIGH (1) או LOW (0).
בבקר ATmega328P שבלוח Arduino Uno, גיליון הנתונים נותן את שני הספים כשבר של מתח ההזנה VCC, וב-ESP32 כשבר של VDD:
| סימן | משמעות | יחידת מידה |
|---|---|---|
| VIH | המתח הקטן ביותר שנקרא בוודאות HIGH | [V] |
| VIL | המתח הגדול ביותר שנקרא בוודאות LOW | [V] |
| VCC | מתח ההזנה של הבקר, 5 V ב-Uno | [V] |
וב-ESP32 (לפי גיליון הנתונים): VIH = 0.75·VDD ו-VIL = 0.25·VDD, כאשר VDD = 3.3 V.
0.8 V < 1.5 V, לכן נקרא LOW. המתח 4.1 V > 3 V, לכן נקרא HIGH.
המתח 2.2 V נמצא בין 1.5 V ל-3 V: בתחום הלא מוגדר. הקריאה אינה מובטחת, והיא עשויה להשתנות מפעם לפעם.
מבוא צף ונגדי משיכה (Floating Input, Pull-up, Pull-down)
נתבונן בלחצן שמחובר רק בין 5 V להדק D7 (הסרטוט הימני). בלחיצה ההדק מגיע ל-5 V ונקרא 1. אבל כשמשחררים את הלחצן, ההדק אינו מחובר לשום דבר: הוא צף, ולקריאה אין משמעות. הפתרון: נגד שמושך את ההדק לרמה קבועה כשהלחצן משוחרר.
בתוכנית נגדיר את ההדק כמבוא רגיל pinMode(7, INPUT), והפונקציה digitalRead(7) תחזיר 1 בלחיצה ו-0 בשחרור.
עם 220 Ω הזרם הוא 22.7 mA: בזבוז ללא תועלת, כי המבוא כמעט אינו צורך זרם. וכשהלחצן משוחרר זורם בנגד רק זרם הזליגה:
וזה קטן בהרבה מ-VIL = 1.5 V, ולכן ההדק נקרא 0 בבטחה.
נגד משיכה למעלה ולוגיקה הפוכה:
זו לוגיקה הפוכה: לחיצה נותנת 0 ושחרור נותן 1. החיבור עם נגד משיכה למעלה הוא הנפוץ ביותר, כי הלחצן מתחבר רק ל-GND, ואין צורך למשוך חוט 5 V אל לוח הלחצנים. ב-ATmega328P יש נגד משיכה למעלה פנימי בכל הדק, ואנו מפעילים אותו בפקודה pinMode(pin, INPUT_PULLUP), כך שאין צורך כלל בנגד חיצוני. ערכו בין 20 kΩ ל-50 kΩ (ערך טיפוסי לפי גיליון הנתונים), כלומר אין זה ערך מדויק.
const int BTN = 2;
const int LED = 13; // on-board LED
void setup() {
pinMode(BTN, INPUT_PULLUP); // idle = HIGH
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
int raw = digitalRead(BTN);
bool pressed = (raw == LOW); // inverted logic
digitalWrite(LED, pressed ? HIGH : LOW);
Serial.print("raw = ");
Serial.print(raw);
if (pressed) Serial.println(" pressed");
else Serial.println(" released");
delay(300);
}raw = 1 released raw = 1 released raw = 1 released raw = 0 pressed raw = 0 pressed raw = 1 released raw = 1 released
קפיצות מגע, זיהוי קצה וסינון קפיצות (Contact Bounce and Debouncing)
זיהוי קצה:
אנו רוצים לספור כמה פעמים נלחץ הלחצן. הטעות הראשונה: להגדיל את המונה כל עוד הלחצן לחוץ:
if (digitalRead(BTN) == LOW) { // wrong: true in every loop while held
count++;
}הפונקציה loop() רצה אלפי פעמים בשנייה, ולכן לחיצה אחת מגדילה את המונה מאות או אלפי פעמים. לחיצה היא שינוי של המצב ולא המצב עצמו, ולכן נחפש את הקצה.
אבל זיהוי קצה לבדו אינו מספיק: כל קפיצה היא גם קצה יורד. לכן בתוכנית הבאה נשלב זיהוי קצה עם סינון קפיצות.
סינון קפיצות בתוכנה:
ב-lastChange נשמור את זמן השינוי האחרון בקריאה. אם עברו מאז DEBOUNCE_MS אלפיות שנייה, והקריאה שונה מהמצב המקובל stableState, נקבל אותה. ואם המצב החדש הוא LOW, זהו קצה יורד ודאי: לחיצה אחת. איננו משתמשים ב-delay(), כי היא עוצרת את כל התוכנית, ואילו millis() נותנת את הזמן מאז ההפעלה בלי לעצור.
const int BTN = 2;
const unsigned long DEBOUNCE_MS = 30;
int lastReading = HIGH; // previous raw reading
int stableState = HIGH; // accepted state
unsigned long lastChange = 0, presses = 0;
void setup() {
pinMode(BTN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
int reading = digitalRead(BTN);
if (reading != lastReading) { // contact moved
lastChange = now; // restart timer
lastReading = reading;
}
bool settled = (now - lastChange >= DEBOUNCE_MS);
if (settled && reading != stableState) {
stableState = reading; // accept it
if (stableState == LOW) { // falling edge
presses++;
Serial.print("t = ");
Serial.print(now);
Serial.print(" ms presses = ");
Serial.println(presses);
}
}
}t = 1232 ms presses = 1 t = 2482 ms presses = 2 t = 3743 ms presses = 3
t = 1200 ms presses = 1 t = 1200 ms presses = 2 t = 1201 ms presses = 3 t = 1202 ms presses = 4 t = 1380 ms presses = 5 t = 1381 ms presses = 6
לחצנו על הלחצן 3 פעמים, והמסך הראשון מראה 3 לחיצות. במסך השני הגדרנו DEBOUNCE_MS = 0, כך שכל קצה מתקבל מיד, וזה זיהוי קצה לבדו. הלחיצה הראשונה לבדה נספרה 6 פעמים: 4 קצוות בלחיצה ו-2 בשחרור, כשהמתכת נגעה שוב לרגע. ואחרי שלוש הלחיצות הגיע המונה ל-17 במקום 3.
השינוי האחרון בקריאה היה ב-1202.6 ms, והפונקציה millis() נותנת מספר שלם, ולכן נשמר lastChange = 1202.
המצב החדש מתקבל כשעוברים DEBOUNCE_MS (כלומר 30 ms) בלי שינוי:
העיכוב של 32 ms בין הנגיעה להדפסה אינו מורגש לאדם, וכל הקפיצות כבר סוננו.
חיבור חיישן 5 V לבקר 3.3 V (Level Shifting with a Voltage Divider)
מודולי חיישנים רבים עובדים ב-5 V והמוצא הספרתי שלהם 5 V, למשל ההדק ECHO בחיישן העל-קולי HC-SR04 (פרק 7). לא נחבר מוצא כזה ישירות להדק של ESP32 או של רספברי פיי. הפתרון הפשוט ביותר לאותות איטיים יחסית הוא מחלק מתח:
| סימן | משמעות | יחידת מידה |
|---|---|---|
| Vin | מתח המוצא של החיישן | [V] |
| Vout | המתח בהדק הבקר | [V] |
| R1 , R2 | הנגד בטור, והנגד אל GND | [Ω] |
שני תנאים לתכנון: Vout ≤ 3.3 V גם כאשר Vin גבוה מעט מ-5 V, ו-Vout ≥ VIH = 2.475 V כדי שייקרא 1 בביטחון.
הערך התקני 18 kΩ נותן ב-5.25 V מתח של 3.38 V, יותר מ-3.3 V. לכן נבחר R2 = 15 kΩ:
3 V ≥ 2.475 V: נקרא HIGH, ו-3.15 V < 3.3 V: ההדק מוגן.
const int SENSOR = 27; // via 10k / 15k divider
int lastState = LOW;
unsigned long objects = 0;
void setup() {
pinMode(SENSOR, INPUT);
Serial.begin(9600);
}
void loop() {
int state = digitalRead(SENSOR);
if (lastState == LOW && state == HIGH) {
objects++; // rising edge
Serial.print("objects = ");
Serial.println(objects);
}
lastState = state;
}objects = 1 objects = 2 objects = 3
בידוד אות תעשייתי 24 V במצמד אופטי (Optocoupler Isolation)
חיישנים תעשייתיים ומפסקי גבול בלוחות בקרה עובדים בדרך כלל במתח ישר של 24 V. מחלק מתח אינו פתרון טוב כאן: החוטים ארוכים, והרעש והקוצים החשמליים במפעל עלולים להגיע לבקר. הפתרון התעשייתי הוא בידוד.
נשתמש במצמד האופטי הנפוץ PC817. הערכים (ערכים טיפוסיים לפי גיליון הנתונים): המתח הקדמי של ה-LED VF ≈ 1.2 V; יחס העברת זרם CTR = ICIF אינו קטן מ-50% ב-IF = 5 mA; ומתח הרוויה VCE,sat ≤ 0.2 V.
גם כאן הלוגיקה הפוכה: יש אות 24 V ← ה-LED מאיר ← הטרנזיסטור מוליך ← ההדק D4 ב-0 V (LOW). בלי אות הטרנזיסטור נתוק, ונגד המשיכה למעלה RPU מושך את ההדק ל-5 V (HIGH).
(א) נחשב את RLED לזרם IF = 5 mA, נבחר ערך תקני ונחשב את ההספק בו.
(ב) נבדוק שהטרנזיסטור נכנס לרוויה עם RPU = 10 kΩ.
(א) המתח על הנגד הוא מתח האות פחות המתח על ה-LED:
נבחר את הערך התקני 4.7 kΩ ונחשב את הזרם ואת ההספק בפועל:
נבחר נגד בהספק של 0.25 W לפחות.
(ב) הזרם שנגד המשיכה למעלה צריך כדי שההדק ירד למתח הרוויה, והזרם שהמצמד מבטיח:
2.43 mA > 0.48 mA: הטרנזיסטור ברוויה, ומתח ההדק כ-0.2 V, נמוך בהרבה מ-VIL = 1.5 V.
const int IN24 = 4; // opto output, 10k pull-up
int lastState = HIGH; // HIGH = no 24 V signal
unsigned long items = 0;
void setup() {
pinMode(IN24, INPUT);
Serial.begin(9600);
}
void loop() {
int state = digitalRead(IN24);
if (lastState == HIGH && state == LOW) {
items++; // 24 V appeared
Serial.print("items = ");
Serial.println(items);
}
lastState = state;
}items = 1 items = 2 items = 3
פסיקות חומרה לספירת פולסים (Hardware Interrupts)
בכל התוכניות הקודמות loop() קראה את ההדק שוב ושוב. שיטה זו נקראת תשאול. אם התוכנית עסוקה בפעולה ארוכה, כמו קריאת חיישן איטי או כתיבה לתצוגה, פולס קצר עלול להגיע ולחלוף בלי שהיא תבחין בו.
דוגמה: למאוורר מחשב יש הדק TACH שנותן שני פולסים בכל סיבוב (לפי המפרט המקובל של מאווררי מחשב). המוצא הוא מסוג "קולט פתוח": הטרנזיסטור מושך את הקו רק ל-GND, ולכן נדרש נגד משיכה למעלה ל-5 V. המאוורר מוזן ב-12 V, אבל המוצא שלו אינו נושא 12 V, ולכן מספיקה אדמה משותפת.
const int TACH = 2; // INT0 on Uno
const unsigned long PULSES_PER_REV = 2;
volatile unsigned long pulses = 0; // set in ISR
unsigned long lastPrint = 0;
void onPulse() { pulses++; } // ISR: short
void setup() {
pinMode(TACH, INPUT); // 10k pull-up to 5V
attachInterrupt(digitalPinToInterrupt(TACH),
onPulse, FALLING);
Serial.begin(9600);
}
void loop() {
if (millis() - lastPrint >= 1000) {
lastPrint += 1000;
noInterrupts(); // copy, then reset
unsigned long n = pulses;
pulses = 0;
interrupts();
Serial.print("pulses = ");
Serial.print(n);
Serial.print(" rpm = ");
Serial.println(n * 60 / PULSES_PER_REV);
}
}pulses = 0 rpm = 0 pulses = 30 rpm = 900 pulses = 46 rpm = 1380 pulses = 54 rpm = 1620 pulses = 58 rpm = 1740 pulses = 59 rpm = 1770
- המטפל בפסיקה קצר: הוא רק מגדיל את המונה. אין בתוכו Serial ואין delay(), אחרת התוכנית עלולה להיתקע.
- משתנה שמשתנה בתוך המטפל מוגדר volatile, כדי ש-loop() תקרא אותו מהזיכרון בכל פעם.
- הבקר ב-Uno קורא משתנה של 4 בתים בכמה צעדים, ולכן נעתיק אותו בין noInterrupts() ל-interrupts().
| סימן | משמעות | יחידת מידה |
|---|---|---|
| n | מהירות הסיבוב | [rpm] |
| N | מספר הפולסים בשנייה אחת | — |
| p | מספר הפולסים בכל סיבוב | — |
פולס כל 16.9 ms בקירוב: תוכנית שיש בה, למשל, delay(100) הייתה מחמיצה את רובם, ואילו הפסיקה קולטת את כולם.
סיכום נוסחאות הפרק:
| החוק | הנוסחה |
|---|---|
| סף הרמה הגבוהה ב-ATmega328P (Uno) | VIH = 0.6·VCC |
| סף הרמה הנמוכה ב-ATmega328P (Uno) | VIL = 0.3·VCC |
| סף הרמה הגבוהה ב-ESP32 | VIH = 0.75·VDD |
| סף הרמה הנמוכה ב-ESP32 | VIL = 0.25·VDD |
| הזרם בנגד משיכה למעלה או למטה בלחיצה | I = VCCR |
| מחלק מתח | Vout = Vin·R2R1 + R2 |
| נגד ה-LED במצמד האופטי | RLED = Vin − VFIF |
| תנאי הרוויה במצמד האופטי | CTR·IF ≥ VCC − VCE,satRPU |
| מהירות סיבוב מתוך הפולסים | n = 60·Np |
מילון מונחים:
| עברית | English | العربية |
|---|---|---|
| מבוא ספרתי | Digital input | المدخل الرقمي |
| רמה לוגית | Logic level | المستوى المنطقي |
| סף רמה גבוהה / נמוכה | V_IH / V_IL threshold | عتبة المستوى العالي / المنخفض |
| תחום לא מוגדר | Undefined region | المنطقة غير المعرّفة |
| מבוא צף | Floating input | المدخل العائم |
| נגד משיכה למעלה | Pull-up resistor | مقاومة الرفع (pull-up) |
| נגד משיכה למטה | Pull-down resistor | مقاومة الخفض (pull-down) |
| לוגיקה הפוכה | Inverted (active-low) logic | المنطق المعكوس |
| קפיצות מגע (ריטוט) | Contact bounce | ارتداد التلامس |
| סינון קפיצות | Debouncing | إزالة الارتداد |
| זיהוי קצה | Edge detection | كشف الحافة |
| קצה עולה / יורד | Rising / falling edge | الحافة الصاعدة / الهابطة |
| מחלק מתח | Voltage divider | مُجزِّء الجهد |
| מצמד אופטי | Optocoupler | المقرن الضوئي |
| יחס העברת זרם | Current transfer ratio (CTR) | نسبة نقل التيار |
| תשאול | Polling | الاستطلاع |
| פסיקה | Interrupt | المقاطعة |
| מטפל בפסיקה | Interrupt service routine (ISR) | معالج المقاطعة |
שאלות לתרגול:
(א) 0.5 V
(ב) 1.6 V
(ג) 2.9 V?