برمجة المتحكّمات بلغة مايكروبايثون · الفصل 9 — ناقل I2Cאפיק I2C · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 9

ناقل I2C

אפיק I2C – The I2C Bus
5 ساعات تعليمية

في الفصل السابق كان كل سلكين يصلان جهازين فقط. لكن المشروع الحقيقي فيه ساعة وشاشة ومحوّل وحسّاس، وأطراف اللوحة محدودة. هنا يأتي ناقل يكفيه سلكان لعشرات الشرائح، ولكل شريحة عنوان. نفهم ما يجري على السلكين بتّا بتّا، ثم نكتب بأنفسنا مكتبات ثلاث شرائح وشاشتين، ونجمعها كلها على ناقل واحد.

في هذا الفصل:
  • نشرح دور SDA وSCL ومقاومتي الرفع، ونحسب حدود قيمتهما.
  • نقرأ معاملة على الناقل: البداية والعنوان والإقرار والبيانات والتوقف.
  • نمسح الناقل ونستخرج عنوان أي شريحة، ونعالج شريحة لا تجيب.
  • نوسّع أطراف اللوحة بالشريحة PCF8574، فنضيء 4 ثنائيات باعثة للضوء عبر طرفين فقط.
  • نقرأ الساعة DS1307 ونضبطها، ونتعامل مع الترميز BCD.
  • نكتب مكتبة لشاشة LCD ومكتبة لشاشة OLED.

9.1 ما هو ناقل I2C؟ – מאפייני האפיק (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 – התנסות ב-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 – התנסות ב-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 عبر جسر PCF8574 – תצוגת 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 ووحدة framebuf – תצוגת 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 4/5 – ב-Raspberry Pi

المكتبات الثلاث pcf8574.py وds1307.py وi2c_lcd.py لا تطلب من الناقل إلا writeto وreadfrom. لذلك تعمل على الباي دون تغيير، بالملف 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
ملاحظة: وعلى الباي يوضع 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 — لوحة القوانين الشاملة. (נוסחאון הספר)