المداخل والمخارج الرقمية
إلى الآن كان البرنامج يكلّم الشاشة فقط. في هذا الفصل يخرج إلى العالم: يشعل ثنائيا باعثا للضوء ويقرأ زرا. الأمر بسيط في مظهره، لكننا سنصادف ثلاث مفاجآت: الزر المضغوط يقرأ 0، والضغطة الواحدة تُحسب عدة ضغطات، والبرنامج نفسه لا يعمل على لوحة أخرى إلا إذا فصلنا العتاد عن المنطق.
- نميّز بين المُدخَل والمُخرَج ونوعيه، ونعرف حدود الجهد والتيار، ونحسب مقاومة الثنائي الباعث للضوء.
- نعرف الملف
board.pyولماذا يبقى البرنامج الذي يستورده واحدا على اللوحتين. - نقرأ زرا بمقاومة السحب الداخلية، ونعالج ارتداد التلامس، ونجمع كل ذلك في إشارة مرور.
3.1 الطرف الرقمي: حالتان، وجهد وتيار – הדק ספרתי: מתחים וזרמים (Digital Pin)
الطرف (הדק) في الوضع الرقمي يعرف حالتين فقط: 1 وهو المستوى العالي، و0 وهو المستوى المنخفض. وفي لوحتينا يعني 1 جهدا قريبا من 3.3 V، ويعني 0 جهدا قريبا من الصفر. ويعمل الطرف بأحد وضعين:
- طرف مُدخَل (כניסה): يقيس الجهد الآتي من الخارج ولا يفرض جهدا، ويكاد لا يسحب تيارا. وتحدّد ورقة البيانات عتبتين: فوق الأولى يُقرأ 1، وتحت الثانية يُقرأ 0، وما بينهما غير مضمون.
- طرف مُخرَج (יציאה): يفرض الجهد بنفسه، ويعطي تيارا للحمل أو يستقبله منه.
ولكل من الوضعين حدود. ما يقوله توثيق مايكروبايثون عن لوحتينا:
| ESP32 | Pico W / Pico 2 W | |
|---|---|---|
| أعلى جهد على الطرف | 3.6 V حدّا مطلقا، والاقتراب منه يعرّض الشريحة للتلف | لا يذكره: نرجع إلى ورقة البيانات |
| تيار الطرف الآمن | 5, 10, 20, 40 mA حسب قوة الدفع drive، والافتراضي 20 mA | لا يذكره: نرجع إلى ورقة البيانات |
3.2 نوعان من المُخرَج – סוגי מוצא (Output Types)
داخل الطرف مفتاحان إلكترونيان. وطريقة استعمالهما هي التي تحدّد نوع المُخرَج:
- مُخرَج الدفع والسحب (דחיפה-משיכה)،
Pin.OUT: عند 1 يصل الطرف بـ3V3، وعند 0 يصل بـGND. هو الوضع المعتاد، ونستعمله في كل برامج هذا الفصل. - مُخرَج المصرف المفتوح (ניקוז פתוח)،
Pin.OPEN_DRAIN: عند 0 يصل الطرف بـGND، وعند 1 لا يصل بشيء، فيحتاج إلى مقاومة خارجية ترفع الجهد. بهذا تتشارك عدة شرائح سلكا واحدا دون تعارض، وسنراه في ناقل I2C في الفصل 9.
3.3 الثنائي الباعث للضوء ومقاومته – נורית ונגד (LED and Resistor)
الثنائي الباعث للضوء (נורית) يمرّر التيار في اتجاه واحد، من المصعد إلى المهبط، والجهد عليه شبه ثابت ونسمّيه الجهد الأمامي VF. لذلك لا نوصله بالطرف مباشرة: لا شيء يحدّ التيار إلا مقاومة نضعها معه على التوالي.
LED_EXT يعطي التيار عند المستوى 1: الثنائي مضاء.| الرمز | المعنى | وحدة القياس |
|---|---|---|
| VOH | جهد الطرف عند 1 | V |
| VF | الجهد الأمامي للثنائي، من ورقة بياناته | V |
| I | التيار الذي نريده | A |
نختار قيمة متوفرة أعلى من المحسوبة، ولتكن 330 Ω، فيقلّ التيار قليلا إلى 3.94 mA ولا يزيد.
3.4 البرنامج الأول: الوميض – מהבהב (Blink)
على كل لوحة من اللوحتين ثنائي باعث للضوء جاهز، فلا نحتاج إلى أي توصيل. نبدأ بـESP32، وفي أغلب لوحات DevKit هو على الطرف 2، وعلينا التأكد من ذلك في لوحتنا:
from machine import Pin
import time
led = Pin(2, Pin.OUT)
while True:
led.value(not led.value())
print("LED:", led.value())
time.sleep(0.5)LED: 1 LED: 0 LED: 1 ...
Pin(2, Pin.OUT) ينشئ كائنا للطرف 2 ويجعله مُخرَجا، وled.value(not led.value()) يقلب حالته: يقرأ الحالة ويكتب عكسها. وفي الإصدارات الحديثة من مايكروبايثون توجد أيضا led.toggle()، لكنها لا تعمل في كل إصدار. ولوضعه عند 1 أو 0 نكتب led.on() أو led.off().
أما على Pico W فالثنائي ليس على طرف عادي، بل موصول بشريحة اللاسلكي، فنصل إليه بالاسم "LED": نكتب Pin("LED", Pin.OUT). وهذا السطر نفسه يتوقف بخطأ على ESP32، لأن الاسم غير معروف هناك.
Pin(2, Pin.OUT) على Pico W لا يعطي أي رسالة، لأن الطرف GP2 موجود فعلا. البرنامج يعمل، والثنائي الذي على اللوحة لا يضيء أبدا.3.5 الملف board.py: فصل العتاد عن المنطق – הפרדת חומרה מלוגיקה (board.py)
نسخ البرنامج وتغيير الأرقام فيه يصلح لبرنامج قصير واحد، ويفشل حين تصبح البرامج كثيرة. والفروق بين اللوحتين لا تقف عند الثنائي:
| ESP32 | Pico W / Pico 2 W | |
|---|---|---|
| الثنائي على اللوحة | رقم طرف: 2 | اسم: "LED" |
| الأطراف 34–39 | للإدخال فقط، وبلا مقاومة سحب داخلية | لا توجد أصلا |
| المحوّل والمؤقّت | يحتاجان إلى ضبط ورقم (الفصلان 6 و4) | لا يحتاجان |
الحل هو ما نفعله في بقية الكتاب: نجمع كل ما يخص اللوحة في ملف واحد اسمه board.py. لكل لوحة ملفها، والأسماء فيهما واحدة والقيم مختلفة. هذا ما نسمّيه فصل العتاد عن المنطق (הפרדת חומרה מלוגיקה): البرنامج يقول ماذا يريد، والملف يعرف أين هو.
# board.py - ESP32 DevKit from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer LED = 2 LED_EXT = 13 BTN = 14
# board.py - Raspberry Pi Pico W / Pico 2 W from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer LED = 'LED' LED_EXT = 15 BTN = 14
نحفظ على اللوحة الملف الخاص بها، باسم board.py. والبرنامج التالي لا يذكر رقما ولا اسم لوحة. شغّلناه عند بناء الكتاب على اللوحتين، فأعطى لكل منهما قيمتها:
board.LED هي الفرق الوحيد.from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
led.on()
print("board.LED is", repr(board.LED))
print("level:", led.value())board.LED is 2 level: 1
board.LED is 'LED' level: 1
والآن نكتب الوميض مرة واحدة لكل اللوحات. سطر واحد تغيّر عن برنامج ESP32، وسطر أضفناه:
from machine import Pin
import time
import board
led = Pin(board.LED, Pin.OUT)
while True:
led.value(not led.value())
print("LED:", led.value())
time.sleep(0.5)LED: 1 LED: 0 LED: 1 ...
3.6 الزر: طرف مُدخَل ومقاومة سحب – לחצן ונגד משיכה (Button and Pull-up)
الزر (לחצן) يوصل طرفين عند الضغط ويفصلهما عند التحرير. لو وصلنا الزر بين الطرف وGND فقط، لكان الطرف عند التحرير عائما (הדק צף): غير موصول بشيء، فيلتقط الضجيج وتتغيّر قراءته عشوائيا. لذلك نضيف مقاومة السحب (נגד משיכה)، وهي موجودة داخل الشريحة ونشغّلها بـPin.PULL_UP.
from machine import Pin
import time
import board
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
led = Pin(board.LED, Pin.OUT)
while True:
pressed = btn.value() == 0
led.value(1 if pressed else 0)
print("pressed" if pressed else "released")
time.sleep(0.5)released pressed pressed released ...
btn.value() == 0 في سطر واحد باسم واضح، ولا نكرّره في البرنامج.3.7 ارتداد التلامس – קפיצות מגע (Contact Bounce)
نريد عدّادا للضغطات، يعدّ عند الانتقال من 1 إلى 0 فقط. لكن التلامس داخل الزر قطعتا معدن، وعند الضغط ترتدّان عدة مرات قبل أن تستقرّا:
الحل في البرنامج بسيط: عندما نرى الانتقال ننتظر حتى يستقر التلامس، ثم نقرأ من جديد. إذا بقي الطرف عند 0 فهي ضغطة حقيقية:
from machine import Pin
import time
import board
DEBOUNCE_MS = 20
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
count = 0
last = 1
while True:
level = btn.value()
if last == 1 and level == 0:
time.sleep_ms(DEBOUNCE_MS)
level = btn.value()
if level == 0:
count += 1
print("count =", count)
last = level
time.sleep_ms(5)count = 1 ...
شغّلنا البرنامج نفسه أيضا بعد حذف الانتظار والقراءة الثانية، مع الدخل نفسه، فكان آخر ما طبعه count = 3: ضغطة واحدة صارت 3 ضغطات.
انتظار 20 ms طويل بالنسبة إلى الارتداد وقصير بالنسبة إلى الإنسان، وهو قيمة نبدأ بها ونعدّلها حسب الزر. وللحل ثمن: البرنامج لا يفعل شيئا آخر أثناء الانتظار، وسنتجاوز ذلك في الفصل 4.
3.8 مشروع: إشارة مرور – רמזור (Traffic Light)
نجمع كل ما تعلّمناه: ثلاثة مخارج وزر واحد. تبقى الإشارة خضراء حتى يضغط أحد المشاة الزر، ثم تكمل دورتها. ونصف المراحل في جدول، فيبقى البرنامج قصيرا ويسهل تغيير الأزمنة.
from machine import Pin
import time
import board
red = Pin(board.LED_EXT, Pin.OUT)
yellow = Pin(board.SERVO, Pin.OUT)
green = Pin(board.BUZZER, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
PHASES = (("red", 1, 0, 0, 3),
("red+yellow", 1, 1, 0, 1),
("green", 0, 0, 1, 0),
("yellow", 0, 1, 0, 1))
while True:
for name, r, y, g, seconds in PHASES:
red.value(r)
yellow.value(y)
green.value(g)
print(name)
if seconds > 0:
time.sleep(seconds)
else:
while btn.value() == 1:
time.sleep_ms(20)red red+yellow green yellow red ...
board.SERVO وboard.BUZZER لأنهما مخرجان حرّان في هذا المشروع. الاسم في الملف يصف استعماله في الكتاب، أما الطرف فلا يعرف ما نوصل به.أهمّ أفكار الفصل:
- الطرف الرقمي مُدخَل أو مُخرَج، وله حدود جهد وتيار نأخذها من التوثيق وورقة البيانات، لا من الذاكرة.
board.pyيجمع ما يخص اللوحة:board.LEDهو 2 في ESP32 و"LED"في Pico W، والبرنامج لا يتغيّر.- مع
Pin.PULL_UPيقرأ الزر المضغوط 0. - الضغطة الواحدة قد تصل انتقالات كثيرة، فننتظر ونقرأ من جديد.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| مقاومة الثنائي الباعث للضوء | R = VOH − VFI |
أوامر الفصل:
| الأمر | عمله |
|---|---|
| Pin(n, Pin.OUT) | طرف مُخرَج من نوع الدفع والسحب. |
| Pin(n, Pin.OPEN_DRAIN) | طرف مُخرَج من نوع المصرف المفتوح. |
| Pin(n, Pin.IN, Pin.PULL_UP) | طرف مُدخَل مع مقاومة سحب داخلية. |
| p.on() , p.off() | نضع الطرف عند 1، أو عند 0. |
| p.value(not p.value()) | نقلب حالة الطرف. |
| p.value() , p.value(x) | نقرأ الطرف، أو نكتب عليه. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| الطرف | הדק | Pin |
| مُخرَج الدفع والسحب | מוצא דחיפה-משיכה | Push-Pull Output |
| مُخرَج المصرف المفتوح | מוצא ניקוז פתוח | Open-Drain Output |
| ثنائي باعث للضوء | נורית | Light-Emitting Diode |
| طرف عائم | הדק צף | Floating Pin |
| مقاومة السحب | נגד משיכה | Pull-up Resistor |
| ارتداد التلامس | קפיצות מגע | Contact Bounce |
أسئلة للتمرين:
Pin(36, Pin.OUT) على ESP32؟from machine import Pin
import time
import board
led = Pin(board.LED, Pin.OUT)
for n in range(6):
led.value(not led.value())
time.sleep(0.2)
print("done:", led.value())board.py اسما جديدا LED_RED على ESP32. لماذا يجب إضافته إلى ملف Pico W أيضا؟board.LED_RED سيتوقف بخطأ على اللوحة التي لا تعرف الاسم.(أ) متى يضيء؟
(ب) هل يعمل مع
Pin.OPEN_DRAIN؟(أ) ماذا يحدث لو كان الارتداد أطول من الانتظار؟
(ب) وأي ضرر من انتظار 500 ms؟