תשעה חיישנים — מה כל אחד מוציא, איך מחברים אותו, ואיזה קוד מריץ אותו על ארדואינו ועל ESP32
חיישן הוא רכיב שהופך גודל פיזיקלי — חום, אור, מרחק, זרם — לגודל חשמלי שהבקר יודע לקרוא. כל השאר בדף הזה נובע משאלה אחת: מה בדיוק יוצא מן הרגל של החיישן. יש חיישנים שמוציאים מתח, יש שרק משנים התנגדות ואינם מוציאים דבר, ויש ששולחים מספר מוכן בפרוטוקול ספרתי. שלושת המקרים דורשים שלושה חיבורים שונים ושלוש שורות קוד שונות.
המתח על הרגל עולה ויורד עם הגודל הנמדד. הבקר קורא אותו בממיר אנלוגי-ספרתי ומקבל מספר. דוגמאות: LM35, פוטנציומטר, ACS712.
אינו מוציא מתח כלל. חייבים לחבר אותו בטור עם נגד קבוע, והנקודה שביניהם היא המבוא. זהו מחלק מתח. דוגמה: נגד תלוי אור.
בתוכו כבר יושב ממיר, והוא שולח את התוצאה כרצף סיביות. אין כאן מדידת מתח אלא פרוטוקול. דוגמאות: DHT22, DS18B20.
הדף הזה עוסק ברכיבים. את מפות הפינים של הלוחות תמצאו במדריכים הייעודיים, ואת תורת הממיר האנלוגי-ספרתי — ברזולוציה, בהנחתה ובפסיקות — בפרק 33.9004. הקישורים בתחתית הדף.
בתוך המארז יושבים שני חיישנים — נגד רגיש לחום וחיישן לחות קיבולי — ולצדם מעגל שממיר את שניהם למספרים ומשדר אותם. לכן הבקר אינו מודד כאן מתח כלל: הוא משחרר את המוליך, מקשיב לרצף של ארבעים סיביות, ומקבל טמפרטורה, לחות וסיבית ביקורת שמאמתת שהקריאה לא נשברה בדרך.
#include <DHT.h>
#define DHT_PIN 4
#define DHT_MODEL DHT22
DHT dht(DHT_PIN, DHT_MODEL);
void setup() {
Serial.begin(9600);
dht.begin();
}
void loop() {
delay(2000); // the sensor answers once per 2 s
float t = dht.readTemperature(); // Celsius
float h = dht.readHumidity(); // %RH
if (isnan(t) || isnan(h)) { // checksum failed - drop the sample
Serial.println("read failed");
return;
}
Serial.print(t); Serial.print(" C ");
Serial.print(h); Serial.println(" %");
}
nan ולא שגיאה. אם התוכנית קוראת פעמיים בשנייה, החיישן פשוט אינו מספיק לענות — והתלמיד מחפש תקלה בחיווט שאינה שם. isnan אינו קישוט: הוא הבדיקה.הרכיב הפשוט ביותר להבנה בדף כולו: הוא מוציא מתח שעולה בקו ישר עם הטמפרטורה, עשרה מילי-וולט לכל מעלת צלזיוס. אין מקדם לחשב ואין טבלה לתרגם — מחלקים את המתח בעשירית ומקבלים מעלות. הליניאריות היא בדיוק מה שהופך אותו לרכיב הנכון ללמד בו מדידה אנלוגית.
34.const int PIN_LM35 = 34; // ADC1 only - ADC2 dies when WiFi is on
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 0 .. 4095
}
void loop() {
long sum = 0; // one sample shakes - average 16
for (int i = 0; i < 16; i++) { sum += analogRead(PIN_LM35); delay(2); }
float volts = (sum / 16.0) * 3.3 / 4095.0;
float celsius = volts * 100.0; // 10 mV per degree
Serial.println(celsius, 1);
delay(500);
}
const int PIN_LM35 = A0;
void setup() {
Serial.begin(9600); // Uno: 10 bit, 5 V reference
}
void loop() {
long sum = 0;
for (int i = 0; i < 16; i++) { sum += analogRead(PIN_LM35); delay(2); }
float volts = (sum / 16.0) * 5.0 / 1023.0;
float celsius = volts * 100.0;
Serial.println(celsius, 1);
delay(500);
}
מה שמייחד אותו אינו הדיוק אלא הכתובת: לכל יחידה כתובת בת שישים וארבע סיביות שנצרבה בה במפעל, ייחודית בעולם. לכן אפשר לתלות עשרה חיישנים על אותם שלושה מוליכים ולפנות לכל אחד בשמו — בדיוק מה שדרוש כשמודדים טמפרטורה בכמה נקודות של אותה מערכת. הוא גם מגיע בגרסה עמידת מים, ולכן הוא הבחירה למים ולנוזלים, שם ה-DHT22 פשוט אינו יכול לחיות.
#include <OneWire.h>
#include <DallasTemperature.h>
const int PIN_BUS = 4; // one wire, many sensors
OneWire bus(PIN_BUS);
DallasTemperature probes(&bus);
void setup() {
Serial.begin(9600);
probes.begin();
probes.setResolution(12); // 12 bit -> 0.0625 C, ~750 ms
Serial.print("found: ");
Serial.println(probes.getDeviceCount());
}
void loop() {
probes.requestTemperatures(); // ask every probe at once
for (int i = 0; i < probes.getDeviceCount(); i++) {
float c = probes.getTempCByIndex(i);
if (c == DEVICE_DISCONNECTED_C) continue;
Serial.print(i); Serial.print(": "); Serial.println(c);
}
delay(1000);
}
הוא אינו מוציא מתח, ולכן אי אפשר לחבר אותו לרגל ולצפות לקריאה. כל תפקידו הוא לשנות התנגדות — ממאות אלפי אוהם בחושך עד אלפים בודדים באור חזק — ולכן חייבים לחבר אותו בטור עם נגד קבוע. הנקודה שבין השניים היא המבוא, וזהו בדיוק מחלק מתח. הבחירה איזה מן השניים יהיה למעלה קובעת אם קריאה גבוהה פירושה אור או דווקא חושך.
const int PIN_LDR = 35; // ADC1
const float VCC = 3.3;
const float R_FIX = 10000.0; // to ground
void setup() {
Serial.begin(115200);
analogReadResolution(12);
}
void loop() {
int raw = analogRead(PIN_LDR);
float v = raw * VCC / 4095.0;
if (v < 0.01) v = 0.01; // never divide by a dark zero
float r_ldr = R_FIX * (VCC - v) / v;
Serial.print(raw); Serial.print(" ");
Serial.print(v, 2); Serial.print(" V ");
Serial.print(r_ldr / 1000.0, 1); Serial.println(" kOhm");
delay(200);
}
const int PIN_LDR = A0;
const float VCC = 5.0;
const float R_FIX = 10000.0;
void setup() {
Serial.begin(9600); // Uno: 10 bit -> 0 .. 1023
}
void loop() {
int raw = analogRead(PIN_LDR);
float v = raw * VCC / 1023.0;
if (v < 0.01) v = 0.01;
float r_ldr = R_FIX * (VCC - v) / v;
Serial.print(raw); Serial.print(" ");
Serial.print(r_ldr / 1000.0, 1); Serial.println(" kOhm");
delay(200);
}
הרכיב הזה אינו יודע מהו מרחק. הוא יודע דבר אחד: לשלוח פרץ קול שהאוזן אינה שומעת, ולהחזיק רגל אחת גבוהה בדיוק כל עוד ההד חוזר. כל השאר קורה בקוד — הבקר מודד את אורך הפולס במיקרו-שניות, ומחלק. המספר 58 שמופיע בכל תוכנית איננו קסם: הקול עושה כסנטימטר בכל 29 מיקרו-שניות, והמרחק נעשה פעמיים — הלוך ושוב.
const int PIN_TRIG = 5;
const int PIN_ECHO = 18; // ESP32: through a 1k/2k divider
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
}
void loop() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); // the burst
digitalWrite(PIN_TRIG, LOW);
unsigned long us = pulseIn(PIN_ECHO, HIGH, 30000UL); // give up after 30 ms
if (us == 0) { // nothing came back - out of range
Serial.println("no echo");
} else {
Serial.print(us / 58.0, 1); // 29 us per cm, there and back
Serial.println(" cm");
}
delay(100); // let the echo die before asking again
}
pulseIn בלי תקרת זמן עוצר את התוכנית כשאין הד חוזר, והלוח נראה תקוע. שני הארגומנטים אינם רשות.חשוב להבין מה הוא באמת מגלה, כי השם מטעה: הוא אינו מגלה אדם ואינו מגלה תנועה — הוא מגלה שינוי בקרינת החום שמגיעה אליו. אדם שיושב בלי לזוז נעלם ממנו כעבור זמן קצר, בעוד וילון שמתנדנד מול חלון שמשי מדליק אותו. הכיפה הלבנה שמעליו אינה קישוט אלא עדשה שמפצלת את שדה הראייה לגזרות, וכל מעבר בין גזרה לגזרה הוא מה שמייצר את ההבדל שהוא מודד.
const int PIN_PIR = 27;
const int PIN_LED = 2;
int last = LOW;
unsigned long tWarm = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_LED, OUTPUT);
tWarm = millis(); // the sensor is blind for ~60 s
}
void loop() {
if (millis() - tWarm < 60000UL) return; // ignore the warm-up window
int now = digitalRead(PIN_PIR);
digitalWrite(PIN_LED, now);
if (now != last) { // report edges, not levels
Serial.println(now == HIGH ? "motion" : "clear");
last = now;
}
delay(50);
}
אין לו מה למדוד בעולם, ובכל זאת הוא הרכיב הראשון שיש לחבר. שני קצותיו מחוברים אל ההזנה ואל האפס, והמגרד שבאמצע מוציא חלק יחסי מן המתח לפי מקומו — כלומר הוא מחלק מתח שהיד מזיזה. הוא ליניארי לחלוטין, הוא מכסה את כל התחום מאפס עד ההזנה, ולכן הוא הכלי לבדוק בו את שרשרת המדידה עצמה לפני שמחברים חיישן אמיתי. קריאה שאינה נעה ממנו פירושה שהתקלה בקוד או בחיווט, ולא בחיישן.
const int PIN_POT = 32; // ADC1 - safe next to WiFi
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 0 .. 4095
}
void loop() {
int raw = analogRead(PIN_POT);
int pct = map(raw, 0, 4095, 0, 100);
Serial.print(raw); Serial.print(" ");
Serial.print(pct); Serial.println(" %");
delay(150);
}
const int PIN_POT = A0;
void setup() {
Serial.begin(9600); // Uno: 0 .. 1023
}
void loop() {
int raw = analogRead(PIN_POT);
int pct = map(raw, 0, 1023, 0, 100);
Serial.print(raw); Serial.print(" ");
Serial.print(pct); Serial.println(" %");
delay(150);
}
32 עד 39.הרכיב הזול ביותר בארגז, והמלמד ביותר. רגל שאינה מחוברת לשום דבר אינה במצב נמוך אלא צפה, וקוראת רעש. לכן לחצן לבדו אינו מספיק — דרוש נגד שקובע את מצב המנוחה. הבקר מספק אחד כזה בפנים: INPUT_PULLUP מחבר נגד פנימי אל ההזנה, ואז המצב הרגיל הוא גבוה והלחיצה מורידה אותו לאפס. הלוגיקה מתהפכת — וזה מבלבל בפעם הראשונה, אבל זה גם חוסך רכיב על הלוח.
const int PIN_BTN = 15;
const unsigned long SETTLE = 25; // ms - longer than the bounce
int stable = HIGH, seen = HIGH;
unsigned long tEdge = 0;
long presses = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_BTN, INPUT_PULLUP); // idle HIGH, pressed LOW
}
void loop() {
int now = digitalRead(PIN_BTN);
if (now != seen) { // the contact just moved
seen = now;
tEdge = millis();
}
if (millis() - tEdge > SETTLE && now != stable) {
stable = now; // it held still long enough - believe it
if (stable == LOW) {
presses++;
Serial.println(presses);
}
}
}
delay — הוא עוצר את כל התוכנית — אלא חותמת זמן שמאמתת שהמצב החדש החזיק מעמד. אותו רעיון בדיוק חוזר בכל מתג, בכל מגע ובכל מקודד.זהו הרכיב היחיד בדף שאינו נוגע במעגל שהוא מודד. הזרם עובר דרך פס נחושת בתוך המארז, השדה המגנטי שהוא יוצר נקרא בידי גשש הול, והמוצא הוא מתח — כשבין הפס למוצא יש בידוד גלווני מלא. זו הסיבה שאפשר למדוד בעזרתו זרם ברשת החשמל בלי לחבר את הבקר אליה. שימו לב לנקודת האפס: כשאין זרם המוצא אינו אפס אלא מחצית ההזנה, וזרם בכיוון ההפוך מוריד אותו מתחת לחצי — כך נמדד גם זרם חילופין.
const int PIN_ACS = 34; // ADC1, behind a 1:2 divider
const float VREF = 3.3;
const float DIVIDER = 2.0; // what the divider took away
const float SENS = 0.185; // V per A - the 5 A part
float vZero = 0; // measured, never assumed
void setup() {
Serial.begin(115200);
analogReadResolution(12);
long s = 0; // keep the load off while this runs
for (int i = 0; i < 200; i++) { s += analogRead(PIN_ACS); delay(2); }
vZero = (s / 200.0) * VREF / 4095.0;
}
void loop() {
long s = 0;
for (int i = 0; i < 64; i++) s += analogRead(PIN_ACS);
float v = (s / 64.0) * VREF / 4095.0;
float amps = (v - vZero) * DIVIDER / SENS;
Serial.print(amps, 2); Serial.println(" A");
delay(200);
}
const int PIN_ACS = A0; // 5 V board - straight in, no divider
const float VREF = 5.0;
const float SENS = 0.185; // V per A - the 5 A part
float vZero = 0;
void setup() {
Serial.begin(9600);
long s = 0;
for (int i = 0; i < 200; i++) { s += analogRead(PIN_ACS); delay(2); }
vZero = (s / 200.0) * VREF / 1023.0;
}
void loop() {
long s = 0;
for (int i = 0; i < 64; i++) s += analogRead(PIN_ACS);
float v = (s / 64.0) * VREF / 1023.0;
float amps = (v - vZero) / SENS;
Serial.print(amps, 2); Serial.println(" A");
delay(200);
}
שאלה שנשאלת בכל כיתה: אם הרספברי פיי הוא המחשב החזק מבין השלושה, למה קוד החיישנים כאן נכתב דווקא לשני האחרים? התשובה מפתיעה — לרספברי פיי אין כניסה אנלוגית כלל. בשום דגם. אין בו ממיר אנלוגי-ספרתי, ולכן הוא אינו יכול לקרוא ישירות אף אחד מן החיישנים האנלוגיים בדף הזה: לא נגד תלוי אור, לא LM35 ולא פוטנציומטר. כדי לקרוא אותם צריך לחבר אליו רכיב המרה חיצוני, למשל MCP3008 על אפיק ה-SPI. את החיישנים הספרתיים — DHT22, DS18B20, PIR ו-HC-SR04 — הוא קורא היטב, אבל דרך ספריות פייתון וברגליים של 3.3 V בלבד.
| רכיב | מודד | סוג אות | הזנה | רגליים | בטוח ל-ESP32 ישירות? |
|---|---|---|---|---|---|
| DHT22 | טמפרטורה ולחות | ספרתי · מוליך יחיד | 3.3–6 V | 3 | כן — עם נגד מושך |
| LM35 | טמפרטורה | אנלוגי ליניארי | 4–30 V | 3 | כן — המוצא נמוך |
| DS18B20 | טמפרטורה | ספרתי · 1-Wire | 3.0–5.5 V | 3 | כן — עם נגד מושך |
| LDR | אור | פסיבי · מחלק מתח | פסיבי | 2 | כן — המחלק מן ההזנה |
| HC-SR04 | מרחק | ספרתי · רוחב פולס | 5 V | 4 | לא — מחלק על ECHO |
| PIR HC-SR501 | תנועה | ספרתי · גבוה או נמוך | 4.5–20 V | 3 | כן — המוצא 3.3V |
| פוטנציומטר | מיקום היד | אנלוגי רציף | פסיבי | 3 | כן — רגליים 32 עד 39 |
| לחצן רגעי | מגע | ספרתי בדיד | פסיבי | 4 | כן — נגד מושך פנימי |
| ACS712 | זרם | אנלוגי סביב חצי ההזנה | 5 V | 3 | לא — מחלק על המוצא |
כל המספרים בטבלה הם ערכי גיליון הנתונים של הרכיב הנפוץ בארגזי המעבדה. דגם אחר מאותה משפחה עשוי להיות שונה, ולכן לפני חיבור אמיתי — לפתוח את גיליון הנתונים של הרכיב שביד.