Im ersten Teil hatten wir die Installation von Thonny, die Einrichtung des Raspberry Pi Pico sowie erste Anwendungsprogramme zur Nutzung der Ein- und Ausgänge kennengelernt. Im zweiten Teil ging es um die Programmierung der bekannten Schnittstellen OneWire, UART und I2C, allesamt Zweitbelegungen der Pins. Da fehlt natürlich noch eine Anwendung für die neueste und schnellste Schnittstelle: SPI oder Serial Peripheral Interface aka 4-wire bus.
Verwendete Hardware
1 |
|
1 |
|
oder 2 |
|
1 |
MCP23S17 Port Expander alternativ klick |
div. |
LEDs oder RGBLED, Jumperkabel |
Im Internet habe ich so gut wie keine Anwendung für die SPI-Schnittstelle auf dem Raspberry Pi Pico gefunden. Und wenn, dann nur für einfache Sensoren mit C++, nicht mit MicroPython. Das möchte ich hiermit ändern und entscheide mich für einen sehr nützlichen, aber auch komplexen IC, den Port Expander MCP23S17, der baugleiche Zwilling der I2C-Variante MCP23017.
Dabei habe ich keine konkrete Anwendung vor Augen, aber 16 zusätzliche Ein- oder Ausgänge kann man immer gebrauchen, zuletzt für mein Modelleisenbahn- Projekt mit vielen Relais-Modulen. Also dieses Mal viel Theorie, sowohl zum Raspberry Pi Pico als auch zu SPI und MCP23x017. Warum jetzt das x im Namen. Wie gesagt, MCP23017 (I2C) und MCP23S17 (SPI) haben bis auf die Schnittstelle zur MCU alles andere gemeinsam.
Zur Erinnerung: Die überwiegend gleiche Pinbelegung der „Zwillinge“
Der Unterschied ist wie gesagt die Schnittstelle, die an den Pins 11 bis 14 zu finden ist. Links im Bild bei der I2C-Variante (aka 2-wire bus) nur SDA und SCK, rechts die vier Anschlüsse für SPI. Die Namensgebung für die Anschlüsse ist teilweise verwirrend und vor allem nicht einheitlich. Und in Zeiten der Political Correctness sollen zwei der möglichen Begriffe abgeschafft werden: Master und Slave. Worum geht es? Der „4-wire bus“ wird nicht nur schneller getaktet, seine Geschwindigkeit ist auch zurückzuführen auf zwei Datenleitungen. Eine Richtung vom Controller (ehedem Master) zum Peripheral (bislang Slave), eine Leitung geht in die Gegenrichtung.
Auf dem Bild noch die alten Begriffe MOSI (Master out, Slave in) bzw. nur SI, an manchen Peripherie-Geräten auch DI, sowie MISO (Master in, Slave out) bzw. SO, auch DO oder SDO. Weitgehend standardisiert ist der Name für die SPI-Taktleitung, nämlich SCK. Anstelle einer Adresse wie bei I2C nutzt SPI eine vierte Leitung, um den Datenaustausch zu initiieren. Auch das trägt zur Geschwindigkeitssteigerung bei. Dieser Anschluss trägt den Namen SS oder CS (Chip Select). Der Querstrich über CS steht für die Negation, also die Datenübertragung erfolgt, wenn der Anschluss auf LOW gesetzt wird.
Wie muss man sich diese Datenübertragung vorstellen? Der MCP23x17 hat Speicherstellen, sogenannte Register, in denen der Mikrocontroller Daten lesen oder in die er Daten schreiben kann. Jedes dieser Register hat eine Adresse, eine Hexadezimalzahl mit einem kryptischen Namen.
In meinem MicroPython-Programm habe ich sämtliche Register mit der üblichen Adresse aufgeführt, auch wenn wir nicht alle verwenden werden.
Registers
IODIRA = 0x00 # Controls the direction of the data I/O for port A.
IODIRB = 0x01 # Controls the direction of the data I/O for port B.
IPOLA = 0x02 # Configures the polarity on the corresponding GPIO-port bits for port A.
IPOLB = 0x03 # Configures the polarity on the corresponding GPIO-port bits for port B.
GPINTENA = 0x04 # Controls the interrupt-on-change for each pin of port A.
GPINTENB = 0x05 # Controls the interrupt-on-change for each pin of port B.
DEFVALA = 0x06 # Controls the default comparison value for interrupt-on-change for port A.
DEFVALB = 0x07 # Controls the default comparison value for interrupt-on-change for port B.
INTCONA = 0x08 # Controls how the associated pin value is compared for the interrupt-on-change for port A.
INTCONB = 0x09 # Controls how the associated pin value is compared for the interrupt-on-change for port B.
IOCON = 0x0A # Controls the device.
GPPUA = 0x0C # Controls the pull-up resistors for the port A pins.
GPPUB = 0x0D # Controls the pull-up resistors for the port B pins.
INTFA = 0x0E # Reflects the interrupt condition on the port A pins.
INTFB = 0x0F # Reflects the interrupt condition on the port B pins.
INTCAPA = 0x10 # Captures the port A value at the time the interrupt occurred.
INTCAPB = 0x11 # Captures the port B value at the time the interrupt occurred.
GPIOA = 0x12 # Reflects the value on the port A.
GPIOB = 0x13 # Reflects the value on the port B.
OLATA = 0x14 # Provides access to the port A output latches.
OLATB = 0x15 # Provides access to the port B output latches.
Auf den zweiten Blick erkennt man eine gewisse Systematik bei den Registernamen. Alle Namen kommen quasi doppelt vor, mit einem unterschiedlichen letzten Buchstaben A oder B. Damit sind die beiden Ports gemeint, die mit ihren acht Bits die tatsächlichen Ein- bzw. Ausgabe-Pins darstellen. Mehr dazu später.
Das Register IOCON fällt aus dieser Systematik heraus. Das werden wir später behandeln. Alle Register, die mit der Interrupt-Funktion zu tun haben, werden wir in diesem Blog nicht ansprechen.
Als kleine Abwechslung von der grauen Theorie schließen wir nun erst einmal den MCP23S17 an den Raspi Pico an.
Auf dem folgenden Bild sind rechts unten die Zweitbelegungen von GP19 = SPI0 TX, GP18 = SPI0 SCK, GP17 = SPI0 CSn sowie GP16 = SPI0 RX ausgewiesen. Hinter dem SPI steht jeweils eine 0 (null), weil weitere SPI-Schnittstellen zur Verfügung stehen. Wieder zwei neue Namen: TX für MOSI, RX für MISO. Verwechslung mit UART ausgeschlossen, weil SPI vorangestellt ist. Also:
Raspberry Pi Pico |
MCP23S17 |
GP19 = SPI0 TX (Pin 25) |
SI = Pin 13 |
GP18 = SPI0 SCK (Pin24) |
SCK = Pin 12 |
GP17 = SPI0 CSn (Pin22) |
Negat CS = Pin 11 |
GP16 = SPI0 RX (Pin21) |
SO = Pin 14 |
Der Port Expander benötigt weitere Anschlüsse:
Raspberry Pi Pico |
MCP23S17 |
3V3 (OUT) = Pin 36 |
VDD = Pin 9 |
GND = Pin 38, 33, 28, 23, 18, 13, 8 oder 3 |
VSS = Pin 10 |
HIGH (3V3) oder LOW (GND) |
A0 = Pin 15 |
HIGH (3V3) oder LOW (GND) |
A1 = Pin 16 |
HIGH (3V3) oder LOW (GND) |
A2 = Pin 17 |
3V3 |
Negat RESET über 10 kOhm Pullup-Widerstand |
Wie gesagt, die Interrupt-Pins und Funktionalität werde ich nicht verwenden. Und GPA0 bis GPA7 und GPB0 bis GPB7 sind die gewonnenen GPIO-Anschlüsse, an die wir versuchsweise LEDs oder Taster (Buttons) anschließen werden.
Zurück zum Programm. Die hardwarenahe Unterstützung der Mikrocontroller erfolgt bei MicroPython (uPy) über das Programm-Modul machine. Hierin ist die Klasse SPI deklariert. Unser Programm beginnt also mit dem Laden der Module
import machine
import utime
Die Instanziierung erfolgt mit den Zeilen
# Assign chip select (CS) pin (and start it high)
cs = machine.Pin(17, machine.Pin.OUT)
# Initialize SPI
spi = machine.SPI(0,
baudrate=1000000,
polarity=1,
phase=1,
bits=8,
firstbit=machine.SPI.MSB,
sck=machine.Pin(18),
mosi=machine.Pin(19),
miso=machine.Pin(16))
Für das Lesen und Beschreiben der Register habe ich kurze Funktionen eingefügt, die zunächst die nötige Abfolge von Device ID, Register und ggf. das zu schreibende Byte in einem Puffer sammeln, dann CS auf LOW setzen, Schreiben und ggf. Lesen, und dann CS wieder auf HIGH setzen.
def reg_write(reg, data):
# Write 1 byte to the specified device and register.
# Construct message (set ~W bit low, MB bit low)
msg = bytearray()
msg.append(DEVID) # DEVID Device OpCode
msg.append(reg)
msg.append(data)
cs.value(0)
spi.write(msg)
cs.value(1)
def reg_read(reg, nbytes=1):
# Read 1 byte from specified register.
# Construct message
msg = bytearray()
msg.append(DEVID | 1) # DEVID Device OpCode | write bit
msg.append(reg)
# Send out SPI message and read
cs.value(0)
spi.write(msg)
data = spi.read(nbytes)
cs.value(1)
return data
Mit den Funktionen reg_write() und reg_read() werden als Argumente der Name des Registers mit der Variablen reg und ggf. das zu schreibende Byte als Variable data übergeben. Den Gerätenamen, die Device ID, hatte ich bereits zu Beginn als DEVID = 0x40 definiert. Diese Angabe stammt aus dem Datenblatt, Figure 3-7, das an dieser Stelle einen kleinen Einschub erfordert.
In der Funktion reg_write() werden zunächst drei Werte in den Puffer gespeichert.
- DEVID=0b0100xxxw mit x als Platzhalter für 0 oder 1 in Abhängigkeit von einer möglichen Geräteadresse und w=0 für Schreiben.
- reg mit den anfangsdefinierten Registeradressen im Hexadezimalsystem
- und die zu schreibenden Daten
In der Funktion reg_read() werden nur die ersten beiden Bytes übertragen. Um anzuzeigen, dass das Gerät etwas senden soll, muss das Read/Write-Bit am Ende der DEVID auf 1 gesetzt werden. Deshalb die Oder-Verknüpfung bei
msg.append(DEVID | 1) # DEVID Device OpCode | write bit
Das Thema Geräteadressen wird noch einmal bei dem Register IOCON vertieft.
Nun kommen wir (endlich 😉) zu den wichtigsten Registern, immer von oben weg:
IODIR legt bitweise fest, ob der jeweilige Anschluss als Eingang (=1) oder als Ausgang (=0) fungieren soll. Da ich ohne zu viel Aufwand beides zeigen möchte, lege ich Port A komplett als Eingang (also 0b11111111 = 0xFF = 255) und Port B komplett als Ausgang fest (also 0b00000000 = 0x00 = 0).
IPOL legt die Polarität fest. Wie heute meistens üblich, werde ich die Taster/Buttons gegen Ground, also LOW schalten. Dennoch möchte ich den korrespondierenden Wert als HIGH angezeigt bekommen. Dafür benötige ich u.a. IPOLA mit der Wertzuweisung 0b11111111).
Interrupt lassen wir aus. Nächster ist
GPPU: Hier wird für die Eingänge festgelegt, ob der interne Pullup-Widerstand geschaltet werden soll. Da ich die Taster gegen GND schalten möchte, schreibe ich nach GPPUA ebenfalls 0b11111111.
Um den tatsächlichen Wert der GPIOs auszulesen, benötige ich GPIOA, jedoch das Beschreiben der Ausgänge an Port B erfolgt über sogenannte Latch-Register (hier: OLATB).
Alle diese Festlegungen erfolgen im Hauptprogramm, jedoch vor der Endlosschleife. Es erfolgt auch eine Wertzuweisung an das Register IOCON, das ich bereits oben erwähnt hatte. Dieses Register ist für beide Ports zuständig und legt mit jedem Bit eine Art Konfiguration fest. Das haben wir oben in der Figure 3-7 des Datenblatts gesehen.
Hier zunächst die Angaben aus dem Datenblatt:
Beim Power-on Reset (POR) oder Reset sind alle Bits auf 0 (null) gesetzt. Wieder ignorieren wir die Angaben zu Interrupts. Damit bleiben Bit 7, Bit 5 und Bit 3 zu betrachten.
Bit 7 legt die Nomenklatur für die Registeradressen fest. Die oben festgelegten Werte stammen aus der BANK 0, die voreingestellt und weit verbreitet verwendet wird. Auch die Arduino-Bibliothek nutzt diese Angaben. Dieses Bit muss also auf 0 (null) gesetzt bleiben.
Wir wollen immer nur ein Byte beschreiben oder auslesen. Deshalb sollte Bit 5 auf 1 gesetzt werden.
Und wenn wir mehrere MCP23S17 parallel verwenden wollen und deshalb Hardware-Adressen mit Hilfe der Anschlüsse A0, A1 und A2 bestimmen wollen, muss Bit 3 = HAEN = Hardware Adress Enable Bit ebenfalls auf 1 gesetzt werden.
Wir schreiben also in das IOCON-Register:
reg_write(IOCON, 0b00101000)
Nun zu den Geräteadressen, die mich viel Schweiß gekostet haben, weil ich zunächst davon ausging, dass ich die gleiche Systematik wie für die I2C-Variante verwenden kann. Mit den drei Anschlüssen A0 bis A2 haben wir 2 hoch 3 = 8 verschiedene Möglichkeiten. Bei I2C von 0x20 bis 0x27.
Aber während bei I2C das höchste Bit (Bit 7) für Lesen und Schreiben auf 0 bzw. 1 gesetzt wird, ist es bei SPI das niedrigste Bit 0. Damit ist die Wertigkeit der Adress-Bits um den Faktor 2 höher. Die Geräteadresse (Device OpCode oder DEVID) lautet demnach 0b0100-A2-A1-A0-R/W.
Wenn wir die Adressierung nicht verwenden und das R/W-Bit zum Schreiben auf 0 gesetzt ist, erhalten wir die Basisadresse 0b01000000 = 0x40. Für die Anschlussmöglichkeiten von A0 bis A2 müssen wir dann entsprechend folgende Werte addieren. Dabei steht H für HIGH, Anschluss an 3,3V und L für LOW, also Anschluss an GND.
LLL -> 0x00, also 0x40 (die Basisadresse. LLH -> 0x02, also 0x42. LHL -> 0x04, also 0x44.
LHH -> 0x06, also 0x46. HLL -> 0x08, also 0x48. HLH -> 0x0A, also 0x4A. HHL -> 0x0C, also 0x4C. Und schließlich HHH -> 0x0E, also 0x4E.
Wer die I2C-Variante mit den I2C-Adressen 0x20 bis 0x 27 kennt, erkennt auf einen Blick, dass bei SPI die Adresse genau doppelt so groß ist. Und das Doppelte von 0x25 ist nicht 0x50, sondern 0x4A. Wir rechnen schließlich im Hexadezimalsystem.
Genug der grauen Theorie und vor allem der Mathematik, jetzt soll es blinken.
An die Anschlüsse GPB0 bis GPB2 schließe ich eine RGBLED an, die in meiner Endlosschleife im schnellen Wechsel die Farbe ändert, während in der Python Shell (REPL=Read-Evaluate-Print-Loop) angezeigt wird, welchen Eingang am Port A ich mit Hilfe eines Jumper-Kabels auf GND lege.
Hier mein Beispielprogramm zum Download. Sie können es leicht für Ihre Zwecke anpassen.
Damit sind die Grundlagen für den Port Expander mit SPI-Schnittstelle am Raspi Pico erklärt und Sie können die vielfältigen Möglichkeiten der hinzugewonnenen GPIO-Pins ausschöpfen. Wer mehr über SPI wissen möchte: hier habe ich eine gute Einführung gefunden.
Hier das Schaltbild sowie die Pintabelle für die Grundbeschaltung:
MCP23S17 |
Raspberry Pi Pico |
SI = Pin 13 |
GP19 = SPI0 TX (Pin 25) |
SCK = Pin 12 |
GP18 = SPI0 SCK (Pin24) |
Negat CS = Pin 11 |
GP17 = SPI0 CSn (Pin22) |
SO = Pin 14 |
GP16 = SPI0 RX (Pin21) |
VDD = Pin 9 |
3V3 (OUT) = Pin 36 |
VSS = Pin 10 |
GND = Pin 38, 33, 28, 23, 18, 13, 8 oder 3 |
A0 = Pin 15 |
HIGH (3V3) oder LOW (GND) |
A1 = Pin 16 |
HIGH (3V3) oder LOW (GND) |
A2 = Pin 17 |
HIGH (3V3) oder LOW (GND) |
Negat RESET über 10 kOhm Pullup-Widerstand |
3V3 |
1 commento
junzo yoshida
pico spi michropython
thamk you wonderful page