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

אפיק 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: אפיק מסונכרן: הנתונים עוברים בשני קווים נפרדים יחד עם פולסי שעון שהבקר מפיק, וכל רכיב נבחר בקו משלו. השם הוא קיצור של 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 (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 (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עוצמת הארה Intensity0 עד 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 (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 (Touch Screen XPT2046)

על הרבה ממסכים אלה יש שכבת מגע התנגדותית. כשלוחצים, שתי שכבות נוגעות זו בזו ונוצר מחלק מתח, ומקום הלחיצה הוא המתח. הרכיב XPT2046 הוא ממיר A/D של 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 (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ערבית
אפיק SPISerial Peripheral Interfaceناقل SPI
קו שעון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 — נוסחאון הספר.