תכנות מיקרו-בקרים בשפת MicroPython · פרק 3 — כניסות ויציאות ספרתיות⌂📚🏠
פרק 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 — נוסחאון הספר.