תכנות מיקרו-בקרים בשפת MicroPython · פרק 2 — פייתון במידה שהבקר צריך⌂📚🏠
פרק 2

פייתון במידה שהבקר צריך

Python for Microcontrollers
4 שעות לימוד

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

בפרק הזה:
  • נשתמש במשתנים, בטיפוסים ובחשבון, ונדפיס מספרים בתבנית ברורה.
  • נקרא טקסט מהמשתמש, נמיר אותו למספר, ונטפל בשגיאה אם אינו מספר.
  • נבנה החלטה בתנאי וחזרה בלולאה, ונאסוף את הקריאות ברשימה.
  • נארגן את הקוד בפונקציות, במחלקות ובספריות, ונתחשב בזיכרון ובדיוק העשרוני.

2.1 משתנים וטיפוסים (Variables and Types)

משתנה הוא שם שקושרים לערך. לא מצהירים על הטיפוס מראש; המפרש קובע אותו מהערך עצמו. הטיפוסים שאנחנו צריכים הם ארבעה:

הטיפוסדוגמהמשמש ל
int מספר שלם20000קריאת הממיר, מונים, מספרי הדקים
float מספר ממשי3.3מתח וטמפרטורה
str מחרוזת"pot"שמות והודעות
bool ערך בוליאניTrueתוצאה של כל השוואה
ארבעה משתנים, והפקודה type מגלה את הטיפוס של כל אחד.
raw = 20000
vref = 3.3
name = "pot"
active = raw > 1000

print(name, raw, active)
print(type(vref), type(name))
ESP32 + Pico W · MicroPython 1.29 · simulated board
pot 20000 True
<class 'float'> <class 'str'>

2.2 חשבון (Arithmetic)

הפעולהמשמעותהדוגמה מעולמנו
+ - *חיבור, חיסור וכפלכפל הקריאה במתח הייחוס
/חילוק, והתוצאה תמיד floatהמתח מהקריאה
//חילוק שלםאחוזים בלי שברים
%שארית החילוקהחלפת מצב כל שני צעדים
**העלאה בחזקהמספר המדרגות 2n
דוגמה 2.1
הממיר נתן את הקריאה 20000 ומתח הייחוס 3.3 V. מהו המתח?
פתרון:
V = N · Vref65535
V = 20000 · 3.365535
V = 1.007 V
V = 1.007 V
אותה דוגמה בקוד, ואחר כך ההבדל בין פעולות החילוק.
raw = 20000
volts = raw * 3.3 / 65535

print("volts   = %.3f" % volts)
print("7 / 2   = %.1f" % (7 / 2))
print("7 // 2  =", 7 // 2)
print("7 % 2   =", 7 % 2)
print("2 ** 12 =", 2 ** 12)
ESP32 + Pico W · MicroPython 1.29 · simulated board
volts   = 1.007
7 / 2   = 3.5
7 // 2  = 3
7 % 2   = 1
2 ** 12 = 4096
טעות נפוצה: הסדר חשוב בחשבון: raw * 3.3 / 65535 מבצע קודם את הכפל. ואילו raw // 65535 * 3.3 נותן אפס לכל קריאה הקטנה מ-65535, כי החילוק השלם משמיט את השבר.
שאלה 2.1: מהי התוצאה של 17 // 5, של 17 % 5 ושל 17 / 5?

2.3 הדפסה ותבניות (Printing and Formatting)

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

%.2f הוא מספר ממשי בשתי ספרות אחרי הנקודה, ו-%d הוא מספר שלם.
volts = 1.0071
temp = 24.5
raw = 20000

print("V = %.2f" % volts)
print("T = %.1f C, raw = %d" % (temp, raw))
print(f"V = {volts:.2f}")
print(f"T = {temp:.1f} C, raw = {raw}")
ESP32 + Pico W · MicroPython 1.29 · simulated board
V = 1.01
T = 24.5 C, raw = 20000
V = 1.01
T = 24.5 C, raw = 20000
כלל: בספר הזה תמיד מדפיסים מספר ממשי בתבנית. מספר שמופיע בלי תבנית עשוי להאריך את ספרותיו, והוא שונה בין לוח ללוח, כפי שנראה ב-2.12.

2.4 קלט והמרה (input, int, float, split)

הפקודה input מדפיסה שאלה וממתינה עד שהמשתמש מקליד שורה ולוחץ Enter בחלון Shell. והחשוב מכול: היא תמיד מחזירה מחרוזת, גם אם הקלדנו מספר. לכן ממירים ב-int או ב-float לפני כל חישוב. ואם הקלדנו שני מספרים בשורה אחת, הפקודה split חותכת את המחרוזת לרשימת חלקים. הקוד הבא לא הורץ בבניית הספר, כי אין לו מקלדת, ולכן לא מציגים לו מסך:

קריאת מספר אחד, ואחר כך שני מספרים בשורה.
text = input("Voltage in volts: ")
volts = float(text)
print("raw =", int(volts * 65535 / 3.3))

line = input("R1 R2 in ohms: ")
parts = line.split()
r1 = float(parts[0])
r2 = float(parts[1])
print("Vout = %.2f V" % (3.3 * r2 / (r1 + r2)))

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

מחרוזת שכתובה בתוכנית במקום input.
line = "10000 4700"
parts = line.split()
print(parts)

r1 = float(parts[0])
r2 = float(parts[1])
print("Vout = %.2f V" % (3.3 * r2 / (r1 + r2)))
ESP32 + Pico W · MicroPython 1.29 · simulated board
['10000', '4700']
Vout = 1.06 V

ומה קורה אם המחרוזת אינה ניתנת להמרה? זו השגיאה הראשונה שאנחנו רואים, והיא אמיתית:

int אינו מקבל נקודה עשרונית.
text = "3.3"
print("%.1f" % float(text))
n = int(text)
print(n)
ESP32 + Pico W · MicroPython 1.29 · simulated board
3.3
ValueError: invalid syntax for integer with base 10: '3.3'

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

במקום להיעצר, התוכנית מדפיסה הודעה וממשיכה.
text = "abc"
try:
    volts = float(text)
    print("%.2f" % volts)
except ValueError:
    print("not a number:", text)
ESP32 + Pico W · MicroPython 1.29 · simulated board
not a number: abc
שאלה 2.2: המשתמש הקליד 12 ב-input. מהו הטיפוס של הערך שקיבלנו? ומה תוצאת n * 2 לפני ההמרה?

2.5 תנאים (if / elif / else)

המיקרו-בקר קיים כדי להחליט: האם המתח גבוה? האם הדלת פתוחה? תנאי מריץ גוש שורות אם ההשוואה נכונה, וגוש השורות מזוהה בהזחה פנימה. משווים ב-==, ב-!=, ב-< וב->, וקושרים תנאים ב-and, ב-or וב-not.

שלושה תחומים, ואחר כך השוואה מורכבת ושלילה.
volts = 2.7

if volts > 3.0:
    print("HIGH")
elif volts > 1.0:
    print("MID")
else:
    print("LOW")

print("in range:", 1.0 <= volts <= 3.0)
print("not low:", not volts < 1.0)
ESP32 + Pico W · MicroPython 1.29 · simulated board
MID
in range: True
not low: True
טעות נפוצה: הסימן = נותן ערך, והסימן == משווה. וההזחה היא חלק מהשפה: שורה שהוזחה בטעות משנה את משמעות התוכנית, או עוצרת אותה. תמיד משתמשים ב-4 רווחים.

2.6 לולאות (for / while)

לולאה חוזרת על גוש שורות. הלולאה for עוברת על אוסף ידוע, ו-range(3) נותן 0, 1 ו-2, בלי 3. ואילו while חוזרת כל עוד התנאי נכון, ומתאימה למקרים שבהם איננו יודעים מראש כמה פעמים. כאן המתח יורד ב-20% בכל צעד, ושואלים: כמה צעדים עד שהוא קטן מוולט אחד?

בלולאה הראשונה אנחנו קובעים את מספר הסיבובים, ובשנייה ההשוואה קובעת אותו.
for n in range(3):
    raw = n * 20000
    print(n, raw, "%.2f V" % (raw * 3.3 / 65535))

volts = 3.3
steps = 0
while volts > 1.0:
    volts = volts * 0.8
    steps += 1
print("steps:", steps, "volts: %.2f" % volts)
ESP32 + Pico W · MicroPython 1.29 · simulated board
0 0 0.00 V
1 20000 1.01 V
2 40000 2.01 V
steps: 6 volts: 0.87

ובתוך כל לולאה, הפקודה continue מדלגת על הסיבוב הנוכחי, ו-break יוצאת מכל הלולאה.

הלולאה שאינה נגמרת:

כמעט כל תוכנית של בקר בנויה על לולאת while True שאינה נעצרת: קוראים, מחשבים, מחליטים, מבצעים, וחוזרים.

קוראיםאת החיישןמחשביםאת המתחמחליטיםif / elseמבצעיםהדק ומסךwhile True
צורתה של כל תוכנית בקר: ארבעה צעדים בלולאה אין-סופית.
מחברים את כל מה שלמדנו. את הפקודה analog_in נסביר בפרק 6, ואת ההדק בפרק 3.
import time
from machine import Pin
import board

pot = board.analog_in(board.POT)
led = Pin(board.LED, Pin.OUT)

while True:
    volts = pot.read_u16() * 3.3 / 65535
    if volts > 2.0:
        led.on()
        state = "ON"
    else:
        led.off()
        state = "OFF"
    print("%.2f V  LED %s" % (volts, state))
    time.sleep(1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
0.50 V  LED OFF
1.50 V  LED OFF
2.50 V  LED ON
3.00 V  LED ON
...

הנקודות בסוף המסך אומרות שהמדמה עצר את התוכנית. הלוח האמיתי ממשיך עד שנלחץ Ctrl-C.

▶ניסוי חי — החלטה לפי מתחמזיזים את מד המתח בעכבר, ורואים מתי הנורית נדלקת. אחר כך משנים את הסף בתוכנית.
⚠
המעבדה אינה זמינה כעת. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר, או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch2_threshold
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch2_threshold

2.7 רשימות (Lists)

רשימה אוספת קריאות רבות תחת שם אחד, ולכל איבר בה יש אינדקס שמתחיל באפס. מהסוף סופרים בשלילי: temps[-1] הוא האיבר האחרון.

21.50-422.01-323.52-222.53-1ii
רשימה של ארבעה איברים: האינדקס מההתחלה למעלה, ומהסוף למטה.
הוספת איבר, הערך הגדול והקטן ביותר, הממוצע וחלק מהרשימה.
temps = [21.5, 22.0, 23.5, 22.5]
print("first %.1f, last %.1f" % (temps[0], temps[-1]))

temps.append(24.0)
print("count:", len(temps))
print("max %.1f, min %.1f" % (max(temps), min(temps)))
print("avg %.2f" % (sum(temps) / len(temps)))

for t in temps[1:3]:
    print("%.1f" % t)
ESP32 + Pico W · MicroPython 1.29 · simulated board
first 21.5, last 22.5
count: 5
max 24.0, min 21.5
avg 22.70
22.0
23.5
טעות נפוצה: אינדקס מחוץ לרשימה עוצר את התוכנית בשגיאה IndexError. ורשימה שגדלה בלי גבול ממלאת בהכרח את הזיכרון הקטן, ולכן מגבילים את אורכה, כמו ב-2.10.

2.8 פונקציות (Functions)

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

ל-vref יש ערך ברירת מחדל, ו-limits מחזירה שני ערכים.
def to_volts(raw, vref=3.3):
    return raw * vref / 65535


def limits(values):
    return min(values), max(values)


print("%.2f" % to_volts(20000))
print("%.2f" % to_volts(20000, 5.0))
low, high = limits([1.2, 0.9, 1.7])
print("low %.1f, high %.1f" % (low, high))
ESP32 + Pico W · MicroPython 1.29 · simulated board
1.01
1.53
low 0.9, high 1.7

משתנה שנותנים לו ערך בתוך פונקציה נשאר בתוכה. ואם רוצים לשנות משתנה מבחוץ, כותבים בתחילתה global count. נצטרך זאת בפסיקות בפרק 5.

2.9 מילונים בקצרה (Dictionaries)

מילון קושר שם לערך, וניגשים לערך לפי השם ולא לפי המיקום. פוגשים אותו בהגדרות ובנתוני JSON בפרק 12.

get אינו נעצר כשהמפתח חסר, אלא מחזיר ערך חלופי.
limit = {"name": "battery", "low": 1.0, "high": 3.0}

print(limit["name"], "%.1f" % limit["high"])
limit["unit"] = "V"
print(limit.get("unit"), limit.get("max", "missing"))
print(sorted(limit))
ESP32 + Pico W · MicroPython 1.29 · simulated board
battery 3.0
V missing
['high', 'low', 'name', 'unit']
הערה: אין להסתמך על סדר המפתחות במיקרו-פייתון. ואם הסדר חשוב, משתמשים ב-sorted כמו בתוכנית.

2.10 מחלקה קטנה (A Small Class)

מחלקה מאגדת נתונים עם הפונקציות שפועלות עליהם, ואובייקט הוא עותק שלה עם הנתונים שלו. הפונקציה __init__ פועלת בעת יצירת כל אובייקט, ו-self הוא האובייקט עצמו. כך בדיוק אנחנו עושים כשכותבים Pin(2): יוצרים אובייקט ממחלקה מוכנה. הדוגמה שלנו היא ממוצע נע: מוסיפים את החדש וזורקים את הישן ביותר, כך שהרשימה אינה מתארכת לעולם, וההשפעה של קריאה חריגה כמו 400 קטנה. לכל חיישן אובייקט ורשימה משלו.

שני אובייקטים מאותה מחלקה, ולכל אחד רשימה משלו.
class Average:
    def __init__(self, size):
        self.size = size
        self.values = []

    def add(self, x):
        self.values.append(x)
        if len(self.values) > self.size:
            self.values.pop(0)
        return sum(self.values) / len(self.values)


pot = Average(3)
ldr = Average(2)
for raw in [100, 110, 90, 400, 105]:
    print(raw, "%.1f" % pot.add(raw))
print("ldr %.1f" % ldr.add(500))
ESP32 + Pico W · MicroPython 1.29 · simulated board
100 100.0
110 105.0
90 100.0
400 200.0
105 198.3
ldr 500.0

2.11 ייבוא (import)

ספרייה היא קובץ שיש בו פונקציות מוכנות. מייבאים אותה כולה, או לוקחים ממנה שם אחד. וכל קובץ שכותבים ושומרים בתיקייה lib נעשה ספרייה. זו ספרייה קטנה שכתבנו:

הקובץ כפי שהוא בחבילת ההורדה.
# convert.py - small unit conversions, the same on every board.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com

VREF = 3.3


def to_volts(raw, vref=VREF):
    return raw * vref / 65535


def to_percent(raw):
    return raw * 100 // 65535
ספרייה מוכנה, שם אחד ממנה, והספרייה שלנו.
import math
from math import sqrt
import convert

print("%.3f %.3f" % (math.pi, sqrt(2)))
print("%.2f V" % convert.to_volts(20000))
print(convert.to_percent(32768), "percent")
ESP32 + Pico W · MicroPython 1.29 · simulated board
3.142 1.414
1.01 V
50 percent
טעות נפוצה: לא נותנים לקבצים שלנו שמות של ספריות, כמו time.py או math.py. המפרש לוקח קודם את הקובץ שלנו, וכל התוכניות שצריכות את המקורי נעצרות.

2.12 במה היא שונה מפייתון של המחשב? (Differences from a PC)

הזיכרון מועט:

כל מה שיוצרים לוקח מהזיכרון, והפקודה gc.mem_free() נותנת את מה שנשאר. כאן לא מדפיסים מספר, כי המספר של המדמה אינו אומר דבר על הלוח שלכם; נסו זאת בו.

משווים לפני ואחרי, בלי להדפיס את המספרים.
import gc

gc.collect()
before = gc.mem_free()
data = [0] * 1000
after = gc.mem_free()
print("the list took memory:", after < before)
ESP32 + Pico W · MicroPython 1.29 · simulated board
the list took memory: True

הדיוק העשרוני:

ב-ESP32 וב-Pico W הקושחה הרגילה שומרת מספר ממשי ב-32 סיביות, בדיוק של כ-7 ספרות. ואילו במדמה ובפייתון של המחשב זה 64 סיביות. לכן מדפיסים בתבנית, ואין משווים שני מספרים ממשיים ב-==:

עשר פעמים 0.1 אינו בדיוק 1.0.
total = 0.0
for i in range(10):
    total += 0.1

print("exactly 1.0:", total == 1.0)
print("close to 1.0:", abs(total - 1.0) < 0.001)
ESP32 + Pico W · MicroPython 1.29 · simulated board
exactly 1.0: False
close to 1.0: True
כלל: את הזמן ואת המונים שומרים תמיד כמספרים שלמים. אחרי 224 מספר ממשי של 32 סיביות אינו מסוגל לייצג כל מספר שלם, ומונה אלפיות השנייה מגיע לגבול הזה אחרי כ-4.66 שעות.

הקבוע const:

ערך שלם שאינו משתנה מצהירים עליו כקבוע בפקודה const מהספרייה micropython, והמפרש מציב את ערכו מראש. והדוגמה הזאת מחשבת גם היא במספרים שלמים בלבד, במיליוולט:

קבועים וחישוב שלם.
from micropython import const

_VREF_MV = const(3300)
_FULL = const(65535)


def to_mv(raw):
    return raw * _VREF_MV // _FULL


print(to_mv(20000), "mV")
ESP32 + Pico W · MicroPython 1.29 · simulated board
1007 mV

לא כל הספריות קיימות:

ספריות הענק של פייתון אינן עובדות כאן, וייבוא שלהן עוצר את התוכנית בשגיאה ImportError. מה שזמין בלוח שלנו מגלים בפקודה help("modules") בשורה האינטראקטיבית, ואת חלק מהחסר אפשר להתקין, כמו בפרק 11. וההפך נכון גם כן: time.sleep_ms ו-machine אינם קיימים בפייתון של המחשב. מי שרוצה להעמיק בשפה עצמה יכול לפנות לספר הפייתון באתר fahed-lab.com כשיהיה זמין.

עיקרי הפרק:

פקודות הפרק:

פקודהמה היא עושה
int() float() str()המרה בין טיפוסים.
"%.2f" % x f"{x:.2f}"הדפסה בשתי ספרות אחרי הנקודה.
input() line.split()קריאת שורה וחיתוכה.
try / except ValueErrorטיפול בשגיאה בלי עצירה.
a.append(x) a.pop(0) len(a)הוספה, מחיקת הראשון, האורך.
gc.mem_free() const()הזיכרון הפנוי; הצהרה על קבוע.

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

עבריתEnglishערבית
משתנהVariableالمتغيّر
מספר שלם / ממשיInteger / Floatعدد صحيح / عدد عشري
מחרוזתStringالنص
לולאהLoopالحلقة
הזחהIndentationالإزاحة
רשימהListالقائمة
מחלקהClassالصنف
אובייקטObjectالكائن
חריגהExceptionالاستثناء
ממוצע נעMoving Averageالمتوسّط المتحرّك
התשובות הסופיות לשאלות הפרק
2.1 3, 2 ו-3.4.
2.2 מחרוזת. והתוצאה "1212", כי כפל מחרוזת משכפל אותה.

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

רמה ראשונה — בסיס
1.
מהו הטיפוס של כל ערך: 3.3, "3.3", 33, 3 > 1?
תשובות: לפי הסדר: float, str, int, bool.
2.
מה נותן range(2, 5)? ומהו האינדקס השלילי של האיבר הראשון ברשימה של 4 איברים?
תשובות: המספרים 2, 3 ו-4. והאינדקס -4.
רמה שנייה — יישום
3.
כתבו פונקציה שממירה קריאה לאחוז שלם, ונסו אותה על 0, 32768 ו-65535.
תשובות:
def percent(raw):
    return raw * 100 // 65535


for raw in (0, 32768, 65535):
    print(raw, percent(raw))
0 0 | 32768 50 | 65535 100
4.
המחרוזת "12,30,18" הגיעה מחיישן בקו טורי. חברו את שלושת המספרים.
תשובות:
line = "12,30,18"
total = 0
for part in line.split(","):
    total += int(part)
print("total:", total)
total: 60
רמה שלישית — העמקה
5.
רוצים את הקריאה הקטנה ביותר ואת הגדולה ביותר במשך שבוע של פעולה. למה לא שומרים את כל הקריאות ברשימה? ומה שומרים במקומן?
תשובות: הרשימה ממלאת את הזיכרון לפני שהשבוע נגמר. שומרים שני משתנים בלבד, ומעדכנים אותם בכל קריאה בהשוואה.
6.
הלוח שלכם מדפיס מונה אלפיות שנייה ששמור ב-float, ואחרי כמה שעות הוא מפסיק לעלות בקביעות. למה? ומה התרופה?
תשובות: אחרי 224 מספר ממשי של 32 סיביות אינו מסוגל לייצג כל מספר שלם, ולכן העליות הקטנות הולכות לאיבוד. התרופה: מספר שלם, או time.ticks_ms().