برمجة المتحكّمات بلغة مايكروبايثون · الفصل 3 — المداخل والمخارج الرقميةכניסות ויציאות ספרתיות · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 3

المداخل والمخارج الرقمية

כניסות ויציאות ספרתיות – Digital Inputs and Outputs
3 ساعات تعليمية

إلى الآن كان البرنامج يكلّم الشاشة فقط. في هذا الفصل يخرج إلى العالم: يشعل ثنائيا باعثا للضوء ويقرأ زرا. الأمر بسيط في مظهره، لكننا سنصادف ثلاث مفاجآت: الزر المضغوط يقرأ 0، والضغطة الواحدة تُحسب عدة ضغطات، والبرنامج نفسه لا يعمل على لوحة أخرى إلا إذا فصلنا العتاد عن المنطق.

في هذا الفصل:
  • نميّز بين المُدخَل والمُخرَج ونوعيه، ونعرف حدود الجهد والتيار، ونحسب مقاومة الثنائي الباعث للضوء.
  • نعرف الملف board.py ولماذا يبقى البرنامج الذي يستورده واحدا على اللوحتين.
  • نقرأ زرا بمقاومة السحب الداخلية، ونعالج ارتداد التلامس، ونجمع كل ذلك في إشارة مرور.

3.1 الطرف الرقمي: حالتان، وجهد وتيار – הדק ספרתי: מתחים וזרמים (Digital Pin)

الطرف (הדק) في الوضع الرقمي يعرف حالتين فقط: 1 وهو المستوى العالي، و0 وهو المستوى المنخفض. وفي لوحتينا يعني 1 جهدا قريبا من 3.3 V، ويعني 0 جهدا قريبا من الصفر. ويعمل الطرف بأحد وضعين:

ولكل من الوضعين حدود. ما يقوله توثيق مايكروبايثون عن لوحتينا:

ESP32Pico W / Pico 2 W
أعلى جهد على الطرف3.6 V حدّا مطلقا، والاقتراب منه يعرّض الشريحة للتلفلا يذكره: نرجع إلى ورقة البيانات
تيار الطرف الآمن5, 10, 20, 40 mA حسب قوة الدفع drive، والافتراضي 20 mAلا يذكره: نرجع إلى ورقة البيانات
تحذير: الطرف يكفي لثنائي باعث للضوء، ولا يكفي لمحرّك أو مرحّل. ومجموع تيارات الأطراف له حدّ أيضا نجده في ورقة البيانات. ولا نوصل 5 V بطرف مباشرة في أي من اللوحتين.

3.2 نوعان من المُخرَج – סוגי מוצא (Output Types)

داخل الطرف مفتاحان إلكترونيان. وطريقة استعمالهما هي التي تحدّد نوع المُخرَج:

Pin.OUT3V3OUTPin.OPEN_DRAIN3V3OUTR
الطرف عند المستوى العالي. إلى اليمين يبقى الخرج عائما حتى ترفعه مقاومة خارجية R.

3.3 الثنائي الباعث للضوء ومقاومته – נורית ונגד (LED and Resistor)

الثنائي الباعث للضوء (נורית) يمرّر التيار في اتجاه واحد، من المصعد إلى المهبط، والجهد عليه شبه ثابت ونسمّيه الجهد الأمامي VF. لذلك لا نوصله بالطرف مباشرة: لا شيء يحدّ التيار إلا مقاومة نضعها معه على التوالي.

13 (ESP32)GNDGP15 (Pico W)LED_EXT330 ΩIVF
الطرف LED_EXT يعطي التيار عند المستوى 1: الثنائي مضاء.
مقاومة الثنائي الباعث للضوء
R = VOH − VFI
الرمزالمعنىوحدة القياس
VOHجهد الطرف عند 1 V
VFالجهد الأمامي للثنائي، من ورقة بياناته V
Iالتيار الذي نريده A
مثال 3.1
ثنائي أحمر جهده الأمامي النموذجي نحو 2 V، ونريد تيارا 5 mA. ما المقاومة؟
الحل:
R = 3.3 − 20.005
R = 260 Ω

نختار قيمة متوفرة أعلى من المحسوبة، ولتكن 330 Ω، فيقلّ التيار قليلا إلى 3.94 mA ولا يزيد.

R = 260 Ω
سؤال 3.1: ثنائي أزرق جهده الأمامي النموذجي نحو 3 V، مع مقاومة 330 Ω على طرف جهده 3.3 V. كم التيار؟ ولماذا قد يضيء ضعيفا؟

3.4 البرنامج الأول: الوميض – מהבהב (Blink)

على كل لوحة من اللوحتين ثنائي باعث للضوء جاهز، فلا نحتاج إلى أي توصيل. نبدأ بـESP32، وفي أغلب لوحات DevKit هو على الطرف 2، وعلينا التأكد من ذلك في لوحتنا:

على ESP32: رقم الطرف. النقاط في آخر الشاشة تعني أن البرنامج ما زال يعمل.
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)
ESP32 · MicroPython 1.29 · simulated board
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)

نسخ البرنامج وتغيير الأرقام فيه يصلح لبرنامج قصير واحد، ويفشل حين تصبح البرامج كثيرة. والفروق بين اللوحتين لا تقف عند الثنائي:

ESP32Pico W / Pico 2 W
الثنائي على اللوحةرقم طرف: 2اسم: "LED"
الأطراف 34–39للإدخال فقط، وبلا مقاومة سحب داخليةلا توجد أصلا
المحوّل والمؤقّتيحتاجان إلى ضبط ورقم (الفصلان 6 و4)لا يحتاجان

الحل هو ما نفعله في بقية الكتاب: نجمع كل ما يخص اللوحة في ملف واحد اسمه board.py. لكل لوحة ملفها، والأسماء فيهما واحدة والقيم مختلفة. هذا ما نسمّيه فصل العتاد عن المنطق (הפרדת חומרה מלוגיקה): البرنامج يقول ماذا يريد، والملف يعرف أين هو.

بداية الملف على ESP32
# board.py - ESP32 DevKit
from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer

LED = 2
LED_EXT = 13
BTN = 14
بداية الملف على Pico W
# 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())
ESP32 · MicroPython 1.29 · simulated board
board.LED is 2
level: 1
Pico W · MicroPython 1.29 · simulated board
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
LED: 1
LED: 0
LED: 1
...
▶تجربة حية — مختبر الوميضنشغّل البرنامج في مايكروبايثون الحقيقي داخل المتصفّح، نبدّل اللوحة، ونرى أن الشيفرة لا تتغيّر.
⚠
المختبر غير متاح الآن. المادة في هذا الفصل كاملة من دونه — نواصل القراءة، ونعود إلى التجربة لاحقا أو نفتحها مباشرة من الرابط الذي تحت الإطار.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch3_blink
نفتح المختبر في صفحة مستقلة ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch3_blink

3.6 الزر: طرف مُدخَل ومقاومة سحب – לחצן ונגד משיכה (Button and Pull-up)

الزر (לחצן) يوصل طرفين عند الضغط ويفصلهما عند التحرير. لو وصلنا الزر بين الطرف وGND فقط، لكان الطرف عند التحرير عائما (הדק צף): غير موصول بشيء، فيلتقط الضجيج وتتغيّر قراءته عشوائيا. لذلك نضيف مقاومة السحب (נגד משיכה)، وهي موجودة داخل الشريحة ونشغّلها بـPin.PULL_UP.

GND14 (BTN)3V3Pin.PULL_UPBTN
عند التحرير ترفع المقاومة الطرف إلى 1، وعند الضغط يصله الزر بـGND فيقرأ 0. الطرف نفسه على اللوحتين.
نضغط الزر ثانية واحدة، والثنائي يتبعه.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
released
pressed
pressed
released
...
خطأ شائع: المستوى هنا معكوس: 0 يعني مضغوطا، و1 يعني محرّرا. لذلك كتبنا btn.value() == 0 في سطر واحد باسم واضح، ولا نكرّره في البرنامج.
▶تجربة حية — مختبر الزرنضغط الزر بالفأرة، فيضيء الثنائي وتتغيّر الكتابة على الشاشة.
⚠
المختبر غير متاح الآن. المادة في هذا الفصل كاملة من دونه — نواصل القراءة، ونعود إلى التجربة لاحقا أو نفتحها مباشرة من الرابط الذي تحت الإطار.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch3_button
نفتح المختبر في صفحة مستقلة ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch3_button
سؤال 3.2: على الأطراف 34–39 في ESP32 لا توجد مقاومة داخلية، فوصلنا مقاومة خارجية 10 kΩ إلى 3V3. كم التيار عند الضغط؟

3.7 ارتداد التلامس – קפיצות מגע (Contact Bounce)

نريد عدّادا للضغطات، يعدّ عند الانتقال من 1 إلى 0 فقط. لكن التلامس داخل الزر قطعتا معدن، وعند الضغط ترتدّان عدة مرات قبل أن تستقرّا:

ارتداد التلامس (קפיצות מגע): انتقالات سريعة وغير مقصودة بين 0 و1 عند الضغط وعند التحرير. تدوم عادة بضع ميلي ثوان، وتختلف بين زر وآخر.
0102030 ms3.3tV
رسم توضيحي لضغطة واحدة، وليس قياسا. لكل زر شكله.

الحل في البرنامج بسيط: عندما نرى الانتقال ننتظر حتى يستقر التلامس، ثم نقرأ من جديد. إذا بقي الطرف عند 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
count = 1
...

شغّلنا البرنامج نفسه أيضا بعد حذف الانتظار والقراءة الثانية، مع الدخل نفسه، فكان آخر ما طبعه count = 3: ضغطة واحدة صارت 3 ضغطات.

انتظار 20 ms طويل بالنسبة إلى الارتداد وقصير بالنسبة إلى الإنسان، وهو قيمة نبدأ بها ونعدّلها حسب الزر. وللحل ثمن: البرنامج لا يفعل شيئا آخر أثناء الانتظار، وسنتجاوز ذلك في الفصل 4.

3.8 مشروع: إشارة مرور – רמזור (Traffic Light)

نجمع كل ما تعلّمناه: ثلاثة مخارج وزر واحد. تبقى الإشارة خضراء حتى يضغط أحد المشاة الزر، ثم تكمل دورتها. ونصف المراحل في جدول، فيبقى البرنامج قصيرا ويسهل تغيير الأزمنة.

LED_EXTSERVOBUZZERBTNGND13 | GP1526 | GP633 | GP1114 | GP14330 Ωred330 Ωyellow330 Ωgreen
تحت كل اسم رقم الطرف على ESP32 ثم على Pico W.
الزر مضغوط في الثانية السابعة. الجدول يحدّد كل مرحلة، والزمن صفر يعني: انتظر الزر.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
red
red+yellow
green
yellow
red
...
ملاحظة: استعملنا الطرفين board.SERVO وboard.BUZZER لأنهما مخرجان حرّان في هذا المشروع. الاسم في الملف يصف استعماله في الكتاب، أما الطرف فلا يعرف ما نوصل به.

أهمّ أفكار الفصل:

ملخّص قوانين الفصل:

القانونالصيغة
مقاومة الثنائي الباعث للضوء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
الإجابات النهائية لأسئلة الفصل
3.1 I = 0.91 mA. الفرق بين الجهدين صغير، فالتيار صغير.
3.2 I = 3.310000 = 0.33 mA.

أسئلة للتمرين:

المستوى الأول — أساسي
1.
ثنائي جهده الأمامي 1.8 V، ونريد تيارا 4 mA من طرف جهده 3.3 V. ما المقاومة؟
الإجابات: R = 375 Ω.
2.
لماذا يفشل Pin(36, Pin.OUT) على ESP32؟
الإجابات: الأطراف 34–39 للإدخال فقط.
المستوى الثاني — تطبيق
3.
اكتبوا برنامجا يومض ثلاث مرات ثم يتوقف، ويصلح للوحتين.
الإجابات:
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())
يطبع: done: 0
4.
أضيفوا إلى ملف board.py اسما جديدا LED_RED على ESP32. لماذا يجب إضافته إلى ملف Pico W أيضا؟
الإجابات: لأن البرنامج الذي يستعمل board.LED_RED سيتوقف بخطأ على اللوحة التي لا تعرف الاسم.
المستوى الثالث — تعمّق
5.
ثنائي مصعده إلى 3V3 ومهبطه إلى الطرف عبر مقاومة.
(أ) متى يضيء؟
(ب) هل يعمل مع Pin.OPEN_DRAIN؟
الإجابات: (أ) عندما يكون الطرف عند 0، أي أن المنطق معكوس. (ب) نعم: عند 0 يمرّ التيار إلى GND، وعند 1 لا يصل الطرف بشيء فينطفئ.
6.
في عدّاد الضغطات انتظرنا 20 ms.
(أ) ماذا يحدث لو كان الارتداد أطول من الانتظار؟
(ب) وأي ضرر من انتظار 500 ms؟
الإجابات: (أ) قد تُحسب الضغطة أكثر من مرة. (ب) يضيع الضغطات السريعة، ويبدو الزر بطيئا.
لوحة القوانين: كل قوانين هذا الفصل مجموعة مع رسومها وجداول رموزها في الفصل 25 — لوحة القوانين الشاملة. (נוסחאון הספר)