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

ناقل SPI

אפיק SPI – The SPI Bus
4 ساعات تعليمية

في الفصل السابق وصلنا عدة شرائح بسلكين، ودفعنا الثمن سرعة محدودة. لكن شاشة ملوّنة فيها عشرات آلاف النقاط لا تنتظر. في هذا الفصل نتعلّم ناقلا أبسط وأسرع: أسلاكه أكثر، ولا عناوين فيه، ويصل إلى ملايين البتّات في الثانية. نبدأ بمصفوفة ضوئية، ثم شاشة، ثم شاشة لمس، ونكتب بأنفسنا مكتبتين صغيرتين.

في هذا الفصل:
  • نشرح أدوار الخطوط SCK وMOSI وMISO وCS، ونختار وضع الساعة الصحيح.
  • نقارن SPI بـI2C، ونحسب زمن نقل صورة كاملة.
  • نتحكّم في مصفوفة 8×8 عبر مسجّلات MAX7219، ونتحقّق من البايتات على الخطوط.
  • نملأ مستطيلا على شاشة ILI9341، ونفهم دور الخط DC.
  • نقرأ موضع اللمس من XPT2046 بأمر write_readinto، ونشارك الناقل بين شريحتين.

10.1 أربعة خطوط بدل العناوين – ארבעה קווים במקום כתובות (Four Lines Instead of Addresses)

في I2C يتكلم الجميع على سلك بيانات واحد، ولكل شريحة عنوان. أما SPI فيحل المسألة بالأسلاك: خط لكل اتجاه، وخط خاص لكل شريحة يقول لها: «دورك الآن». والمتحكّم هو الذي يولّد الساعة دائما.

ناقل SPI (אפיק SPI): ناقل متزامن: البيانات تسير على خطين منفصلين مع نبضات ساعة يولّدها المتحكّم، وكل شريحة تُختار بخط خاص بها. الاسم اختصار لـSerial Peripheral Interface.
الخطالاتجاهمعناه
SCKمن المتحكّم إلى الشريحةنبضات الساعة (שעון). كل نبضة تنقل بتّا واحدا.
MOSIمن المتحكّم إلى الشريحةالبيانات الخارجة من المتحكّم.
MISOمن الشريحة إلى المتحكّمالبيانات الداخلة إلى المتحكّم. بعض الشرائح لا تحتاج إليه.
CSمن المتحكّم إلى شريحة واحدةاختيار الشريحة (בחירת רכיב). فعّال عند 0: تسمع الشريحة فقط حين يكون خطها منخفضا.
controllerESP32 / Pico WSCKMOSIMISOCS1CS2MAX7219SCKMOSIMISOCSILI9341SCKMOSIMISOCS
شريحتان على ناقل واحد: ثلاثة خطوط مشتركة، وخط CS خاص بكل شريحة.
ملاحظة: قد تجدون على الوحدات أسماء أخرى لهذه الخطوط: DIN أو SDI بدل MOSI، وDO أو SDO بدل MISO، وCLK بدل SCK، وLOAD أو SS بدل CS. المعنى واحد.

10.2 الساعة ووضعها – השעון ומצבו (Clock Polarity and Phase)

داخل كل طرف من الطرفين مسجّل إزاحة. مع كل نبضة ساعة يخرج بتّ من المتحكّم إلى الشريحة، ويخرج في اللحظة نفسها بتّ من الشريحة إلى المتحكّم. فالنقل تبادل في الأصل: لا يوجد في SPI «كتابة» بلا «قراءة»، وإنما نتجاهل أحد الاتجاهين حين لا نحتاج إليه. ولا يحتوي الناقل على عناوين ولا على إشارة تأكيد، ولذلك هو سريع.

على الطرفين أن يتفقا على أمرين، يصفهما عددان في machine.SPI:

mode 0polarity=0, phase=0mode 1polarity=0, phase=1mode 2polarity=1, phase=0mode 3polarity=1, phase=1
الأوضاع الأربعة. الخط الأخضر هو لحظة أخذ البتّ.

العددان معا يعطيان أربعة أوضاع. والوضع مكتوب في ورقة بيانات الشريحة، ولا نخمّنه. الشرائح الأربع في هذا الفصل تعمل على الوضع 0، وهو القيمة الافتراضية في machine.SPI: polarity=0, phase=0. ومعها تأتي قيمتان افتراضيتان أخريان: 8 بتّات في الكلمة، والبتّ الأعلى أولا.

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

كم يستغرق النقل؟ لأن كل نبضة تنقل بتّا واحدا، فالحساب مباشر:

زمن النقل على SPI
t = N · bf
الرمزالمعنىوحدة القياس
tأقل زمن لنقل البيانات s
Nعدد الوحدات المنقولة (نقاط أو بايتات)—
bعدد البتّات في الوحدة—
fتردد الساعة Hz
مثال 10.1
شاشة فيها 240 · 320 نقطة، ولكل نقطة 16 بتّا، وساعة الناقل 10 MHz. ما أقل زمن لرسم صورة كاملة؟ وكم صورة في الثانية على الأكثر؟
الحل:
t = N · bf
t = 76800 · 16107
t = 122.88 ms

عدد الصور في الثانية هو مقلوب هذا الزمن: نحو 8.1. والوقت الحقيقي أطول، لأن البرنامج يحتاج أيضا إلى وقته.

t = 122.88 ms
سؤال 10.1: الشاشة نفسها على ناقل I2C بتردد 400 kHz: كم يستغرق نقل الصورة، إذا أهملنا بتّات العنوان والتأكيد؟

10.3 مقارنة مع I2C – השוואה ל-I2C (SPI versus I2C)

I2CSPI
عدد الأسلاك2، لأي عدد من الشرائح3 مشتركة، وخط CS لكل شريحة
اختيار الشريحةعنوان يُرسل على الناقلسلك خاص
الاتجاهاتجاه واحد في كل لحظةالاتجاهان معا
السرعة في هذا الكتاب400 kHz1–10 MHz
الحد الأعلى على ESP32—حتى 80 MHz على الأطراف الافتراضية، حسب التوثيق
هل تجيب الشريحة؟نعم: بتّ تأكيد بعد كل بايتلا: لا يوجد تأكيد
البحث عن الشرائحi2c.scan()لا يوجد
الاستعمال المناسبحسّاسات بطيئة كثيرةشاشات، بطاقات، بيانات كثيرة
انتبهوا: لأنه لا يوجد تأكيد، فلا يعرف المتحكّم هل الشريحة موصولة أصلا. spi.write() ينجح دائما، حتى لو كان السلك مقطوعا. نكتشف الخطأ من سلوك الشريحة فقط. وهذا صحيح أيضا عن المُحاكاة: تسجّل ما يخرج على الخطوط، ولا توجد شريحة تستقبله.

10.4 الناقل في مايكروبايثون – האפיק במיקרו-פייתון (SPI in MicroPython)

الأطراف والرقم الداخلي للناقل مختلفة بين اللوحتين، ولذلك أخفيناها في الملف board.py:

على ESP32
# board.py - ESP32 DevKit
from machine import ADC, I2C, PWM, SPI, UART, Pin, Timer

SCK = 18
MOSI = 23
MISO = 19
CS = 5
DC = 4
RST = 15


def spi(baudrate=1000000):
    return SPI(2, baudrate, sck=Pin(SCK), mosi=Pin(MOSI), miso=Pin(MISO))

وعلى Pico W الملف نفسه، مع SPI(0) والأطراف SCK = 18, MOSI = 19, MISO = 16, CS = 17, DC = 20, RST = 21.

خط CS ليس جزءا من الكائن SPI: هو طرف خرج عادي نتحكّم فيه بأنفسنا. نُنزله قبل النقل، ونرفعه بعده، وعند رفعه تُنفّذ أغلب الشرائح ما وصلها. هذا هو البرنامج الأول. طلبنا من المُحاكاة أن تعرض تغيّرات الأطراف، ثم البايتات على الناقل:

نرسل بايتين داخل نافذة CS واحدة.
import board
from machine import Pin

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)

cs.value(0)
spi.write(bytes([0x0A, 0x03]))
cs.value(1)
print('sent 2 bytes')
ESP32 · MicroPython 1.29 · simulated board
sent 2 bytes
[pins]
5 0
5 1
[spi]
0a 03
Pico W · MicroPython 1.29 · simulated board
sent 2 bytes
[pins]
17 0
17 1
[spi]
0a 03

نقرأ المُخرَج: القسم [pins] يعرض رقم الطرف ومستواه. نزل CS إلى 0 ثم صعد إلى 1، وبينهما خرج البايتان 0a 03. ولقراءة الاتجاهين معا نستعمل write_readinto. في المثال التالي نضع في العالم المُحاكى بايتين تعيدهما الشريحة:

كل بايت خارج يقابله بايت داخل في الوقت نفسه.
import board
from machine import Pin

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)

tx = bytes([0x12, 0x34])
rx = bytearray(2)
cs.value(0)
spi.write_readinto(tx, rx)
cs.value(1)
print('sent     %02x %02x' % (tx[0], tx[1]))
print('received %02x %02x' % (rx[0], rx[1]))
ESP32 + Pico W · MicroPython 1.29 · simulated board
sent     12 34
received a1 b2
[spi]
12 34
الأمرعمله
board.spi(baud)ناقل على أطراف اللوحة، بالتردد المطلوب.
spi.write(data)يرسل البايتات، ويتجاهل ما يعود.
spi.read(n)يقرأ n بايت، ويرسل أصفارا.
spi.write_readinto(tx, rx)يرسل tx ويكتب ما يعود في rx. لهما الطول نفسه.
spi.init(baudrate=f)يغيّر تردد الناقل وهو يعمل.
ملاحظة: الناقل في board.py عتادي: ينفّذه جزء خاص من الشريحة. وفي مايكروبايثون أيضا machine.SoftSPI الذي يعمل على أي طرف، لكنه أبطأ، ولا نحتاج إليه في هذا الفصل.

قبل أن نوصل شيئا، نفحص أمرا قد يوقعنا في خطأ. الاسمان DC وRST يقعان على ESP32 على طرفين نستعملهما لغرض آخر في الكتاب:

نسأل الملف board.py نفسه.
import board

print('DC  =', board.DC, ' DHT =', board.DHT)
print('RST =', board.RST, ' NEOPIXEL =', board.NEOPIXEL)
print('shared:', board.DC == board.DHT or board.RST == board.NEOPIXEL)
ESP32 · MicroPython 1.29 · simulated board
DC  = 4  DHT = 4
RST = 15  NEOPIXEL = 15
shared: True
Pico W · MicroPython 1.29 · simulated board
DC  = 20  DHT = 10
RST = 21  NEOPIXEL = 7
shared: False
انتبهوا: على ESP32 الطرف 4 هو DC وDHT معا، والطرف 15 هو RST وNEOPIXEL معا. لا نستعمل الشاشة وحسّاس DHT أو شريط NeoPixel في دائرة واحدة على هذه الأطراف. في مشروع يحتاج إليها جميعا ننقل أحدها إلى طرف حر. وعلى Pico W لا يوجد هذا التعارض.

10.5 مصفوفة 8×8 مع MAX7219 – מטריצה 8×8 עם MAX7219 (LED Matrix with MAX7219)

المصفوفة (מטריצה) فيها 64 ثنائيا باعثا للضوء، ولا يكفي عدد أطراف اللوحة لوصلها مباشرة. الشريحة MAX7219 تتولّى الأمر: تحتفظ بصورة المصفوفة في مسجّلات داخلية، وتمسح الصفوف بنفسها، ولا نرسل نحن سوى الصورة. الاتجاه هنا واحد، ولذلك لا نحتاج إلى MISO.

VCCGNDDINCSCLKMAX72195V (VIN)GNDMOSICSSCKESP32 / Pico W
توصيل وحدة MAX7219. المصفوفة نفسها موصولة بالشريحة على الوحدة.
انتبهوا: حسب ورقة البيانات، الشريحة تعمل بتغذية بين 4 V و5.5 V، وتطلب على مداخلها مستوى عاليا لا يقل عن 3.5 V. أطراف لوحتينا تعطي 3.3 V فقط: أقل من المطلوب. كثيرون يصلونها مباشرة وتعمل، لكن هذا غير مضمون، والحل الصحيح محوّل مستوى (ממיר רמות). والأهم: لا نوصل خرجا بجهد 5 V بأي طرف من أطراف اللوحة.

كل أمر للشريحة حزمة من 16 بتّا، أي بايتان: رقم المسجّل ثم قيمته. أهم المسجّلات:

المسجّلالاسمالقيم
0x01 … 0x08الصفوف الثمانيةكل بتّ مضاء أو مطفأ
0x09فك الترميز Decode Mode0 للمصفوفة، بلا فك ترميز
0x0Aشدة الإضاءة Intensityمن 0 إلى 15
0x0Bعدد الصفوف الممسوحة Scan Limit7 لكل الصفوف
0x0Cالإيقاف Shutdown0 إيقاف، 1 تشغيل
0x0Fاختبار الإضاءة Display Test1 يضيء كل شيء، 0 عمل عادي
سؤال 10.2: نريد ضبط شدة الإضاءة على 8. ما البايتان اللذان نرسلهما؟

نرسم الآن قلبا. كل صف بايت، وكل بتّ فيه نقطة مضاءة أو مطفأة:

D7D6D5D4D3D2D1D0reg 0x010x00reg 0x020x66reg 0x030xFFreg 0x040xFFreg 0x050x7Ereg 0x060x3Creg 0x070x18reg 0x080x00
كل صف له مسجّل، والقيمة في اليمين هي ما نكتبه فيه.

نضع ما عرفناه في مكتبة صغيرة، كتبناها لهذا الكتاب. الناقل وخط CS يصلان إليها من الخارج، ولذلك لا تعرف شيئا عن اللوحة:

الملف max7219.py، على كل اللوحات
# max7219.py - driver for an 8x8 LED matrix with the MAX7219 chip.
# spi: a machine.SPI in mode 0. cs: an output Pin, active low.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com

REG_DIGIT0 = 0x01
REG_DECODE = 0x09
REG_INTENSITY = 0x0A
REG_SCAN = 0x0B
REG_SHUTDOWN = 0x0C
REG_TEST = 0x0F


class MAX7219:
    def __init__(self, spi, cs, intensity=3):
        self.spi = spi
        self.cs = cs
        self.cs.value(1)
        self._send(REG_TEST, 0)
        self._send(REG_DECODE, 0)
        self._send(REG_SCAN, 7)
        self.intensity(intensity)
        self.clear()
        self._send(REG_SHUTDOWN, 1)

    def _send(self, reg, value):
        self.cs.value(0)
        self.spi.write(bytes([reg, value]))
        self.cs.value(1)

    def intensity(self, level):
        if not 0 <= level <= 15:
            raise ValueError('intensity is 0 to 15')
        self._send(REG_INTENSITY, level)

    def show(self, rows):
        for i in range(8):
            self._send(REG_DIGIT0 + i, rows[i])

    def clear(self):
        self.show(bytes(8))
القلب على الخطوط. البايتات مصدرها المكتبة.
import board
from machine import Pin
from max7219 import MAX7219

HEART = (0x00, 0x66, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00)

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
matrix = MAX7219(spi, cs)
matrix.show(HEART)
print('heart sent')
ESP32 + Pico W · MicroPython 1.29 · simulated board
heart sent
[spi]
0f 00
09 00
0b 07
0a 03
01 00
02 00
03 00
04 00
05 00
06 00
07 00
08 00
0c 01
01 00
02 66
03 ff
04 ff
05 7e
06 3c
07 18
08 00

نتحقّق من قائمة [spi] مقابل جدول المسجّلات: السطور الأربعة الأولى هي ضبط الاختبار وفك الترميز والمسح والشدة. ثم ثمانية أسطر تمسح الصفوف، ثم 0c 01 تشغّل الشريحة، وأخيرا ثمانية أسطر هي القلب: 02 66 يعني أن الصف الثاني قيمته 0x66. مسحنا المصفوفة قبل التشغيل حتى لا تظهر محتويات عشوائية عند بدء العمل.

ملاحظة: اتجاه الصور على الوحدة الحقيقية يتعلّق بطريقة تركيب المصفوفة: في بعض الوحدات تظهر الصورة مقلوبة أو مدارة. في هذه الحالة نقلب البتّات أو ترتيب الصفوف في البرنامج.

الزمن هنا صغير جدا: ثمانية صفوف من 16 بتّا على 1 MHz تأخذ 128 µs. لذلك لا نشعر بأي انتظار مع المصفوفة. أما مع الشاشة فالقصة مختلفة.

10.6 شاشة ملوّنة ILI9341 – מסך גרפי ILI9341 (Graphic Display ILI9341)

الشريحة ILI9341 تقود شاشة من 240 · 320 عنصر صورة (פיקסל)، ونسمّي عنصر الصورة اختصارا النقطة. هنا يوجد نوعان من البايتات: أوامر وبيانات. والشريحة لا تستطيع أن تميّز بينهما بقراءة البايت نفسه، فنضيف خطا آخر:

الخط DC (קו נתונים/פקודה): خط خرج من المتحكّم: عند 0 يكون البايت الجاري أمرا، وعند 1 يكون بيانات. ولا علاقة له بالجهد المستمر رغم تشابه الاسم.

والخط RST يعيد الشريحة إلى حالتها الابتدائية عند 0، ونحرّكه مرة واحدة عند البدء. وللشاشة خط LED للإضاءة الخلفية.

VCCGNDCSRESETDCSDISCKLEDSDOILI93413V3GNDCSRSTDCMOSISCK3V3MISOESP32 / Pico WESP32: DC = 4 (DHT) RST = 15 (NEOPIXEL)
توصيل الشاشة. على ESP32 لا يبقى الطرفان 4 و15 لحسّاس DHT ولا لشريط NeoPixel.

نرتّب الرسم في ثلاثة أوامر: نحدّد نافذة الأعمدة، ونافذة الصفوف، ثم نكتب الألوان، وتنتقل الشريحة وحدها من نقطة إلى التي تليها:

الأمرالاسمالبيانات
0x11الخروج من النوم SLPOUTبلا، ثم انتظار
0x3Aصيغة النقطة COLMOD0x55: 16 بتّا لكل نقطة
0x36اتجاه الرسم MADCTL0x48 في إعدادنا
0x29تشغيل الشاشة DISPONبلا
0x2Aنافذة الأعمدة CASETالبداية والنهاية، كل منهما بايتان
0x2Bنافذة الصفوف PASETالبداية والنهاية، كل منهما بايتان
0x2Cكتابة الذاكرة RAMWRلون كل نقطة، بايتان لكل نقطة

اللون في 16 بتّا: 5 للأحمر، و6 للأخضر، و5 للأزرق. وهذا هو التنسيق RGB565. نأخذ البتّات العليا من كل لون ونضعها في مكانها. فاللون البرتقالي (255, 165, 0) يصبح 0xFD20.

المكتبة تكتفي بمهمة واحدة: ملء مستطيل. الحروف والخطوط والصور خارجها. وهذا مقصود: نقرأ كل سطر فيها ونفهمه:

الملف ili9341_mini.py، على كل اللوحات: الثوابت واللون
# ili9341_mini.py - fill rectangles on an ILI9341 display. Nothing more.
# spi: a machine.SPI. cs, dc, rst: output Pins.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import struct
import time

SLPOUT = 0x11
DISPON = 0x29
CASET = 0x2A
PASET = 0x2B
RAMWR = 0x2C
MADCTL = 0x36
COLMOD = 0x3A
WIDTH = 240
HEIGHT = 320
CHUNK = 256


def rgb565(r, g, b):
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
الصنف: البدء وإرسال الأوامر
class ILI9341:
    def __init__(self, spi, cs, dc, rst):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.cs.value(1)
        rst.value(0)
        time.sleep(0.01)
        rst.value(1)
        time.sleep(0.12)
        self._cmd(SLPOUT)
        time.sleep(0.12)
        self._cmd(COLMOD, b'\x55')
        self._cmd(MADCTL, b'\x48')
        self._cmd(DISPON)

    def _cmd(self, command, data=b''):
        self.cs.value(0)
        self.dc.value(0)
        self.spi.write(bytes([command]))
        if data:
            self.dc.value(1)
            self.spi.write(data)
        self.cs.value(1)
تتمة الصنف: ملء المستطيل
    def fill_rect(self, x, y, w, h, color):
        if x < 0 or y < 0 or w < 1 or h < 1:
            raise ValueError('bad rectangle')
        if x + w > WIDTH or y + h > HEIGHT:
            raise ValueError('outside the screen')
        self._cmd(CASET, struct.pack('>HH', x, x + w - 1))
        self._cmd(PASET, struct.pack('>HH', y, y + h - 1))
        left = w * h
        n = min(left, CHUNK)
        block = struct.pack('>H', color) * n
        self.cs.value(0)
        self.dc.value(0)
        self.spi.write(bytes([RAMWR]))
        self.dc.value(1)
        while left >= n:
            self.spi.write(block)
            left -= n
        if left:
            self.spi.write(block[:2 * left])
        self.cs.value(1)
مستطيل صغير جدا، ليمكننا قراءة ما يخرج على الخطوط.
import board
from machine import Pin
from ili9341_mini import ILI9341, rgb565

spi = board.spi(10000000)
cs = Pin(board.CS, Pin.OUT, value=1)
dc = Pin(board.DC, Pin.OUT, value=0)
rst = Pin(board.RST, Pin.OUT, value=1)

lcd = ILI9341(spi, cs, dc, rst)
lcd.fill_rect(10, 20, 3, 2, rgb565(255, 0, 0))
print('filled', 3 * 2, 'pixels')
ESP32 + Pico W · MicroPython 1.29 · simulated board
filled 6 pixels
[spi]
11
3a
55
36
48
29
2a
00 0a 00 0c
2b
00 14 00 15
2c
f8 00 f8 00 f8 00 f8 00 f8 00 f8 00

نقرأ الأسطر الأخيرة: 2a هو أمر نافذة الأعمدة، ثم البيانات 00 0a 00 0c، أي من 10 إلى 12. ثم 2b والصفوف من 20 إلى 21. وبعد الأمر 2c يخرج اللون الأحمر f8 00 ست مرات، مرة لكل نقطة. الخط DC يكون 0 عند الأوامر و1 عند البيانات، حسب ما كتبناه في _cmd.

والآن الشاشة كاملة. نكتب اللون على دفعات من 256 نقطة حتى لا نحجز في الذاكرة 153600 بايت. ثم نخطئ عمدا في آخر سطر، لنرى أن المكتبة تحرس حدود الشاشة:

السطر الأخير يطلب مستطيلا يتجاوز الحد 240، فتوقفه المكتبة برسالة واضحة.
import board
from machine import Pin
from ili9341_mini import ILI9341, WIDTH, HEIGHT, rgb565

BAUD = 10000000
spi = board.spi(BAUD)
cs = Pin(board.CS, Pin.OUT, value=1)
dc = Pin(board.DC, Pin.OUT, value=0)
rst = Pin(board.RST, Pin.OUT, value=1)

lcd = ILI9341(spi, cs, dc, rst)
lcd.fill_rect(0, 0, WIDTH, HEIGHT, rgb565(0, 0, 255))

pixels = WIDTH * HEIGHT
print('pixels:', pixels)
print('at least %.1f ms on the wire' % (pixels * 16 / BAUD * 1000))
lcd.fill_rect(230, 0, 20, 10, rgb565(0, 0, 255))
ESP32 + Pico W · MicroPython 1.29 · simulated board
pixels: 76800
at least 122.9 ms on the wire
ValueError: outside the screen
قبل التركيب: هذا الإعداد مختصر عن قصد، ولم نجرّبه على شاشة حقيقية: في بناء الكتاب خرجت البايتات إلى المُحاكاة. وتحتاج بعض الوحدات إلى أوامر إضافية من مصنّعها، أو تظهر فيها الألوان مبدّلة، فنغيّر عندها قيمة MADCTL حسب ورقة بيانات الوحدة.

10.7 شاشة اللمس XPT2046 – מסך מגע XPT2046 (Touch Screen XPT2046)

فوق كثير من هذه الشاشات طبقة لمس مقاومة (מגע התנגדותי). عند الضغط تتلامس طبقتان فيتكوّن مجزّئ جهد، وموضع الضغط هو الجهد. الشريحة XPT2046 هي محوّل تماثلي رقمي من 12 بتّا يقيس هذا الجهد، ولها خط T_IRQ ينخفض عند اللمس.

القراءة تبادل حقيقي على الناقل. نرسل بايت أمر ثم بايتين فارغين، وفي هذين البايتين تعود القيمة. 0xD0 يقيس المحور X، و0x90 يقيس المحور Y. والنتيجة 12 بتّا تجلس في وسط البايتين، ولذلك نزيح 3 خانات إلى اليمين.

TCLKTDINTDOTCSTIRQXPT2046SCKMOSIMISOCSBTNESP32 / Pico W
طبقة اللمس وحدها. في هذا المثال خط T_IRQ على طرف اللوحة المسمى BTN.
لمستان في المُحاكاة: عند الثانية 1 والثانية 3. أما القيم الخام فمن العالم المُحاكى.
import time
import board
from machine import Pin

CMD_X = 0xD0
CMD_Y = 0x90
RAW_MIN = 200
RAW_MAX = 3900

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
irq = Pin(board.BTN, Pin.IN, Pin.PULL_UP)


def read_axis(cmd):
    tx = bytes([cmd, 0, 0])
    rx = bytearray(3)
    cs.value(0)
    spi.write_readinto(tx, rx)
    cs.value(1)
    return ((rx[1] << 8) | rx[2]) >> 3


def to_pixel(raw, size):
    return (raw - RAW_MIN) * size // (RAW_MAX - RAW_MIN)


while True:
    if irq.value() == 0:
        x = read_axis(CMD_X)
        y = read_axis(CMD_Y)
        print('raw', x, y, ' pixel', to_pixel(x, 240), to_pixel(y, 320))
    time.sleep(1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
raw 2048 1000  pixel 119 69
raw 600 3000  pixel 25 242
...

الثابتان RAW_MIN وRAW_MAX مثال فقط. كل شاشة تحتاج إلى معايرة (כיול) خاصة بها: نلمس زاويتين معروفتين، ونسجّل ما نقرؤه، ونكتب القيمتين في البرنامج.

10.8 شريحتان على ناقل واحد – שני רכיבים על אפיק אחד (Sharing the Bus)

الشاشة ولوحة اللمس على وحدة واحدة عادة، وتشتركان في الخطوط الثلاثة. يبقى أمران: لكل شريحة خط CS خاص بها، ولا ينزل إلا واحد منهما في كل لحظة. والشريحتان لا تتحملان السرعة نفسها: الشاشة على 10 MHz، أما اللمس فنقرأه على 1 MHz. لذلك نبدّل التردد قبل كل قراءة، ثم نعيده.

في board.py اسم واحد لخط CS. فنستعير للمس طرف TRIG، لأن الحسّاس فوق الصوتي غير موصول في هذه الدائرة. نرسم نقطة خضراء حيث لمس المستخدم:

قراءة اللمس ورسم النقطة على ناقل واحد.
import board
from machine import Pin
from ili9341_mini import ILI9341, rgb565

LCD_BAUD = 10000000
TOUCH_BAUD = 1000000
TOUCH_CS = board.TRIG

spi = board.spi(LCD_BAUD)
cs_lcd = Pin(board.CS, Pin.OUT, value=1)
cs_touch = Pin(TOUCH_CS, Pin.OUT, value=1)
dc = Pin(board.DC, Pin.OUT, value=0)
rst = Pin(board.RST, Pin.OUT, value=1)
lcd = ILI9341(spi, cs_lcd, dc, rst)


def read_axis(cmd):
    rx = bytearray(3)
    spi.init(baudrate=TOUCH_BAUD)
    cs_touch.value(0)
    spi.write_readinto(bytes([cmd, 0, 0]), rx)
    cs_touch.value(1)
    spi.init(baudrate=LCD_BAUD)
    return ((rx[1] << 8) | rx[2]) >> 3


def scale(raw, size):
    pos = (raw - 200) * size // 3700
    return max(0, min(size - 5, pos))


x = scale(read_axis(0xD0), 240)
y = scale(read_axis(0x90), 320)
lcd.fill_rect(x, y, 5, 5, rgb565(0, 255, 0))
print('dot drawn at', x, y)
ESP32 + Pico W · MicroPython 1.29 · simulated board
dot drawn at 119 69
تحذير: الخطأ الكلاسيكي: ننسى رفع CS لإحدى الشريحتين. عندها تسمع الشريحتان البتّات معا، وتحاولان الجواب معا على MISO. لذلك نضبط كل خطوط CS على 1 قبل أن نرسل أول بايت على الناقل: value=1 في السطر الذي ينشئ الطرف.

10.9 قارئ البطاقات RC522 – קורא כרטיסים RC522 (RFID Reader RC522)

الوحدة RC522 تقرأ بطاقات لاسلكية قريبة (RFID) على تردد 13.56 MHz، وتتصل بالمتحكّم عبر SPI أيضا. بروتوكول البطاقات طويل ومعقّد، ولا فائدة من كتابته من الصفر. هذا هو الموقف الذي نستعمل فيه مكتبة من طرف ثالث.

SDASCKMOSIMISORST3.3VGNDRC522CSSCKMOSIMISORST3V3GNDESP32 / Pico W
توصيل قارئ البطاقات. الطرف SDA هو خط الاختيار رغم اسمه، والتغذية 3.3 V وليس 5 V. وعلى ESP32 يقع RST على الطرف 15.

المكتبة المعروفة هي micropython-mfrc522 من Stefan Wendler، برخصة MIT. لكن ملف الوصف فيها يقول إن صاحبها جرّبها على ESP8266 وWiPy فقط، لذلك نحتاج على لوحتينا إلى نسخة معدّلة، وعلينا فحص هذه النسخة كما نتعلم في الفصل التالي. هذا هو شكل البرنامج، بالأوامر التي تعرضها أمثلة المكتبة. هذا البرنامج لم يُشغَّل في بناء الكتاب، لأن المكتبة ليست جزءا من حزمتنا والبطاقات غير موجودة في المُحاكاة، وقد تختلف الأوامر قليلا من نسخة إلى أخرى:

قراءة رقم البطاقة. لا يُشغَّل في بناء الكتاب.
import time
import board
from mfrc522 import MFRC522

rdr = MFRC522(board.SCK, board.MOSI, board.MISO, board.RST, board.CS)

while True:
    stat, kind = rdr.request(rdr.REQIDL)
    if stat == rdr.OK:
        stat, uid = rdr.anticoll()
        if stat == rdr.OK:
            print('card', bytes(uid).hex())
    time.sleep(0.5)
انتبهوا: رقم البطاقة ليس كلمة سر: بعض الأنواع يمكن نسخ رقمها. يكفي لمشروع تعليمي، ولا نبني عليه وحده قفلا لشيء مهم.

أهمّ أفكار الفصل:

ملخّص قوانين الفصل:

القانونالصيغة
زمن النقلt = N · bf

أوامر الفصل:

الأمرعمله
board.spi(baud)إنشاء الناقل بالتردد المطلوب.
Pin(board.CS, Pin.OUT, value=1)خط الاختيار، مرفوعا من البداية.
spi.write(data)إرسال البايتات.
spi.write_readinto(tx, rx)إرسال وقراءة معا.
spi.init(baudrate=f)تغيير التردد بين شريحتين.
MAX7219(spi, cs)مصفوفة 8×8؛ show(rows) وintensity(n) وclear().
ILI9341(spi, cs, dc, rst)الشاشة؛ fill_rect(x, y, w, h, color).
rgb565(r, g, b)لون من ثلاثة بايتات إلى 16 بتّا.
framebuf.FrameBuffer(buf, w, h, fmt)الرسم في الذاكرة؛ text وline وpixel.

قاموس المصطلحات – מילון מונחים:

بالعربيةبالعبريةEnglish
ناقل SPIאפיק SPISerial Peripheral Interface
خط الساعةקו שעוןClock Line
اختيار الشريحةבחירת רכיבChip Select
قطبية الساعةקוטביות שעוןClock Polarity
طور الساعةמופע שעוןClock Phase
مسجّل الإزاحةאוגר הזזהShift Register
المصفوفةמטריצהMatrix
شدة الإضاءةעוצמת הארהIntensity
محوّل المستوىממיר רמותLevel Shifter
عنصر الصورة، النقطةפיקסלPixel
أمر وبياناتפקודה ונתוניםCommand and Data
طبقة اللمسשכבת מגעTouch Layer
المعايرةכיולCalibration
الإجابات النهائية لأسئلة الفصل
10.1 t = 3.07 s. أي 25 مرة أبطأ، وهذا سبب وجود الشاشات الملوّنة على SPI.
10.2 0a 08: رقم المسجّل 0x0A، ثم القيمة.

أسئلة للتمرين:

المستوى الأول — أساسي
1.
ما دور الخط CS؟ وعند أي مستوى يكون فعّالا؟
الإجابات: يختار الشريحة التي تسمع الناقل. فعّال عند 0.
2.
ما البايتان اللذان يضيئان الصف الثالث كله في MAX7219؟
الإجابات: 03 ff: المسجّل 0x03، والقيمة 0xFF.
3.
حوّلوا الأخضر (0, 255, 0) إلى RGB565.
الإجابات: 0x07E0.
المستوى الثاني — تطبيق
4.
اكتبوا برنامجا يرسم الحرف X على المصفوفة، ويطبع بايتات الصفوف.
الإجابات:
import board
from machine import Pin
from max7219 import MAX7219

X = (0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81)

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
matrix = MAX7219(spi, cs)
matrix.show(X)
for row in X:
    print('%02x' % row)
يطبع: 81 42 24 18 18 24 42 81
5.
في مايكروبايثون وحدة جاهزة للرسم في الذاكرة هي framebuf، وتنسيقها MONO_HMSB يطابق المصفوفة: كل بايت صف، والبتّ الأعلى هو النقطة اليسرى. ارسموا بها الحرف A على المصفوفة.
الإجابات:
import framebuf
import board
from machine import Pin
from max7219 import MAX7219

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
matrix = MAX7219(spi, cs)

rows = bytearray(8)
fb = framebuf.FrameBuffer(rows, 8, 8, framebuf.MONO_HMSB)
fb.text('A', 0, 0, 1)
matrix.show(rows)

for row in rows:
    line = ''
    for bit in range(7, -1, -1):
        line += '#' if row >> bit & 1 else '.'
    print(line)
Console
...##...
..####..
.##..##.
.######.
.##..##.
.##..##.
.##..##.
........
6.
مستطيل من 64 · 64 نقطة، 16 بتّا لكل نقطة، على ساعة 20 MHz. ما أقل زمن لرسمه؟
الإجابات: t = 3.28 ms.
7.
برنامج يكتب إلى MAX7219 بلا رفع CS بعد كل حزمة. ماذا نرى؟ ولماذا؟
الإجابات: لا يتغيّر شيء على المصفوفة: الشريحة تنفّذ الحزمة عند صعود CS. والبرنامج لا يبلغ عن أي خطأ، لأن SPI بلا تأكيد.
المستوى الثالث — تعمّق
8.
وصلنا مصفوفتين على التوالي: خرج الأولى يدخل إلى الثانية. والشريحة تزيح كل 16 بتّا إلى التي تليها، وتنفّذ ما عندها عند صعود CS. والمسجّل 0x00 لا يفعل شيئا. اكتبوا دالة تكتب في الشريحة البعيدة فقط.
الإجابات: نرسل الحزمة المطلوبة أولا فتنتقل إلى البعيدة، ثم حزمة فارغة تبقى في القريبة، ونبقي CS منخفضا طوال ذلك:
import board
from machine import Pin

spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)


def send_far(reg, value):
    cs.value(0)
    spi.write(bytes([reg, value, 0x00, 0x00]))
    cs.value(1)


send_far(0x0A, 8)
print('done')
9.
على ESP32 نريد شاشة ILI9341 وحسّاس DHT معا. ما المشكلة؟ وكيف نحلّها دون تغيير المكتبة؟
الإجابات: DC وDHT على الطرف 4. نترك board.DHT مكانه، وننشئ dc على طرف حر بدل board.DC. المكتبة تأخذ الأطراف من الخارج فلا تتغيّر.
10.
لماذا نزيح النتيجة في read_axis 3 خانات إلى اليمين؟
الإجابات: النتيجة 12 بتّا موزعة على 16 نبضة: البتّ الأول صفر، ثم 12 بتّا، ثم ثلاثة بتّات زائدة في الأسفل. الإزاحة تُسقط هذه الثلاثة.
لوحة القوانين: كل قوانين هذا الفصل مجموعة مع رسومها وجداول رموزها في الفصل 25 — لوحة القوانين الشاملة. (נוסחאון הספר)