תכנות מיקרו-בקרים בשפת MicroPython · פרק 9 — אפיק I2C⌂📚🏠
פרק 9

אפיק I2C

The I2C Bus
5 שעות לימוד

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

בפרק הזה:
  • נסביר את תפקידם של SDA ו-SCL ושל נגדי המשיכה, ונחשב את גבולות ערכם.
  • נקרא עסקה על האפיק: התחלה, כתובת, אישור, נתונים ועצירה.
  • נסרוק את האפיק, נמצא את הכתובת של כל שבב, ונטפל בשבב שלא עונה.
  • נרחיב את הדקי הלוח בעזרת השבב PCF8574 ונדליק 4 נוריות דרך שני הדקים בלבד.
  • נקרא את השעון DS1307 ונכוונן אותו, ונתמודד עם הקידוד BCD.
  • נכתוב ספרייה למסך LCD וספרייה למסך OLED.

9.1 מאפייני האפיק (Bus Features)

האפיק I2C הוא שני חוטים בלבד: חוט הנתונים SDA וחוט השעון SCL. עליהם מחובר התקן ראשי (Master, בקר ראשי), שהוא הבקר שלנו, ועוד הרבה התקני עבד (Slave). רק הראשי מייצר את השעון ופותח כל שיחה, וכל עבד שותק עד ששומע את הכתובת שלו.

ControllerSDASCL3V34.7 kΩPCF85740x20ADS11150x48DS13070x68SDASCL
אפיק אחד ועליו שלושה שבבים, שני נגדי משיכה משותפים, וכתובת שונה לכל שבב.
UARTI2C
מספר החוטיםTX, RX, אדמהSDA, SCL, אדמה
שעוןאין חוט: אסינכרוניב-SCL: סינכרוני
מספר ההתקניםשנייםהרבה, לכל אחד כתובת של 7 סיביות
כיווןשני הכיוונים יחדכיוון אחד בכל רגע
מהירות נפוצהעד 115200 bit/s100 kHz או 400 kHz
אישוראיןסיבית אישור אחרי כל בית

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

9.2 נגדי משיכה (Pull-up Resistors)

אין באפיק שבב שדוחף את החוט למעלה. כל שבב יכול דבר אחד: לחבר את החוט לאדמה, או לעזוב אותו. זה נקרא ניקוז פתוח (Open Drain). אז מי מעלה את החוט כשכולם עוזבים אותו? נגד המשיכה (Pull-up) המחובר להזנה. יש בכך יתרון גדול: אם שני שבבים טעו ודיברו יחד, אף אחד מהם לא מתנגש בשני, החוט פשוט נשאר נמוך ושום דבר לא נשרף.

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

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

גבולות נגד המשיכה
Rmin = VDD − VOLIOL
סמלפירושיחידה
VOLהמתח הגבוה ביותר המותר ברמה הנמוכה, 0.4 V V
IOLזרם המשיכה, 3 mA בשתי המהירויות A
זמן עלייה
tr = 0.8473 · R · Cb
סמלפירושיחידה
trזמן העלייה, הגבול שלו 1000 ns ב-100 kHz ו-300 ns ב-400 kHz s
Cbקיבול האפיק, הגבול שלו 400 pF F
דוגמה 9.1
אפיק במתח 3.3 V וקיבולו 100 pF. מהו תחום הערכים של נגד המשיכה ב-100 kHz? והאם 4.7 kΩ מתאים ב-400 kHz?
פתרון:
Rmin = VDD − VOLIOL
Rmin = 3.3 − 0.43 mA
Rmin = 967 Ω
Rmax = tr0.8473 · Cb
Rmax = 1000 ns0.8473 · 100 pF
Rmax = 11.8 kΩ
tr = 0.8473 · R · Cb
tr = 0.8473 · 4.7 kΩ · 100 pF
tr = 398 ns

הזמן גדול מ-300 ns, ולכן הערך הזה לא מתאים ב-400 kHz עם הקיבול הזה, ומתאים ב-100 kHz.

Rmin = 967 Ω, Rmax = 11.8 kΩ
שאלה 9.1: שלושה מודולים על אפיק אחד, ובכל מודול שני נגדי משיכה של 10 kΩ. מהי ההתנגדות בפועל על כל חוט?

9.3 התחלה, Acknowledge וסיום (A Bus Transaction)

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

SCLSDAA6A5A4A3A2A1A0WACKSP
הראשי פותח שיחה עם הכתובת 0x27 לכתיבה: S התחלה, ACK שהעבד מייצר, P עצירה.
דוגמה 9.2
איזה בית מופיע על החוט כשפונים לשבב 0x27 לכתיבה? ולקריאה?
פתרון:

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

b = 2 · A + d
b = 2 · 39 + 0
b = 78

כלומר 0x4E לכתיבה ו-0x4F לקריאה.

הערה: במיקרו-פייתון כותבים את הכתובת בשבע סיביות, כמו 0x27, והיחידה מוסיפה את סיבית הכיוון בעצמה. אבל גיליונות נתונים מסוימים כותבים את הכתובת בשמונה סיביות, למשל 0x4E. אם הסריקה לא תואמת, זה הדבר הראשון שבודקים.
דוגמה 9.3
כמה זמן לוקח לשדר כתובת ובית נתונים אחד ב-100 kHz, בלי תנאי ההתחלה והעצירה?
פתרון:

כל בית הוא 8 פולסים ואישור, כלומר 9 פולסים.

t = 2 · 9 · T
t = 18 · 10 µs
t = 180 µs
t = 180 µs
שאלה 9.2: מה מונע משבב אחר באפיק לענות במקום השבב המיועד?

9.4 כתובת וסריקת האפיק (Scanning the Bus)

מפרק 6 אנחנו מכירים את הפונקציה board.i2c(), שמכוונת את האפיק ל-400 kHz. זה מתאים לממיר ADS1115. אבל גיליונות הנתונים של PCF8574 ו-DS1307 מגדירים רק 100 kHz. לכן בפרק הזה יוצרים את האפיק בעצמנו, במהירות של השבב האיטי ביותר עליו, ועם שמות ההדקים מ-board. השורה הזאת עובדת בשני הלוחות:

הסריקה: התוכנית הראשונה שמריצים עם כל שבב חדש.
from machine import I2C, Pin
import board

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
found = i2c.scan()
print("found", len(found), "chips")
for addr in found:
    print("0x%02X" % addr)
ESP32 + Pico W · MicroPython 1.29 · simulated board
found 3 chips
0x20
0x48
0x68
פקודהמה היא עושה
I2C(0, scl=Pin(n), sda=Pin(n), freq=100000)יוצר את אפיק מספר 0 על שני ההדקים האלה.
i2c.scan()מנסה את כל הכתובות, ומחזיר רשימה של מי שענה באישור.
i2c.writeto(addr, data)התחלה, כתובת, בתים, עצירה.
i2c.readfrom(addr, n)התחלה, כתובת, קריאה של n בתים, עצירה.

מה אם השבב לא עונה? writeto נעצר בשגיאה מסוג OSError, ותוכנית בלי טיפול תיעצר כאן. כך תופסים אותה:

אין שבב על האפיק: מגלים זאת והתוכנית לא נעצרת.
from machine import I2C, Pin
import board

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
try:
    i2c.writeto(0x27, bytes([0]))
    print("ACK from 0x27")
except OSError:
    print("no answer from 0x27")
print("the program goes on")
ESP32 + Pico W · MicroPython 1.29 · simulated board
no answer from 0x27
the program goes on
תסמיןהסיבה הסבירה ביותר
הרשימה ריקהאין נגדי משיכה, או ש-SDA ו-SCL מוחלפים, או שאין הזנה לשבב.
כתובת לא צפויההדקי הכתובת מחוברים אחרת, או שזו גרסה אחרת של השבב.
כל הכתובות עונות, או שהסריקה נתקעתאחד החוטים מחובר לאדמה.
עובד רק לפעמיםחוט ארוך, או קיבול גדול וזמן עלייה ארוך, או מהירות גבוהה ממה שהשבב מחזיק.
אזהרה: מודולים רבים מוזנים ב-5 V ויש בהם נגדי משיכה ל-5 V: האפיק עולה למתח גבוה ממה שהדק הלוח מחזיק. מחברים את נגדי המשיכה ל-3.3 V, או מציבים ממיר רמות בין שני הצדדים.

9.5 התנסות ב-PCF8574 (I/O Expander)

השבב PCF8574 הוא מרחיב הדקים: שמונה הדקים P0–P7 שכותבים אליהם וקוראים מהם דרך האפיק. אין אוגרים: כותבים בית אחד ושמונת ההדקים משתנים, וקוראים בית אחד ומקבלים את מצבם.

הכתובת 0x20 ועוד שלוש הסיביות A2 A1 A0 שקובעים בחיווט, וכך מגיעים לשמונה שבבים על אותו אפיק. לגרסה PCF8574A כתובות אחרות שמתחילות ב-0x38.

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

VCCSDASCLGNDP0P1P2P3PCF8574Board3V3SDASCLGND3V3
4 נוריות דרך שני הדקים בלבד של הלוח. A0, A1 ו-A2 מחוברים לאדמה ולא צוירו, ולכן הכתובת היא 0x20.

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

כותבים בית וקוראים אותו: P0 נמוך כי כתבנו אותו כך, ו-P4 נמוך בגלל הלחצן. מה שתחת [pcf] הוא מה שהגיע לשבב.
from machine import I2C, Pin
import board

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
i2c.writeto(0x20, bytes([0b11111110]))
value = i2c.readfrom(0x20, 1)[0]
print("P7..P0 = {:08b}".format(value))
ESP32 + Pico W · MicroPython 1.29 · simulated board
P7..P0 = 11101110
[pcf]
fe
טעות נפוצה: כדי לקרוא הדק שמשמש כניסה, חייבים קודם לכתוב אליו 1. אם כתבנו אליו 0, נקרא תמיד 0, יהיה מצב הלחצן אשר יהיה. לכן הספרייה שלנו מתחילה בכתיבת 0xFF. ואין צורך ב-Pin.IN כאן: בשבב אין אוגר כיוון.

עכשיו נכתוב מחלקה קטנה, ונשים לב שהיא לא מייבאת את machine: התוכנית נותנת לה את האפיק מוכן, כמו ב-ads1115.py.

הספרייה pcf8574.py
# pcf8574.py - driver for the PCF8574 eight-line I2C port expander.
# ESP32 and Pico: pass a machine.I2C. Raspberry Pi 4/5: pass a pi_i2c.PiI2C.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com


class PCF8574:
    def __init__(self, i2c, addr=0x20):
        self.i2c = i2c
        self.addr = addr
        self.write(0xFF)

    def write(self, value):
        self.state = value & 0xFF
        self.i2c.writeto(self.addr, bytes([self.state]))

    def read(self):
        return self.i2c.readfrom(self.addr, 1)[0]

    def set_pin(self, n, level):
        if level:
            self.write(self.state | (1 << n))
        else:
            self.write(self.state & ~(1 << n))

    def get_pin(self, n):
        return (self.read() >> n) & 1

הפונקציה set_pin משנה הדק אחד, אבל השבב מבין רק בית שלם. לכן הספרייה שומרת את הערך האחרון ב-self.state ולא משנה את שאר ההדקים. עכשיו אור שרץ על ארבע הנוריות:

האור הרץ: fe פירושו P0 נמוך, כלומר דלוק.
from machine import I2C, Pin
from pcf8574 import PCF8574
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
pcf = PCF8574(i2c, 0x20)

for n in (0, 1, 2, 3, 2, 1, 0):
    pcf.write(~(1 << n))
    print("LED", n, "on")
    time.sleep(0.25)
pcf.write(0xFF)
ESP32 + Pico W · MicroPython 1.29 · simulated board
LED 0 on
LED 1 on
LED 2 on
LED 3 on
LED 2 on
LED 1 on
LED 0 on
[pcf]
ff
fe
fd
fb
f7
fb
fd
fe
ff
הלחצן על P4 והנורית על P0, באותו שבב.
from machine import I2C, Pin
from pcf8574 import PCF8574
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
pcf = PCF8574(i2c, 0x20)

while True:
    pressed = pcf.get_pin(4) == 0
    pcf.set_pin(0, 0 if pressed else 1)
    print("pressed" if pressed else "released")
    time.sleep(1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
released
pressed
pressed
released
...
הערה: לשבב יש הדק INT שיורד כשכל כניסה משתנה. מחברים אותו לפסיקה בלוח, כמו בפרק 5, ואז אין צורך לשאול את האפיק כל הזמן.
שאלה 9.3: חיברנו את A2 להזנה ואת A1 ו-A0 לאדמה. מהי הכתובת של PCF8574?

9.6 התנסות ב-DS1307 (Real-Time Clock)

השעון של הבקר נעצר כשההזנה מתנתקת. לעומתו שעון זמן אמת (RTC) הוא שבב שסופר שניות בלי הפסקה, עם גביש קטן וסוללה. הכתובת של DS1307 היא תמיד 0x68.

X1X2VBATGNDVCCSDASCLDS130732.768 kHz+−3 V+5 VSDASCL
שבב השעון: גביש וסוללת גיבוי. נגדי המשיכה ל-3.3 V כמו בסעיף 9.2 ולא צוירו.
אזהרה: לפי גיליון הנתונים השבב מוזן בין 4.5 V ל-5.5 V, ועובד רק במהירות 100 kHz. לכן ההזנה מ-5 V, והאפיק עצמו נשאר ב-3.3 V עם נגדי המשיכה.

בתוך השבב שבעה אוגרים בקידוד עשרוני בינארי (BCD): כל ספרה עשרונית בארבע סיביות. השנייה 45 נשמרת כ-0100 0101, כלומר 0x45, ולא כ-45 בבינארי רגיל. כך אדם קורא את הערך ישירות בהקסדצימלי.

דוגמה 9.4
איזה בית כותבים כדי לייצג את הדקה 37? ומה ערכו של הבית 0x59 אם קראנו אותו?
פתרון:

העשרות 3 בארבע הסיביות העליונות, והיחידות 7 בארבע התחתונות, ולכן הבית 0011 0111, כלומר 0x37. בעשרוני זה 55.

הבית 0x59: ארבע הסיביות העליונות 5 והתחתונות 9, כלומר 5 · 10 + 9 = 59.

אוגרמשמעותהערה
0שניותסיבית 7 היא CH: ב-1 השעון נעצר
1דקות
2שעותסיבית 6: 0 למערכת של 24 שעות
3יום בשבועמ-1 עד 7, ואת משמעותו אנחנו קובעים
4יום בחודש
5חודש
6שנהמ-00 עד 99

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

הבית הראשון בכתיבה הוא מספר האוגר. את הערכים קוראים כפי שכתבנו, בקידוד BCD.
from machine import I2C, Pin
import board

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
regs = bytes([0, 0x30, 0x59, 0x09, 0x01, 0x02, 0x11, 0x26])
i2c.writeto(0x68, regs)
i2c.writeto(0x68, bytes([0]))
data = i2c.readfrom(0x68, 7)
print(" ".join("%02X" % b for b in data))
ESP32 + Pico W · MicroPython 1.29 · simulated board
30 59 09 01 02 11 26
הספרייה ds1307.py
# ds1307.py - driver for the DS1307 real-time clock, 24-hour mode.
# ESP32 and Pico: pass a machine.I2C. Raspberry Pi 4/5: pass a pi_i2c.PiI2C.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com


def to_bcd(x):
    return ((x // 10) << 4) | (x % 10)


def from_bcd(b):
    return (b >> 4) * 10 + (b & 0x0F)


class DS1307:
    def __init__(self, i2c, addr=0x68):
        self.i2c = i2c
        self.addr = addr

    def set_time(self, year, month, day, hour, minute, second, weekday=1):
        values = (second, minute, hour, weekday, day, month, year - 2000)
        regs = [to_bcd(v) for v in values]
        self.i2c.writeto(self.addr, bytes([0] + regs))

    def now(self):
        self.i2c.writeto(self.addr, bytes([0]))
        r = self.i2c.readfrom(self.addr, 7)
        return (2000 + from_bcd(r[6]), from_bcd(r[5]), from_bcd(r[4]),
                from_bcd(r[2] & 0x3F), from_bcd(r[1]),
                from_bcd(r[0] & 0x7F))
מכוונים את השעון, והוא סופר לבדו ועובר מ-09:59 ל-10:00.
from machine import I2C, Pin
from ds1307 import DS1307
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
rtc = DS1307(i2c)
rtc.set_time(2026, 11, 2, 9, 59, 58)

for i in range(4):
    y, mo, d, h, mi, s = rtc.now()
    print("%04d-%02d-%02d %02d:%02d:%02d" % (y, mo, d, h, mi, s))
    time.sleep(1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
2026-11-02 09:59:58
2026-11-02 09:59:59
2026-11-02 10:00:00
2026-11-02 10:00:01
טעות נפוצה: לפי גיליון הנתונים המצב ההתחלתי של אוגרי השבב לא מוגדר, וייתכן שסיבית CH דלוקה, ואז השעון לא סופר לעולם. כתיבת השניות ב-set_time מאפסת אותו. אסור לקרוא ל-set_time בכל הפעלה: זה מחזיר את השעון לאותו זמן בכל פעם. מכווננים פעם אחת, ואז מוחקים את השורה או הופכים אותה לתנאי, כמו בתרגיל בסוף הפרק.

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

9.7 תצוגת LCD 16x2 (Text Display)

מסך הטקסט 16×2 בנוי סביב הבקר HD44780, ובמקור הוא דורש לפחות שישה הדקים. הגשר הקטן שמורכב מאחוריו הוא בסך הכול PCF8574: הוא הופך את מה שכותבים לאפיק להדקי המסך. לכן כל מה שדרוש הוא שני חוטי האפיק. כתובת הגשר הנפוצה היא 0x27, או 0x3F עם PCF8574A, והסריקה אומרת לנו.

SDASCLP0P1P2P3P4P5P6P7PCF8574BoardSDASCLRSRWELightD4D5D6D7Hello, I2C16 x 2
חלוקת ההדקים בגשר הנפוץ. המסך מקבל נתונים בארבע סיביות ב-D4–D7.

הגשר הנפוץ מחבר למסך ארבע סיביות נתונים בלבד. לכן כל בית מפצלים לשני חצאים: ארבע העליונות קודם, ואז התחתונות. ההדק E הוא פקודת "קח עכשיו": המסך קורא את הנתונים ברגע שהוא יורד. ההדק RS מבדיל בין פקודה (0) לתו (1).

דוגמה 9.5
אילו בתים כותבים לגשר כדי להציג את התו A, שערכו 0x41, כשהתאורה פועלת?
פתרון:

בכל בית: חצי התו בסיביות העליונות, עם RS = 1 ו-Light = 1, וההדק E עולה ואחר כך יורד.

החצי העליון 4: 0x4D ואחריו 0x49.

החצי התחתון 1: 0x1D ואחריו 0x19.

קודם נראה איך משתמשים בספרייה i2c_lcd.py, ואחר כך נציץ לתוכה:

מה שתחת [LCD] הוא תוכן המסך בסוף התוכנית, אחרי פענוח הפולסים על האפיק סיבית אחרי סיבית.
from machine import I2C, Pin
from i2c_lcd import LCD
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
lcd = LCD(i2c, 0x27)
lcd.line(0, "Hello, I2C")

for n in range(1, 4):
    lcd.line(1, "Count: %d" % n)
    time.sleep(1)
print("done")
ESP32 + Pico W · MicroPython 1.29 · simulated board
done
[LCD]
|Hello, I2C      |
|Count: 3        |

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

דוגמה 9.6
כמה זמן לוקח תו אחד? ושורה של 16 תווים ב-100 kHz?
פתרון:

תו הוא שתי פקודות writeto, ובכל אחת כתובת ושני בתים, כלומר 6 בתים, ו-9 פולסים לכל בית.

t = 6 · 9 · T
t = 54 · 10 µs
t = 540 µs
t16 = 16 · t
t16 = 16 · 540 µs
t16 = 8.64 ms
t16 = 8.64 ms
טעות נפוצה: מסך מואר בלי אותיות, או שורה של ריבועים? בדרך כלל זה הפוטנציומטר הקטן שמאחורי הגשר: הוא קובע את הניגודיות של המסך, ומכווננים אותו לפני שחושדים בקוד. והמסך מכיר רק את האותיות הלטיניות שבזיכרונו, ולכן עברית וערבית לא מופיעות. מסכים רבים כאלה מוזנים ב-5 V, ולכן שמים לב למפלסי האפיק כמו בסעיף 9.4.

זו הספרייה i2c_lcd.py במלואה, כפי שהיא בלוח, ואחריה הסבר הפקודות שלה:

# i2c_lcd.py - HD44780 text display behind a PCF8574 I2C bridge.
# Bridge lines: P0=RS P1=RW P2=E P3=light P4..P7=D4..D7.
# ESP32 and Pico: pass a machine.I2C. Raspberry Pi 4/5: pass a pi_i2c.PiI2C.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time

RS = 0x01
ENABLE = 0x04
LIGHT = 0x08
ROW_START = (0x00, 0x40, 0x14, 0x54)


class LCD:
    def __init__(self, i2c, addr=0x27, cols=16):
        self.i2c = i2c
        self.addr = addr
        self.cols = cols
        self.light = LIGHT
        time.sleep(0.05)
        for high in (0x30, 0x30, 0x30, 0x20):
            self._nibble(high, 0)
            time.sleep(0.005)
        self._send(0x28, 0)
        self._send(0x0C, 0)
        self._send(0x06, 0)
        self.clear()

    def _nibble(self, high, rs):
        out = high | rs | self.light
        self.i2c.writeto(self.addr, bytes([out | ENABLE, out]))

    def _send(self, value, rs):
        self._nibble(value & 0xF0, rs)
        self._nibble((value << 4) & 0xF0, rs)

    def clear(self):
        self._send(0x01, 0)
        time.sleep(0.002)

    def move(self, col, row):
        self._send(0x80 | (ROW_START[row] + col), 0)

    def write(self, text):
        for ch in text:
            self._send(ord(ch), RS)

    def line(self, row, text):
        self.move(0, row)
        self.write((text + " " * self.cols)[:self.cols])

    def backlight(self, on):
        self.light = LIGHT if on else 0
        self.i2c.writeto(self.addr, bytes([self.light]))
פקודהמה היא עושה
(0x30, 0x30, 0x30, 0x20)המסך מתעורר במצב של 8 סיביות, והרצף הזה מגיליון הנתונים שלו מעביר אותו למצב של 4 סיביות.
self._send(0x28, 0)פקודה: 4 סיביות ושתי שורות.
self._send(0x0C, 0)פקודה: המסך פועל, בלי סמן.
self._send(0x06, 0)פקודה: המיקום מתקדם ימינה אחרי כל תו.
self._send(0x01, 0)פקודת המחיקה, והיא איטית, ולכן ממתינים אחריה.
0x80 | ...פקודת קביעת מיקום. השורה השנייה מתחילה ב-0x40.

9.8 תצוגת OLED (Graphic Display)

מסך OLED קטן, 128×64 נקודות עם השבב SSD1306, שולט בכל נקודה. הכתובת הנפוצה שלו 0x3C, והוא עובד ב-400 kHz, ולכן כאן מספיק לנו board.i2c(). הרעיון: מציירים בזיכרון, ואז שולחים את התמונה כולה בבת אחת. במיקרו-פייתון יש מודול מוכן לציור בזיכרון, framebuf. לכן מספיקה ספרייה קצרה שמעבירה את הזיכרון למסך:

הספרייה oled.py, למסך 128×64 בלבד
# oled.py - SSD1306 graphic display, 128 x 64, on I2C. MicroPython only.
# Drawing commands come from framebuf: text, line, rect, fill, pixel.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import framebuf

WIDTH = 128
HEIGHT = 64
SETUP = (0xAE, 0x20, 0x00, 0xA1, 0xC8, 0xA8, HEIGHT - 1, 0xDA, 0x12,
         0x81, 0x7F, 0x8D, 0x14, 0xAF)


class OLED(framebuf.FrameBuffer):
    def __init__(self, i2c, addr=0x3C):
        self.i2c = i2c
        self.addr = addr
        self.data = bytearray(1 + WIDTH * HEIGHT // 8)
        self.data[0] = 0x40
        pixels = memoryview(self.data)[1:]
        super().__init__(pixels, WIDTH, HEIGHT, framebuf.MONO_VLSB)
        for byte in SETUP:
            self._command(byte)

    def _command(self, byte):
        self.i2c.writeto(self.addr, bytes([0x00, byte]))

    def show(self):
        for byte in (0x21, 0, WIDTH - 1, 0x22, 0, HEIGHT // 8 - 1):
            self._command(byte)
        self.i2c.writeto(self.addr, self.data)

המחלקה OLED ירשה מ-framebuf.FrameBuffer את כל פקודות הציור. וכל כתיבה מתחילה בבית בקרה: 0x00 לפקודות ו-0x40 לנתונים. לכן שמרנו לו את הבית הראשון בזיכרון, ולא מעתיקים את התמונה בשליחה.

המודול framebuf כאן אמיתי, והדפסנו את הפינה העליונה של הזיכרון אחרי הציור.
from oled import OLED
import board

screen = OLED(board.i2c(), 0x3C)
screen.fill(0)
screen.text("OK", 0, 0)
screen.rect(0, 12, 128, 20, 1)
screen.show()

for y in range(8):
    row = ""
    for x in range(16):
        row += "#" if screen.pixel(x, y) else "."
    print(row)
ESP32 + Pico W · MicroPython 1.29 · simulated board
..####...##..##.
.##..##..##.##..
.##..##..####...
.##..##..###....
.##..##..####...
.##..##..##.##..
..####...##..##.
................
הערה חשובה: בתוכנית הזאת רואים את הזיכרון בלבד: בבניית הספר השבב על האפיק הוא רק אוגרים שמקבלים מה שכותבים אליהם. לכן תקינות הפקודות לא נבדקה על מסך אמיתי, וגם לא התמונה שמופיעה עליו. רשימת ההגדרות נלקחה מגיליון הנתונים של השבב, ובמודולים מסוימים ייתכן שצריך לשנות את ערכיה.
פקודהמה היא עושה
screen.fill(0)מוחק את הזיכרון.
screen.text("OK", x, y)טקסט בגופן של 8×8 נקודות.
screen.pixel(x, y, 1)מצייר נקודה. בלי הערך השלישי הוא קורא אותה.
screen.line(x1, y1, x2, y2, 1)קו.
screen.rect(x, y, w, h, 1)מלבן.
screen.show()שולח את הזיכרון למסך. בלעדיו שום דבר לא מופיע.

9.9 קשר ל-ADS1115 (Everything on One Bus)

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

שעון וקורא מתח על מסך, וכל זה על שני הדקים של הלוח.
from machine import I2C, Pin
from ads1115 import ADS1115
from ds1307 import DS1307
from i2c_lcd import LCD
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
adc = ADS1115(i2c)
rtc = DS1307(i2c)
lcd = LCD(i2c)
rtc.set_time(2026, 11, 2, 9, 30, 0)

while True:
    y, mo, d, h, mi, s = rtc.now()
    volts = adc.read_volts(0)
    lcd.line(0, "%02d:%02d:%02d" % (h, mi, s))
    lcd.line(1, "A0 = %.3f V" % volts)
    print("%02d:%02d:%02d  %.3f V" % (h, mi, s, volts))
    time.sleep(1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
09:30:00  0.500 V
09:30:01  1.250 V
09:30:02  2.500 V
[LCD]
|09:30:02        |
|A0 = 2.500 V    |
...

רק הכתובת מפרידה בין השבבים, ולכן שני התקנים לא יכולים לחלוק את אותה כתובת. אם דרושים שני התקנים זהים עם כתובת קבועה, משתמשים באפיק שני I2C(1) על שני הדקים אחרים.

9.10 ב-Raspberry Pi

שלוש הספריות pcf8574.py, ds1307.py ו-i2c_lcd.py מבקשות מהאפיק רק writeto ו-readfrom. לכן הן עובדות ב-Pi בלי שינוי, עם הקובץ pi_i2c.py שכתבנו בפרק 6, ועם אותה הכנה. אבל oled.py לא עובדת, כי framebuf הוא חלק ממיקרו-פייתון ואינו קיים בפייתון הרגיל.

שורות ה-import וה-PiI2C() הן ההבדל היחיד מתוכנית הבקר.
import time
from pcf8574 import PCF8574
from pi_i2c import PiI2C

pcf = PCF8574(PiI2C(), 0x20)

for n in range(4):
    pcf.write(~(1 << n))
    print("LED", n, "on")
    time.sleep(0.25)
pcf.write(0xFF)
Raspberry Pi 4/5 · Python 3 · simulated bus
LED 0 on
LED 1 on
LED 2 on
LED 3 on
הערה: גם ב-Pi, SDA על GPIO2 ו-SCL על GPIO3, כמו בפרק 6. תיעוד Raspberry Pi קובע מהירות ברירת מחדל של 100 kHz, והיא מתאימה לשבבים האלה.

עיקרי הפרק:

סיכום הכללים בפרק:

הכללהנוסחה
נגד משיכה מינימליRmin = VDD − VOLIOL
זמן עלייהtr = 0.8473 · R · Cb
בית הכתובתb = 2 · A + d

פקודות הפרק:

פקודהמה היא עושה
I2C(0, scl=Pin(n), sda=Pin(n), freq=f)יוצר את האפיק, בשני הלוחות.
i2c.scan()רשימת הכתובות שענו.
i2c.writeto(addr, data)כתיבת בתים.
i2c.readfrom(addr, n)קריאת בתים.
PCF8574(i2c, addr)מרחיב הדקים: write, read, set_pin, get_pin.
DS1307(i2c)השעון: set_time, now.
LCD(i2c, addr)מסך טקסט: line, move, write, clear, backlight.
OLED(i2c, addr)מסך ציור: כל פקודות framebuf, ואחר כך show.

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

עבריתEnglishערבית
בקר ראשיMasterالجهاز الرئيسي
עבדSlaveالجهاز التابع
ניקוז פתוחOpen Drainالمصرف المفتوح
נגד משיכהPull-up Resistorمقاومة الرفع
תנאי התחלהStart Conditionشرط البداية
תנאי עצירהStop Conditionشرط التوقف
אישורAcknowledgeالإقرار
כתובתAddressالعنوان
מרחיב הדקיםI/O Expanderموسّع الأطراف
שעון זמן אמתReal-Time Clockساعة الزمن الحقيقي
קוד BCDBinary-Coded Decimalالترميز العشري الثنائي
גשרBridgeالجسر
ניגודיותContrastالتباين
התשובות הסופיות לשאלות הפרק
9.1 R = 103 = 3.33 kΩ.
9.2 הכתובת: כל עבד משווה את שבע הסיביות הראשונות לכתובת שלו, ומושך את קו האישור רק אם הם תואמים.
9.3 0x20 + 4 = 0x24.

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

רמה ראשונה — בסיס
1.
מהם בתי הכתובת של השעון 0x68 לכתיבה ולקריאה?
תשובות: 0xD0 ו-0xD1.
2.
המירו ל-BCD: 37 ו-8. והמירו מ-BCD: 0x24 ו-0x59.
תשובות: 0x37, 0x08; 24, 59.
3.
הסריקה החזירה רשימה ריקה. מנו שלוש סיבות אפשריות.
תשובות: אין נגדי משיכה, או ששני חוטים מוחלפים, או שאין הזנה לשבב.
4.
כתבנו ל-PCF8574 את הבית 0b11110000. אילו מבין ארבע הנוריות על P0–P3 דולקות?
תשובות: כולן: ארבעת ההדקים התחתונים הם 0, והנורית נדלקת ב-0.
רמה שנייה — יישום
5.
קיבול האפיק 200 pF. מהו נגד המשיכה הגדול ביותר ב-100 kHz? וב-400 kHz?
תשובות: R = 5.9 kΩ ב-100 kHz, ו-R = 1.77 kΩ ב-400 kHz.
6.
כתבו תוכנית שסופרת מ-0 עד 15 בבינארי על ארבע הנוריות, כל חצי שנייה. מהו הבית האחרון שמגיע לשבב?
תשובות:
from machine import I2C, Pin
from pcf8574 import PCF8574
import board
import time

i2c = I2C(0, scl=Pin(board.SCL), sda=Pin(board.SDA), freq=100000)
pcf = PCF8574(i2c, 0x20)

for n in range(16):
    pcf.write(~n)
    time.sleep(0.5)
print("done")
הבית האחרון: f0.
7.
כמה זמן לוקח למלא את המסך כולו, 32 תווים, ב-100 kHz? וכמה אם המהירות 400 kHz?
תשובות: 17.28 ms, ו-4.32 ms.
רמה שלישית — העמקה
8.
כתבו פונקציה שמחזירה True אם השעון סופר, בבדיקת הסיבית CH. והשתמשו בה כדי לכוונן את הזמן פעם אחת בלבד.
תשובות:
def running(rtc):
    rtc.i2c.writeto(rtc.addr, bytes([0]))
    return not rtc.i2c.readfrom(rtc.addr, 1)[0] & 0x80
ואז כותבים: if not running(rtc): rtc.set_time(...).
9.
למה now() בספרייה מתחילה בפקודה writeto(addr, bytes([0]))? ומה קורה בלעדיה?
תשובות: היא מחזירה את מצביע האוגרים לשניות. בלעדיה הקריאה מתחילה מהמקום שבו המצביע נעצר, והשדות מתערבבים.
10.
על אפיק אחד שמים רק ADS1115 ומסך OLED. באיזו מהירות בוחרים? אחר כך מוסיפים DS1307: מה משתנה? ולמה שני הראשונים לא נפגעים?
תשובות: 400 kHz מספיקים לשניהם. עם DS1307 יורדים ל-100 kHz, כי כל השבבים שומעים את אותו שעון. שני השבבים המהירים יותר עובדים בכל מהירות נמוכה יותר, אבל שליחת התמונה למסך נעשית איטית פי ארבעה.
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 25 — נוסחאון הספר.