MCP23x1y Portexpander

Ü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

Die MCP23x1y Familie umfasst fünf Mitglieder:

MCP23x1y Familie
MCP23x1y Familie in der PDIP Ausführung

Hier die wichtigsten Eigenschaften der MCP23x1y Portexpander im Überblick:

Wichtigste Unterschiede der MCP23x1y Portexpander im Überblick
Wichtigste Unterschiede 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

Ein Vertreter der MCP23x1y Familie - Pinout des MCP23017

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.

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:

Der MCP23017 an einem Arduino Nano
Der MCP23017 an einem Arduino Nano

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:

LED Bars zum Testen der MCP23x1y Portexpander
LED Bars mit gemeinsamer Kathode (links) oder gemeinsamer Anode (rechts)

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):

Beispielschaltung mit LED Bars am MCP23017

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(): kombiniert setPinMode() und setPin() in einer Funktion.
  • setPortX(): kombiniert setPortMode() und setPort() 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:

Der MCP23S17 an einem Arduino Nano
Der MCP23S17 an einem Arduino Nano

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.

Der MCP23018 an einem Arduino Nano
Der MCP23018 an einem Arduino Nano

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:

Beispielschaltung mit LED Bars am MCP23018

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:

Der MCP23S18 an einem Arduino Nano
Der MCP23S18 an einem Arduino Nano

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.

Der MCP23016 an einem Arduino Nano
Der MCP23016 an einem Arduino Nano

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:

Ausgabe von I2C_speed_test_mcp23017_nano.ino
Ausgabe von I2C_speed_test_mcp23017_nano.ino

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: 

Ausgabe von I2C_speed_test_mcp23017_esp32.ino
Ausgabe von I2C_speed_test_mcp23017_esp32.ino

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.

Tatsächliche I2C Taktrate am ESP32

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.

Maximale "Toggle" Frequenz mit dem MCP23S17 am Arduino Nano
Maximale „Toggle“ Frequenz mit dem MCP23S17 am Arduino Nano

10 thoughts on “MCP23x1y Portexpander

    1. 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).

  1. Hallo
    Gehören die ICs MCP23S08, MCP23008 und MCP23009 auch zu dieser IC Familie?

    1. 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

  2. Vor einiger Zeit habe ich diese IC benützt. Ohne wissen daß sie auch eine Biblothek hatten… So jetzt geht es viel einfacher !

  3. 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

    1. 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

  4. 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.

    1. 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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.