الاتصال التسلسلي UART
حتى الآن كلّم المتحكّم أطرافا تعرف حالتين: مرتفع ومنخفض. لكن بعض الأجهزة تريد جملا كاملة: متحكّم آخر، وحدة بلوتوث، حاسوب. في هذا الفصل نرسل البايتات بتّا بعد بتّ على سلك واحد، بلا ساعة مشتركة، ونرى كيف يفهم الطرف الآخر أين يبدأ كل بايت، وماذا نفعل حين يصل نصف جملة أو تصل جملة تالفة.
- نشرح الإطار: بتّ البداية والبيانات والتماثل والتوقف، ونحسب زمن البايت من معدّل الإرسال.
- نوصل لوحتين توصيلا صحيحا، ونعرف لماذا نحتاج إلى أرضي مشترك.
- نستعمل
writeوreadوany، ونجمع الأسطر من أجزاء متفرّقة. - نبني بروتوكولا نصّيا بسيطا فيه فحص للأخطاء ومهلة انتظار.
- نشرح لماذا لا يوصل منفذ RS-232 باللوحة مباشرة، ولماذا وحدة HC-05 هي UART من جهة الشيفرة.
8.1 لماذا تسلسلي؟ ولماذا بلا ساعة؟ – פרוטוקול וסנכרון (Serial and Asynchronous)
لنقل بايت واحد بالتوازي نحتاج إلى ثمانية أسلاك. أما الاتصال التسلسلي (תקשורת טורית) فيرسل البتّات واحدا بعد الآخر على سلك واحد. الزمن أطول قليلا، وعدد الأسلاك أقل كثيرا، والكابل يمكن أن يطول.
الكلمة المهمّة في الاسم هي غير متزامن (אסינכרוני): لا يوجد سلك ساعة بين الطرفين. لكل طرف ساعته، ويتفقان مسبقا على معدّل الإرسال (קצב שידור, Baud)، أي عدد البتّات في الثانية. والمستقبل يضبط توقيته من جديد عند بداية كل بايت. ولأن لكل اتجاه سلكا خاصا، يمكن للطرفين أن يرسلا في الوقت نفسه، وهذا ما نسمّيه ثنائي الاتجاه الكامل (דו-כיווני מלא).
| المزايا | الحدود |
|---|---|
| سلكان وأرضي فقط، وشيفرة بسيطة جدا. | طرفان فقط على الخط: لا عناوين ولا أجهزة كثيرة. |
| لا ساعة مشتركة، فالتوصيل سهل. | يجب الاتفاق مسبقا على المعدّل وشكل الإطار. |
| إرسال واستقبال في الوقت نفسه. | أبطأ من SPI، والمسافة قصيرة بجهود المتحكّم. |
8.2 الإطار وزمن البايت – מסגרת וזמן בית (The Frame)
الخط الساكن مرتفع. كي يعرف المستقبل أن بايتا بدأ، يخفض المرسل الخط لمدة بتّ واحد: هذا بتّ البداية (סיבית התחלה). بعده تأتي بتّات البيانات، الأقل وزنا أولا، ثم بتّ التوقف (סיבית עצירה) المرتفع. هذا كله هو الإطار (מסגרת). والرسم يبيّن البايت 0x41، أي الحرف A.
A بالشكل 8N1: بدايةٌ، ثمانية بتّات تبدأ بالأقل وزنا، توقّف.الشكل الشائع يُكتب 8N1: ثمانية بتّات بيانات، بلا تماثل، وبتّ توقف واحد. فالبايت يكلّف 10 بتّات على الخط وليس 8.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| tbyte | زمن إرسال بايت واحد | s |
| nframe | عدد بتّات الإطار، 10 في 8N1 | — |
| B | معدّل الإرسال | bit/s |
في كل ثانية نرسل 960 بايتا على الأكثر، وليس 1200.
| المعدّل bit/s | زمن البتّ µs | زمن البايت ms | بايت في الثانية |
|---|---|---|---|
| 9600 | 104.17 | 1.042 | 960 |
| 19200 | 52.08 | 0.521 | 1920 |
| 38400 | 26.04 | 0.260 | 3840 |
| 57600 | 17.36 | 0.174 | 5760 |
| 115200 | 8.68 | 0.087 | 11520 |
كيف ينجو الاتصال بلا ساعة مشتركة؟ يبدأ المستقبل العدّ عند هبوط بتّ البداية، ويقرأ كل بتّ من منتصفه. آخر قراءة في الإطار تقع بعد 9.5 بتّ من البداية، ويُسمح بأن ينحرف التوقيت نصف بتّ قبل أن تقع القراءة في البتّ الخطأ. فأكبر فرق نظري بين الساعتين هو 5.3 في المئة. وهذا حدّ نظري، والهامش الفعلي أصغر منه، لكنه يفسّر لماذا تكفي ساعة ليست دقيقة جدا، ولماذا لا يصلح الإطار الطويل بلا بتّ بداية جديد.
بتّ التماثل (סיבית זוגיות) اختياري، يُضاف بعد البيانات ليكشف خطأ في بتّ واحد. في التماثل الزوجي يكون مجموع الوحدات، مع بتّ التماثل، عددا زوجيا: البايت 0x41 فيه وحدتان، فبتّ التماثل 0. والبايت 0x43 فيه ثلاث وحدات، فبتّ التماثل 1. ونطلبه في مايكروبايثون عبر uart.init(9600, parity=0) للزوجي، أو parity=1 للفردي.
8.3 التوصيل: التقاطع والأرضي – חיבור בין שני בקרים (Wiring)
الإرسال عند طرف يدخل إلى الاستقبال عند الطرف الآخر. والسلك الثالث هو الأرضي: الجهد يُقاس بالنسبة إلى مرجع، وبلا مرجع مشترك لا يعني 3.3 V شيئا عند الطرف الآخر.
والمسافة؟ جهود المتحكّم ضعيفة أمام الضجيج، ولذلك نبقى في حدود المنضدة نفسها وأسلاك قصيرة، وكلما ارتفع المعدّل قصرت المسافة الممكنة. وللمسافات الأطول نحوّل الجهود إلى معيار آخر، كما نرى في القسم 8.7.
8.4 أوامر UART في مايكروبايثون – פקודות UART
| ESP32 | Pico W / Pico 2 W | |
|---|---|---|
| عدد وحدات UART | 3: UART0 وUART1 وUART2 | 2: UART0 وUART1 |
| سطر الأوامر REPL | على UART0، فلا نستعملها | على USB |
ما يعطيه board.uart() | UART2، TX 17، RX 16 | UART1، TX GP8، RX GP9 |
| حرية اختيار الطرف | كل طرف لـTX، ولـRX أيضا 34–39 | مجموعة محددة لكل وحدة |
# board.py - ESP32 DevKit
from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer
TX = 17
RX = 16
def uart(baudrate=9600):
return UART(2, baudrate, tx=Pin(TX), rx=Pin(RX))يذكر توثيق مايكروبايثون أن الطرفين الافتراضيين 17 و16 قد يكونان مشغولين في بعض اللوحات، لأنهما يتعارضان مع ذاكرة PSRAM المركّبة عليها. فعلى لوحة كهذه نغيّر رقمي الطرفين في board.py وحده، ولا يتغيّر سطر في أي برنامج. وهذا بالضبط سبب وجود الملف. نختار للاستقبال طرفا مثل 36، وهو مدخل فقط، وللإرسال طرفا غير مستعمل في المشروع. والمقطع التالي لا يشغَّل في بناء الكتاب:
TX = 25 RX = 36
قبل أن نوصل أي جهاز، نجرّب الوحدة وحدها: نوصل TX بـRX في اللوحة نفسها بسلك واحد. فكل ما نرسله يعود إلينا. هذا الفحص يفصل مشكلة الشيفرة عن مشكلة الجهاز الآخر.
import board
import time
uart = board.uart(9600)
print("sent:", uart.write(b"hello\n"))
time.sleep(0.1)
print("waiting:", uart.any())
print("received:", uart.read())
print("empty:", uart.read())sent: 6 waiting: 6 received: b'hello\n' empty: None
| الأمر | عمله |
|---|---|
| uart = board.uart(9600) | ينشئ الوحدة بهذا المعدّل، والشكل الافتراضي 8N1. |
| uart.write(b"hello\n") | يرسل بايتات، ويعيد عددها. لا يقبل عددا: نرسل نصا أو bytes. |
| uart.any() | هل وصل شيء؟ صفر يعني لا. |
| uart.read() | يأخذ كل ما وصل. ويعيد None إذا لم يصل شيء. |
any() قد يعيد 1 رغم وجود أكثر من بايت في بعض اللوحات. لذلك نعامله كجواب نعم أو لا، ولا نبني على العدد نفسه: نقرأ بـread() ما وصل كله.8.5 الأسطر تصل قطعا – קליטת שורות (Receiving Lines)
المتحكّم أسرع من الخط كثيرا. فحين نقرأ قد نجد نصف جملة، والباقي ما زال في الطريق. وإذا لم يصل شيء يعيد read() القيمة None، ولذلك نفحصها قبل أي عملية. والحل أن نجمع ما يصل في مخزن (חוצץ)، ونأخذ منه سطرا كلما ظهرت فيه علامة نهاية السطر.
import board
import time
uart = board.uart(9600)
buf = b""
while True:
data = uart.read()
if data:
buf += data
print("buffer:", buf)
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
print("line:", line.decode())
time.sleep(0.05)buffer: b'TEM' buffer: b'TEMP 24\nHU' line: TEMP 24 buffer: b'HUM 60\n' line: HUM 60 ...
uart.readline() يبدو أسهل. لكن التوثيق يقول إنه قد يعود قبل نهاية السطر إذا انتهت المهلة، وبالمهلة الافتراضية صفر قد نحصل على نصف سطر. لذلك نكتب المخزن بأنفسنا، وهو لا يعلّق البرنامج ولا يقطع الجملة. وفي الكتاب لا تُظهر المُحاكاة هذا السلوك.while داخل if، وليس if ثانية؟8.6 لوحتان تتحادثان: بروتوكول نصّي – פרוטוקול טקסט פשוט (A Text Protocol)
البروتوكول (פרוטוקול) هو اتفاق على شكل الرسائل. أبسط اتفاق: كل رسالة سطر نصّي. سهل قراءته بالعين، وسهل تجربته من الحاسوب. هذه لوحة تنفّذ أمرين وتردّ على كل سطر:
from machine import Pin
import board
import time
led = Pin(board.LED, Pin.OUT)
uart = board.uart(9600)
buf = b""
def handle(line):
if line == b"ON":
led.on()
return b"OK ON\n"
if line == b"OFF":
led.off()
return b"OK OFF\n"
return b"ERR\n"
while True:
data = uart.read()
if data:
buf += data
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
answer = handle(line.strip())
uart.write(answer)
print(line, "->", answer, "LED =", led.value())
time.sleep(0.01)b'ON' -> b'OK ON\n' LED = 1 b'BLINK' -> b'ERR\n' LED = 1 b'OFF\r' -> b'OK OFF\n' LED = 0 [uart_tx] b'OK ON\n' b'ERR\n' b'OK OFF\n' ...
انتبهوا إلى line.strip(): بعض برامج الحاسوب تنهي السطر بالبايتين \r\n، ودونه لا يطابق b"OFF".
ماذا لو تلف بتّ في الطريق؟ يصير ON شيئا آخر، وقد نحصل على أمر صحيح آخر بالصدفة. فنضيف إلى كل سطر مجموع فحص (סכום ביקורת): عددا يحسبه المرسل من النص، ويحسبه المستقبل من جديد ويقارن. ونأخذ أبسط حساب، وهو XOR لكل الأحرف. يكفي هذا لكشف أي خطأ في بتّ واحد، ونكتبه في مكتبة صغيرة يستعملها الطرفان:
# textlink.py - one line of text with a check, for example "LED,1*5A\n".
# The two hex digits are the XOR of all the characters before the star.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
def checksum(text):
total = 0
for ch in text:
total ^= ord(ch)
return total
def pack(text):
return ("%s*%02X\n" % (text, checksum(text))).encode()
def unpack(line):
try:
text, check = line.decode().strip().split("*")
if int(check, 16) == checksum(text):
return text
except ValueError:
pass
return NoneLED,1؟ وكم يستغرق بمعدّل 9600 bit/s؟نحسب XOR لخمسة الأحرف فنجد 0x50، فالسطر هو LED,1*50 ومعه نهاية السطر، أي 9 بايتات.
from machine import Pin
from textlink import pack, unpack
import board
import time
led = Pin(board.LED, Pin.OUT)
uart = board.uart(9600)
buf = b""
while True:
data = uart.read()
if data:
buf += data
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
text = unpack(line)
if text == "LED,1":
led.on()
elif text == "LED,0":
led.off()
uart.write(pack("ACK" if text else "NAK"))
print(line, "->", text, "LED =", led.value())
time.sleep(0.01)b'LED,1*50' -> LED,1 LED = 1 b'LED,0*00' -> None LED = 1 b'garbage' -> None LED = 1 b'LED,0*51' -> LED,0 LED = 0 [uart_tx] b'ACK*49\n' b'NAK*44\n' b'NAK*44\n' b'ACK*49\n' ...
والجانب المرسل يجب ألا ينتظر إلى الأبد، فالطرف الآخر قد يكون مطفأ. نعطيه مهلة (פסק זמן) بالطريقة غير الحاجبة التي تعلّمناها في الفصل 4:
ask يعيد None في الحالتين الأخيرتين.from textlink import pack, unpack
import board
import time
uart = board.uart(9600)
def ask(text, timeout_ms=500):
uart.write(pack(text))
start = time.ticks_ms()
buf = b""
while time.ticks_diff(time.ticks_ms(), start) < timeout_ms:
data = uart.read()
if data:
buf += data
if b"\n" in buf:
return unpack(buf.split(b"\n", 1)[0])
time.sleep_ms(10)
return None
for command in ("LED,1", "LED,0", "LED,1"):
print(command, "->", ask(command))
time.sleep(1)LED,1 -> ACK LED,0 -> None LED,1 -> None
8.7 معيار RS-232 ومحوّل المستويات – תקן RS-232 (RS-232)
المنفذ التسلسلي في أجهزة صناعية كثيرة يتبع معيار RS-232. الإطار هو نفسه تماما، لكن الجهود مختلفة: مجال واسع موجب وسالب، والمنطق مقلوب. فالجهد السالب هو 1 والموجب هو 0. والجهد الكبير يقاوم الضجيج ويسمح بكابل طوله عدة أمتار.
8.8 وحدة بلوتوث هي UART من جهة الشيفرة – מודול Bluetooth קלאסי (Classic Bluetooth Modules)
الوحدتان HC-05 وHC-06 تنفّذان بلوتوث الكلاسيكي داخلهما، وتظهران للمتحكّم كجهاز UART عادي: ما يصل من الهاتف يخرج على طرف TX في الوحدة. فالبرنامج uart_cmd.py نفسه يصبح تحكّما من الهاتف دون تغيير سطر، والفرق كله في السلك. ونحتاج إلى هذه الوحدة لأن وحدة bluetooth في مايكروبايثون تدعم البلوتوث منخفض الطاقة، وهو موضوع الفصل 17، وليس الكلاسيكي.
المعدّل الافتراضي في أغلب هذه الوحدات هو 9600، ورمز الاقتران الشائع 1234. هذه قيم مصنع نموذجية، ونتحقّق منها في ورقة بيانات الوحدة التي بأيدينا. والفرق بينهما: HC-06 تابع فقط، أما HC-05 فيمكن ضبطها لتبدأ الاتصال بنفسها.
uart_cmd.py. ثم أرسلوا من الهاتف، عبر أي تطبيق طرفية بلوتوث، السطرين ON وOFF. وتذكّروا أن تضبطوا التطبيق ليرسل نهاية السطر.8.9 على Raspberry Pi 4/5 – ב-Raspberry Pi
على الباي لا توجد machine، بل المنفذ ملف في النظام، ونفتحه بالمكتبة pyserial التي تثبّت بالأمر pip install pyserial. ونفعّل المنفذ أولا من sudo raspi-config (قسم Interface Options). اسم المنفذ قد يختلف بين الطرازات، فنراجع توثيق Raspberry Pi. ومن جهة الأسلاك جهود الباي أيضا 3.3 V. والمقطع التالي لم يشغَّل في بناء الكتاب:
uart_cmd.py ويطبع جوابهاimport serial
port = serial.Serial("/dev/serial0", 9600, timeout=1)
port.write(b"ON\n")
print(port.readline())
port.close()أهمّ أفكار الفصل:
- الإطار يبدأ بهبوط الخط، وهذا الهبوط هو ما يزامن المستقبل: لا سلك ساعة.
- البايت يكلّف 10 بتّات في 8N1، فبمعدّل 9600 نرسل 960 بايتا في الثانية.
- TX إلى RX، وأرضي مشترك دائما. ونبدأ بفحص الحلقة.
- الأسطر تصل قطعا، فنجمعها في مخزن ولا نفترض أن قراءة واحدة تكفي.
- نضيف إلى البروتوكول مجموع فحص ومهلة انتظار، فإطار سليم لا يعني رسالة سليمة، والصمت أيضا جواب.
- RS-232 جهوده خطيرة على الطرف، وبينهما شريحة تحويل مستويات.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| زمن البايت | tbyte = nframeB |
أوامر الفصل:
| الأمر | عمله |
|---|---|
| board.uart(baud) | الوحدة والأطراف المناسبة للوحة. |
| uart.write(data) | يرسل، ويعيد عدد البايتات. |
| uart.any() | هل في المخزن شيء؟ |
| uart.read() | كل ما وصل، أو None. |
| uart.readline() | سطر، وقد يعيد سطرا ناقصا عند انتهاء المهلة. |
| uart.init(9600, parity=0) | يغيّر الإعدادات، وهنا تماثل زوجي. |
| buf.split(b"\n", 1) | يقسم المخزن عند أول نهاية سطر. |
| serial.Serial(port, 9600, timeout=1) | فتح المنفذ على الباي. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| الاتصال التسلسلي | תקשורת טורית | Serial Communication |
| غير متزامن | אסינכרוני | Asynchronous |
| معدّل الإرسال | קצב שידור | Baud Rate |
| الإطار | מסגרת | Frame |
| بتّ البداية | סיבית התחלה | Start Bit |
| بتّ التوقف | סיבית עצירה | Stop Bit |
| بتّ التماثل | סיבית זוגיות | Parity Bit |
| ثنائي الاتجاه الكامل | דו-כיווני מלא | Full Duplex |
| محوّل المستويات | ממיר רמות | Level Shifter |
| البروتوكول | פרוטוקול | Protocol |
| المخزن | חוצץ | Buffer |
| مجموع الفحص | סכום ביקורת | Checksum |
| المهلة | פסק זמן | Timeout |
if نأخذ سطرا واحدا ويبقى الآخر في المخزن حتى يصل شيء جديد.أسئلة للتمرين:
3 ثم 4؟import board
import time
uart = board.uart(9600)
buf = b""
total = 0
while True:
data = uart.read()
if data:
buf += data
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
total += int(line)
print("sum =", total)
time.sleep(0.01)OK، واكتبوا السطر كاملا.OK*04.(ب) ما الحل إذا كان الخط كثير الضجيج؟
ask_retry التي تستعمل ask وتحاول حتى ثلاث مرات.def ask_retry(text, tries=3):
for i in range(tries):
answer = ask(text)
if answer == "ACK":
return True
print("try", i + 1, "failed")
return False