תכנות מיקרו-בקרים בשפת MicroPython · פרק 22 — תכנות ב-Python וספריית gpiozero⌂📚🏠
פרק 22

תכנות ב-Python וספריית gpiozero

Python and gpiozero
3 שעות לימוד

עכשיו עוזבים את הבקר ועוברים למחשב שלם בגודל כרטיס. ב-Raspberry Pi 4/5 אין מיקרו-פייתון ואין machine, אלא לינוקס ופייתון רגיל. הרעיונות שלמדנו נשארים כמו שהם: הדק, יציאה, כניסה ואירוע. אבל דרך ההתקנה, ההפעלה והתזמון משתנה, והפרק הזה מראה מה משתנה ומה נשאר.

בפרק הזה:
  • נכין את מערכת ההפעלה, נתחבר ל-Pi דרך SSH, ונדע מה שונה בין Pi 4 ל-Pi 5.
  • נעבוד בתוך סביבה וירטואלית, ונבין למה pip מסרב לעבוד מחוצה לה.
  • נבחין בין מספור BCM למספור הפיזי, ונקפיד על מתח של 3.3 V.
  • נשתמש ב-LED, ב-Button, ב-PWMLED, ב-Servo, ב-DistanceSensor ובאירועים.
  • נפעיל את התוכנית שלנו אוטומטית באתחול בעזרת שירות systemd.

22.1 התקנת המערכת וחיבור מרחוק (Setup and SSH)

בסעיף 1.2 השווינו בין הלוחות וראינו ש-Pi הוא מחשב ולא בקר. מותקנת עליו מערכת הפעלה, Raspberry Pi OS, והתוכנית שלנו היא אחת מעשרות תוכנות שרצות בו יחד. ההתקנה נעשית פעם אחת:

  1. מתקינים במחשב שלנו את התוכנה Raspberry Pi Imager מן האתר הרשמי, ובוחרים בה את דגם הלוח, את מערכת ההפעלה ואת כרטיס ה-microSD.
  2. לפני הכתיבה ממלאים את הגדרות ההתאמה האישית: שם המכשיר, משתמש וסיסמה, רשת WiFi והפעלת SSH. שמות המסכים עשויים להשתנות בין גרסאות.
  3. מכניסים את הכרטיס ל-Pi ומדליקים אותו, ממתינים לסיום האתחול הראשון, ואז מתחברים מן המחשב שלנו לשורת הפקודה:
התחברות, עדכון וכיבוי. מחליפים את שני השמות במה שבחרתם בהתאמה האישית.
ssh student@mypi.local
sudo apt update
sudo apt full-upgrade
python --version
sudo shutdown -h now
SSH: פרוטוקול שפותח את שורת הפקודה של ה-Pi על מחשב אחר דרך הרשת, כך שאין צורך במסך ובמקלדת שמחוברים ל-Pi.

את פקודות המערכת שבפרק הזה איננו מריצים בזמן בניית הספר, כי הן דורשות Pi אמיתי. אבל התוכניות של gpiozero שיש מתחתיהן מסך פלט אכן הורצו.

Pi 4Pi 5
מקור ההזנה שהחברה ממליצה עליו5 V, 3 A5 V, 5 A
ההדקים40 הדקים, באותו סדר40 הדקים, באותו סדר
מי שולט בהדקיםהשבב הראשישבב נפרד: RP1
הספרייה gpiozeroעובדתעובדת
הספרייה RPi.GPIOעובדתלא עובדת
אזהרה: לא מנתקים את ה-Pi מהחשמל בזמן שהוא פועל, כפי שעשינו עם הבקר: המערכת כותבת לכרטיס כל הזמן, וניתוק פתאומי עלול לפגוע בקבצים שלו. מכבים אותו קודם בפקודה sudo shutdown -h now, וממתינים עד שהנורית הירוקה מפסיקה להבהב.

22.2 סביבה וירטואלית (Virtual Environment)

בבקר היינו מעתיקים את הספרייה ללוח וזהו. ב-Pi המערכת עצמה נשענת על פייתון, ואם נשנה ספרייה שהיא משתמשת בה, דבר אחר עלול להתקלקל. לכן Raspberry Pi OS אוסרת, מאז הגרסה Bookworm, להתקין ספריות בעזרת pip ישירות בפייתון של המערכת:

שורת השגיאה כפי שהתיעוד הרשמי מציג אותה.
$ pip install smbus2
error: externally-managed-environment
סביבה וירטואלית: תיקייה שיש בה עותק של פייתון ושל ספריותיו השייך לפרויקט, ואינו משפיע לא על המערכת ולא על פרויקטים אחרים.
פרויקט חדש: מתקינים את gpiozero מן המערכת, יוצרים את הסביבה, מפעילים אותה ומתקינים בתוכה.
sudo apt install python3-gpiozero
mkdir ~/project
cd ~/project
python -m venv --system-site-packages env
source env/bin/activate
pip install smbus2
python blink.py
deactivate

האפשרות --system-site-packages גורמת לסביבה לראות גם את ספריות המערכת, ובהן gpiozero שהתקנו בעזרת apt. אחרי ההפעלה מופיע (env) בתחילת השורה, ומאותו רגע pip כותב רק בתוך התיקייה env. בקובץ pi_i2c.py מפרק 6 השתמשנו ב-smbus2, וזה המקום הנכון להתקין אותה.

שאלה 22.1: פתחנו חלון SSH חדש וכתבנו python app.py, ולמרות שהתקנו אתמול את smbus2, הופיעה שגיאה שהספרייה אינה קיימת. מה הסיבה?

22.3 מספור הדקים ומתח (BCM Numbering)

לכל הדק ב-Pi יש שני מספרים. המספר הפיזי הוא מקומו על המחבר, מ-1 עד 40, ומספר BCM הוא מספרו בשבב. הספרייה gpiozero משתמשת במספר BCM, ולכן הפקודה LED(17) פירושה GPIO17, שהוא ההדק הפיזי 11.

123V35V34GPIO25V56GPIO3GND78GPIO4GPIO14910GNDGPIO151112GPIO17GPIO181314GPIO27GND1516GPIO22GPIO2317183V3GPIO24
18 ההדקים הראשונים במחבר: המספרים בעיגולים הם פיזיים, והשמות סביבם לפי BCM. הפקודה pinout מדפיסה את המפה המלאה על ה-Pi.
אזהרה: הדקי ה-Pi פועלים על 3.3 V בלבד. שני ההדקים 5V במחבר הם להזנה, ואין לחבר אותם לשום הדק אחר. מתח של 5 V על הדק כניסה עלול להרוס את השבב, ואין לזה תיקון: מחליפים את הלוח כולו.
GPIO17 (11)GPIO2 (3)GND (9)330 ΩLEDהלחצן
נורית על GPIO17, ולחצן בין GPIO2 לאדמה.
דוגמה 22.1
רוצים זרם של כ-4 mA בנורית שמתח הפעולה הקדמי הטיפוסי שלה הוא 2 V, כשהיא מחוברת להדק שמתחו 3.3 V. מהי ההתנגדות הדרושה?
פתרון:
R = VPIN − VFI
R = 3.3 − 20.004
R = 325 Ω

בוחרים את הערך הסטנדרטי הקרוב 330 Ω, והזרם יהיה כ-3.94 mA.

R = 330 Ω
שאלה 22.2: מהו מספר ה-BCM של ההדק הפיזי 12? ומה כותבים כדי ליצור עליו נורית?

22.4 נורית ולחצן (LED, Button)

הספרייה gpiozero אינה מתחילה מההדק כמו machine, אלא מההתקן: אומרים "נורית על 17" והיא דואגת לשאר. כל פלט במסכים הבאים בא מהרצת הספרייה האמיתית על הדקים מדומים, ולכן הוא אידיאלי ואינו מתיימר להיות מדידה בלוח.

הפקודות on() ו-off(), והמאפיין is_lit מדווח לנו על המצב.
from gpiozero import LED
import time

led = LED(17)

for i in range(3):
    led.on()
    print("on,  is_lit =", led.is_lit)
    time.sleep(0.5)
    led.off()
    print("off, is_lit =", led.is_lit)
    time.sleep(0.5)
Raspberry Pi 4/5 · Python 3 · simulated bus
on,  is_lit = True
off, is_lit = False
on,  is_lit = True
off, is_lit = False
on,  is_lit = True
off, is_lit = False

עכשיו ללחצן. המחלקה Button מפעילה בעצמה את נגד המשיכה הפנימי, ואחר כך מתרגמת בשבילנו את המשמעות: is_pressed נותנת True בלחיצה, אף על פי שההדק יורד אז ל-0. אין צורך להפוך את הערך כפי שעשינו בפרק 3.

הלחצן נלחץ ב-0.5 שנייה ומשתחרר ב-1.5 שנייה, והנורית עוקבת אחריו.
from gpiozero import LED, Button
import time

led = LED(17)
button = Button(2)

for step in range(5):
    pressed = button.is_pressed
    led.value = pressed
    print("t=%.1f pressed=%s lit=%s" % (step * 0.5, pressed, led.is_lit))
    time.sleep(0.5)
Raspberry Pi 4/5 · Python 3 · simulated bus
t=0.0 pressed=False lit=False
t=0.5 pressed=True lit=True
t=1.0 pressed=True lit=True
t=1.5 pressed=False lit=False
t=2.0 pressed=False lit=False
הערה: ההדק GPIO2 הוא גם קו SDA של אפיק I2C. אם נשתמש באפיק באותו פרויקט, נעביר את הלחצן להדק אחר, כמו GPIO27.
הערה: למה לא משתמשים בספרייה RPi.GPIO, שמופיעה בדוגמאות רבות ברשת? כי היא פונה ישירות לאוגרי ההדקים בשבב הראשי, וב-Pi 5 ההדקים עברו לשבב RP1, ולכן הספרייה נעצרת בהודעת שגיאה. הספרייה gpiozero בוחרת בעצמה את השכבה המתאימה ללוח, וב-Pi 5 זו lgpio. לכן אותה תוכנית עובדת בשני הלוחות.

22.5 אירועים והמתנה (Events, pause)

לולאת בדיקה עובדת, אבל לחיצה קצרה עלולה לחמוק בין שתי קריאות. החלופה היא אירוע: נותנים לספרייה פונקציה, והיא קוראת לה בעת הלחיצה.

שתי לחיצות. הפונקציה pressed נקראת מעצמה בכל אחת מהן.
from gpiozero import LED, Button
import time

led = LED(17)
button = Button(2)
count = 0


def pressed():
    global count
    count += 1
    led.toggle()
    print("press %d, lit=%s" % (count, led.is_lit))


button.when_pressed = pressed

for second in range(4):
    time.sleep(1)
print("done, %d presses" % count)
Raspberry Pi 4/5 · Python 3 · simulated bus
press 1, lit=True
press 2, lit=False
done, 2 presses

הרעיון דומה לפסיקה בפרק 5, אבל ההבדל גדול: הפונקציה כאן אינה פסיקה אמיתית, אלא רצה בתהליכון (thread) רקע של פייתון. לכן מותר בה מה שנמנענו ממנו בפסיקה, כמו הדפסה ויצירת רשימות. המחיר הוא שזמן התגובה פחות יציב.

לולאת for בתוכנית שלמעלה קיימת לצורך ההצגה בלבד: היא מחזיקה את התוכנית בחיים כמה שניות ואז התוכנית מסתיימת, וכך אנחנו רואים את הפלט. בפרויקט אמיתי שמים במקומה pause(), שממתינה לנצח. לכן את התוכנית הבאה לא הרצנו בבניית הספר, כי היא אינה מסתיימת:

הצורה האמיתית: bounce_time מטפל בקפיצות המגע, ו-pause() משאירה את התוכנית ממתינה לאירועים.
from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(2, bounce_time=0.05)
button.when_pressed = led.toggle

pause()
טעות נפוצה: כותבים button.when_pressed = pressed בלי סוגריים. עם סוגריים הפונקציה נקראת פעם אחת עכשיו, ותוצאתה None מושמת, ולכן לא קורה דבר בלחיצה.
שאלה 22.3: מחקנו את השורה pause() מן התוכנית האחרונה. מה קורה כשמריצים אותה?

22.6 בהירות, סרוו ומרחק (PWMLED, Servo, DistanceSensor)

תוכנית הבהירות רצה כאן על הדקים מדומים ששומרים רק את ערך ה-PWM, ולכן אין אור ואין פולסים. DistanceSensor, לעומת זאת, דורש מדידת זמן אמיתית, ולכן תוכנית החניה לא הורצה בבניית הספר: היא עברה הידור בלבד, ואין מתחתיה מסך.

value בין 0 ל-1, במקום duty_u16 בין 0 ל-65535. יש גם led.pulse(), שפועמת ברקע.
from gpiozero import PWMLED
import time

led = PWMLED(18, frequency=1000)

for percent in (0, 25, 50, 75, 100):
    led.value = percent / 100
    print("duty %3d%%  value %.2f" % (percent, led.value))
    time.sleep(1)
Raspberry Pi 4/5 · Python 3 · simulated bus
duty   0%  value 0.00
duty  25%  value 0.25
duty  50%  value 0.50
duty  75%  value 0.75
duty 100%  value 1.00
מחסום חניה: המרחק במטרים, והסרוו בין שני קצותיו.
from gpiozero import Servo, DistanceSensor
import time

servo = Servo(18)
sensor = DistanceSensor(echo=24, trigger=23, max_distance=1)

while True:
    metres = sensor.distance
    print("%.2f m" % metres)
    if metres < 0.2:
        servo.max()
    else:
        servo.min()
    time.sleep(0.5)

מחברים את הזנת הסרוו למקור 5 V נפרד, עם אדמה משותפת, ומן ההדק לוקחים את האות בלבד. Servo מייצר את הפולסים בתוכנה, ולכן הסרוו עלול לרעוד מעט, כי לינוקס עשויה לעכב את התוכנית.

HC-SR04ECHOGNDGPIO24GND1 kΩ2 kΩ5 V3.3 V
מחלק מתח על ECHO. ההדק TRIG הוא כניסה בחיישן, ולכן מחברים אותו ישירות ל-GPIO23; החיישן מוזן מ-5V.
דוגמה 22.2
מוצא ECHO בחיישן HC-SR04 הוא 5 V, וההדק עומד ב-3.3 V. משתמשים ב-R1 = 1 kΩ וב-R2 = 2 kΩ. מהו המתח על ההדק?
פתרון:
V = 5 · R2R1 + R2
V = 5 · 21 + 2
V = 3.33 V
V = 3.33 V

אין ב-Pi ממיר אנלוגי לספרתי. כדי לקרוא פוטנציומטר או נגד תלוי אור משתמשים בממיר החיצוני ADS1115 דרך I2C, כפי שעשינו בפרק 6.

22.7 ההבדלים ממיקרו-פייתון

מיקרו-פייתון בבקרפייתון ב-Pi
הפעלהתוכנית אחת שמתחילה מ-main.pyתוכנית אחת מבין רבות, מתחילים אותה בעצמנו או בעזרת שירות
גישה להדקיםmachine.Pingpiozero
מספר ההדקמספר השבב, דרך board.pyBCM
נגד משיכה ללחצןמבקשים אותו: Pin.PULL_UPאוטומטי ב-Button
אירוע בלחיצהPin.irq, פסיקה קצרהwhen_pressed, תהליכון רקע
קריאה אנלוגיתADC פנימיממיר חיצוני בלבד
התקנת ספריותהעתקה או mippip בסביבה וירטואלית, או apt
דיוק התזמוןגבוה יותר, כי אין מערכת שמפריעה לתוכניתנמוך יותר: לינוקס מחלקת את הזמן בין התוכניות
כיבוימנתקים את החשמלמכבים קודם את המערכת

השורה שלפני האחרונה מסבירה את חלוקת התפקידים בפרק הבא: התזמון המדויק והקרבה לחיישן שייכים לבקר, והאחסון, הרשת ודפי האינטרנט שייכים ל-Pi.

22.8 הרצה אוטומטית (systemd)

בבקר main.py מתחיל מעצמו כשמזינים חשמל. ב-Pi שום דבר מזה לא קורה: אם נסגור חלון SSH, התוכנית תיעצר, ואחרי אתחול מחדש היא לא תרוץ. הפתרון הוא שירות: קובץ שמתאר למנהל המערכת systemd איך להריץ את התוכנית שלנו. נשמור את תוכנית pause() מסעיף 22.5 בשם button_led.py ונכתוב:

/etc/systemd/system/button-led.service
[Unit]
Description=Button and LED
After=multi-user.target

[Service]
User=student
WorkingDirectory=/home/student/project
ExecStart=/home/student/project/env/bin/python -u button_led.py
Restart=on-failure

[Install]
WantedBy=multi-user.target
יוצרים את הקובץ, מודיעים למנהל, מפעילים ומתחילים, ואז עוקבים ועוצרים. את הפקודות האלה לא הרצנו בבניית הספר.
sudo nano /etc/systemd/system/button-led.service
sudo systemctl daemon-reload
sudo systemctl enable --now button-led
systemctl status button-led
journalctl -u button-led -f
sudo systemctl stop button-led
שאלה 22.4: השירות אינו פועל אחרי האתחול. איזו פקודה מראה לנו את סיבת הכישלון? ואיזו פקודה שכחנו, אם השירות אינו מתחיל מעצמו אחרי האתחול אף על פי שהוא עובד כשמפעילים אותו ידנית?

עיקרי הפרק:

פקודהמה היא עושה
LED(17)נורית על GPIO17: on, off, toggle, is_lit, value.
Button(2, bounce_time=0.05)לחצן עם נגד משיכה פנימי, וטיפול בקפיצות המגע.
button.is_pressedTrue בלחיצה.
button.when_pressed = ff נקראת בלחיצה. המקבילה שלה היא when_released.
from signal import pausepause() ממתינה לאירועים לנצח.
PWMLED(18).value = 0.5בהירות בין 0 ל-1.
Servo(18)סרוו: min, mid, max, value.
DistanceSensor(echo=24, trigger=23)distance במטרים. על ECHO יש מחלק מתח.
python -m venv --system-site-packages envסביבה וירטואלית שרואה את ספריות המערכת.
systemctl enable --now NAMEהפעלת השירות עכשיו ובכל אתחול.
journalctl -u NAME -fמעקב אחר הפלט של השירות.

מילון מונחים:

עבריתEnglishערבית
מערכת הפעלהOperating Systemنظام التشغيل
שורת פקודהCommand Lineسطر الأوامر
סביבה וירטואליתVirtual Environmentالبيئة الافتراضية
מספור פיזיPhysical Numberingالترقيم الفيزيائي
אירועEventالحدث
תהליכוןThreadالخيط
שירותServiceالخدمة
התשובות הסופיות לשאלות הפרק
22.1 הסביבה לא הופעלה בחלון הזה. כותבים source env/bin/activate ומריצים שוב.
22.2 GPIO18, וכותבים LED(18).
22.3 התוכנית מסתיימת מיד אחרי השורה האחרונה, ואין מי שיגיב ללחצן.
22.4 journalctl -u button-led מראה את הסיבה. את systemctl enable שכחנו.

שאלות לתרגול:

רמה ראשונה — בסיס
1.
לפי איור המחבר: מהו מספר ה-BCM של ההדק הפיזי 16? ומהו המספר הפיזי של GPIO27?
תשובות: GPIO23, וההדק הפיזי 13.
2.
מה עושה האפשרות --system-site-packages? ולמה אנחנו זקוקים לה כאן?
תשובות: היא גורמת לסביבה לראות את ספריות המערכת, ולכן משתמשים ב-gpiozero שהותקנה בעזרת apt בלי להתקין אותה שוב.
3.
נורית שמתח הפעולה הקדמי הטיפוסי שלה 1.8 V מחוברת בנגד 470 Ω להדק של ה-Pi. מהו הזרם בקירוב?
תשובות: I = 3.19 mA.
רמה שנייה — יישום
4.
כתבו תוכנית שמדליקה את הנורית כל עוד הלחצן לחוץ, בעזרת אירועים ובלי לולאה.
תשובות:
from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(2)
button.when_pressed = led.on
button.when_released = led.off
pause()
5.
תוכנית מהרשת פותחת בשורה import RPi.GPIO as GPIO ועובדת ב-Pi 4, אבל נכשלת ב-Pi 5. למה? ואיזה פתרון אנחנו מעדיפים?
תשובות: ההדקים ב-Pi 5 נמצאים בשבב RP1, ו-RPi.GPIO אינה מגיעה אליו. כותבים אותה מחדש בעזרת gpiozero.
רמה שלישית — העמקה
6.
רוצים מחלק מתח למוצא של 5 V בנגדים R1 = 2.2 kΩ ו-R2 = 3.3 kΩ. מהו המתח על ההדק? והאם הוא בטוח?
תשובות: V = 3 V, והוא נמוך מ-3.3 V, ולכן הוא בטוח.
7.
חבר שלכם רוצה לייצר אות בתזמון מדויק מאוד כדי להפעיל מנוע צעד ולקרוא שלושה חיישנים אנלוגיים. היכן תציבו את העבודה הזאת: ב-Pi או בבקר? נמקו.
תשובות: בבקר: התזמון שלו יציב יותר, כי לינוקס עשויה לעכב את התוכנית, ויש בו ממיר פנימי. ה-Pi מקבל ממנו את התוצאות.
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 25 — נוסחאון הספר.