מבואות תקביליים והתאמת אותות
רוב הגדלים בעולם האמיתי הם תקביליים: הטמפרטורה, האור, הלחץ והמתח משתנים באופן רציף. הבקר מבין רק מספרים, ולכן הוא ממיר את המתח שבמבוא התקבילי שלו למספר שלם. בפרק זה נכיר את הממיר התקבילי-ספרתי, נקרא מבוא תקבילי בתוכנית, נמיר את התנגדות החיישן למתח במחלק מתח, נקטין את הרעש בעזרת מיצוע בתוכנה, ולבסוף נמיר את הקריאה ליחידות שהמשתמש מבין, כמו °C ו-cm.
- נסביר דגימה וקוונטיזציה, ונחשב את Nmax ואת הרזולוציה בלוח Uno ובלוח ESP32.
- נמיר ערך גולמי למתח בנוסחה V = N·VrefNmax ולהפך, ונימנע משגיאות חישוב במספרים שלמים.
- נחבר חיישן התנגדותי במחלק מתח ונבחר לו נגד קבוע מתאים.
- נקטין מתח הגבוה ממתח הייחוס במחלק מתח לפני שנקרא אותו.
- נקטין את הרעש בעזרת ממוצע של כמה קריאות בתוכנה.
- נמיר את הקריאה ליחידות הנדסיות לפי תחום החיישן שבדף הנתונים, ביחס ישר או בעזרת הפונקציה map().
- נשווה בין Uno, ESP32 ורספברי פיי: הממיר, תחום הערכים וההפרדה, ונקרא חיישן ברספברי פיי בפייתון.
הממיר התקבילי-ספרתי (Analog-to-Digital Converter)
חיישן תקבילי נותן מתח שיכול לקבל כל ערך בתוך התחום שלו, למשל 1.37 V או 1.3712 V. הבקר, לעומת זאת, עובד רק עם מספרים בינאריים. הגשר ביניהם הוא יחידה בתוך הבקר שנקראת ממיר תקבילי-ספרתי.
הממיר פועל בשני שלבים:
- דגימה: הממיר מודד את המתח רק ברגעים נפרדים, לא באופן רציף. בלוח Uno קריאה אחת נמשכת כ-100 µs, כלומר לכל היותר כ-10,000 קריאות בשנייה (לפי התיעוד של Arduino).
- קוונטיזציה: הממיר מקרב כל דגימה ל"מדרגה" הקרובה ביותר מתוך מספר מוגבל של מדרגות. לכן במוצא אנו רואים מדרגות ולא קו רציף.
מספר סיביות, מתח ייחוס ורזולוציה:
את מספר המדרגות קובע מספר הסיביות n: ממיר של n סיביות נותן 2n ערכים, מ-0 עד Nmax. המתח הגבוה ביותר שהממיר מודד הוא מתח הייחוס Vref, והמרחק בין שתי מדרגות סמוכות הוא הרזולוציה (ההפרדה): השינוי הקטן ביותר במתח שהממיר מבחין בו. הערך הגולמי עומד ביחס ישר למתח: 0 V נותן N = 0, ו-Vref נותן N = Nmax.
| סימן | משמעות | יחידת מידה |
|---|---|---|
| n , N | מספר הסיביות של הממיר · הערך הגולמי (מ-0 עד Nmax) | — |
| V , Vref | המתח במבוא · מתח הייחוס (המתח הגבוה ביותר שהממיר מודד) | [V] |
| Δ V | רזולוציה: רוחב מדרגה אחת | [V] |
| הלוח | n | Nmax | Vref | רזולוציה Δ V |
|---|---|---|---|---|
| Arduino Uno (ATmega328P) | 10 | 1023 | 5 V | 4.88 mV |
| ESP32 | 12 | 4095 | 3.3 V | 0.806 mV |
כדי לחשב את הערך הגולמי הצפוי למתח מסוים נהפוך את הנוסחה: N = V·NmaxVref, וניקח את החלק השלם. דף הנתונים של ATmega328P כותב במכנה 2n במקום Nmax; ההבדל אינו עולה על ערך גולמי אחד, ובספר אנו משתמשים ב-Nmax כי כך מתקבל בדיוק Vref עבור Nmax.
(א) מהו המתח במבוא?
(ב) איזה ערך גולמי נצפה לקבל עבור מתח של 1.2 V?
(א) נציב בנוסחה:
(ב) נהפוך את הנוסחה וניקח את החלק השלם:
שינוי מתח הייחוס: בלוח Uno בוחרים את הייחוס בפקודה analogReference(): DEFAULT פירושו 5 V, INTERNAL הוא ייחוס פנימי של 1.1 V (ערך טיפוסי לפי דף הנתונים), ו-EXTERNAL הוא המתח המחובר להדק AREF. עם 1.1 V מתקבל Δ V = 1.11024 = 1.07 mV; זה מועיל לחיישן שמתחו קטן, אבל כל מתח מעל 1.1 V נקרא 1023.
קריאת מבוא תקבילי בתוכנית (analogRead)
בלוח Uno יש שישה מבואות תקביליים, מ-A0 עד A5. הפקודה analogRead(A0) מחזירה את הערך הגולמי, בין 0 ל-1023. לקריאה תקבילית אין צורך ב-pinMode(). נתחיל בחיישן התקבילי הפשוט ביותר: הפוטנציומטר, שהוא מחלק מתח שאת היחס שלו משנים ביד.
הפונקציה map() ומלכודת המספרים השלמים:
הפונקציה map(x, inMin, inMax, outMin, outMax) מעבירה מספר מתחום אחד לתחום אחר לפי קו ישר. אבל היא מחשבת במספרים שלמים בלבד וקוצצת את השבר (אינה מעגלת), ולכן היא מתאימה לאחוזים או לזווית, ואינה מתאימה כשאנו צריכים שברים. התוכנית הבאה קוראת את הפוטנציומטר ומחשבת את המתח בשלוש דרכים:
const int POT = A0;
void setup() { Serial.begin(9600); }
void loop() {
int n = analogRead(POT); // 0..1023
long vMap = map(n, 0, 1023, 0, 5); // whole volts only
long mV = map(n, 0, 1023, 0, 5000); // millivolts, integer
float v = n * 5.0 / 1023.0; // float math
Serial.print("N = "); Serial.print(n);
Serial.print(" map V = "); Serial.print(vMap);
Serial.print(" mV = "); Serial.print(mV);
Serial.print(" V = "); Serial.println(v, 3);
delay(500);
}N = 0 map V = 0 mV = 0 V = 0.000 N = 100 map V = 0 mV = 488 V = 0.489 N = 511 map V = 2 mV = 2497 V = 2.498 N = 1000 map V = 4 mV = 4887 V = 4.888 N = 1023 map V = 5 mV = 5000 V = 5.000
בשורה N = 1000 מחזירה map(n, 0, 1023, 0, 5) את המספר 4, והמתח האמיתי הוא 4.888 V: שגיאה של 0.89 V, כלומר כ-18% מן התחום! החישוב הנכון הוא בשורה n * 5.0 / 1023.0: הנקודה העשרונית גורמת לחישוב להתבצע במספרי float. אילו כתבנו n * 5 / 1023, היו מתקבלים רק מספרים שלמים מ-0 עד 5.
חיישן התנגדותי במחלק מתח (Resistive Sensor in a Divider)
חיישנים רבים משנים את ההתנגדות שלהם ולא את המתח: נגד תלוי-טמפרטורה (NTC), נגד תלוי-אור (LDR) וחיישן כפיפה. הממיר מודד רק מתח, ולכן נמיר את ההתנגדות למתח במעגל הפשוט ביותר: מחלק מתח. המרת האות של החיישן למתח שמתאים למבוא של הבקר נקראת התאמת אותות.
| סימן | משמעות | יחידת מידה |
|---|---|---|
| Vout | המתח בנקודת החיבור, אל המבוא התקבילי | [V] |
| RS , RF | התנגדות החיישן (למטה) · הנגד הקבוע (למעלה) | [Ω] |
איך בוחרים את הנגד הקבוע? כלל אצבע: בוחרים RF קרוב להתנגדות החיישן באמצע תחום המדידה. כך Vout משתנה בערך כמה שיותר, ואינו "נדבק" ל-0 V או ל-5 V. לכן את הנגד תלוי-הטמפרטורה "10 kΩ" (זו ההתנגדות שלו ב-25 °C) מחברים עם נגד קבוע של 10 kΩ.
הטמפרטורה עלתה, ולכן ההתנגדות של NTC קטנה, המתח קטן והערך הגולמי קטן.
התוכנית הבאה קוראת את A0, מחשבת את המתח כמו בסעיף 4.2, ואחר כך מחשבת ממנו את התנגדות החיישן. השורה שמחשבת את rs היא נוסחת מחלק המתח אחרי שבודדנו ממנה את RS. אם הממיר קורא 1023, כנראה שהחיישן מנותק.
const int SENSOR = A0;
const float VCC = 5.0;
const float R_FIXED = 10000.0; // ohm, top resistor
void setup() { Serial.begin(9600); }
void loop() {
int n = analogRead(SENSOR);
if (n >= 1023) {
Serial.println("sensor open?");
} else {
float v = n * VCC / 1023.0;
float rs = R_FIXED * v / (VCC - v); // sensor at the bottom
Serial.print("N = "); Serial.print(n);
Serial.print(" V = "); Serial.print(v, 2);
Serial.print(" Rs = "); Serial.print(rs / 1000.0, 2);
Serial.println(" kOhm");
}
delay(1000);
}N = 788 V = 3.85 Rs = 33.53 kOhm N = 511 V = 2.50 Rs = 9.98 kOhm N = 270 V = 1.32 Rs = 3.59 kOhm
שלוש השורות מתאימות לנגד תלוי-הטמפרטורה ב-0 °C, ב-25 °C וב-50 °C. הערך המחושב קרוב מאוד להתנגדות האמיתית; ההבדל הקטן נובע מן הקוונטיזציה. בנגד תלוי-הטמפרטורה נעסוק עוד בפרק 5.
הקטנת מתח גבוה במחלק מתח:
כדי למדוד מתח גבוה מ-Vref, למשל מצבר של 12 V, נקטין אותו במחלק מתח כך שלא יעלה על 5 V גם במקרה הגרוע ביותר. עם R1 = 22 kΩ ו-R2 = 10 kΩ נחשב את היחס ואת המתח הגבוה ביותר שנוכל למדוד:
בתוכנית נחשב את המתח v ב-A0 כמו בסעיף 4.2, ואחר כך נכפיל ביחס R1 + R2R2 כדי "לבטל" את מחלק המתח:
float vBat = v * (22.0 + 10.0) / 10.0; // volts at the battery
רעש ומיצוע בתוכנה (Noise and Averaging)
גם כאשר הגודל הנמדד אינו משתנה, הערך הגולמי קופץ כמה מדרגות למעלה ולמטה. זהו רעש: אותות לא רצויים שמגיעים מחוטים ארוכים, ממנועים קרובים, מרשת החשמל (50 Hz) ומן הממיר עצמו. הקטנת הרעש נקראת סינון. הסינון הפשוט ביותר נעשה בתוכנה: קוראים את המבוא כמה פעמים ברצף ומחשבים ממוצע. הרעש אקראי: יש בו ערכים מעל הערך האמיתי וערכים מתחתיו, ולכן הם מבטלים זה את זה בסכום.
const int SENSOR = A0;
const int COUNT = 8;
void setup() { Serial.begin(9600); }
void loop() {
int raw = analogRead(SENSOR); // one single reading
long sum = raw;
for (int i = 1; i < COUNT; i++) {
sum += analogRead(SENSOR);
}
float avg = sum / (float)COUNT; // average of 8 readings
Serial.print("raw = "); Serial.print(raw);
Serial.print(" avg = "); Serial.println(avg, 1);
delay(500);
}raw = 516 avg = 511.4 raw = 522 avg = 513.1 raw = 506 avg = 511.9 raw = 519 avg = 511.6 raw = 524 avg = 513.5
בניסוי הזה מתח קבוע נותן N = 512. הקריאות הבודדות נעות בין 500 ל-524 בגלל הרעש, ואילו הממוצעים נשארים רק בין 511.4 ל-513.5. המחיר: כל תוצאה דורשת שמונה קריאות. לחיישן טמפרטורה איטי אפשר למצע קריאות רבות; ללחצן או לחיישן הגנה מהיר נמצע מעט קריאות.
המרה ליחידות הנדסיות (Converting to Engineering Units)
הערך הגולמי N אינו אומר דבר למשתמש. אנו רוצים להציג יחידות הנדסיות: °C, cm, bar. דף הנתונים של החיישן נותן את תחום המדידה ואת המתח שמתאים לו: הערך הקטן ביותר בתחום נותן את המתח הקטן ביותר, והערך הגדול ביותר נותן את המתח הגדול ביותר. בחיישן לינארי הקשר ביניהם הוא קו ישר, ולכן מספיק יחס ישר פשוט. כל הדרך היא שלושה צעדים:
- קוראים את הערך הגולמי N בפקודה analogRead().
- ממירים את הערך הגולמי למתח: V = N·VrefNmax (סעיף 4.1).
- ממירים את המתח לגודל הנמדד ביחס ישר, לפי דף הנתונים.
נמיר את הערך הגולמי למתח, ואחר כך את המתח למעלות ביחס ישר:
המרה בעזרת הפונקציה map():
כאשר תחום החיישן מתאים לכל התחום של הממיר, כלומר מ-0 V עד 5 V, מספיקה שורה אחת עם הפונקציה map(): כותבים בה את הערך הגולמי, אחר כך את תחום הממיר (0 ו-1023), ואחר כך את הערך הקטן ביותר ואת הערך הגדול ביותר בתחום החיישן. דוגמה: חיישן מפלס תקבילי נותן 0 V כשהמכל ריק, ו-5 V כשהמים מגיעים לגובה 200 cm (זו דוגמה להמחשה; את התחום האמיתי קוראים בדף הנתונים של החיישן). התוכנית מחשבת את הגובה ואת אחוז המילוי:
const int LEVEL = A0;
const int H_MAX = 200; // cm at 5 V, from the datasheet
void setup() { Serial.begin(9600); }
void loop() {
int n = analogRead(LEVEL);
long h = map(n, 0, 1023, 0, H_MAX); // whole cm
long fill = map(n, 0, 1023, 0, 100); // percent
Serial.print("N = "); Serial.print(n);
Serial.print(" h = "); Serial.print(h);
Serial.print(" cm fill = "); Serial.print(fill);
Serial.println(" %");
delay(1000);
}N = 0 h = 0 cm fill = 0 % N = 256 h = 50 cm fill = 25 % N = 512 h = 100 cm fill = 50 % N = 1023 h = 200 cm fill = 100 %
אם המוצא של החיישן הוא מ-1 V עד 5 V באותו תחום, נכתוב ב-map() במקום האפס את הערך הגולמי של 1 V, שהוא N = 204 (הקו המקווקו בגרף):
long h = map(n, 204, 1023, 0, H_MAX); // sensor output 1..5 V
(א) מה מחזירה map(n, 0, 1023, 0, 6)?
(ב) מהו הלחץ המדויק?
שלושה לוחות, שלושה ממירים (Uno, ESP32 and Raspberry Pi)
אותו חיישן מתחבר לשלושה לוחות נפוצים במעבדה: Arduino Uno, ESP32 ורספברי פיי. הלוח קובע שלושה דברים: איזה מתח מותר בהדק, אם יש בכלל ממיר תקבילי-ספרתי, וכמה סיביות יש בו. וההבדל הגדול ביותר: ברספברי פיי אין ממיר תקבילי-ספרתי באף דגם, ולכן הוא קורא חיישן תקבילי רק דרך ממיר חיצוני, למשל הרכיב MCP3008 (עשר סיביות, שמונה ערוצים, מתחבר לאפיק SPI).
| Arduino Uno | ESP32 | Raspberry Pi | |
|---|---|---|---|
| מתח בהדקים | 5 V | 3.3 V — אינו עומד ב-5 V | 3.3 V — אינו עומד ב-5 V |
| ממיר תקבילי-ספרתי | מובנה, 10 סיביות | מובנה, 12 סיביות | אין — רק MCP3008 חיצוני (10 סיביות) |
| תחום הערכים הגולמיים | 0 עד 1023 | 0 עד 4095 | 0 עד 1023 (דרך MCP3008) |
| מתח הייחוס | 5 V | כ-3.3 V | 3.3 V |
| הפרדה | 4.88 mV | 0.81 mV | 3.22 mV |
| שפת התכנות | ארדואינו (C++) | ארדואינו או מיקרו-פייתון | פייתון |
משתמשים באותה נוסחה בשלושתם, N = V·NmaxVref, ולוקחים את החלק השלם:
אותו מתח נותן שלושה מספרים שונים, כי מספר הסיביות ומתח הייחוס שונים. לכן בכל תוכנית כותבים את Nmax ואת Vref הנכונים ללוח.
חיישנים ברספברי פיי בפייתון:
ברספברי פיי לא כותבים תוכנית ארדואינו אלא תוכנית בשפת פייתון. החיישנים הספרתיים מתחברים ישירות להדקי GPIO (במתח של 3.3 V), והתקביליים דרך MCP3008. הנה ארבע דוגמאות קצרות לחיישנים מן הספר הזה. הספרייה gpiozero כבר מותקנת במערכת Raspberry Pi OS, ואת ספריית DHT22 מתקינים פעם אחת בפקודה:
pip install adafruit-circuitpython-dht
from gpiozero import Button
from signal import pause
btn = Button(17) # GPIO17, internal pull-up
btn.when_pressed = lambda: print("pressed")
btn.when_released = lambda: print("released")
pause()from gpiozero import MCP3008
from time import sleep
lm35 = MCP3008(channel=0) # SPI, Vref = 3.3 V
while True:
n = lm35.raw_value # 0 .. 1023
v = n * 3.3 / 1023
t = v / 0.010 # 10 mV per degree C
print("N = %d V = %.3f V T = %.1f C" % (n, v, t))
sleep(1)import time
import board
import adafruit_dht
dht = adafruit_dht.DHT22(board.D27) # GPIO27
while True:
try:
print("T = %.1f C RH = %.1f %%" % (dht.temperature, dht.humidity))
except RuntimeError:
pass # a missed reading: try again
time.sleep(2)import glob, time
dev = glob.glob("/sys/bus/w1/devices/28-*/temperature")[0]
while True:
with open(dev) as f:
t = int(f.read()) / 1000 # millidegrees C
print("T = %.2f C" % t)
time.sleep(1)סיכום נוסחאות הפרק:
| החוק | הנוסחה |
|---|---|
| הערך הגולמי הגדול ביותר | Nmax = 2n − 1 |
| רזולוציה | Δ V = Vref2n |
| מערך גולמי למתח | V = N·VrefNmax |
| ממתח לערך גולמי | N = V·NmaxVref |
| מחלק מתח, החיישן למטה | Vout = VCC·RSRF + RS |
| LM35: ממתח לטמפרטורה | T = V10 mV/°C |
מילון מונחים:
| עברית | English | العربية |
|---|---|---|
| ממיר תקבילי-ספרתי (ADC) | Analog-to-digital converter | المحوّل التماثلي-الرقمي |
| דגימה | Sampling | أخذ العيّنات |
| קוונטיזציה | Quantization | التكميم |
| מספר סיביות | Number of bits | عدد البتات |
| ערך גולמי | Raw value (count) | القيمة الخام |
| מתח ייחוס | Reference voltage | جهد المرجع |
| רזולוציה (הפרדה) | Resolution | دقّة التمييز |
| מחלק מתח | Voltage divider | مُجزِّء الجهد |
| התאמת אותות | Signal conditioning | ملاءمة الإشارة |
| רעש | Noise | الضجيج |
| סינון | Filtering | الترشيح |
| תחום מדידה | Measuring range | مدى القياس |
| יחידות הנדסיות | Engineering units | الوحدات الهندسية |
שאלות לתרגול:
(א) מהו Nmax?
(ב) מהי הרזולוציה?
(ג) מהו הערך הגולמי עבור מתח של 2 V?
(א) n / 1023 * 5
(ב) map(n, 0, 1023, 0, 5)
(ג) n * 5.0 / 1023.0 עם שלוש ספרות אחרי הנקודה?
(א) האם Vout גדל או קטן לעומת 25 °C?
(ב) חשבו את Vout ואת הערך הגולמי.
(א) מהו המתח ב-A0?
(ב) מהו הערך הגולמי?
(ג) מהו המתח הגבוה ביותר שאפשר למדוד במחלק הזה?
(א) כתבו את שורת map() המתאימה.
(ב) מה היא מחזירה?
(ג) מהי הטמפרטורה המדויקת?
(א) מהו הערך הגולמי ב-1 V וב-5 V?
(ב) כתבו את שורת map() המתאימה.
(ג) מהי הטמפרטורה כאשר N = 614?