برمجة المتحكّمات بلغة مايكروبايثون · الفصل 2 — بايثون بالقدر الذي يحتاجه المتحكّمפייתון במידה שהבקר צריך · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 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().