קלט אנלוגי
כניסה ספרתית מכירה שני מצבים בלבד: 0 או 1. אבל העולם לא מדבר כך: טמפרטורה, אור ומיקום של ידית הם גדלים שמשתנים ברציפות, והחיישן מתרגם אותם למתח. בפרק הזה נקרא את המתח הזה בשלושה לוחות, ונגלה שההבדל ביניהם גדול ממה שנראה: אחד דורש כיוון, השני לא, ולשלישי אין ממיר בכלל.
- נחשב את הרזולוציה q ממספר הסיביות וממתח הייחוס.
- נקרא פוטנציומטר ב-ESP32 וב-Pico W, ונבין למה אותה תוכנית נכשלת כשמעבירים אותה כמות שהיא.
- נכתוב תוכנית אחת שעובדת בשני הלוחות בעזרת הקובץ
board.py. - נמיר את הקריאה למתח, לאחוזים ולהתנגדות, ונפחית רעש בעזרת מיצוע.
- נחבר את הממיר החיצוני ADS1115, הדרך היחידה לקלט אנלוגי ב-Raspberry Pi 4/5.
6.1 ממתח למספר (Analog to Digital)
התוכנית מבינה רק מספרים. לכן יש בתוך הבקר ממיר, שמודד את המתח בהדק ושומר את התוצאה באוגר, ומשם התוכנית קוראת אותה.
המספר שמתקבל תמיד שלם, ולכן הממיר אינו רואה את כל המתחים: הוא מחלק את התחום למדרגות שוות, וכל מתח בתוך אותה מדרגה נותן אותו מספר. הסרטוט הבא מראה זאת בממיר קטן של 3 סיביות, שיש לו 8 מדרגות בלבד. הקו המקווקו הוא התשובה שהיינו רוצים, והמדרגות הן מה שמתקבל. ההפרש ביניהם נקרא שגיאת קוונטיזציה.
6.2 רזולוציה, מתח ייחוס וזמן המרה (Resolution, Reference, Conversion Time)
שלושה גדלים מתארים כל ממיר:
- מספר הסיביות n: קובע את מספר המדרגות, 2n.
- מתח הייחוס Vref: המתח הגבוה ביותר שהממיר יכול להבחין בו.
- זמן ההמרה: כמה זמן לוקח לממיר לתת מספר אחד.
| סמל | פירוש | יחידה |
|---|---|---|
| q | הרזולוציה, השינוי הקטן ביותר במתח שהממיר רואה | V |
| Vref | מתח הייחוס | V |
| n | מספר הסיביות של הממיר | — |
ועכשיו נקודה חשובה במיקרו-פייתון. הממיר הוא של 12 סיביות, אבל הפקודה read_u16() נותנת תמיד מספר בין 0 ל-65535, כלומר בתחום של 16 סיביות, בכל הלוחות. כך התוכנית נשארת נכונה גם כשעוברים ללוח עם ממיר אחר. המחיר: המספר קופץ בצעדים של כ-16, ולא של 1:
import board
import time
pot = board.analog_in(board.POT)
for i in range(6):
print(pot.read_u16())
time.sleep(1)19861 19861 19877 19877 19893 19893
| סמל | פירוש | יחידה |
|---|---|---|
| N | הערך של read_u16(), מ-0 עד 65535 | — |
| V | המתח בהדק | V |
read_u16() נתן את המספר 23829. מהו המתח המחושב?ההפרש מן המתח האמיתי קטן ממדרגה אחת, וזו שגיאת הקוונטיזציה.
6.3 מה יש בכל לוח?
| ESP32 | Pico W / Pico 2 W | Pi 4 / Pi 5 | |
|---|---|---|---|
| שפה | מיקרו-פייתון | מיקרו-פייתון | פייתון על לינוקס |
| ממיר פנימי | 12 סיביות | 12 סיביות | אין |
| הדקים אנלוגיים | 32–39; 0, 2, 4, 12–15, 25–27 | GP26, GP27, GP28 | — |
| כיוון התחום | חובה: atten | אין: תמיד עד 3.3 V | — |
| קריאה | read_u16(), read_uv() | read_u16() | ממיר חיצוני דרך I2C |
| מגבלה עם WiFi | הדקים מחוץ ל-32–39 לא עובדים | אין מגבלה | — |
6.4 התוכנית הראשונה: קריאת פוטנציומטר
פוטנציומטר הוא החיישן הפשוט ביותר: מחברים את שני ההדקים החיצוניים ל-3V3 ול-GND, וההדק האמצעי נותן מתח לפי מיקום הידית.
from machine import ADC, Pin
import time
pot = ADC(Pin(34), atten=ADC.ATTN_11DB)
while True:
raw = pot.read_u16()
print(raw)
time.sleep(1)0 16388 32776 49147 65535 ...
נקרא את התוכנית שורה אחר שורה:
| פקודה | מה היא עושה |
|---|---|
| from machine import ADC, Pin | מייבאים את מחלקות הממיר וההדק מן הספרייה שבנויה בבקר. |
| ADC(Pin(34), atten=ADC.ATTN_11DB) | יוצרים ממיר על הדק 34, עם תחום שמגיע למתח ההזנה. |
| pot.read_u16() | לוקחים קריאה אחת, מספר בין 0 ל-65535. |
| time.sleep(1) | ממתינים שנייה, כדי שנספיק לקרוא את המסך. |
from machine import ADC, Pin
import time
pot = ADC(Pin(26))
while True:
raw = pot.read_u16()
print(raw)
time.sleep(1)0 16388 32776 49147 65535 ...
לו העברנו את התוכנית של ESP32 כמות שהיא ל-Pico W, היא הייתה נעצרת בשורה הראשונה, כי אין שם הדק 34. השגיאה בכיוון ההפוך ערמומית יותר, כי היא לא עוצרת את התוכנית:
atten: המתח עולה מ-1.65 V ל-3.3 V, והמספר לא משתנה.from machine import ADC, Pin
import time
pot = ADC(Pin(34))
while True:
print(pot.read_u16())
time.sleep(1)32776 65535 65535 65535 65535 ...
atten הממיר של ESP32 עובד בתחום הקטן ביותר, כ-1 V. כל מתח גבוה יותר נותן 65535. התוכנית לא נעצרת ולא מציגה שגיאה, ואנחנו חושבים שהפוטנציומטר מקולקל.| הקבוע | התחום הלינארי לפי התיעוד |
|---|---|
ADC.ATTN_0DB | 100 mV – 950 mV |
ADC.ATTN_2_5DB | 100 mV – 1250 mV |
ADC.ATTN_6DB | 150 mV – 1750 mV |
ADC.ATTN_11DB | 150 mV – 2450 mV |
שימו לב לעמודה השנייה: גם בתחום הגדול ביותר, הקריאה מהימנה רק בין 0.15 V ל-2.45 V. מחוץ לזה לוח אמיתי קורא פחות מן המתח בפועל. הסימולציה בספר אידיאלית ואינה מראה זאת. למדידה מדויקת משתמשים ב-read_uv(), שקיים רק ב-ESP32 ומפעיל את הכיול של השבב.
adc.width(), adc.read() ו-adc.atten(). אלה פקודות ישנות שעובדות ב-ESP32 בלבד, ואינן קיימות ב-Pico W. לכן הספר משתמש ב-read_u16(), הפקודה המשותפת.6.5 תוכנית אחת לכל הלוחות
בפרק 3 הכרנו את הקובץ board.py, שבו כל מה ששייך ללוח. וההבדל שראינו עכשיו מקומו שם: הפונקציה analog_in מסתירה את כיוון התחום, והתוכנית לא מזכירה מספרים, רק שמות.
# board.py - ESP32 DevKit
from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer
POT = 34
LDR = 35
def analog_in(pin):
# 11 dB attenuation: the input range reaches the 3.3 V supply.
return ADC(Pin(pin), atten=ADC.ATTN_11DB)# board.py - Raspberry Pi Pico W / Pico 2 W
from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer
POT = 26
LDR = 27
def analog_in(pin):
# The range of the Pico is fixed at 0 to 3.3 V: nothing to configure.
return ADC(Pin(pin))ומוסיפים ספרייה קטנה נוספת, analog.py, עם חישובים שאינם תלויים בלוח:
# analog.py - helpers for analog readings, the same on every board.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
VREF = 3.3
def to_volts(raw):
return raw * VREF / 65535
def scale(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
def average(adc, n=16):
total = 0
for _ in range(n):
total += adc.read_u16()
return total // nimport board
import time
from analog import to_volts, scale
pot = board.analog_in(board.POT)
while True:
raw = pot.read_u16()
volts = to_volts(raw)
percent = scale(raw, 0, 65535, 0, 100)
print("raw = %5d V = %.3f %3.0f%%" % (raw, volts, percent))
time.sleep(1)raw = 0 V = 0.000 0% raw = 16388 V = 0.825 25% raw = 32776 V = 1.650 50% raw = 49147 V = 2.475 75% raw = 65535 V = 3.300 100% ...
הפונקציה scale מעבירה מספר מתחום לתחום, וכאן היא נותנת את האחוזים. נזדקק לה שוב ושוב.
"%.3f". המספרים העשרוניים ב-ESP32 וב-Pico W פחות מדויקים מאשר במחשב, ובלי תבנית יופיעו ספרות אחרונות שונות.6.6 חיישן אמיתי: נגד תלוי אור (LDR)
נגד תלוי אור לא נותן מתח, אלא משנה את התנגדותו: ככל שיש יותר אור, ההתנגדות קטנה. לכן מחברים אותו עם נגד קבוע RF במחלק מתח, והממיר קורא את המתח ביניהם.
| סמל | פירוש | יחידה |
|---|---|---|
| RLDR | התנגדות החיישן | Ω |
| RF | הנגד הקבוע | Ω |
| V | המתח הנמדד | V |
import board
import time
from analog import to_volts
R_FIXED = 10000
ldr = board.analog_in(board.LDR)
while True:
v = to_volts(ldr.read_u16())
if v < 0.05:
print("V = %.2f too dark to measure" % v)
else:
r = R_FIXED * (3.3 / v - 1)
print("V = %.2f R = %6.0f ohm" % (v, r))
time.sleep(1)V = 3.00 R = 999 ohm V = 2.20 R = 5000 ohm V = 1.10 R = 20000 ohm V = 0.40 R = 72559 ohm V = 0.00 too dark to measure ...
if v < 0.05 והמתח היה V = 0, התוכנית הייתה נעצרת בחילוק באפס. כל חילוק בקריאה של חיישן דורש בדיקה.6.7 רעש: למה המספרים רועדים? (Noise)
השאירו את הפוטנציומטר במקום וקראו: המספרים רועדים. הרעש הזה טבעי, ומקורו במעגל עצמו. התרופה הפשוטה ביותר היא לקחת כמה קריאות ולחשב ממוצע, כי העליות מבטלות את הירידות.
import board
from analog import average
pot = board.analog_in(board.POT)
def single():
return pot.read_u16()
def smooth():
return average(pot, 16)
def spread(read):
low = high = read()
for _ in range(49):
x = read()
low = min(low, x)
high = max(high, x)
return high - low
print("single reads, spread :", spread(single))
print("average of 16, spread :", spread(smooth))single reads, spread : 785 average of 16, spread : 266
המחיר של המיצוע הוא זמן: 16 קריאות לוקחות פי 16. בגודל איטי כמו טמפרטורה זה לא משנה, אבל באות מהיר המיצוע מטשטש את מה שרוצים למדוד.
6.8 מקריאה להחלטה: בקרה דו-מצבית (On/Off Control)
רוצים נורית שנדלקת בחושך. התוכנית הפשוטה משווה לסף אחד, אבל ליד הסף די ברעש כדי להדליק ולכבות את הנורית שוב ושוב. הפתרון הוא שני ספים מרוחקים: מדליקים מתחת לראשון, מכבים מעל השני, וביניהם לא משנים דבר. זו היסטרזיס.
import board
import time
from machine import Pin
from analog import to_volts
DARK = 1.0
BRIGHT = 1.4
led = Pin(board.LED, Pin.OUT)
ldr = board.analog_in(board.LDR)
while True:
v = to_volts(ldr.read_u16())
if v < DARK:
led.on()
elif v > BRIGHT:
led.off()
print("V = %.2f LED = %d" % (v, led.value()))
time.sleep(1)V = 1.60 LED = 0 V = 1.20 LED = 0 V = 0.95 LED = 1 V = 1.05 LED = 1 V = 0.95 LED = 1 V = 1.20 LED = 1 V = 1.30 LED = 1 V = 1.50 LED = 0 ...
שימו לב לשורת ההדק: Pin(board.LED, Pin.OUT). ב-ESP32 הערך הוא 2, וב-Pico W הוא השם "LED", כי נורית הלוח שם מחוברת לשבב האלחוטי. התוכנית לא צריכה לדעת זאת.
6.9 ממיר חיצוני: ADS1115 (External ADC)
כשהממיר הפנימי לא מספיק, או שאינו קיים, מחברים שבב ממיר חיצוני. ADS1115 הוא שבב נפוץ וזול, עם 4 כניסות ודיוק של 16 סיביות, והוא מדבר באפיק I2C שנלמד בפרק 9. כאן נשתמש בו כמו שהוא.
ADDR אל GND נותן את הכתובת 0x48.הדבר הראשון שעושים עם כל שבב I2C הוא סריקת האפיק, כדי לוודא את החיבור ואת הכתובת:
import board i2c = board.i2c() print([hex(a) for a in i2c.scan()])
['0x48']
ועכשיו הספרייה. כתבנו אותה בעצמנו כי היא קצרה, וכי הקריאה בה מלמדת איך מדברים עם שבב:
# ads1115.py - driver for the ADS1115 16-bit converter.
# ESP32 and Pico: pass a machine.I2C. Raspberry Pi 4/5: pass a pi_i2c.PiI2C.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time
REG_CONV = 0
REG_CONFIG = 1
START = 0x8000
SINGLE_ENDED = 4
RANGE_4V = 0x0200
ONE_SHOT = 0x0100
RATE_128 = 0x0080
NO_COMPARATOR = 0x0003
LSB = 4.096 / 32768
class ADS1115:
def __init__(self, i2c, addr=0x48):
self.i2c = i2c
self.addr = addr
def _write(self, reg, value):
self.i2c.writeto(self.addr, bytes([reg, value >> 8, value & 0xFF]))
def _read(self, reg):
self.i2c.writeto(self.addr, bytes([reg]))
data = self.i2c.readfrom(self.addr, 2)
return (data[0] << 8) | data[1]
def read_raw(self, channel):
mux = (SINGLE_ENDED + channel) << 12
config = START | mux | RANGE_4V | ONE_SHOT | RATE_128 | NO_COMPARATOR
self._write(REG_CONFIG, config)
while not self._read(REG_CONFIG) & START:
time.sleep(0.001)
value = self._read(REG_CONV)
return value - 65536 if value & 0x8000 else value
def read_volts(self, channel):
return self.read_raw(channel) * LSB| פקודה | מה היא עושה |
|---|---|
| config = START | mux | ... | בונים את אוגר ההגדרות משדותיו: התחל המרה, איזו כניסה, איזה תחום, איזו מהירות. |
| self._write(REG_CONFIG, config) | שולחים את האוגר, וההמרה מתחילה. |
| while not ... & START | ממתינים עד שהשבב מודיע שההמרה הסתיימה. |
| value - 65536 if value & 0x8000 | המספר מסומן: הסיבית העליונה מציינת מספר שלילי. |
המעריך כאן 15 ולא 16, כי סיבית אחת מיועדת לסימן.
קטן בערך פי 6 מן המדרגה של הממיר הפנימי, אבל כל קריאה לוקחת כמה אלפיות שנייה. אין ממיר שעדיף בכל דבר.
import board
import time
from ads1115 import ADS1115
adc = ADS1115(board.i2c())
while True:
print("A0 = %.4f V" % adc.read_volts(0))
time.sleep(1)A0 = 0.5000 V A0 = 1.2500 V A0 = 2.5000 V ...
6.10 ב-Raspberry Pi 4/5
ב-Raspberry Pi 4/5 אין ממיר אנלוגי לספרתי, וגם machine לא עובד בו: זה מחשב עם לינוקס, וכותבים לו פייתון רגיל. ובכל זאת אפשר להשתמש באותה ספרייה ads1115.py. היא מבקשת מן האפיק שתי פעולות בלבד: writeto ו-readfrom, ולכן כותבים קובץ קטן שנותן לאפיק את שתי הפעולות האלה בעזרת הספרייה smbus2.
# pi_i2c.py - gives the Pi 4/5 I2C bus the two methods of machine.I2C
# that the book's drivers use, so the same driver runs on all boards.
# Needs: pip install smbus2, and I2C enabled in raspi-config.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
from smbus2 import SMBus, i2c_msg
class PiI2C:
def __init__(self, bus=1):
self.bus = SMBus(bus)
def writeto(self, addr, data):
self.bus.i2c_rdwr(i2c_msg.write(addr, data))
def readfrom(self, addr, n):
msg = i2c_msg.read(addr, n)
self.bus.i2c_rdwr(msg)
return bytes(msg)ההכנה נעשית פעם אחת, בחלון הפקודות: מפעילים את I2C דרך sudo raspi-config (בחלק Interface Options), ואז מתקינים את הספרייה בסביבה נפרדת:
python3 -m venv ~/lab ~/lab/bin/pip install smbus2 ~/lab/bin/python ads_read_pi.py
import time
from ads1115 import ADS1115
from pi_i2c import PiI2C
adc = ADS1115(PiI2C())
while True:
print("A0 = %.4f V" % adc.read_volts(0))
time.sleep(1)A0 = 0.5000 V A0 = 1.2500 V A0 = 2.5000 V ...
gpiozero יש מחלקה מוכנה לממיר MCP3008.עיקרי הפרק:
- התוכנית רואה מדרגות ולא מתח רציף, וגודל המדרגה הוא הרזולוציה.
read_u16()נותן 0–65535 בכל הלוחות, בלי קשר למספר הסיביות של הממיר.- ב-ESP32 לא שוכחים את
atten, ועם WiFi בוחרים הדקים 32–39. - מרכזים את מה ששייך ללוח ב-
board.py, והתוכנית נשארת אחת. - מיצוע מפחית רעש על חשבון זמן, ושני ספים מונעים רעידות ליד ההחלטה.
- ב-Raspberry Pi 4/5 קלט אנלוגי עובר דרך ממיר חיצוני, באותה ספרייה.
סיכום הכללים בפרק:
| הכלל | הנוסחה |
|---|---|
| רזולוציה | q = Vref2n |
| מקריאה למתח | V = N · Vref65535 |
| התנגדות חיישן במחלק | RLDR = RF · (VrefV − 1) |
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| ADC(Pin(n), atten=ADC.ATTN_11DB) | ממיר על הדק ב-ESP32. |
| ADC(Pin(n)) | ממיר על הדק ב-Pico W. |
| adc.read_u16() | קריאה בין 0 ל-65535, בכל הלוחות. |
| adc.read_uv() | מתח מכויל במיקרו-וולט, ב-ESP32 בלבד. |
| i2c.scan() | רשימת הכתובות של השבבים המחוברים לאפיק. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| ממיר אנלוגי לספרתי | Analog-to-Digital Converter | المحوّل التماثلي الرقمي |
| רזולוציה | Resolution | التمييز |
| מתח ייחוס | Reference Voltage | جهد المرجع |
| זמן המרה | Conversion Time | زمن التحويل |
| שגיאת קוונטיזציה | Quantization Error | خطأ التكميم |
| פוטנציומטר | Potentiometer | مقياس الجهد |
| נגד תלוי אור | Light Dependent Resistor | مقاومة ضوئية |
| מחלק מתח | Voltage Divider | مجزّئ الجهد |
| רעש | Noise | الضجيج |
| היסטרזיס | Hysteresis | التخلّف |
| אפיק | Bus | الناقل |
שאלות לתרגול:
read_u16() נתן את המספר 20000. מהו המתח בהדק?#: מאפס סימנים עד 10.import board
import time
from analog import scale
pot = board.analog_in(board.POT)
while True:
level = scale(pot.read_u16(), 0, 65535, 0, 10)
print("#" * int(level))
time.sleep(1)import board
import time
from analog import to_volts
sensor = board.analog_in(board.POT)
while True:
v = to_volts(sensor.read_u16())
print("T = %.1f C" % (v * 100))
time.sleep(1)(א) כמה מעלות שווה מדרגה אחת?
(ב) למה תחום קטן יותר משתלם ב-ESP32?
ATTN_0DB התחום קטן לכ-1 V, המדרגה קטנה באותו יחס, ומתח החיישן נשאר בתחום הלינארי.ads1115.py עובדת ב-Raspberry Pi בלי שינוי?(ב) איזו פקודה הייתה נכשלת לו השתמשנו ב-
time.sleep_ms?writeto ו-readfrom, ואינה מייבאת machine. (ב) sleep_ms קיימת רק במיקרו-פייתון, ולא בפייתון רגיל.