תקשורת טורית UART
עד עכשיו הבקר דיבר עם הדקים שמכירים שני מצבים בלבד: גבוה ונמוך. אבל יש התקנים שרוצים משפטים שלמים: בקר אחר, מודול Bluetooth, מחשב. בפרק הזה נשלח בתים סיבית אחרי סיבית על חוט אחד, בלי שעון משותף, ונראה איך הצד השני יודע היכן מתחיל כל בית, ומה עושים כשמגיע חצי משפט או משפט פגום.
- נסביר את המסגרת: סיבית התחלה, סיביות נתונים, סיבית זוגיות וסיבית עצירה, ונחשב את זמן הבית מקצב השידור.
- נחבר שני לוחות נכון, ונבין למה צריך אדמה משותפת.
- נשתמש ב-
write, ב-readוב-any, ונרכיב שורות שמגיעות בחלקים. - נבנה פרוטוקול טקסט פשוט, עם בדיקת שגיאות ופסק זמן.
- נסביר למה אי אפשר לחבר יציאת RS-232 ישירות ללוח, ולמה מודול HC-05 הוא UART מבחינת הקוד.
8.1 פרוטוקול וסנכרון (Serial and Asynchronous)
כדי להעביר בית אחד במקביל דרושים שמונה חוטים. בתקשורת טורית (Serial) שולחים את הסיביות זו אחר זו על חוט אחד. זה לוקח קצת יותר זמן, אבל החוטים מעטים בהרבה והכבל יכול להיות ארוך.
המילה החשובה בשם היא אסינכרוני: אין חוט שעון בין שני הצדדים. לכל צד יש שעון משלו, והם מסכימים מראש על קצב השידור (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
| 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. המסגרת זהה לחלוטין, אבל המתחים שונים: תחום רחב, חיובי ושלילי, והלוגיקה הפוכה. מתח שלילי הוא 1 ומתח חיובי הוא 0. המתח הגדול עמיד בפני רעש ומאפשר כבל באורך של כמה מטרים.
8.8 מודול Bluetooth קלאסי (Classic Bluetooth Modules)
המודולים HC-05 ו-HC-06 מממשים בתוכם Bluetooth קלאסי, ומופיעים לבקר כהתקן UART רגיל: מה שמגיע מהטלפון יוצא בהדק TX של המודול. לכן התוכנית uart_cmd.py עצמה הופכת לשליטה מהטלפון בלי לשנות שורה, וכל ההבדל הוא בחוט. אנחנו זקוקים למודול הזה כי המודול bluetooth במיקרו-פייתון תומך ב-Bluetooth בצריכה נמוכה, נושא פרק 17, ולא בקלאסי.
קצב ברירת המחדל ברוב המודולים האלה הוא 9600, וקוד הצימוד הנפוץ הוא 1234. אלה ערכי יצרן אופייניים, ובודקים אותם בגיליון הנתונים של המודול שבידינו. ההבדל ביניהם: HC-06 הוא עבד בלבד, ואילו את HC-05 אפשר להגדיר כך שיפתח את החיבור בעצמו.
uart_cmd.py. אחר כך שלחו מהטלפון, בכל אפליקציית טרמינל ל-Bluetooth, את שתי השורות ON ו-OFF. זכרו להגדיר באפליקציה שתשלח סוף שורה.8.9 ב-Raspberry Pi 4/5
ב-Raspberry Pi אין machine, והיציאה היא קובץ במערכת. פותחים אותה בספרייה pyserial, שמתקינים בפקודה pip install pyserial. קודם מפעילים את היציאה מ-sudo raspi-config (בחלק Interface Options). שם היציאה עשוי להשתנות בין הדגמים, ולכן בודקים בתיעוד של Raspberry Pi. גם מבחינת החוטים, המתחים ב-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) | פתיחת היציאה ב-Pi. |
מילון מונחים:
| עברית | 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