Über den Beitrag
In einem meiner ersten Beiträge hatte ich über den Portexpander MCP23017 und meine zugehörige Bibliothek berichtet. Mittlerweile habe ich die Bibliothek so angepasst, dass ihr sie auch für andere Mitglieder der MCP23x1y Familie einsetzen könnt. Deshalb komme ich noch einmal auf das Thema zurück. In diesem Beitrag werde ich primär auf die Unterschiede der MCP23x1y ICs eingehen. Für die Details, die bei allen Vertretern der MCP23x1y Portexpander gleich sind, schaut bitte weiterhin in meinen Beitrag über den MCP23017.
Die etwas sperrige Bezeichnung MCP23x1y ist übrigens nicht offiziell. „MCP23x“ hätte auch die Modelle MCP23008 und MCP23S08 umfasst, auf die ich hier nicht näher eingehe. Sie besitzen lediglich 8 GPIOs.
Folgendes kommt auf euch zu:
- Überblick über die MCP23x1y Familie
- Gebrauch der MCP23x1y Portexpander im Detail
- I2C vs. SPI – Geschwindigkeitstests
Überblick über die MCP23x1y Familie
Die MCP23x1y Familie umfasst fünf Mitglieder:
- den MCP23016 (Link zum Datenblatt)
- die zwei MCP23017 / MCP23S17 (Link zum Datenblatt)
- und schließlich die zwei MCP23018 / MCP23S18 (Link zum Datenblatt)
Hier die wichtigsten Eigenschaften der MCP23x1y Portexpander im Überblick:
Der gängigste Vertreter ist der MCP23017. Er ist, so wie der MCP23018, I2C gesteuert. Wenn ihr die GPIOs (General Purpose Input Output) in einer höheren Geschwindigkeit schalten oder auslesen wollt, dann greift zu einer der SPI gesteuerten „S“-Varianten.
Der maximale Strom, der über einen Pin fließen darf, liegt für alle Vertreter der MCP23x1y Familie bei 25 mA. Allerdings darf dieser Strom nur bei den „18er“ Varianten gleichzeitig an allen Pins fließen. Bei diesen gibt es wiederum eine Einschränkung hinsichtlich der Stromrichtung. Die GPIOs des MCP23018 und MCP23S18 sind „Open Drain“ Ein-/Ausgänge. Das bedeutet, dass sie sich wie ein einfacher Transistor verhalten. Ihr könnt sie öffnen (auf OUTPUT setzen) und so „Strom nach Ground durchlassen“, aber ihr könnt sie nicht als Stromquelle benutzen.
Der MCP23016 hat eine Sonderstellung. Als einziger besitzt er eine unterschiedliche Registerstruktur. Außerdem benötigt er einen externen Taktgeber. Da er zudem vom Hersteller Microchip als „not recommended for new designs“ gekennzeichnet wurde, habe ich mir nicht die Mühe gemacht, meine Bibliothek für ihn anzupassen. Ich liste ihn aber der Vollständigkeit halber mit auf.
Gebrauch der MCP23x1y Portexpander im Detail
Im Folgenden gehe ich für die Mitglieder der MCP23x1y – Familie jeweils auf die folgenden Punkte ein:
- Pinout
- Anschluss an den Mikrocontroller
- Einfache Input / Output Operationen mit der Bibliothek MCP23017_WE
Wie ihr seht, habe ich den Namen der Bibliothek beibehalten. Der Grund dafür ist, dass ich diejenigen, die die Bibliothek schon benutzen, nicht verwirren wollte.
MCP23017
Pinout
Wir starten mit dem MCP23017. Ihr versorgt ihn über VDD/VSS mit 1.8 bis 5.5 Volt. Der Anschluss an die I2C Pins des Mikrocontrollers erfolgt über SDA und SCL. Die I2C-Verbindung funktioniert oft ohne Pull-Up Widerstände, ich würde sie aber empfehlen. Die Interrupt-Pins brauchen wir für die in diesem Beitrag verwendeten Sketche nicht. Den Reset-Pin verbindet ihr mit einem I/O-Pin eures Mikrocontrollers.
Die Einstellung der I2C-Adresse erfolgt über das Schema: 0b00100-A2-A1-A0. Ist Ax HIGH, ersetzt ihr es durch eine 1, sonst durch eine Null. Wenn ihr also beispielsweise A0, A1 und A2 mit LOW verbindet, ist die Adresse 0b00100000 = 0x20 = 32.
Wichtiger Hinweis!!! Das Design des MCP23017 hat sich 2022 geändert. Leider hat Microchip den Pins GPA7 und GPB7 ihre Input-Funktion genommen. Die Pins sind nun reine OUTPUT Pins. Was für eine schlechte Idee! Äußerlich gibt es keinen Unterschied, d.h. ihr müsst testen, welche Version ihr habt. Ich habe die Beispielsketche und den Beitragstext bisher nicht angepasst!
Beispielschaltung
Um die Beispielsketche auszuprobieren, verbindet jeden GPIO mit einer LED. Bedenkt bei der Auswahl, dass der Gesamtstrom an VDD 125 Milliampere nicht überschreiten sollte. D.h. wenn alle LEDs leuchten, darf jede einzelne im Schnitt nur ca. 7.8 Milliampere verbrauchen. Die Schaltung an einem Arduino Nano könnte so aussehen:
Wenn ihr, so wie ich, keine Lust habt, sechzehn Widerstände und LEDs zu verkabeln, dann empfehle ich LED Bars mit integrierten Widerständen wie unten abgebildet. Es gibt sie mit gemeinsamer Anode oder Kathode:
Gefunden habe ich die LED Bars übrigens bei AliExpress. Damit ist der Aufbau schnell gemacht. Und sie verbrauchen lediglich 2 bis 3 Milliampere pro LED. So sah es bei mir auf dem Breadboard aus (ohne Pull-Ups):
Beispielsketch für den MCP23017
Der folgende Beispielsketch zeigt, wie ihr euer MCP23017 Objekt erzeugt und die LEDs mit verschiedenen Funktionen steuert.
#include <Wire.h> #include <MCP23017.h> #define RESET_PIN 5 #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) /* There are several ways to create your MCP23017 object: * MCP23017 myMCP = MCP23017(MCP_ADDRESS) -> uses Wire / no reset pin (if not needed) * MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN) -> uses Wire / RESET_PIN * MCP23017 myMCP = MCP23017(&wire2, MCP_ADDRESS) -> uses the TwoWire object wire2 / no reset pin * MCP23017 myMCP = MCP23017(&wire2, MCP_ADDRESS, RESET_PIN) -> all together */ MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN); int wT = 500; // wT = waiting time void setup(){ Wire.begin(); myMCP.Init(); delay(wT); myMCP.setPortMode(0b11111111, A); // Port A: all pins are OUTPUT myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT myMCP.setAllPins(A,ON); // Port A: all pins are HIGH myMCP.setAllPins(B,ON); // Port B: all Pins are HIGH delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW delay(wT); byte portValue = 0; for(int i=0; i<8; i++){ portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPort(portValue, A); delay(wT); } portValue = 0; for(int i=0; i<8; i++){ portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPort(portValue, B); delay(wT); } myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW delay(wT); myMCP.setPin(3, A, HIGH); // Pin 3 / PORT A is HIGH myMCP.setPin(1, B, HIGH); // Pin 1 / PORT B is HIGH delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT myMCP.setPinX(1,A,OUTPUT,HIGH); // A1 HIGH delay(wT); myMCP.togglePin(1, A); // A1 LOW delay(wT); myMCP.togglePin(1, A); // A1 HIGH delay(wT); // the following two lines are similar to setPinX(2,B,OUTPUT,HIGH); myMCP.setPinMode(2,B,OUTPUT); // B2 is OUTPUT myMCP.setPin(2,B,HIGH); // B2 is HIGH delay(wT); myMCP.setPortX(0b00001111,0b10001111,B); // B0-B4: OUTPUT/HIGH, B7: INPUT, HIGH; } void loop(){ }
Dieser Beitrag soll keine Wiederholung meines ersten Beitrages über den MCP23017 sein, aber ein paar Erklärungen sind sicherlich angebracht, damit ihr nicht zwischen den Beiträgen hin- und herspringen müsst.
Die GPIOs des MCP23017 funktionieren im Prinzip wie die eines Arduinos oder anderer Mikrocontroller. Ihr könnt sie als INPUT oder OUTPUT einstellen und ihr könnt sie HIGH oder LOW schalten. Wenn ihr die LEDs über die GPIOs mit Strom versorgt, müssen die Pins OUTPUT/HIGH sein, damit die LEDs leuchten.
Ich habe der Bibliothek eine Reihe von Funktionen spendiert, mit denen ihr einzelne Pins oder ganze Ports ansprechen könnt. Dabei ist „0“ grundsätzlich INPUT bzw. LOW. „1“ ist entsprechend OUTPUT bzw. HIGH. Anstelle von HIGH oder LOW könnt ihr auch „ON“ oder „OFF“ verwenden. Das sind alles nur Synonyme für 0 oder 1.
setPinMode()
: bestimmt den Modus (INPUT / OUTPUT) für einen Pin.setPortMode()
: setzt INPUT / OUTPUT für einen ganzen Port, aber individuell für jeden Pin.setPin()
: bringt einen Pin auf HIGH oder LOW.setPort()
: HIGH/LOW für einen ganzen Port, aber individuell für jeden Pin.setAllPins()
: bringt alle Pins eines Ports einheitlich auf HIGH oder LOW.setPinX()
: kombiniertsetPinMode()
undsetPin()
in einer Funktion.setPortX()
: kombiniertsetPortMode()
undsetPort()
in einer Funktion.togglePin()
: wechselt einen Pin von HIGH nach LOW oder umgekehrt.
MCP23S17
Pinout
Der MCP23S17 wird im Gegensatz zum MCP23017 per SPI gesteuert. Entsprechend besitzt er einen SCK (Serial Clock), SI (Slave In), SO (Slave Out) und einen CS (Chip Select) Pin.
Es ist zunächst erstaunlich, dass er auch noch die drei Adresspins A0, A1 und A2 hat. Wenn ihr den MCP23S17 per SPI ansprecht, müsst ihr wie gewohnt zunächst den CS Pin auf LOW ziehen. Dann sendet ihr ein Kontroll-Byte, das aus der Adresse und dem Read/Write Bit besteht. Das hat den Vorteil, dass ihr acht MCP23S17 ICs an ein und dieselbe CS Leitung hängen könnt. Es folgt das Registerbyte und dann die Daten. Um diese Dinge müsst ihr euch natürlich nicht im Detail kümmern, da die Bibliothek das übernimmt.
Beispielschaltung
Die Schaltung für die Steuerung per Arduino Nano ist nicht sonderlich überraschend:
Beispielsketch
Da sich der MCP23S17 vom MCP23017 nur durch die SPI-Kommunikation unterscheidet, ist auch der Beispielsketch nur geringfügig unterschiedlich. Eine Funktion kommt hinzu, und zwar setSPIClockSpeed()
. Damit könnt ihr die SPI Taktrate im Rahmen der Möglichkeiten eures Mikrocontrollers und des MCP23S17 (max. 10 MHz) wählen. Die Voreinstellung der Bibliothek ist 8 MHz. Ansonsten braucht der Sketch wohl keine weitere Erläuterung.
#include <SPI.h> #include <MCP23S17.h> #define CS_PIN 7 // Chip Select Pin #define RESET_PIN 5 #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) /* There are two ways to create your MCP23S17 object: * MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS); * MCP23S17 myMCP = MCP23S17(&SPI, CS_PIN, RESET_PIN, MCP_ADDRESS); * The second option allows you to create your own SPI objects, * e.g. in order to use two SPI interfaces on the ESP32. */ MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS); int wT = 500; // wT = waiting time void setup(){ SPI.begin(); myMCP.Init(); // myMCP.setSPIClockSpeed(8000000); // Choose SPI clock speed (after Init()!) delay(wT); myMCP.setPortMode(0b11111111, A); // Port A: all pins are OUTPUT myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT myMCP.setAllPins(A,ON); // Port A: all pins are HIGH myMCP.setAllPins(B,ON); // Port B: all Pins are HIGH delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW delay(wT); byte portValue = 0; for(int i=0; i<8; i++){ portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPort(portValue, A); delay(wT); } portValue = 0; for(int i=0; i<8; i++){ portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPort(portValue, B); delay(wT); } myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW delay(wT); myMCP.setPin(3, A, HIGH); // Pin 3 / PORT A is HIGH myMCP.setPin(1, B, HIGH); // Pin 1 / PORT B is HIGH delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all pins are LOW myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT myMCP.setPinX(1,A,OUTPUT,HIGH); // A1 HIGH delay(wT); myMCP.togglePin(1, A); // A1 LOW delay(wT); myMCP.togglePin(1, A); // A1 HIGH delay(wT); // the following two lines are similar to setPinX(2,B,OUTPUT,HIGH); myMCP.setPinMode(2,B,OUTPUT); // B2 is OUTPUT myMCP.setPin(2,B,HIGH); // B2 is HIGH delay(wT); myMCP.setPortX(0b00001111,0b10001111,B); // B0-B4: OUTPUT/HIGH, B7: INPUT, HIGH; } void loop(){ }
MCP23018
Der MCP23018 ist wiederum I2C gesteuert. Es gibt aber einen fundamentalen Unterschied zum MCP23017. Um eine LED am MCP23018 zum Leuchten zu bringen, müsst ihr ihn als Stromsenke benutzen. Das heißt, ihr versorgt die LED nicht über den MCP23018 mit Strom, sondern lasst den Strom über den MCP23018 abfließen. Damit das passieren kann, muss sich der jeweilige Pin im Zustand OUTPUT/LOW befinden.
Pinout
Der MCP23018 hat aber noch eine weitere Besonderheit. Neben der unterschiedlichen Zuordnung der GPIOs besitzt er nur einen einzigen Adresspin. Die Adresse wird über die am Adresspin anliegende Spannung eingestellt. Ein recht ungewöhnliches Konzept, dessen Vorteil sich mir nicht wirklich erschließt.
Die Skala für die „Adressspannung“ wird dabei durch die Versorgungsspannung VDD vorgegeben. Legt ihr 1/16tel der Versorgungsspannung an den Adresspin an, dann ist die Adresse 0x20. Bei 3/16tel ist sie 0x21, bei 5/16tel ist sie 0x22, bei 7/16tel ist sie 0x23, usw. Die höchste Adresse 0x27 stellt ihr durch Anlegen einer Spannung von 15/16tel VDD ein. Die 0x20 könnt ihr auch einstellen, indem ihr am Adresspin auf VSS Niveau geht (0/16tel) und die 0x27, indem ihr auf VDD Niveau geht. Bei den anderen Adressen sind die Toleranzen geringer. Eine Übersicht über die Toleranzen und Beispieltabellen findet ihr im Datenblatt auf Seite 11.
Wenn ihr acht MCP23018 ICs betreiben wollt, dann könnte eine Schaltung (reduziert auf den I2C-Teil) so aussehen:
Beispielschaltung
In meiner Beispielschaltung werden die LEDs über den 5 Volt Ausgang des Arduino Nano versorgt. Der Strom fließt über den MCP23018 ab. Die Adresse habe ich über einen Spannungsteiler eingestellt. Da ich hier nur einen einzelnen MCP23018 verwende, hätte ich den Adresspin alternativ mit GND oder VDD verbinden können.
Wie zuvor habe ich mir das Leben einfach gemacht und LED Bars mit integrierten Widerständen verwendet. In diesem Fall natürlich die Ausführung mit gemeinsamer Anode:
Ich habe auch hier die Pull-Ups für die I2C Leitungen weggelassen (falls ihr sie vermisst). Bei den späteren Geschwindigkeitstests habe ich sie allerdings hinzugefügt.
Beispielsketch
Auf den ersten Blick sieht der Sketch nicht sehr viel anders als die bisherigen aus. Wenn ihr genauer hinschaut, erkennt ihr aber, dass ich die LEDs über den OUTPUT/INPUT steuere und nicht über HIGH/LOW.
#include <Wire.h> #include <MCP23018.h> #define RESET_PIN 5 #define MCP_ADDRESS 0x20 /* There are several ways to create your MCP23018 object: * MCP23018 myMCP = MCP23018(MCP_ADDRESS) -> uses Wire / no reset pin (if not needed) * MCP23018 myMCP = MCP23018(MCP_ADDRESS, RESET_PIN) -> uses Wire / RESET_PIN * MCP23018 myMCP = MCP23018(&wire2, MCP_ADDRESS) -> uses the TwoWire object wire2 / no reset pin * MCP23018 myMCP = MCP23018(&wire2, MCP_ADDRESS, RESET_PIN) -> all together * Successfully tested with two I2C busses on an ESP32 */ MCP23018 myMCP = MCP23018(MCP_ADDRESS, RESET_PIN); int wT = 500; // wT = waiting time void setup(){ Wire.begin(); myMCP.Init(); delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all Pins are LOW myMCP.setPortMode(0b11111111, A); // Port A: all pins are OUTPUT = LEDs are on! myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT delay(wT); myMCP.setPortMode(0b00000000, A); // Port A: all pins are INPUT = LEDs are off myMCP.setPortMode(0b00000000, B); // Port B: all pins are INPUT delay(wT); byte portModeValue = 0; // = 0b00000000 for(int i=0; i<8; i++){ portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPortMode(portModeValue, A); delay(wT); } portModeValue = 0; for(int i=0; i<8; i++){ portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPortMode(portModeValue, B); delay(wT); } myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT delay(wT); myMCP.setPinMode(3, A, OUTPUT); // Pin 3 / PORT A is OUTPUT/LOW myMCP.setPinMode(1, B, OUTPUT); // Pin 1 / PORT B is OUTPUT/LOW delay(wT); myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT myMCP.setPinX(1,A,OUTPUT,LOW); // A1 HIGH delay(wT); myMCP.togglePin(1, A); // A1 LOW delay(wT); myMCP.togglePin(1, A); // A1 HIGH delay(wT); // the following two lines are similar to setPinX(2,B,OUTPUT,LOW); myMCP.setPinMode(2,B,OUTPUT); // B2 is OUTPUT/LOW myMCP.setPin(2,B,LOW); // B2 is still OUTPUT/LOW delay(wT); myMCP.setPortX(0b10001111,0b10000000,B); // B0-B4: OUTPUT/LOW, B7: OUTPUT, HIGH; } void loop(){ }
Befindet sich ein Pin im Zustand OUPUT/LOW leuchtet die angeschlossene LED. Anstatt sie durch den Wechsel auf INPUT/LOW auszuschalten, erreicht ihr dasselbe durch Wechsel auf OUTPUT/HIGH.
MCP23S18
Pinout
Der MCP23S18 bietet nun keine großen Überraschungen mehr. Er ist schlicht die SPI Variante des MCP23018. Im Gegensatz zum MCP23S17 besitzt er allerdings nicht die Option, dass er mit verschiedenen Adressen angesprochen werden kann. Entsprechend müsst ihr jeden einzelnen MCP23S18 mit einer eigenen CS Leitung verbinden.
Trotzdem erwartet der MCP23S18 bei Schreib- oder Leseoperationen als erstes Byte einen „Device Opcode“, bestehend aus der 0x20 und dem Read/Write Bit. Darum kümmert sich die Bibliothek. Allerdings müsst ihr bei der Erzeugung des MCP23S18 Objektes immer auch die 0x20 (MCP_SPI_CTRL_BYTE) übergeben. Das hat einfach nur Kompatibilitätsgründe.
Beispielschaltung
Der Vollständigkeit halber zeige ich hier noch einmal eine Beispielschaltung:
Beispielsketch
Auch der Beispielsketch ist jetzt nicht mehr überraschend.
#include <SPI.h> #include <MCP23S18.h> #define CS_PIN 7 // Chip Select Pin #define RESET_PIN 5 #define MCP_SPI_CTRL_BYTE 0x20 // Do not change /* There are two ways to create your MCP23S18 object: * MCP23S18 myMCP = MCP23S18(CS_PIN, RESET_PIN, MCP_CTRL_BYTE); * MCP23S18 myMCP = MCP23S18(&SPI, CS_PIN, RESET_PIN, MCP_CTRL_BYTE); * The second option allows you to create your own SPI objects, * e.g. in order to use two SPI interfaces on the ESP32. */ MCP23S18 myMCP = MCP23S18(CS_PIN, RESET_PIN, MCP_SPI_CTRL_BYTE); int wT = 500; // wT = waiting time void setup(){ SPI.begin(); myMCP.Init(); // myMCP.setSPIClockSpeed(8000000); // Choose SPI clock speed (after Init()!) delay(wT); myMCP.setAllPins(A,OFF); // Port A: all pins are LOW myMCP.setAllPins(B,OFF); // Port B: all Pins are LOW myMCP.setPortMode(0b11111111, A); // Port A: all pins are OUTPUT = LEDs are on! myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT delay(wT); myMCP.setPortMode(0b00000000, A); // Port A: all pins are INPUT = LEDs are off myMCP.setPortMode(0b00000000, B); // Port B: all pins are INPUT delay(wT); byte portModeValue = 0; // = 0b00000000 for(int i=0; i<8; i++){ portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPortMode(portModeValue, A); delay(wT); } portModeValue = 0; for(int i=0; i<8; i++){ portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc. myMCP.setPortMode(portModeValue, B); delay(wT); } myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT delay(wT); myMCP.setPinMode(3, A, OUTPUT); // Pin 3 / PORT A is OUTPUT/LOW myMCP.setPinMode(1, B, OUTPUT); // Pin 1 / PORT B is OUTPUT/LOW delay(wT); myMCP.setPortMode(0,A); // Port A: all pins are INPUT myMCP.setPortMode(0,B); // Port B: all pins are INPUT myMCP.setPinX(1,A,OUTPUT,LOW); // A1 HIGH delay(wT); myMCP.togglePin(1, A); // A1 LOW delay(wT); myMCP.togglePin(1, A); // A1 HIGH delay(wT); // the following two lines are similar to setPinX(2,B,OUTPUT,LOW); myMCP.setPinMode(2,B,OUTPUT); // B2 is OUTPUT/LOW myMCP.setPin(2,B,LOW); // B2 is still OUTPUT/LOW delay(wT); myMCP.setPortX(0b10001111,0b10000000,B); // B0-B4: OUTPUT/LOW, B7: OUTPUT, HIGH; } void loop(){ }
MCP23016
Pinout
Wie ihr rechts seht, fällt der MCP23016 im Vergleich zu den anderen Mitgliedern der MCP23x1y Familie aus der Reihe. Die GPIOs sind anders benannt und einen Resetpin gibt es gar nicht. Außerdem gibt es einen CLK und TP Pin.
CLK muss über einen Kondensator mit GND und über einen Widerstand mit VDD verbunden werden. Darüber wird der Takt eingestellt. Das Datenblatt empfiehlt die Kombination 33 pF / 3.9 kΩ. Mit 30 pF / 3.9 kΩ ging es bei mir auch. An TP könnt ihr das Clock-Signal überprüfen, ansonsten lasst ihr ihn unverbunden.
Der größte Unterschied zu den anderen Familienmitgliedern ist seine unterschiedliche Registerarchitektur. Da er zudem nicht mehr empfohlen wird, habe ich darauf verzichtet, ihn in meiner Bibliothek zu implementieren. Stattdessen habe ich die Bibliothek CyMCP23016 ausprobiert, die ihr hier auf GitHub findet. Insgesamt ist die Auswahl an Bibliotheken für den MCP23016 ist nicht sehr groß.
Beispielschaltung mit einem Arduino Nano
Die CyMCP23016 Bibliothek ist mit einem Beispielsketch ausgestattet, den ich mit der folgenden Schaltung ausprobiert habe. In dem Sketch wird nur eine LED an GPIO 0.0 geschaltet. Dadurch wird das Prinzip aber ausreichend klar. Ich drucke den Sketch hier nicht ab.
I2C vs. SPI – Geschwindigkeitstests
Wenn ich persönlich die Wahl zwischen I2C und SPI habe, tendiere ich zu I2C, da weniger Leitungen benötigt werden. Das gilt insbesondere, wenn mehrere Geräte angesteuert werden sollen. Jedoch hat SPI bei Anwendungen, die eine hohe Übertragungsgeschwindigkeit erfordern, die Nase vorn. Dazu habe ich ein paar Tests durchgeführt.
I2C am Arduino Nano
Der MCP23017 beherrscht einen I2C Takt von 1.7 MHz, der MCP23018 sogar 3.4 MHz. Das Problem dabei ist aber, dass die meisten Mikrocontroller diese hohen Taktraten nicht unterstützen, so auch die ATmega328P basierten Arduinos wie der UNO, Nano oder Pro Mini. Am Arduino Nano habe ich 100 kHz (Standard) und 400 kHz (Fast I2C) getestet. Dazu habe ich die Zeit bestimmt, die es braucht, um einen Port 10000 Mal auszulesen (digitalRead()
für einen ganzen Port, sozusagen).
Hier zunächst der Sketch:
#include <Wire.h> #include <MCP23017.h> #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) #define RESET_PIN 5 MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN); void setup(){ Serial.begin(115200); long startTime = 0; long readTime = 0; byte portStatus = 0; Wire.begin(); myMCP.Init(); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@100kHz [ms]: "); Serial.println(readTime); Serial.print("Duration@100kHz per Read [ms]: "); Serial.println(readTime/10000.0); Wire.setClock(400000); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@400kHz [ms]: "); Serial.println(readTime); Serial.print("Duration@400kHz per Read [ms]: "); Serial.println(readTime/10000.0); } void loop(){ }
Und hier das Ergebnis:
I2C am ESP32
Dann habe ich den Test mit dem ESP32 wiederholt und dabei versucht 1.7 MHz einzustellen, da ich meinte der ESP32 könne das.
#include <Wire.h> #include <MCP23017.h> #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) #define RESET_PIN 18 MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN); void setup(){ Serial.begin(115200); long startTime = 0; long readTime = 0; byte portStatus = 0; Wire.begin(); myMCP.Init(); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@100kHz [ms]: "); Serial.println(readTime); Serial.print("Duration@100kHz per Read [ms]: "); Serial.println(readTime/10000.0); Wire.setClock(400000); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@400kHz [ms]: "); Serial.println(readTime); Serial.print("Duration@400kHz per Read [ms]: "); Serial.println(readTime/10000.0); Wire.setClock(1700000); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@1.7MHz [ms]: "); Serial.println(readTime); Serial.print("Duration@1.7MHz per Read [ms]: "); Serial.println(readTime/10000.0); } void loop(){ }
Der magere Geschwindigkeitszuwachs bei 1.7 MHz wunderte mich zunächst:
Eine Messung der tatsächlichen Taktrate mit dem Logic Analyzer ergab jedoch, dass auch der ESP32 die 1.7 MHz Taktrate nicht liefern kann. Bei 655 kHz war Schluss. Das deckt sich (ungefähr) mit dem, was ich an anderen Stellen zu dem Thema gefunden habe, z.B. hier auf GitHub.
SPI am Arduino Nano
Der Arduino Nano und alle weiteren Boards, die auf dem ATmega328P basieren, können eine SPI Taktrate bis zu 16 MHz liefern. Der MCP23S17 beherrscht eine SPI Taktrate von 10 MHz. Allerdings könnt ihr 10 MHz nicht am Arduino Nano einstellen, sondern nur Quotienten aus 16 MHz und ganzzahligen Teilern, also 8 MHz, 4 MHz, 2 MHz, usw. 8 MHz ist also das Maximum. Gebt ihr andere Werte an, wird die nächstmögliche, niedrigere Frequenz genommen.
Hier der Sketch:
#include <SPI.h> #include <MCP23S17.h> #define CS_PIN 7 // Chip Select Pin #define RESET_PIN 5 #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS); void setup(){ Serial.begin(115200); long startTime = 0; long readTime = 0; byte portStatus = 0; SPI.begin(); myMCP.Init(); myMCP.setSPIClockSpeed(2000000); // Choose SPI clock speed (after Init()! startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@2MHz [ms]: "); Serial.println(readTime); Serial.print("Duration@2MHz per Read [µs]: "); Serial.println(readTime/10.0); myMCP.setSPIClockSpeed(4000000); // Choose SPI clock speed (after Init()!) startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@4MHz [ms]: "); Serial.println(readTime); Serial.print("Duration@4MHz per Read [µs]: "); Serial.println(readTime/10.0); myMCP.setSPIClockSpeed(8000000); startTime = millis(); for(long i=0; i<10000; i++){ portStatus = myMCP.getPort(A); } readTime = millis() - startTime; Serial.print("Duration@8MHz [ms]: "); Serial.println(readTime); Serial.print("Duration@8MHz per Read [µs]: "); Serial.println(readTime/10.0); } void loop(){}
Und hier das Ergebnis:
Bei Verwendung eines ATmega328P basierten Boards könnt ihr also fast die zehnfache Leserate erreichen, wenn ihr vom MCP23017 auf den MCP23S17 wechselt.
Das Auslesen der eigenen Pins eines Arduinos ist immer noch wesentlich schneller. Ein digitalRead()
braucht auf dem Arduino Nano nur ca. 3.5 µs. Und das direkte Auslesen der Register PINB/PIND geht noch einmal schneller. Dafür werden nämlich nur ca. 0.44 µs benötigt. Für Details schaut in meinen Beitrag über Portmanipulation.
Maximale „Toggle“ Geschwindigkeit
Zu guter Letzt habe ich probiert, wie schnell man einen GPIO des MCP23S17 zwischen HIGH und LOW hin- und herschalten kann („toggeln“). Dabei habe ich den Pin A1 ausgewählt und den folgenden, kurzen Sketch auf dem Arduino Nano ausgeführt:
#include <SPI.h> #include <MCP23S17.h> #define CS_PIN 7 // Chip Select Pin #define RESET_PIN 5 #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS); void setup(){ Serial.begin(115200); SPI.begin(); myMCP.Init(); myMCP.setSPIClockSpeed(8000000); myMCP.setPinX(1,A,OUTPUT,LOW); } void loop(){ myMCP.setPort(0b00000010,A); myMCP.setPort(0,A); }
Die maximale Frequenz liegt bei ca. 16 kHz. Zum Vergleich: Mit digitalRead() erreicht man 127 kHz, über Portmanipulation sogar ca. 2.3 MHz.
Hallo Wolfgang,
erst mal möchte ich mich ganz herzlich bei dir bedanken, für die viele Mühe die du dir hier machst. Ich lese immer wieder gern bei dir nach und konnte so schon viele Tip’s in meine Arbeiten mit einfließen lassen. Gerade die Geschichte, das beim 23017 zwei Leitungen nicht mehr Eingang sein können…
Leider ist mir ein Fehler in deinen Aussagen zum MCP23″S“17 aufgefallen, Mit denen ich mich gerade auseinander setze (wegen oben genannter Änderung). Du sagst, daß man mehrere MCP23S17 (8) an einer CS-Leitung betreiben könnte. Ich benutze hier zwei MCP23S17 mit deiner Bibliothek an einem ESP32. Alle Ports sind auf Eingang mit PullUp. Setze ich beide IC’s auf die gleiche CS-Leitung. Funktioniert das auslesen nicht richtig, mit Effekten die mich schon an meinem Verstand zweifeln ließ. Offensichtlich wird hier immer nur der Zustand des in der Adresse höheren IC’s angezeigt, auch für den IC mit der kleineren Adresse. Ein IC ist auf Add 0x20 der zweite auf 0x21. Sollte nach deiner Aussage also funktionieren. Es funktioniert sofort, wenn ich beiden IC’s eine eigene CS-Leitung gönne.
Darf ich dich bitten dies einmal zu überprüfen? Über eine Rückmeldung wäre ich sehr dankbar.
Liebe Grüße
Susann
Hallo Susann,
ich habe es sicherheitshalber noch einmal selbst mit einem ESP32 und zwei MCP23S17 probiert und es funktioniert ohne Probleme über eine CS Leitung. Stellt sich die Frage, was bei dir anders ist. Am naheliegendsten wäre, dass du die Adresse hardwareseitig nicht richtig eingestellt hast. Für 0x20 müssen A0/A1/A2 LOW sein, für 0x21 muss A0 HIGH sein und A1/A2 LOW. Kannst du das noch einmal prüfen?
Bei mir kam der folgende Sketch zum Einsatz:
Wie erwartet leuchten die LEDs, die ich an die beiden PORT A der MCP23S17 ICs angeschlossen habe, der Reihe nach auf. Wenn ich die Konfiguration der Adresspins austausche, dann ändert sich entsprechend die Reihenfolge. Also genau, wie es sein soll.
Vielleicht erkennst du im Code irgendetwas?
Falls du nicht weiter kommst, dann schicke mir mal deinen Sketch (wolfgang.ewald@wolles-elektronikkiste.de). Code wird im Kommentarfeld nur über Umwege richtig dargestellt.
Mehr fällt mir im Moment nicht ein.
VG, Wolfgang
Das Problem ist gelöst und hing mit den Resets in der init() Funktion zusammen. Es trat unter den folgenden Bedigungen auf:
– Variante: MCP23S17
– Reset Pin < 99
– eine gemeinsame Resetleitung
– SPI – Adressmethode mit gemeinsamer CS-Leitung
– mehr als zwei MCP23x1y
Eine neue Version der Bibliothek ist verfügbar (1.6.5).
Hallo Wolfgang,
ich wollte mich noch mal ganz doll bei dir bedanken, für die viele Mühe die du dir gemacht hast. Dein Enthusiasmus ist wirklich fantastisch. Schön das wir das Problem lösen konnten und den Fehler beheben konnten. Mach bitte weiter so, weil ich glaube, das du damit sehr vielen auch weiter hilfst.
Viele Grüße
Susann
Vielen Dank für das Feedback – das sehr nett, dass du dich noch einmal bei den Kommentaren meldest. Gibt Rückenwind!
Das ist doch selbstverständlich. Bei so viel netter Hilfe, muss man auch mal ganz lieb danke sagen.
Übrigens mein Projekt ist jetzt fertig, gerade heute habe ich das zweite Gerät in betrieb genommen. Schicke dir mal die Tage noch ein Bild davon.
Hallo Wolle,
Als erstes vielen vielen Dank für die suuuper Erläuterung – insbesondere die SPI Variante.
Einfach nur Daumen hoch und DANKE!
Der Hinweis das der GPIO.(A/B)7 nur als Ausgang genutzt werden kann trifft nur auf den I2C-Vertreter zu.
Die SPI-Version (MCP23S17) hat diese „Behinderung“ nicht.
Ich persönlich finde die SPI Variante eh‘ besser, da man
a) nicht mit irgendwelchen Bus-Pullup’s hantieren muss (unnötige Fehlerquelle)
b) mit (theoretisch) 8 MCP23S17 am SPI-Bus kommt wohl eher der Arduino an seine Grenzen (Stichwort: „Spaghetti-Code“)
c) mit setzten von SPIClock(8000000) geht das ganze ab wie eine Rakete… 😉
Mach weiter so, jedenfalls liegt die Seite in meinen Favoriten. 😉
MfG
Thomas Bauer
Vielen Dank!!!
Keins deiner Beispiel-sketche funktionier
erster fehler ist immer in zeile 12 beim überprüfen. Wenn man den eintrag des Reset_Pin raus löscht ist der Nächste fehler in der Zeile 16
die Fehlerausgabe vom compiler: class MCP23017′ has no member named ‚Init‘, did you mean ‚Init‘?
schade es sah so vielversprechend aus
Hallo, die Beispiele sind x-fach geprüft und funktionieren alle. Sicher! Ich vermute, dass du eine weitere Bibliothek installiert hast, die eine gleichnamige Klasse namens MCP23017 enthält. Die müsstest du entfernen.
VG, Wolfgang
English Article is different from German
Switches on breadboard are shown as INPUT to MCP 23017
English version page have no switch on bread board
athar.kaludi@gmail.com
Did you sent email already to me from first question
I compared the English and the German version side by side and I don’t see a difference. Please tell me which image you mean which is different. You can right-click on the image and send the link. Please do it for the English and the German version.
I send you an e-mail. Maybe it’s your spam folder. Or please send an email to me (wolfgang.ewald@wolles-elektronikkiste.de).
Hallo
Gehören die ICs MCP23S08, MCP23008 und MCP23009 auch zu dieser IC Familie?
Hallo Achim, wie ich in der Einleitung schrieb, ist die Definition der MCP23x1y meine eigene Kreation. Die von dir genannten Modelle sind natürlich auch verwandt. Ich habe sie bisher nicht in meine Bibliothek integriert und deswegen auch im Beitrag ausgelassen. Im Prinzip sind sie sehr ähnlich, besitzen aber nur einen Port, also 8 GPIOs.
VG, Wolfgang
Vor einiger Zeit habe ich diese IC benützt. Ohne wissen daß sie auch eine Biblothek hatten… So jetzt geht es viel einfacher !
Schön! Viel Spaß dann noch bei den Projekten.
Hallo,
Ich bin neu hier als auch mit ESP32 und MCP23017. Alle pins von MCP23017 sollen Inputs sein
Was ich möchte ist beide ports auslesen, z,b
varA = Read portA of MCP23107
varB = Read portB of MCP23107
Hast keinen Beispiel code dafür?
Danke in voraus
Freundliche Grüsse
Luis
Hallo Luis,
mcp.setPortMode(0,A); was dasselbe ist wie mcpsetPortMode(0b00000000,A), setzt alle Pins des Port A auf Input. mcp.setPortMode(0,B), macht dasselbe für Port B.
Auslesen ist auch ganz einfach:
byte portA = mcp.getPort(A);
byte portB = mcp.getPort(B);
Dieser Beitrag ist die Fortsetzung meines Beitrages: https://wolles-elektronikkiste.de/portexpander-mcp23017
Schau da mal rein und außerdem hat die Bibliothek noch einen Beispielsketch namens mcp23017_gpio_reading.ino.
Viel Erfolg!
VG, Wolfgang
Hallo Wolfgang,
ich habe mir mal deine Schaltungen sehr genau angesehen. Irgendwei ergibt sich mir nicht der Sinn, wie bei allen Arduino Verschaltungen gezeichnet, dass der RESET (invertiert ) PIN18 am Controller hängt. Normal ziehtman dieses Reset mit einem PullUp auf Vc, und löst den Reset-Vorgang eigentlich nur über Transistorstufe aus, damit im Falle eines Kabelabrisses, oder einer anderen Störung nicht der oder bei in Sammlung geschalteten Portexpandern für immer die Lichter ausgehen. Sobald laut Datenblatt die Spannung an diesem PIN unter 1,8 Volt sinkt, geht der Chip in den RESET-Moodus.
Es wäre für mein Verständnis doch sinnvoller den RESET nur zu Nutzen, wenn dieser benötigt wird, und nicht von einem Dauerschaltzustand einer Software / Treiber abängig zu machen. Dann lieber ein zusätzliche Bauelement, oder einen manuellen Reset-Taster, als einen PIN für diese Funktion zu verschwenden. Der Fall Reset wird ja nur dann relavant, wenn die Konfiguration im laufenden Betrieb geändert wird, bzw wenn es eine Störung auf dem BUS System gab.
Zudem so meine Erfahrung und Feststellung, wenn man mehrere dieser 23×17 über einen BUS nutzt, sind kleine 100nF Kerkos zwischen Vc und GND pro Chip, und bei Schaltlasten ( alle GPA / GPB auf Output), sowie bei Lasten pro I/O Pin über 5mA bis max I/O Summe max 16*8mA/ > 125mA ein Stützelko unabdingbar. Ich verwende an dieser Stelle immer 1- 2,2µF Tantal Elkos 16 V.
Hallo,
du kannst auch einfach RESET auf HIGH setzen oder einen Schalter dran bauen. Problematisch kann das bei der Sketchentwicklung werden. Lädt man eine neue Version auf den Mikrocontroller und führt keinen Reset des MCP23017 (oder welchen du auch immer verwendest), dann bleiben die Register in dem Zustand, den du mit dem vorherigen Sketch eingestellt hast. Und im Normalfall bleibt es nicht bei der ersten Version.
Einige Register werden durch die init() Funktion auf einen definierten Zustand gebracht, aber nicht alle. Durch Reset, den ich in der init() Funktion implementiert habe, beginnt man sozusagen wieder mit einem neuen weißen Blatt Papier. Würde ich das nicht machen, würden viele nicht an den Reset denken und ich haufenweise Rückmeldungen bekommen, dass die Bibliothek nicht funktioniert. Schön wäre es, wenn die Portexpander ein Register hätten, über das man einen Reset durchführen kann, haben sie aber nicht.
VG, Wolfgang