القراءة التماثلية
المدخل الرقمي يعرف حالتين فقط: 0 أو 1. لكن العالم لا يتكلم هكذا: الحرارة والضوء وموضع المقبض مقادير تتغيّر باستمرار، والحسّاس يترجمها إلى جهد. في هذا الفصل نقرأ هذا الجهد على ثلاث لوحات، ونكتشف أن الفرق بينها أكبر مما يبدو: واحدة تحتاج إلى ضبط، والثانية لا تحتاج، والثالثة لا تملك محوّلا أصلا.
- نحسب التمييز q من عدد البتّات وجهد المرجع.
- نقرأ مقياس جهد على ESP32 وعلى Pico W، ونفهم لماذا يخطئ البرنامج نفسه إذا نقلناه كما هو.
- نكتب برنامجا واحدا يعمل على اللوحتين بواسطة الملف
board.py. - نحوّل القراءة إلى جهد ونسبة مئوية ومقاومة، ونخفّف الضجيج بالمتوسّط.
- نوصل المحوّل الخارجي ADS1115، وهو الطريق الوحيد إلى قراءة تماثلية على Raspberry Pi 4/5.
6.1 من جهد إلى عدد – ממיר A/D (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 على الباي دون تغيير؟(ب) أي أمر فيها كان سيفشل لو استعملنا
time.sleep_ms؟writeto وreadfrom، ولا تستورد machine. (ب) sleep_ms موجود في مايكروبايثون فقط، وليس في بايثون العادي.