Portexpander MCP23017

Über den Beitrag

In meinem Beitrag über die Porterweiterung am ESP-01 hatte ich den MCP23017 schon einmal kurz beschrieben, hier möchte ich nun im Detail auf seine vielfältigen Möglichkeiten eingehen. Im ersten Teil des Beitrages möchte ich euch zeigen wie man den MCP23017 (und den MCP23S17) mit Hilfe einer Bibliothek einsetzt. Der zweite Teil ist ein Blick hinter die Kulissen für diejenigen, die etwas tiefer einsteigen wollen. Dabei gehe ich auf die zahlreichen Register des MCP23017 ein. 

MCP23017 – eine Kurzbeschreibung

Der MCP23017 ist ein 16-Bit I/O Portexpander, der über komfortable Interruptfunktionen verfügt. Die 16 I/O Pins sind in zwei Ports (A und B) organisiert, die separat (Byte Mode) oder zusammen (Sequential Mode) angesprochen werden. Die Versorgungsspannung sollte zwischen 1.8 und 5.5 Volt liegen. Der maximale Strom an den I/O Pins beträgt 25 mA in beide Richtungen. In Summe soll der Eingangsstrom an VDD 125 mA nicht überschreiten und über VSS (GND) sollen nicht mehr als 150 mA abfließen. Die Kommunikation erfolgt über I2C. Ein Datenblatt für den MCP23017 gibt es z.B. hier.

Der MCP23017 ist hinsichtlich seiner Flexibilität das Schweizer Taschenmesser unter den gängigen Portexpandern, wenn man ihn z.B. mit dem 74HC595 Schieberegister oder dem PCF8574 vergleicht. 

Pinout des MCP23017

Pinout des MCP23017 alt vs. neu (ab 2022)

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!

Die 16 I/O Pins werden den beiden Ports entsprechend als GPA0 bis GPA7 bzw. GPB0 bis GPB7 benannt. Die Stromversorgung erfolgt über VDD und VSS. Die Beschaltung der Pins A0, A1 und A2 legt die I2C Adresse nach dem folgenden Schema fest:

1 0 0 A2 A1 A0 

Sind A0 bis A2 beispielsweise auf LOW, dann ist die Adresse 100000 (binär) = 32 (dezimal) = 0x20 (hexadezimal). SDA und SCL sind die beiden I2C Pins. Der Resetpin ist low-aktiv. INTA und INTB sind die Interruptpins für die beiden Ports. Die Polarität der Interruptpins könnt ihr einstellen. Ebenso könnt ihr beide Interruptpins zusammenschalten (Mirror Funktion). 

Ansteuerung mit der Bibliothek

Ich habe eine Bibliothek geschrieben, die ihr hier auf Github findet und herunterladen könnt. Ihr könnt die Bibliothek aber auch direkt über die Bibliotheksverwaltung der Arduino IDE installieren. Die Bibliothek ist so konzipiert, dass die beiden Ports A und B im Byte Mode, also getrennt angesprochen werden. Vielleicht werde ich die Bibliothek irgendwann nochmal um den Sequential Mode erweitern.  

Einfache Input-/Output Anwendungen

Die Funktionalität der I/O Pins ist mit der Funktionalität der Arduino I/O Pins vergleichbar. So können die Pins als Input oder Output eingesetzt werden, sie werden HIGH oder LOW geschaltet und sie können sowohl als Stromlieferant wie auch als Stromsenke fungieren. Wenn sie als  Input geschaltet werden, dann können die Pins als Interruptpins dienen. Aber im ersten Beispiel werden zunächst einfach nur 16 LEDs gesteuert.     

Schaltplan zum Steuern von 16 LEDs
Schaltplan zum Steuern von 16 LEDs

Die Adresspins liegen in meiner Beispielschaltung auf LOW, somit ist die I2C Adresse 0x20. Der Resetpin ist mit dem Arduino Pin 5 verbunden. Die Pins von Port A und B steuern jeweils acht LEDs einer LED Leiste. Die I2C Leitungen SDA und SCL bekommen Pull-Ups mit 4.7 kOhm Widerständen. Ohne die Pull-Ups hat es bei mir allerdings auch gut funktioniert.

In diesem Beispiel werden die folgenden Funktionen verwendet (das MCP23017 Objekt heißt hier myMCP):

  • MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN) erzeugt euer MCP23017 Objekt. Ihr müsst die I2C Adresse übergeben. Zusätzlich könnt ihr den Reset Pin übergeben und / oder ein Wire Objekt. Mit Letzterem könnt ihr zum Beispiel beide I2C Busse eines ESP32 nutzen. Ihr könnt den Reset Pin auch mit VDD verbinden und so einen Pin sparen. Dann solltet ihr aber einen Dummy Reset Pin mit einem Wert von >= 99 übergeben. Das löst einen Software Reset anstelle eines Hardware Resets in Init() aus.
  • myMCP.Init(); initialisiert das Objekt mit einigen Voreinstellungen
  • myMCP.setPinMode( pin,port,direction ); entspricht der Arduino pinMode Funktion, wobei hier noch der Port als Parameter hinzukommt. 
    • zulässige Angaben für direction sind: INPUT/OUTPUT/INPUT_PULLUP, OFF/ON oder 0/1
    • „0“= INPUT, „1“ =OUTPUT
  • myMCP.setPortMode( value,port ); pinMode für einen ganzen Port; value gibt man sinnvollerweise als Binärzahl an
  • myMCP.setPortMode( value,port,INPUT_PULLUP); mit dieser Variante bekommen alle input pins ein pull-up; keine Auswirkung auf output pins. 
  • myMCP.setPin( pin,port,level ); entspricht der digitalWrite Funktion;
    •  zulässige Angaben für level sind: 0/1, OFF/ON, LOW/HIGH
  • myMCP.setPort( value,port ); digitalWrite für ganzen Port; value gibt man wieder sinnvollerweise als Binärzahl an
  • myMCP.togglePin( pin,port ); wechselt einen Pin von LOW auf HIGH bzw. von HIGH auf LOW
  • myMCP.setPinX( pin,port,direction,level ); „extended version“ der setPin Funktion bzw. Kombination aus setPinMode und setPin
  • myMCP.setPortModeX( direction,value,port ); „extended version“ der setPort Funktion
  • myMCP.setAllPins( port,level ); setzt alle Pins eines Ports auf LOW oder HIGH

Beispielsketch für einfache Input/Output Anwendungen

Hier ein Sketch, der mit diesen Funktionen spielt und sie verdeutlicht:

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
#define RESET_PIN 5  

/* 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
 * Successfully tested with two I2C busses on an ESP32
 */
MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

int wT = 1000; // wT = waiting time

void setup(){ 
  Serial.begin(9600);
  Wire.begin();
  if(!myMCP.Init()){
    Serial.println("Not connected!");
    while(1){} 
  }
  myMCP.setPortMode(0b11111101, A);  // Port A: all pins are OUTPUT except pin 1
  myMCP.setPortMode(0b11111111, B);  // Port B: all pins are OUTPUT
  delay(wT);
  myMCP.setAllPins(A, ON); // alle LEDs switched on except A1
  delay(wT);
  myMCP.setPinX(1, A, OUTPUT, HIGH); // A1 switched on 
  delay(wT); 
  myMCP.setPort(0b11110000, B); // B4 - B7 switched on
  delay(wT);
  myMCP.setPort(0b01011110, A); // A0,A5,A7 switched off
  delay(wT);
  myMCP.setPinX(0,B,OUTPUT,HIGH); // B0 switched on
  delay(wT);
  myMCP.setPinX(4,B,OUTPUT,LOW); // B4 switched off
  delay(wT);
  myMCP.setAllPins(A, HIGH); // A0 - A7 all on
  delay(wT);
  myMCP.setPin(3, A, LOW); // A3 switched off
  delay(wT);
  myMCP.setPortX(0b11110000, 0b01101111,B); // at port B only B5,B6 are switched on
  delay(wT);
  myMCP.setPinMode(0,B,OUTPUT); // B0 --> OUTPUT
  for(int i=0; i<5; i++){  // B0 blinking
    myMCP.togglePin(0,B); 
    delay(200);
    myMCP.togglePin(0,B);
    delay(200);
  }
  for(int i=0; i<5; i++){ // B7 blinking
    myMCP.togglePin(7,B);
    delay(200);
    myMCP.togglePin(7,B);
    delay(200);
  }
}

void loop(){ 
} 

 

In meinem Beispiel fließt der Strom (technische Stromrichtung Plus -> Minus) vom MCP23017 durch die LEDs nach GND. Stattdessen könnte man den MCP23017 natürlich auch als Stromsenke einsetzen. Dann würde eine LED leuchten, wenn der zugehörige Pin OUTPUT und LOW ist und eine geeignete Spannung anliegt, also genau wie am Arduino. 

Pinstatus auslesen

Um den Pinstatus im GPIO Register auszulesen werden folgende Funktionen verwendet:

  • myMCP.getPin( pin,port ); liefert den Level eines Pins (als bool)
  • myMCP.getPort( port ); liefert den Status eines ganzen Ports (als byte), sprich den Inhalt des GPIO Registers

Hier habe ich eine Beispielschaltung für euch:

Schaltplan zum Testen der GPIO-Lesefunktionen
Schaltplan zum Testen der GPIO-Lesefunktionen

Und hier der zugehörige Beispielsketch:

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)

/* A hardware reset is performed during init(). If you want to save a pin you can define a dummy 
 * reset pin >= 99 and connect the reset pin to HIGH. This will trigger a software reset instead 
 * of a hardware reset. 
 */
#define RESET_PIN 5
/* 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
 * Successfully tested with two I2C busses on an ESP32
 */
MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

int wT = 1000; // wT = waiting time
byte portStatus;
bool pinStatus;

void setup(){ 
  Serial.begin(9600);
  Wire.begin();
  if(!myMCP.Init()){
    Serial.println("Not connected!");
    while(1){} 
  }  
  myMCP.setPortMode(0b00000000, A);  // Port A: all pins are INPUT
  myMCP.setPortPullUp(0b11110000, A);  // Port A: Pin 4 - 7 are pulled up
}

void loop(){ 
  
  portStatus = myMCP.getPort(A); // query the complete port status 
  Serial.print("Status GPIO A: ");
  Serial.println(portStatus, BIN);
  
  pinStatus = myMCP.getPin(5, A); // query one pin status
  Serial.print("Status Port A, Pin 5: ");
  Serial.println(pinStatus, BIN);
 
  Serial.println("-------------------------------------");
  delay(1000);
} 

 

Als INPUT konfigurierte I/Os können einen internen Pull-Up bekommen: 

  • myMCP.setPinPullUp( pin,port ); setzt einen Pull-Up mit einem 100 kOhm Widerstand
  • myMCP.setPortPullUp( value,port ); ist das Pendant für einen ganzen Port

Pull-Down Widerstände müsst ihr extern hinzufügen. 

So sieht die Ausgabe des Beispielsketches aus:

Ausgabe von mcp23017_gpio_reading.ino
Ausgabe von mcp23017_gpio_reading.ino

Interrupt-on-Change

Alle 16 I/O Pins lassen sich als Interruptpins konfigurieren. Dabei gibt es zwei Modi, nämlich den Interrupt-on-Change und den Interrupt-on-Defval-Deviation. Ich beginne mit der Interrupt-on-Change Funktion, bei der jeder LOW-HIGH oder HIGH-LOW Wechsel einen Interrupt auslöst. Der Interrupt führt zu einem Polaritätswechsel an dem jeweiligen Interruptausgang INTA oder INTB. Alternativ könnt ihr INTA und INTB zusammenlegen. Außerdem könnt ihr die Polarität der Interruptausgänge einstellen. 

Nur als INPUT eingestellte Pins können als Interruptpins fungieren, aber diese Einstellung übernimmt die Bibliothek.

Folgende Funktionen habe ich für Interrupt-on-Change implementiert:

  • myMCP.setInterruptOnChangePin( pin,port ); richtet einen einzelnen Pin als Interrupt-on-Change Pin ein
  • myMCP.setInterruptOnChangePort( value,port ); richtet mehrere oder alle Pins eines Ports als Interrupt-on-Change Pin ein 
  • myMCP.setInterruptPinPol( level ); legt den Level des aktiven Interruptausgangs fest
    •  level = HIGH –> active-high, level = LOW –> active-low (Voreinstellung) 
    •  ich habe nur eine Einstellung für beide Ausgänge implementiert, also beide active-high oder beide active-low 
  • myMCP.setIntOdr( value ); value = 1 oder ON –> Interruptausgänge gehen in den Open Drain Zustand, die Interruptpin Polarität wird überschrieben; value = 0 oder OFF –> active-low oder active-high (beide)
  • myMCP.deleteAllInterruptsOnPort( port ); macht die Einrichtung der Interruptpins rückgängig
  • myMCP.setIntMirror ( value ); value = 1 oder ON –> INTA / INTB werden gespiegelt, value = 0 oder OFF –> INTA / INTB sind separat für ihre Ports zuständig (Voreinstellung)
  • myMCP.getIntFlag( port ); liefert den Wert des Interrupt Flag Registers als byte zurück. Im Interrupt Flag Register ist das Bit gesetzt welches den für den letzten Interrupt verantwortlichen Pin repräsentiert
  • myMCPgetIntCap( port ); liefert den Wert des Interrupt Capture Registers zurück. Es enthält den Wert des GPIO Registers zum Zeitpunkt des Interrupts. 

Zum Testen habe ich die folgende Schaltung aufgebaut:

Schaltplan zum Testen der Interrupt on Change Funktion mit dem MCP23017
Schaltplan zum Testen der Interrupt-on-Pin-Change Funktion

Die Pins an Port B werden als Interruptpins eingerichtet und bekommen über die Taster HIGH-Signale. Die LEDs an Port A sollen anzeigen an welchem Pin der Interrupt aufgetreten ist. 

Beispielsketch für Interrupt-on-Change

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
/* A hardware reset is performed during init(). If you want to save a pin you can define a dummy 
 * reset pin >= 99 and connect the reset pin to HIGH. This will trigger a software reset instead 
 * of a hardware reset. 
 */
#define RESET_PIN 5  
int interruptPin = 3;
volatile bool event; 

/* There are several ways to create your MCP23017 object:
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS) -> uses Wire / no reset pin 
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN) -> uses Wire / RESET_PIN
 * MCP23017 myMCP = MCP23017(&Wire, MCP_ADDRESS) -> passing a TwoWire object / no reset pin
 * MCP23017 myMCP = MCP23017(&Wire, MCP_ADDRESS, RESET_PIN) -> "all together"
 */
MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

void setup(){ 
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), eventHappened, RISING);
  Serial.begin(9600);
  Wire.begin();
  if(!myMCP.Init()){
    Serial.println("Not connected!");
    while(1){} 
  } 
  myMCP.setPortMode(0b11111111,A);
  myMCP.setPort(0b11111111, A); // just an LED test
  delay(1000); 
  myMCP.setAllPins(A, LOW);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH); // set INTA and INTB active-high
  delay(10);
  myMCP.setInterruptOnChangePort(0b11111111, B); //set all B pins as interrrupt Pins
  delay(10);
  myMCP.getIntCap(B); // ensures that existing interrupts are cleared
  event=false;
}  

void loop(){ 
  if(event){
    byte intFlagReg = myMCP.getIntFlag(B);
    byte eventPin = log(intFlagReg)/log(2);
    byte intCapReg = myMCP.getIntCap(B);
    Serial.println("Interrupt!");
    Serial.print("Interrupt Flag Register: ");
    Serial.println(intFlagReg, BIN); 
    Serial.print("Interrupt Capture Register: ");
    Serial.println(intCapReg, BIN); 
    Serial.print("Pin No.");
    Serial.print(eventPin);
    Serial.print(" went ");
    if((intFlagReg&intCapReg) == 0){  //LOW-HIGH or HIGH-LOW interrupt?
      Serial.println("LOW");
    }
    else{
      Serial.println("HIGH");
    }
    myMCP.setPort(intFlagReg, A);
    delay(200);
    intCapReg = myMCP.getIntCap(B);
    event = false; 
  }
}

void eventHappened(){
  event = true;
}

 

Ausgabe des Interrupt-on-Pin-Change Sketches
Ausgabe des Interrupt-on-Pin-Change Sketches

Hinweis 1: Bei kurzem Drücken des Tasters (< 200 ms) wird ein LOW-HIGH Interrupt gemeldet, bei längerem Drücken gibt es erst den LOW-HIGH und dann den HIGH-LOW Interrupt. 

Hinweis 2: der Interrupt bleibt so lange aktiv, bis eine getIntCap oder getPort Abfrage erfolgt. Wenn es dumm läuft und man fragt zum falschen Zeitpunkt ab oder es kommt zum falschen Zeitpunkt der nächste Interrupt, bleibt der Interrupt ungewollt bestehen. Deswegen habe ich in Zeile 63 eine zusätzliche getIntCap Abfrage eingefügt, die auf den ersten Blick überflüssig erscheint. Dadurch ist sichergestellt, dass der MCP23017 bereit ist für den nächsten Interrupt.

Interrupt-on-DefVal-Deviation

Hier wird die Polarität der Interruptpins mit der Vorgabe im sogenannten DEFVAL Register verglichen. Eine Abweichung (Deviation) führt zu einem Interrupt. Wenn ihr das Interrupt Capture oder das GPIO Register auslest, löscht ihr dadurch den Interrupt. Wenn allerdings die Interruptbedingung zu diesem Zeitpunkt immer noch erfüllt ist, dann wird sofort der nächste Interrupt ausgelöst.

Für diese Interruptmethode habe ich die folgenden zusätzlichen Funktionen implementiert:

  • myMCP.setInterruptOnDefValDevPin( intpin,port,defvalstate ); intpin ist der Interruptpin, defvalstate ist das Vorgabelevel und eine Abweichung davon führt zum Interrupt
  • myMCP.setInterruptOnDefValDevPort( intpins,Port,defvalstate );
    • intpins sind die Interruptpins, z.B. würde B10000001 die Pins 0 und 7 zum Interruptpin machen
    • defvalstate ist die Vorgabe für das DEFVAL Register; eine Abweichung führt zum Interrupt 

Als Beispiel habe ich die folgende Schaltung gewäht:

Schaltplan zum Testen der Interrupt-on-DefVal-Deviation Funktion am MCP23017
Schaltplan zum Testen der Interrupt-on-DefVal-Deviation Funktion

Die Port B Pins werden als Interruptpins definiert. B0 bis B3 werden mit internen Pull-Ups auf HIGH gelegt, hingegen bekommen B4 bis B7 Pull-Down Widerstände. Durch Tasterdruck wird die Polarität am jeweiligen Pin umgedreht. Port A dient wie im letzten Beispiel wieder der Anzeige des für den Interrupt verantwortlichen Pin. 

Beispielsketch für Interrupt-on-Defval-Deviation

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
/* A hardware reset is performed during init(). If you want to save a pin you can define a dummy 
 * reset pin >= 99 and connect the reset pin to HIGH. This will trigger a software reset instead 
 * of a hardware reset. 
 */
#define RESET_PIN 5 
int interruptPin = 3;
volatile bool event = false;

/* There are several ways to create your MCP23017 object:
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS) -> uses Wire / no reset pin 
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN) -> uses Wire / RESET_PIN
 * MCP23017 myMCP = MCP23017(&Wire, MCP_ADDRESS) -> passing a TwoWire object / no reset pin
 * MCP23017 myMCP = MCP23017(&Wire, MCP_ADDRESS, RESET_PIN) -> "all together"
 */
MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

void setup(){ 
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), eventHappened, RISING);
  Serial.begin(9600);
  Wire.begin();
  if(!myMCP.Init()){
    Serial.println("Not connected!");
    while(1){} 
  }
  myMCP.setPortMode(0b11111111,A);
  myMCP.setPort(0b11111111, A); // just an LED test 
  delay(1000); 
  myMCP.setAllPins(A, LOW);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH);
  delay(10);
  myMCP.setInterruptOnDefValDevPort(0b11111111, B, 0b00001111); // interrupt pins, port, DEFVALB
  myMCP.setPortPullUp(0b00001111, B); // pull-up for B0-B3
  delay(10);
  myMCP.getIntCap(B); // deletes all interrupts
  event=false;
}  

void loop(){ 
  myMCP.getIntCap(B);
  if(event){
    byte intFlagReg = myMCP.getIntFlag(B);
    byte eventPin = log(intFlagReg)/log(2);
    byte intCapReg = myMCP.getIntCap(B);
    Serial.println("Interrupt!");
    Serial.print("Interrupt Flag Register: ");
    Serial.println(intFlagReg, BIN); 
    Serial.print("Interrupt Capture Register: ");
    Serial.println(intCapReg, BIN); 
    Serial.print("Pin No.");
    Serial.print(eventPin);
    Serial.print(" went ");
    if((intFlagReg&intCapReg) == 0){ // HIGH/LOW or LOW/HIGH change?
      Serial.println("LOW");
    }
    else{
      Serial.println("HIGH");
    }
    myMCP.setPort(intFlagReg, A);
    delay(400);
    intCapReg = myMCP.getIntCap(B);
    event = false;
  }
}

void eventHappened(){
  event = true;
}

 

Der entscheidende Unterschied zum Interrupt-On-Change ist, dass bei dieser Methode der Polaritätswechsel nur in eine Richtung zum Interrupt führt. Entsprechend sieht die Ausgabe aus:

Ausgabe des Interrupt-on-DefVal-Dev Sketches
Ausgabe des Interrupt-on-DefVal-Dev Sketches

Der MCP23S17

Der MCP23S17 unterscheidet sich vom MCP23017 nur durch die Ansteuerung per SPI anstelle von I2C. Dadurch sind die Anschlüsse geringfügig unterschiedlich. Alle Funktionen sind identisch. In meiner Bibliothek findet ihr einen Beispielsketch und die Verkabelung dazu. 

MCP23017 / MCP23S17 intern

Hier dann noch wie angekündigt ein paar zusätzliche Detailinformationen über den MCP23017 für die, die noch Lust haben. 

DerMCP23017 ist nur ein Vertreter der größeren MCP23XXX Familie, die sich untereinander durch die Anzahl der I/O Pins, die Ansteuerung (I2C vs SPI) und die externe Beschaltung unterscheiden. Der MCP23017 ist wohl der populärste Vertreter. Über die anderen habe ich einen separaten Beitrag geschrieben, den ihr hier findet.

Die Register des MCP23017

Zunächst einmal muss man sich entscheiden, wie man die Register adressieren möchte. Dafür gibt es das BANK bit im IOCON Register. Ist dieses auf  1 gesetzt, dann befinden sich die Portregister in zwei getrennten Banks. Ist es hingegen auf 0 gesetzt, befinden sich die Register in derselben Bank und die Adressen sind sequentiell. Ich habe mich für letzteres entschieden und die Alternative auch nicht als Option implementiert. Die Register sind damit wie folgt definiert:

Registerübersicht des MCP23017 für IOCON.BANK = 0
Registerübersicht des MCP23017 wenn IOCON.BANK = 0

IODIR – I/O Direction Register

Im IODIR Register wird festgelegt, ob die Pins INPUT oder OUTPUT Pins sind. Es ist das einzige Register mit dem Start- bzw. Resetwert 0bx11111111. „1“ bedeutet INPUT, „0“ bedeutet OUTPUT, was für mich unlogisch klingt, aber vielleicht ist es auch nur die Gewohnheit aus der Arduinowelt. Weil mich das aber irritiert, habe ich die Werte in meiner Bibliothek entsprechend umgedreht. Mit myMCP.setPortMode() und myMCP.setPinMode() wird das IODIR Register direkt angesprochen. So bedeutet myMCP.setPortMode(B11111111, A) beispielsweise, dass alle Pins des Port A OUTPUT Pins sind. 

IPOL – Input Polarity Register

Wenn man die Bits in diesem Register setzt, dann wird im entsprechenden GPIO Register der invertierte Level der Pins gespeichert. Weil mir nicht einfiel wozu ich das gebrauchen könnte,  habe ich in meiner Bibliothek keinen Zugriff auf dieses Register vorgesehen. 

GPINTEN – Interrupt-on-Change Control Register

In diesem Register wird kontrolliert, welche Pins als Interrupt Pins verwendet werden. 0 = Disable, 1 = Enable. Will man nur Interrupt-on-Change implementieren, sind keine weiteren Einstellungen notwendig. Für den Fall aber, dass man Interrupt-on-Defval-Deviation implementieren möchte, muss man zusätzliche Einstellungen in den Registern DEFVAL und INTCON vornehmen. Auf das GPINTEN Register wird in meiner Bibliothek indirekt über die setInterruptOnChangePin() und setInterruptOnDefValDevPin() Funktionen bzw. deren Pendants für ganze Ports zugegriffen. 

DEFVAL – Default Value Register

Dieses Register wird für die Einstellung von Interrupts-on-Defval-Deviation benötigt. Weicht ein Wert im GPIO Register vom DEFVAL Register ab, wird ein Interrupt ausgelöst, sofern die entsprechenden Einstellungen im GPINTEN und INTCON Register vorgenommen wurden. 

INTCON – Interrupt Control Register

In diesem Register wird festgelegt, unter welchen Bedingungen Interrupts ausgelöst werden:

  • „0“: Vergleich mit vorherigem Pinstatus (Interrupt-on-Change)
  • „1“: Vergleich mit DEFVAL (Interrupt-on-DefVal-Deviation)

Allerdings sind die Einstellungen nur an den Pins wirksam, für die die entsprechenden Bits in GPINTEN gesetzt wurden. 

IOCON – I/O Expander Configuration Register

In diesem Register können einige Sondereinstellungen für den MCP23017 vorgenommen werden.

  • BANK – Adressierungsmethode für die Register
    • „1“: Register befinden sich in separaten Banks 
    • „0“: Register befinden in derselben Bank
    • einen Wechsel der Einstellung habe ich in meiner Bibliothek nicht vorgesehen
  • MIRROR – ist in meiner Bibliothek über setIntMirror() einstellbar
    • „1“: INTA und INTB sind verbunden (gespiegelt)
    • „0“: INTA und INTB sind separat für Port A bzw. B zuständig
  • SEQOP – sequentielle Adressierung
    • „1“: Disabled – der Adresszeiger wird nicht inkrementiert
    • „0“: Enabled – der Adresszeiger wird automatisch inkrementiert 
    • einen Wechsel der Einstellung habe ich in meiner Bibliothek nicht vorgesehen
  • DISSLW – Einstellung der Flankensteilheit des SDA Outputs
    • „1“: Disabled
    • „0“: Enabled
    • einen Wechsel der Einstellung habe ich nicht vorgesehen
  • HAEN – nicht relevant für den MCP23017
  • ODR – Open Drain für die Interruptpins INTA und INTB
    • „1“: Open Drain ist aktiv – überschreibt die INTPOL Einstellung
    • „0“: Disabled – Polarität des Interruptsignals wird durch INTPOL bestimmt
    • Einstellung erfolgt über setIntODR(); nur eine gemeinsame Einstellung für beide Pins ist in meiner Bibliothek vorgesehen (entweder beide 0 oder beide 1)
  • INTPOL – Polarität der Interruptpins
    • „1“: active-high
    • „0“: active-low
    • in meiner Bibliothek ist nur eine gemeinsame Einstellung beider Pins vorgesehen
  • GPPU – GPIO Pull-Up Register
    • „1“: Pull-Up mit 100 kOhm Widerstand
    • „0“: kein Pull-Up
    • in meiner Bibliothek implementiert durch setPinPullUp() oder setPortPullUp()
    • wirkt nur auf als Input konfigurierte Pins

INTF – Interrupt Flag Register (read-only)

In diesem Register wird festgehalten an welchem Pin der letzte Interrupt verursacht wurde. Das gesetzte Bit verrät den „Schuldigen“. In meiner Bibliothek fragt getIntFlag() den Inhalt ab. 

INTCAP – Interrupt Capture Value Register (read-only)

Dieses Register hält den Inhalt des GPIO Registers zum Zeitpunkt des letzten Interrupts vor. Ich habe die Abfrage durch die Funktion getIntCap() implementiert. 

GPIO – General Purpose I/O Port Register

Enthält den Pinstatus (HIGH/LOW). Ein Schreiben in das Register verändert auch das OLAT (Output Latch) Register. Der Schreibzugriff ist bei mir nur indirekt über verschiedene Funktionen implementiert. Der Lesezugriff erfolgt in meiner Bibliothek über getPin() oder getPort().

OLAT – Output Latch Register

Der Lesezugriff gibt den Zustand des Registers wieder und nicht den des Ports (das würde über den GPIO Read erfolgen). D.h. zum Beispiel, dass ein als LOW eingestellter Pin, an dem ein externes HIGH anliegt, hier als LOW gelesen würde, wohingegen im GPIO Register HIGH angezeigt würde. 

112 thoughts on “Portexpander MCP23017

  1. Hallo und guten Tag,
    vielen Dank für das wunderbare Tutorial.

    Allerdings habe ich noch eine Frage:

    Muss ich bzw. wo kann ich der Bibliothek „sagen“ dass ich für SCL und SDA nicht PIN 4 und 5 nehme so wie im Beispiel, sondern IO 21 und 22 (ESP32 – CYD, daher auch die Knappheit an Ports).

    Vielen Dank
    Stefan

    1. Hallo,

      du sagst es nicht der MCP23017 Bibliothek (genauer gesagt dem Objekt), sondern du änderst die Pins mit Wire.begin:

      Wire.begin(SDA_PIN, SCL_PIN); // in deinem Fall SDA_PIN = 21, SCL_PIN =22.

      Wire solltest du beim Kreieren deines MCP23017 Objektes übergeben:
      MCP23017 myMCP = MCP23017(&Wire, MCP_ADDRESS, RESET_PIN);

      VG, Wolfgang

  2. Hallo Wolfgang

    vielen Dank für deine mühe und für diese sehr gute Anleitung.

    Ich hätte nur eine frage zum Interrupt PIN, wenn ich mehrere MCP23017 anschließe. Ich benötige 8 Stück, alle mit Pin 1-4 als Input und 5-8 als Output. Damit ich das problem mit den neueren MCPs ab 2022 umgehe.
    Nun zu meiner Frage: Müssen alle Interrupt PINs (A und B) von allen 8 MCP23017 an den selben PIN von meinem Arduino Nano
    („int interruptPin = 3;“ (Beispielsketch für Interrupt-on-Change/ Zeile 9))?
    Oder muss ich mehrere interruptPins für jeden MCP am Arduino einrichten?
    Soweit ich mich belesen habe funktioniert beim Nano nur PIN 2 und 3 als Interrupt PIN.

    1. Hallo, ein ATmega328P-basierter Arduino wie der UNO R3 oder der Nano haben in der Tat nur die beiden Interruptpins 2 und 3. Wenn du die Interrupts von acht MCP23017 an einem Nano verfolgen willst, dann sehe ich folgende Möglichkeiten:
      1) Du hängst alle Interruptleitungen an Pin 2 oder Pin 3 und wenn ein Interrupt ausgelöst wird, dann fragst du alle MCP23017 einzeln ab, ob ein Interrupt ausgelöst wurde. Das kostet natürlich einige Zeit und problematisch wird es, wenn in der Zeit weitere Interrupts ausgelöst werden.
      2) Wie 1) nur, dass die eine Hälfte an Pin 2 und die andere an Pin 3 hängt. Dann brauchst du nur die Hälfte der Zeit für die Abfrage.
      3) Du nutzt Pin Change Interrupts auf dem Nano:
      https://wolles-elektronikkiste.de/interrupts-teil-2-pin-change-interrupts
      Sie sind nicht so komfortabel wie die externen Interrupts an Pin 2 und Pin 3, aber so könntest du jede Interruptleitung an einen eigenen Pin hängen.
      4) Du verzichtest auf Interrupts auf der Arduinoseite, hängst alle Interruptleitungen an einen eigenen Pin und fragst die Pins mit digitalRead() permanent ab.
      5) Du nimmst als 9. MCP einen MCP23S17 (der wird über SPI angesteuert). An den hängst du alle Interruptleitungen an individuelle Pins. Dann erzeugt ein Interrupt an einem MCP23107 einen Interrupt am MCP23S17, der wiederum einen Interrupt am Arduino erzeugt. Am MCP23S17 kannst du dann auslesen welcher MCP23017 der „Schuldige“ war.
      Mehr fällt mir im Moment nicht ein.
      VG, Wolfgang

      Mehr fällt mir im Moment nicht ein.

  3. Hallo Wolfgang,
    super Beschreibung! Echt toll welche Mühe du dir gegeben hast.
    Hast du eine Idee, warum bei mir Init fehlschlägt. Ich habe in eine Ausgabe in deine Init-Routine eingebaut.

    setIntCon(0b10101010, A);
    if( ( res = readMCP23017(INTCONA)) != 0b10101010){
    Serial.print („readMCP23017 failed „);
    Serial.println (res, BIN);
    return false;
    }

    als Ergebnis bekomme ich „readMCP23017 failed 11111111“

    der I2C scheint i.O., da mein I2C Scanner den MCP23017 findet.

    1. Dass der I2C Scanner den MCP23017 findet, ist ja schon mal gut und schließt einige Fehler aus. Wenn das Beschreiben bzw. Lesen des INTCONA Registers nicht klappt, dann „fühlt sich“ der MCP23017 nicht angesprochen. Ich würde am ehesten vermuten, dass etwas bei der Objekterstellung im Argen liegt. Nutzt du diese Methode:
      MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN); ?
      Wenn ja, ist auch die richtige Adresse definiert (ich schätze, das hast du als Erstes geprüft, ich frage aber trotzdem sicherheitshalber)? Ist der Resetpin des MCP23017 richtig mit dem ausgewählten Pin des Mikrocontrollers verbunden? Oder hast du einen Resetpin größer/gleich 99 gewählt? In diesem Fall müsste der Resetpin des MCP23017 mit VCC bzw. HIGH verbunden sein.
      Wenn du eine andere Methode zur Objekterstellung gewählt hat, dann teile mir bitte mit, um welche es sich handelt.
      Und welchen Mikrocontroller / welches Board nutzt du?

      1. Der Restpin liegt auf HGH. Alles „hinter“ dem MCP23017 ist fest verlötet. Nur die Spannungsversorgung und der I2C gehen via Kabel über ein Breadbaord an einen ESP32. Am I2C hängt auch noch ein LCD Display, welches klaglos funktioniert. Der MCP war permanent mit 5V über den ESP versorgt. Die Verbindung habe ich jetzt kurz unterbrochen und den ESP neu gestartet. Jetzt geht alles wie gewünscht. Wenn ich deinen Code richtig verstehe, wird beim „Init“ und RestPin >= 99 „softReset“ aufgerufen. Ich vermute mal, dass das den MCP in einen definierten Zustand bringen soll. Kann was schiefgehen? Mein „Stromentzug“ führt doch auch zu einem Reset, oder nicht?

        1. Genau, softReset soll für einen definierten Ausgangszustand sorgen. Sonst hat man u. U. das Problem, dass man mit dem Code herumspielt, eine neue Version hochlädt, aber die Einstellungen im MCP aus dem vorherigen Programmlauf erbt. Da kann eigentlich nichts schief gehen und wie du schon schreibst, führt auch eine Stromunterbrechung zum Reset.

          Ich habe eben noch einmal folgenden Sketch auf einem ESP32-WROOM-32 Entwicklungsboard folgenden Sketch getestet:

          #include <Wire.h>
          #include <MCP23017.h>
          #define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
          #define RESET_PIN 99  
          
          MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);
          
          void setup(){ 
            Serial.begin(9600);
            Wire.begin();
            if(!myMCP.Init()){
              Serial.println("Not connected!");
              while(1){} 
            }
            else{
              Serial.println("connected");
            }
          }
          
          void loop(){} 
          

          Resetpin des MCP auf High, Adresspins alle auf LOW. Er gibt „connected“ aus, ganz so wie er soll. Ist also die Frage, was bei dir anders ist. Das Display sollte nicht stören, sofern es eine andere Adresse hat.

          Du könntest mir mal deinen Sketch schicken (Wolfgang.Ewald@wolles-elektronikkiste.de). Vielleicht fällt mir da noch irgendetwas auf.

          1. Mein Code sieht – bis auf die Adresse – genauso aus. Und er läuft im Moment. Irgendwie ist der MCP in einen Zustand gekommen, wo „softReset“ nicht mehr gewirkt hat und nur noch „Stromentzug“ half.
            Momentan ist RESET bei mir fest auf 5V verlötet. Wenn das Problem noch mal auftaucht, werde ich zum Lötkolben greifen und RESET eine eigene Leitung zum ESP spendieren. Ist vielleicht das der sichere „Reset“.
            Siehst du eine Chance in deinem Code zu prüfen, ob der softReset erfolgreich war? Müssen bestimmte Register einen bestimmten Wert haben, wenn der MCP erfolgreich zurückgesetzt wurde?

            1. Die Registereinstellungen nach dem softReset sind dieselben, wie nach einem Hardwarereset bzw. Neustart. Eigentlich ist gezielte Modifizierung von INTCONA schon Test genug, ob der MCP23017 erreichbar und die Register beschreibbar sind. Ich wüsste nicht, warum das klappen und softReset nicht klappen sollte. Du könntest mit:

              uint16_t regSum = 0; 
              for(int i = 0; i<21; i++){
                regSum += readMCP23017(i);
              }
              Serial.println(regSum);
              

              hinter softReset() prüfen, ob die regSum 510 ist. Jeweils 255 für die die IODIR Register.
              Oder am Ende von init, da sind es dann 526 (jeweils 8 für die IOCON Register kommen hinzu).

  4. Hallo Wolfgang,

    wie kommst Du auf:

    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!

    Ich kann bei microchip keinen Hinweis darauf finden.
    In den Datenblätter ist auch nichts vermerkt.

    Mit freundlichen Grüßen
    Wolfgang Werner

    1. Hallo Wolfgang,

      hier findest du das aktuelle Datenblatt:
      https://www.microchip.com/en-us/product/mcp23017

      Auf der ersten Seite findest du den Hinweis zu GPA7/GPB7 in der Beschreibung sowie das von mir abgebildete Pinoutschema. Ich hatte schon gehofft, Mikrochip hätte es wieder rückgängig gemacht, ist aber nicht so.

      VG, Wolfgang

  5. Hallo Wolfang,

    welch ein Glück habe ich noch die Chips vor 2022.

    Allerdings habe ich ein kleines Software Problem.

    In meiner Verschaltung sind insgesamt 6 MCP23017 verbaut, eine siebter wird noch hinzu kommen.
    Wenn ich nun die Adresse 0x26 vergebe und entsprechend die Adresse einstelle sagt mir die Software 0x26 not available.
    Ich habe verschiedene MCP23017 getestet und immer ergibt sich das gleiche Problem, alle anderen Adressen werden gefunden.

    Hatte schonmal jemand das Problem mit der Adresse 0x26?

    Grüße
    Tobi

    1. Hi, das Problem ist mir nicht bekannt. Muss ich mal ausprobieren. D.h. also:

      0x26: A2 = HIGH, A1 = HIGH, A0 = LOW geht nicht, und
      0x27: A2 = HIGH, A1 = HIGH, A0 = HIGH geht dann wieder?
      Schon sehr merkwürdig! Vielleicht etwas ganz profanes wie ein kaputtes Kabel?

      Da du ja mehrere MCP23017 probiert hast, dürfte es an denen nicht liegen.

      Und wenn du mal einen I2C Scanner drauf los lässt, findet der eine andere Adresse? Oder gar keine? Einen I2C Scanner gibt’s z.B. hier:
      https://wolles-elektronikkiste.de/i2c-scanner

      1. Vielen Dank für die schnelle Antwort.

        Ich war schlicht zu doof und habe A0 mit A3 vertauscht…
        Adresse 0x26 funktioniert einwandfrei 🙂

        1. Ich bin immer wieder froh zu hören, dass ich nicht der einzige bin dem solche Sachen passieren.

  6. Sorry für die wahrscheinlich blöde Frage, bin Anfänger:
    Kann man an einem Portexpander ein I2S Mikrofon (z.B. inmp441) oder ein I2S Amplfifier (MAX98357A) anschließen? Falls ja, wie geht das, insbesondere die Pinzuweisung? Habe es nur mit LEDs und Buttons hinbekommen. Benutze einen esp32. Danke für einen Hinweis.

    1. Hier darf man alle Fragen stellen! Du kannst nicht „einfach so“ den I2S Bus unterbrechen, in dem du einen MCP23017 dazwischen setzt. Wenn es überhaupt möglich sein sollte, dann wäre das ein ziemlich kompliziertes Unterfangen. Aber wahrscheinlich geht das schon wegen der benötigte Geschwindigkeit nicht.

      Warum willst du den MCP23017 überhaupt benutzen? Weil du mehrere I2S Bauteile hast? Ich habe nicht viel Erfahrung mit I2S, aber so wie ich das sehe, kann man mehrere I2S Bauteile mit einem I2S Bus betreiben.

  7. Hiho,

    will jetzt einen 23017 einsetzen (hab zum Glück noch welche in der Schublade, die Preise sind ja sportlich aktuell^^) und bin dabei hier über den Artikel gestolpert. Vielen Dank dafür, wird mir meine Arbeit deutlich erleichtern 🙂

    Hab allerdings auch den hier gefunden:
    https://hackaday.com/2023/02/03/mcp23017-went-through-shortage-hell-lost-two-inputs/
    anscheinend sollte man als Eingang den achten Pin je nicht nutzen, da (aus den Kommentaren):
    „It cannot handle pulsing on these inputs as it scrambles the i2c communication“
    . Hab mich aufgrund dessen jetzt dazu entschieden nur 14 und nicht 16 Button an einem anzubinden, um mir den möglichen Ärger im Dauerbetrieb entsprechend zu ersparen^^. Vielleicht ist es ja auch noch für jemand anders hilfreich.

    Gruß

    1. Hi, der Hinweis ist äußerst wertvoll. Und er bereitet mir so richtig schlechte Laune. Was für eine blöde Idee! Will sich Microchip für das misslungendste Produktupdate bewerben? Ich weise jetzt im Beitrag auf die neue Version hin.

  8. Hallo Wolfgang,
    vielen Dank für deine sehr schön beschriebene Anleitung! Ich habe einmal versucht das ein oder andere zu probieren aber es funktioniert nicht wirklich. Ich arbeite (hier im Versuch) mit einen ESP32 Pico Kit V4. Eigentlich suche ich für mein Projekt eine Variante mit Interrupt um Schalter einzulesen. Dein Beispiel funktioniert (OnChange) aber die 8 Schalter werden nur „zufällig“ mit der richtigen Nummer eingelesen – häufig erkennt der ESP (egal welchen der acht Schalter ich betätige) den an Port A0.
    Meine Frage: in vielen Tutorials ist immer #include angegeben aber im Setup() finde ich fast nie Wire.begin()…….ist das korrekt? Ich nutze die default I2C Pins (SDA, SCL 21,22). Nur wenn ich das im Setup einfüge läuft überhaupt etwas mit I2C.
    Irgendwo habe ich gelesen, dass die Wire Library sich mit den Interrupt vom MCP nicht „verträgt“. Kannst du das bestätigen und-/ oder beginnt deine I2C Verbindung mit mcp.begin()?

    Danke, André

    1. Hallo André,

      in einigen Bibliotheken wird Wire.h von den Bibliotheksdateien aus eingebunden und auch Wire.begin() aufgerufen. Dann muss man das nicht im Beispielsketch tun. Da meine Bibliothek auch für den „SPI-Bruder“ MCP23S17 geschrieben ist, macht das hier keinen Sinn. Aber egal, wie das gelöst ist, sollte da irgendetwas fehlen, dann würde gar nichts gehen.

      Irgendwo habe ich gelesen, dass die Wire Library sich mit den Interrupt vom MCP nicht „verträgt“.“ — Da hat jemand Unsinn geschrieben. Die Wire Library „weiß nicht“ wie der Interrupt des MCP23017 funktioniert.

      …oder beginnt deine I2C Verbindung mit mcp.begin()?“ Die Frage verstehe ich nicht. Wir reden doch von meinem Beispielsktech mcp23017_interrupt_on_change.ino, richtig? Und da findest du in der Version im Beitrag in Zeile 22 Wire.begin(). In der Version, die du mit der Bibliothek installierst, ist es in Zeile 37 (weil da noch ein bisschen Kommentar – Blabla drinsteht). Immer im Setup.

      Kannst du bestätigen, dass du den originalen, unveränderten Sketch getestet hast? Ohne Wire.begin() herauszunehmen oder hinzuzufügen?
      Falls du etwas verändert hast -was hat du verändert?
      Du sprichst von Schaltern – hast du tatsächlich Schalter verwendet? Oder Taster?
      Haben die Leitungen zwischen Taster (bzw. Schalter) und dem MCP23017 Pull-Down Widerstände (so wie bei mir)?

      VG, Wolfgang

      1. Hallo Wolfgang,

        danke für die schnelle Antwort. Ich habe Wire.h() tatsächlich übersehen….. Ich nutze genau diesen Sketch, mußte aber die Ports A und B tauschen weil ich das auf einer Lochrasterkarte aufgelötet habe und der Port A feste Pullup (10k) hat. Der Port B ist flexibel und ohne Widerstände. Ich habe dort jetzt die Pins als Ausgang (LED) definiert. An Port A hängen (natürlich) Taster. Es läuft zur Zeit alles gut aber ich bekomme die LED noch nicht zum leuchten…….nicht einmal beim booten (da sollten diese ja einmal für eine Sekunde angehen. Ich denke aber, ich finde den Fehler…….(LED an Port B sind mit Widerständen an GND.

        André

  9. Erst einmal Danke für die sehr gute Beschreibung des Schweizer Messers. Ich möchte Deine library nutzen, die von Adafruit gefällt mir nicht und der Standard mit „wire.beginTransmission.. ist mir zu sperrig.
    Das blöde ist, Dein Beispiel-Sketch, der als Gerippe sehr gut aussieht, steigt gleich mit einem SoftWDT-Reset aus:
    kryptische Zeichen plus Not connected!
    Ich benutzte NodeMCU LUA Amica, I2C an D1,D2. Ein OLED-Display an I2C zur Kontrolle angestöpselt funktioniert.
    Reset-PIN liegt auf VDD und Port ist 99. PullUp-Widerstände an SDA und SCL bringen keine Änderung.
    Für einen Tipp wäre ich dankbar.

    1. Eigenartig, ich habe es gerade mal mit dem Board probiert, da ich es vorher noch nicht ausprobiert hatte. Als Board habe ich NodeMCU 1.0(ESP-12E Module) eingestellt ohne Änderung der Voreinstellungen. Als Sketch kam mcp23017_basic_input_output.ino zum Einsatz. Einzige Änderung im Sketch: Reset Pin auf 99.

      Verbindungen:
      D1 – SCL
      D2 – SDA
      GND – GND
      3V – VCC
      Adresspins: alle an GND.
      GPIOs – an zwei LED Bars

      Eigentlich kann man nicht viel anders machen, außer sich bei den Anschlüssen zu vertun. Du könntest auch mal einen I2C Scanner Sketch ausprobieren:

      https://wolles-elektronikkiste.de/i2c-scanner

      Mer fällt mir leider nicht ein.

      1. Danke für die prompte Antwort.
        Ich habe es auch mitNodeMCU1.0 probiert, den i2C-Scanner hatte ich als erstes geladen,
        der hat mir immer nur mein Display angezeigt, 3 verschiedene MCPs ausprobiert.
        Immer das selbe Ergebnis.
        Dann das Breadbord getauscht und es lief.
        Es gibt schon üble Fehler…

        1. Das stimmt. Ich hatte mal ein defektes Breadboardkabel und habe stundenlang den Fehler gesucht – überall, nur nicht dort!

  10. Hallo Stefan oder Wolfgang

    Danke für den Hinweis! Vielleicht erklärt das mein Phänomen:
    Ich habe 2 MCP23017 angeschlossen am ESP32. Für beide habe ich alle Ports Ab, also 4x den Interrupt definiert. Mit SW-Reset 99. Ohne Widerstände.

    FEHLER: Es werden ca 20-40 Interrupt gelesen, dann von 1 oder 2 Ports nichts mehr gemeldet (stumm).

    Hast du oder Stefan einen Tipp? Danke

    1. Hallo Pierre, wenn mit Interrupts etwas schiefgeht, dann meistens weil mit dem Timing: Warte auf einen Interrupt -> Interrupt tritt ein -> Aktionen -> Interrupt wieder scharf etwas nicht stimmt. Das ist sehr schwer aus der Ferne zu beurteilen. Wenn du möchtest, dann kannst du mir deinen Code an wolfgang.ewald@wolles-elektronikkiste.de senden. Dann schaue ich mal, ob mir irgendetwas auffällt. Was sich ansonsten bei der Fehlersuche immer wieder bewährt hat ist Reduktion. Ich würde zum Beispiel mal nur mit einem MCP23017 arbeiten und schauen, ob die Dinge dann auch irgendwann aus dem Ruder laufen.

  11. Hallo Wolfgang,
    vorab erst mal ein großes Lob an deine gigantische Arbeit. Ich bin gerade ein wenig am ESP32 mit Touch und anderen Sachen, so dass mir die Pin’s ausgehen. Zuerst habe ich den PCF8575 in mein Programm aufgenommen. Da ich keinen vor Ort hatte, habe ich das Programm soweit geschrieben und auf den ESP32 hoch geladen. Der ESP macht jedoch keinen Mucks mehr. Wenn ich den PCF rausgeworfen habe, dann geht es wieder.
    Daraufhin habe ich mir den MCP23017 genommen, jedoch mit selbigen Ergebnis. Er zeigt nicht den fehlenden Baustein. Leider nichts. Hatte ihn ohne Reset-Pin definiert.

    Hoffentlich hast Du eine Lösung.

    Danke und viele Grüße

    Uli

    1. Hallo Uli,

      ich habe noch keine konkrete Idee, woran es liegen könnte. Zumindest scheint es ja auf der Programm- und nicht auf der Hardwareseite zu sein. Magst du mir deinen Sketch (Version mit MCP23017) mal zusenden? An: wolfgang.ewald@wolles-elektronikkiste.de. Vielleicht finde ich ja was.
      VG, Wolfgang

  12. Hallo Wolfgang,
    toller Artikel! Ich möchte mit einem D1 Mini und einem MCP23017 eine 8fach Relaiskarte ansteuern.
    Die Eingänge der Relaiskarte sind Lowaktiv (Kathode vom Optokoppler auf der Karte).
    Somit setze ich bei mir im Programm (mit ESPHome in HomeAssistant) die Ausgänge auf output und inverted auf true. Soweit funktioniert das auch sehr gut, aber beim Einschalten scheint der 23017 einen kurzen Low Impuls rauszugeben wenn ein Ausgang initialisiert wird (die Relais klicken kurz vom ersten bis achten, wie ein schnelles Lauflicht).
    Ich habe schon überlegt die Spannungsversorgung der Relaiskarte über einen D1 Mini GPIO erst nach einer gewissen Zeit zu aktivieren um das Verhalten beim Start zu verhindern – das ist aber irgendwie … Pfusch 🙂
    Vielleicht Kannst du mir einen Lösungstipp geben?
    LG Sascha

    1. Hallo Sascha,

      definiere den Reset Pin mal auf 99:
      #define RESET_PIN 99
      und verbinde den Reset Pin des MCP23017 mit VCC.

      In init() wird ein Reset ausgeführt. Entweder ein Hardware-Reset (Standard) oder ein Software-Reset (wenn RESET PIN größer/gleich 99). Der Software-Reset ist schneller. Wenn du Glück hast, dann ist der MCP23017 damit durch, bevor deine Relaiskarte bereit ist. Viel Hoffnung habe ich allerdings nicht.

      Dann könntest du noch probieren, Pull-Up Widerstände an den Ausgängen des MCP23017 zu installieren. Ist aber keine schöne Lösung.

      Sonst fällt mir auch nur die Einschaltverzögerung für die Relaiskarte ein.

      VG, Wolfgang

      Dann wird anstelle

      1. Hi Wolfgang,
        Danke für deine Antwort.
        Da ich keinen Einfluss auf die Initialisierung habe (ESPhome unter HomeAssistant bietet keine weiteren Befehlsmöglichkeiten – ich müsste in den Librarycode gehen) habe ich das ganze jetzt quasi über einen Board-Enable folgendermaßen gelöst:
        Version 1: die Versorgungsspannung der OPVs auf der Relaisplatine wird über einen separaten Pin versorgt (zusätzlich zur Relaisspannung). Diese VSS habe ich nun abgekoppelt und an den SPIO D1 von meinem D1 Mini gehängt. Somit kann ich über meinen Code den D1 schalten und damit die OPVs freigeben und dadurch das Flattern beim Booten/Init verhindern. Die OPVs ziehen nur max 3,4mA also kein Problem für den D1
        Einzige Kurx daran – VSS ist dann 3,3V und nicht 5V – lief bisher aber problemlos. Ich werde noch die StatusLED überbrücken, dann ist der Eingangsstrom des OPVs wie bei 5V
        Version 2: wie V1 aber die OPVs 2-8 erhalten ihre VSS über das Netzteil. Die Spannung wird aber über das Relais 1 erst durchgeschaltet. D.h. mein D1 Mini schaltet das erste Relais (dessen OPV vom D1 GPIO versorgt wird) und über den Relaiskontakt bekommen die anderen sieben OPVs ihre VSS. Vorteil zu V1 – die restlichen OPVs laufen mit 5V vom Netzteil.

        LG Sascha

  13. Hallo Wolfgang,
    vielen Dank für den ausführlichen Artikel und deine Bibliothek!
    Eine Anmerkung habe ich:
    Wenn jemand wie ich den Reset Pin nicht anschließt, resultiert das in einem ziemlich instabilen Betrieb (an D1 Mini, Spannungsversorgung 3.3V statt 5). Habe Stunden lang nach Fehler in der Schaltung gesucht, da ich einen ziemlich komplizierten Aufbau habe (weitere 4 Module noch an I²C angeschlossen)… Pull-Ups bei SCL/SDA sind egal, aber für den Reset Pin definitiv benötigt! Ich habe einen 4,7 kΩ Wiederstand verwendet (möglicherweise nicht benötigt und kann direkt an + angeschlossen werden), damit war der Spuk beseitigt. Der Reset-Pin sollte also auf keinen Fall floating bleiben.

    Viele Grüße,
    Stefan

  14. Moingsen zusammen.
    Schön das es eine Erweiterung der Bibliothek gibt.
    Mich interessiert in diesem Zusammenhang, wie schreibt/entwickelt man eigentlich eine Bibliothek?
    Ich _nehme-an_ mit Hilfe des Datenblattes, in dem ja entsprechende Pegelstände/Signale beschrieben werden, nur reicht es mit meinem Wissen nicht weiter.
    Vielleicht hast du ja mal Lust darüber etwas zu schreiben, da ich mit jetzigem Stand zwar Bibliotheken verwenden kann, wäre mal selbst eine zu schreiben eine spannende Sache.
    Feiertags-grüße,
    Xaver

    1. Hallo Xaver,

      ein Kommentar reicht für eine Anleitung natürlich nicht aus, deswegen ist das mit einem Beitrag darüber eine gute Idee. Die kurze Version ist: im Prinzip sind die Bauteile wie Schalttafeln. Dabei sind die Register die Schalter oder Anzeigetafeln. Um Einstellungen vorzunehmen, muss man bestmmte Bits setzen oder löschen. Dafür schreibt man Funktionen und versucht, ihnen verständliche Namen zu geben. Die Herausforderung liegt darin, dass die meisten Datenblätter schwer lesbar sind und dass manche Einstellungen mit anderen nicht kompatibel sind. Das ist die wirklich sehr vereinfachte Darstellung!

      VG, Wolfgang

  15. Hallo Wolle,

    ich benutze gerade deine lib zusammen mit einem TCA i2c multiplexer und hatte gehofft, dass ich nur eine MCP23017 Klasse erstellen müsste und ich glaube hier liegt der Hund begraben. Ich bin leider kein gelernter ITler und möchte meine Unzulänglichkeiten im Voraus entschuldigen. 😉
    Alle MCPs sind gleich konfiguriert mit
    uint8_t portConfigA=0b11111111; //1 – Output, 0 – Input
    uint8_t portConfigB=0b00000000; //1 – Output, 0 – Input
    Ich scanne durch meine TCA Kanäle durch und schiebe jedes mal
    myMCP.setPortMode(portConfigA, A, INPUT_PULLUP);
    myMCP.setPortMode(portConfigB, B, INPUT_PULLUP);
    hinterher, wenn ich einen MCP am Kanal finde. Die Adressen aller MCPs sind default (0x20).
    Wenn ich aber jetzt vom MCP an Kanal 4 die Pins 0-3 (j) auf high setze mit myMCP.setPin(j,A,1); und dann die Pins 4-7 auf Kanal 5 auf high setze, sind danach alle Pins am MCP auf Kanal 5 auf high, er setzt Pins 0-3 bei Kanal 5 auf High sobald Pin 4 auf high gesetzt wird, wenn ich das richtig mitbekomme.
    Wenn ich danach alle Pins wieder auf low setzen möchte, setzt er zuerst 0-3 auf Kanal 4 low ABER 4-7 auf Kanal 4 HIGH ?!, dann 4-7 auf Kanal 5. Im nächsten loop bemerkt er dann, huch hier sind aber auf Kanal 4 noch welche high, die müssten low sein und korrigiert das Ganze.

    Meine Vermutung ist, dass bei der setPin Funktion im Speicher noch der Zustand der anderen Pins des myMCP Objekts vorhanden ist und daher in Wirklichkeit nicht ein einziger Pin gesetzt wird, sondern ein ganzes Byte gesendet wird mit nur einem veränderten Bit.
    Ist die Annahme korrekt? Hast du einen Vorschlag, wie ich das Problem lösen kann, ohne mehrere MCP Objekte anzulegen?

    Hier meine update Funktion, ich hoffe das ist einigermaßen verständlich.

    void update_IOext() {
    byte portStatus=0;
    uint8_t numIOext=0;
    //loop through all tca
    for (byte tca=0;tca=0x70)&&(tcaI2cAddr[tca]<0x78)) {
    for (int i=0;i<8;i++) {
    //check multiplexer config, address only channels with IOext(3)
    if (i2cDev[tca][i]==IOext) {
    set_TCA_Channel(tca,i); //select active channel (i) of multiplexer (tca)
    portStatus=myMCP.getPort(A); //read portstatus of A
    //compare bitwise dOc with portStatus, change portStatus if not equal
    for (int j=0;j<8;j++) {
    bool bit1=bitRead(portStatus,j);
    bool bit2=dOc[tca][8*numIOext+j];
    if (bit2!=bit1) {
    Serial.print("Pin ");
    Serial.print(j);
    Serial.print(" auf tca #");
    Serial.print(tca);
    Serial.print(" an Kanal ");
    Serial.print(i);
    Serial.print(" wird von Wert ");
    Serial.print(bit1);
    Serial.print(" auf Wert ");
    Serial.print(bit2);
    Serial.println(" gesetzt");
    myMCP.setPin(j,A,bit2);
    }
    }
    portStatus=myMCP.getPort(B); //read portStatus of B
    //compare bitwise dIc with portStatus, change dIc if not equal
    for (int j=0;j<8;j++) {
    bool bit1=bitRead(portStatus,j);
    bool bit2=dIc[tca][8*numIOext+j];
    if (bit2!=bit1) {
    dIc[tca][8*numIOext+j]=bit1;
    }
    }
    numIOext++; //loop through IOext on multiplexer #tca
    }
    }
    }
    //loop through all channels
    }
    }

    1. Hallo Daniel,
      deine Vermutung ist richtig. Das MCP Objekt speichert den Zustand (HIGH/LOW) der Pins intern in der Variable gpioA bzw gpioB. Der Grundzustand ist das alles LOW ist, d.h. gpioA = 0b00000000 (ich schreibe das immer mit allen Nullen). Jetzt setzt du auf Kanal 4 die Pins 0-3 auf HIGH. Dann wird zunächst gpioA = 0b00001111. Und dann wird gpioA in das enentsprechende Register des aktiven MCP (also 4) geschrieben. Noch alles gut soweit. Jetzt gehst du einen Kanal weiter und möchtest 4-7 HIGH setzen. Dann erfolgt keine Abfrage des tatsächlichen Zustands des aktiven MCP, sondern es wird einfach gpioA = 11111111; und dann in das Register geschrieben.

      Um das zu verhindern müsstest du jeweils den ganzen Port beschreiben. D.h., um in deinem Beispiel zu bleiben, für Kanal 4: myMCP.setPort(0b00001111, A) und für Kanal 5: myMCP.setPort(0b11110000, A). Um nicht „Buch führen“ zu müssen gibt es noch eine andere Möglichkeit. D.h. wenn du in Kanal 5 die Pins 4-7 auf HIGH setzen möchtest und 0-3 sollen unverändert bleiben, du weißt aber nicht mehr was du zuvor für 0-3 eingestellt hast, dann würde das so aussehen:
      byte portStatusA = 0; //Initialisierung
      portStatusA = getPort(A);
      portStatusA |= (1<<4); // setzt nur Bit 4
      portStatusA |= (1<<5); // setzt nur Bit 5
      portStatusA |= (1<<6); // setzt nur Bit 6
      portStatusA |= (1<<7); // setzt nur Bit 7
      myMCP.setPort(portStatusA, A);

      Oder kürzer:
      portStatusA |= (1<<4) | (1<<5) | (1<<6) | (1<<7);

      Oder: über eine for – Schleife.

      Diese Binäroperationen mit „<<“ sind etwas gewöhnungsbedürftig, aber überaus hilfreich. Hier gibt es eine Einführung:
      https://wolles-elektronikkiste.de/binaerlogik-und-portmanipulation
      Hoffe das hilft!
      VG, Wolfgang

      1. Moin,

        danke für die fixe Antwort! Ich hatte mich in der Zwischenzeit mit deiner Lib beschäftigt und mich mal im source code eingelesen (mein erstes Mal) und dort meine Vermutung bestätigt gefunden, wie du auch schon erklärt hast. Ich habe dann aus meinem bool array jeweils ein byte gebaut und setPort benutzt, wie du auch schon vorgeschlagen hast. Funktioniert einwandfrei!
        Ich hatte zuerst nur die setPin Funktion genutzt, weil ich gehofft habe, dadurch weniger traffic zu verursachen, aber es ist ja sogar das Gegenteil der Fall gewesen.
        Ich benutze in meinem ganzen Sketch übrigens sowohl einen TCA als auch den ADS1115 mit der Lib von dir, wie ich gerade gemerkt habe. Vielen Dank für die tollen Tutorials!

  16. Hallo Wolfgang,

    ich schließe mich der allgemeinen Meinung, dass dies hier ein toller Beitrag ist, voll an. Großes Lob!
    Meine ersten Versuche habe ich nun mit dem MCP23017 an einem Arduino Mega 2560 R3 erfolgreich gemacht.
    Ich mache gerade eine Steuerung für Gleise und Blockstrecken an meiner Modellbahn. Da sind viele Eingänge und Ausgänge nötig, mehr als die 32 Digitaleingänge des Mega 2560. Bei den Eingängen hat mich die Erfahrung gelehrt, dass die Signale von Wippschaltern entprellt werden müssen. Kann man die Bounce2-Bibliothek hier verwenden? Die ist sehr gut und elegant einbindbar. Allerdings kann sie nach meinem Wissen nur für Hardware-PINs verwendet werden. Gibt es für den MCP23017 auch so eine elegante Lösung?

    Schon einmal im Voraus herzlichen Dank für Deine Gedanken.

    Beste Grüße

    Bernhard

    1. Hallo Bernhard,
      erstmal Danke für das Lob. Ich mache das mit dem Entprellen normalerweise ganz pragmatisch. Wenn ich in einer Schleife Schalterstellungen abfrage und eine Schalterstellung ändert sich, dann füge ich einfach ein kleines delay ein, dass das Prellen abwartet und das war’s. In den meisten Fällen kann man ein paar Millisekunden Wartezeit tolerieren. Mit der Bounce2 Bibliothek müsste ich mich erst einmal beschäftigen, deshalb kann ich die Frage nicht beantworten.
      VG, Wolfgang

      1. Hallo Wolfgang,

        die Antwort kam schnell. Respekt und danke!

        Das ist eine klare Aussage mit dem Entprellen über ein kurzes delay. Ich wollte es eigentlich vermeiden – gerade, da ich vor kurzem die Vorzüge der Bounce2 kennengelernt habe. Der Code sieht damit einfach eleganter aus. Ob es auch schneller ist, weiß ich (noch) nicht.

        Beste Grüße

        Bernhard

  17. Hallo Wolfgang,
    Danke für die ausführliche Beschreibung.
    Ich möchte über den Baustein eine LED Bar ansteuern (https://de.rs-online.com/web/p/led-displays/2473090).
    Wenn ich das Datenblatt richtig verstehe würde ich aber mit der LED Bar aber den max. Strom des Bausteins überschreiten. Hast Du eine Idee, wie man dieses Problem lösen könnte ?

    Gruß,
    Frank

    1. Hallo Frank,
      für meinen Beitrag habe ich ähnliche LED Bars eingesetzt (Kingsbright DC-7G3EWA). Diese haben 7 grüne und 3 rote LEDs, ansonsten ähnliche Werte hinsichtlich des Stromverbrauchs. Der tatsächliche Stromverbrauch lag bei Einsatz von 330 Ohm Vorwiderständen bei 6 mA für die grünen und 6.5mA für roten LEDs. Das ist im Grenzbereich (125 mA an VSS), aber OK. Vielleicht misst du das auch mal nach, falls du ein Multimeter besitzt (lohnende Anschaffung!). Wenn du über dem Grenzwert liegst, könntest du einen höheren Vorwiderstand nehmen. Eventuell kommst du damit unter den Grenzwert und es ist noch hell genug. Wenn das auch nicht ausreicht, dann würde ich die LEDs über Transistoren schalten. Mit einzelnen Transistoren wäre das eine elende Verdrahtungsarbeit. Aber dafür gibt es Transistoren-Arrays, z.B. den UL2803 als IC:
      https://smile.amazon.de/POPESQ%C2%AE-ULN2803-Sockel-Socket-Darlington/dp/B00U4X0IFA/ref=sr_1_5?keywords=uln2803a&qid=1645809849&sr=8-5
      Oder als Modul:
      https://smile.amazon.de/Darlington-Transistormodul-Industrielles-Elektrisches-Darlington-Transistor-Arrays-Modul-I2C-Schnittstelle/dp/B08FYS6BXH/ref=sr_1_16?keywords=uln2803a&qid=1645810320&sr=8-16
      Viel Erfolg!
      VG, Wolfgang

    2. Hallo Frank,
      eine späte weitere Antwort: Du könntest den MCP23018 einsetzen. Der kann bis zu 400 mA Gesamtstrom an VSS verarbeiten. Ich habe die Bibliothek gerade für den MCP23018 und den MCP23S18 (SPI anstelle I2C) erweitert. Allerdings muss man die Logik umdrehen, da die MCP23x18 ICs nur als Senke benutzt werden können. Ein Beitrag dazu folgt noch.
      VG, Wolfgang

  18. Hallo Wolfgang.
    Du hast eine kleinen Kommentarfehler, der mich ein bischen Nerven gekostet hat.
    Diese Zeile:
    myMCP.setPortMode(B11111101, A); // Port A: all pins are OUTPUT except pin 1
    bezw der Kommentar müsste doch heißen:
    // Port A: all pins are OUTPUT except pin 6

    Mich hatte es ein bischen gewundert, weil mein Schleifendurchlauf zwar eine grundsätzliche Funktionalität der LED zeigte (Anschluss, Adressierung, Verdrahtung), in dem sie ein mal leutete, dann aber eben nicht mehr. Auch bei Aktivierung der ganzen A Bank, nur eben nicht als ich nur A0, A1 und A2 brauchte.
    Jedenfalls funzt es jetzt endlich so bei mir.

    1. Hallo Xaver,
      da ist kein Kommentarfehler. Das Bit 0 eines Bytes steht generell rechts, das Bit 7 links. A0 ist Bit 0 zugeordnet, A1 ist Bit 1 zugeordnet usw. Alles andere wäre sehr unlogisch. Und 0 ist INPUT und 1 ist OUTPUT.
      VG, Wolfgang

      1. Wolfgang,
        deshalb wundere ich mich ja ….
        dieser Ausdruck:
        myMCP.setPortMode(B11100000, A);
        funktioniert mit diesem Aufruf:
        myMCP.setPinX(1, A, OUTPUT, LOW); //LED 1,gelb, Bank_A; on; (der korrekte Aufruf erfolgt im Wechsel LOW/HIGH bei PIN 1/2)
        myMCP.setPinX(2, A, OUTPUT, HIGH); //LED 1, gruen, Bank_A; on;
        myMCP.setPinX(0, A, OUTPUT, HIGH); //LED 2, rot, Bank_A; on;
        mit dem Aufruf
        myMCP.setPortMode(B00000111, A); (so wie ich deine Anweisung verstanden habe)
        funktionierte es nicht.

        1. Ich weiß nicht, was genau du in welcher Reihenfolge ausführst. Ich glaube aber, da liegt ein Missverständnis vor.
          setPinMode() hat dieselbe Wirkung wie der Arduino pinMode() Befehl, nur dass du den Port mit spezifizieren musst.

          setPortMode() ist setPinMode für einen ganzen Port, also z.B. setPortMode(B11110000,A) = A0-A3 INPUT, A4-A7 OUTPUT.

          setPin() funktioniert wie digitalWrite, nur dass der Port zusätzlich spezifiziert worden ist.

          setPinX() kombiniert setPinMode() und setPin(). Du kannst also OUTPUT/INPUT und HIGH/LOW in einer Anweisung festlegen. Was auch immer du vorher hinsichtlich INPUT/OUTPUT oder HIGH/LOW festgelegt hast, wird überschrieben.

          Um bei deinem Beispiel und Einfachheit halber für nur einen Pin:
          myMCP.setPortMode(B11100000, A); setzt die Pins A5-A7 auf OUTPUT, Rest auf INPUT)

          myMCP.setPinX(1, A, OUTPUT, LOW); setzt PIN A1 auf OUTPUT / HIGH, egal, was du vorher definiert hast. LED an A1 geht an.

          Wenn du dann myMCP.setPortMode(B11100000, A); noch einmal ausführst, dann wird Pin 1 auf INPUT/HIGH gehen. Die LED geht aus. Wenn du hingegen myMCP.setPortMode(B00000111, A); ausführst, ändert sich nichts (für Pin 1).

          Wenn ich dein Problem falsch oder gar nicht verstanden haben, dann sende mir deinen Sketch und schreibe hinter jede Zeile, was du erwarten würdest und was tatsächlich passiert. (wolfgang.ewald@wolles-elektronikkiste.de)

  19. Hallo Wolfgang!

    Auch ich bedanke mich für den tollen Beitrag, der so einiges wesentlich verständlicher macht.

    Dennoch eine Frage: kann man die IO-Ports eines MCP23017 gleichzeitig immer nur als DI oder DO
    oder auch gemischt deklarieren?

    Hintergrund:
    Ich bin dabei, mir einen „Verdrahtungstester“ zu bauen (2×128 Pins) und möchte dabei abwechselnd
    einen einzelnen Ausgang setzen und auf den anderen Pins messen, was zurückkommt. Da hier jeder
    Pin gegen alle anderen Pins kontrolliert werden muss, würde das bei einer gemeinsamen Deklaration
    der Ports in dieser Form ja nicht funktionieren (oder man bräuchte die doppelte Anzahl an IOs…).

    Beste Grüße,
    Herbert

    1. Hallo Herbert,
      mischen ist kein Problem. Mit myMCP.setPortMode(B11110000, A) würdest u z.B. die PortA Pins 0-3 auf INPUT und 4-7 auf OUTPUT setzen. Mit setPinMode( pin, port, mode ) kannst du die Pins auch einzeln konfigurieren.
      Viel Spaß und Erfolg bei deinem Projekt!
      VG, Wolfgang

  20. Hallo Herr Ewald,

    Ich habe an einem D1-Mini 4xPortexpander MCP23017 angeschlossen.
    Ist es möglich, den setPin-Befehl zum Einschalten der Ports irgendwie in eine Schleife einzubinden bzw. aus einem Array auszulesen, um einen bestimmten MCP mit einem bestimmten Port anzusprechen?

    Gruß
    Peter

    1. Hallo Peter,

      ich bin mir nicht sicher, ob ich die Frage richtig verstehe.

      Bei vier MCP23017 würde ich diese als Array definieren:
      for(int i=0; i<4;i++){
      MCP23017 myMCP[i] = MCP23017(0x20 + i);
      }

      Im setup müssen die dann alle intialisiert werden:

      for(int i=0; i<4;i++){
      myMCP[i].Init();
      }

      Und wenn du jetzt z.B. die Pins 3 bis 7 der Ports A und B der MCP23017 1 und 2 anschalten möchtest, dann würdest du schreiben:
      for(int i=1; i<3; i++){
      for(int j=3; j&lt8; i++){
      myMCP(i).setPin(j, A, HIGH);
      myMCP(i).setPin(j, B, HIGH);
      }
      }

      Du kannst das auch noch weiter treiben, da A nichts anderes als 0 ist und B nichts anderes als 1.

      Wenn das die Frage nicht beantwortet, dann melde dich noch einmal.

      VG, Wolfgang

      1. Hallo Wolfgang,

        vielen Dank für die schnelle Antwort,
        dein Denkanstoß hilft mir sehr !

        Ich kann also die ganzen Variablen, myMCP ->[1], 4, A(0) und 1(HIGH) oder 0(LOW) in Arrays setzen,
        und in der richtigen Reihenfolge aufrufen und muss nicht die ganzen 60 setPin-Befehle komplett im Programm schreiben.

        Ich werde das mal testen.

        Vielen lieben Dank für den Denkanstoß

        Gruß
        Peter

  21. Moin Wolle !
    Vielen Dank für diesen tollen Beitrag, die leistungsstarke Bibo sowie die anschaulichen und lauffähigen Sketche.
    Hast mir sehr geholfen, meinem neu erworbenen Portexpander Leben einzuhauchen. Nun kann ich damit weiter an einem Projekt arbeiten, für das ich sehr viele DI-Pins benötige.
    Es ist schön, dass es hier im Netz immer wieder nette und kluge Menschen gibt, die bereit sind, ihr Wissen mit uns weniger Klugen zu teilen 😉 .

    In diesem Sinne – Liebe Grüße & so long
    TOM.

  22. Moingsen nochmal.
    Hier hat sich der Fehlerteufel eingeschlichen ->
    Der MCP23S17
    Der MCP23S17 unterscheidet sich vom MCP23017 nur die Ansteuerung per SPI anstelle von SPI.

    Ich würde sagen, ein SPI zu viel, ein I2C zu wenig?
    Beste Grüße,
    Xaver

  23. Hallo Wolfgang,
    zunächst einmal vielen Dank für Deine stets informativen und hilfreichen Blogbeiträge:-)
    Bitte weiter so.
    Speziell zum 23017 Portexpander hätte ich eine Frage auf die Du evtl. die Antwort weißt:
    in welchem Zustand sind die Ports nach dem Hochfahren, bevor der Baustein in der setup-Routine initialisiert wird?
    Ich möchte mit dem Baustein die Stellung von Schaltern abfragen, die nach Masse schalten oder offen sind. Dazu sollen die Ports als Eingänge mit den pull-up-Widerständen initialisiet werden. Blöd wäre es allerdings, wenn es passieren könnte, daß die Ports noch vor der initialisierung in der Setuproutine (teilweise) als Ausgänge mit high-Level arbeiten. Dann würde ein geschlossener Schalter einen Kurzschluß bedeuten. Insofern wäre ein definierter Power-up Zustand interessant.
    Weißt Du da etwa dazu?
    Viele Grüße
    michael

    1. Hallo Michael,

      wenn du ein wenig hochscrollst, dann findest du eine Tabelle mit den Registern des MCP23017 und in der letzten Spalte steht der Registerwert nach dem Hochfahren bzw. nach einem Reset. Alle Register haben den Wert 0, bis auf IODIRA und IODIRB (Input/Output Direction). Letztere sind mit Einsen beschrieben, beim MCP23017 heißt das aber INPUT. Also um die Frage kurz zu beantworten: alles ist auf Input, Low und die Pull-Ups sind aus.

  24. Grüß dich Wolfgang.
    Von meiner Seite mal ein großes Kompliment für die „Erklärbär“-Funktion, also welche Funktionen es gibt und welche Werte da verwendet werden können. Gerade für Neulinge eine tolle Sache um mal einen Überblick zu bekommen.
    Ich habe bei meinem Arduino-Sketch eine bemerkenswerte Auffälligkeit.
    Erst habe ich zwei LED´s jeweils zwei Stati zugeordnet, die eine leuchtet bei Automatik (menu1) funktionierte. Dann habe ich deine weitere LED leuchten lassen wollen, was auch funktionierte, nun wollen die beiden ersten LED´s nicht mehr leuchten, nur noch ein kurzes aufflackern beim Statuswechsel.
    if (menu <= 1)
    {
    myMCP.setPinX(1, A, OUTPUT, HIGH); //LED 1,gruen, Bank_A; on;
    myMCP.setPinX(0, A, OUTPUT, LOW); //led 0 gelb Bank_A; off;
    }
    else
    {
    myMCP.setPinX(1, A, OUTPUT, LOW); //LED 1, gruen, Bank_A; on;
    myMCP.setPinX(0, A, OUTPUT, HIGH); //led 0, gelb, Bank_A; ON;
    }
    Bei deinem Sketch leuchten alle drei, was zeigt das es kein Kontaktproblem ist. Nun habe ich die Ports von 0 und 1 auf 3 und 4 geändert, jetzt geht es wieder.
    Hast du da eine Erklärung für? Kann das mit einem fehlenden Reset zusammenhängen?
    Beste Grüße,
    Xaver

    1. Hallo Xaver,

      die Init-Funktion setzt eigentlich allen Register in einen definierten Status und funktioniert deswegen wie ein Reset. Kannst du mir mal deinen Sketch zusenden (wolfgang.ewald@wolles-elektronikkiste.de)? Vielleicht fällt mir ja irgendetwas auf.

      VG, Wolfgang

      1. Danke für das Angebot Wolfgang.
        Ich werde erst einmal den Chip tauschen um sicherzustellen das nicht der den Fehler verursacht, zumal der Code auf den anderen Ports ja einwandfrei und wie erwartet funktioniert.
        VG, Xaver

  25. Hallo Wolfgang,

    ich habe mir aus dem MCP23017 und dem 4040 einen I2C-fähigen Zähler gebaut.
    Nun wäre es schön einen Overflow via Interrupt mitzubekommen. Gibt es eine Funktion bzw. Möglichkeit die du empfehlen kannst, bei einem bestimmten Muster einen Interrupt auslösen zu lassen?

    Danke
    Daniel

    1. Hi Daniel,
      d.h. du möchtest einen Interrupt auslösen wenn der 4040 bis zu einer gewissen Zahl gezählt hat, richtig? Was dann bedeutet, dass der 4040 an seinen 12 Ausgängen ein bestimmtes Muster vorliegen hat. Zuerst dachte ich es wäre ganz easy mit dem „Interrupt-on-DefVal-Deviation“ und der Funktion setInterruptOnDefValDevPort( intpins,Port,defvalstate ). Nur ist das Problem, dass der Interrupt nicht ausgelöst wird, wenn defvalstate mit den Pinleveln übereinstimmt, sondern wenn defvalstate nicht übereinstimmt. Das heißt genau die umgekehrte Logik. Das lässt sich aber leider nicht einstellen.
      Mit Interrupts habe ich nur eine vergleichsweise komplexe Lösung. Du könntest du die Chips so verbinden: Q11-B3, Q10-B2, Q9-B1, Q8-B0, Q7-A7,….Q0-A0. Wenn dann z.B. bis dezimal 3782 = B111011000110 zählen willst, dann setzt du erst einmal nur einen Interrupt der auslöst, wenn B3 auf HIGH geht. Dann setzt du den nächsten Interrupt auf B2, dann auf B1. Dann wäre der nächste Interrupt bei A7 (da B0 ja LOW bleibt), usw. Es ist nicht ganz so kompliziert wie es sich anhört. Mit ein paar Shift- und Logikoperationen lässt sich das automatisieren. Der 4040 dabei nur nicht schneller zählen als man für das Auslesen und Neusetzen der Interrupts braucht.
      Die andere Möglichkeit besteht im aktiven Abfragen, also ungefähr so:
      unsigned int limit = 3782;
      if(((myMCP.getPort(B)<<8) + myMCP.getPort(A)) >= limit){….Aktionen….}
      Abfragen ist natürlich weniger elegant als sich per Interrupt informieren zu lassen.
      Hoffe, das hilft.
      VG, Wolfgang

      1. Hallo Wolfgang,

        ich glaube auch das eine SW Lösung sehr komplex ist. Mittlerweile denke ich, dass ein HW Lösung mit 74HC 21 die beste Lösung ist. Ich habe nur Angst, dass die ganzen Interrupt die anderen Anwendung auf dem ESP32 negativ beeinflussen. Deswegen eigentlich auch die Lösung aus MC23017 und 4040, sonst hätte ich fast den Trigger direkt an das Board anschließen können.
        Ein SW Lösung wäre natürlich nur den Q11 anzusehen.

        Gruss
        Daniel

        1. Die Abfrage wäre ja nicht so kompliziert, denn es ist nur eine Zeile. Nur wenn dein Programm mit vielen anderen Dingen beschäftigt ist, dann erfährst du das Erreichen des gewünschten Zählers erst entsprechend spät. Bei Abfragen von nur Q11 bist du andererseits nicht besonders flexibel und kannst nur bis zu 2048 zählen. Aber mir fällt im Moment nichts anderes ein.

          1. Hallo Wolfgang,

            richtig. Das Polling ist natürlich auch möglich, ist auch nicht so ideal.
            Ich werde wohl dann doch ein 74HC21 verwenden und kann immerhin den Wert 4064 mit einem Interrupt erkennen.

            Gruss
            Daniel

  26. Hallo Wolfgang,
    bin sehr erfreut über diesen Artikel. Im Augenblick tüftle ich an einer Möglichkeit, mithilfe eines Arduino – welchen genau, weiß ich noch nicht – ein MIDI-Basspedal mit 30 bzw. 32 Tasten zu planen und bauen. Bisher habe ich immer nur Bauanleitungen für ein 13-Tasten-Bass-Pedal gefunden. Diese passen nämlich genau zu den 14 digitalen Eingängen des Arduinos. Bei 30 bzw. 32 Tasten hört die Welt leider auf. Zumindest hatte ich bis dato noch nichts gefunden.
    Mit dieser Porterweiterung müßte es ja möglich sein, die „restlichen 17 bzw. 19 Tasten“ auch anzusteuern, oder bin ich da auf dem falschen Dampfer unterwegs ?

    MfG Schneidhofer

    1. Hallo, wenn du einfach nur digitale Ein-/Ausgänge braucht, also keine PWM Funktionalität o.ä. dann liegst du mit dem MPC23017 richtig. Der hat 16 Ein-/Ausgänge, d.h. einer würde gerade nicht reichen. Die gute Nachricht lautet aber, dass du die analogen Pins A0 – A6 auch als digitale Eingänge nutzen kannst. Dann würdest du mit einem MCP23017 hinkommen.

      Als Alternative könntest du den Arduino Mega 2560 einsetzen, gibt’s z.B. hier:

      https://smile.amazon.de/Mikrocontroller-ATmega2560-ATMEGA16U2-USB-Kabel-Kompatibel/dp/B01MA5BLQI/ref=sr_1_1_sspa?__mk_de_DE=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=1GQ7R28AQELQA&dchild=1&keywords=arduino+mega+2560&qid=1619708023&sprefix=Arduino+mega%2Caps%2C172&sr=8-1-spons&psc=1&spLa=ZW5jcnlwdGVkUXVhbGlmaWVyPUFYTVo3WldCWlMzOUkmZW5jcnlwdGVkSWQ9QTA1NTM2NTAxOFQ4S1pSMVQ1OTJaJmVuY3J5cHRlZEFkSWQ9QTA2MzUyNzBVMDk1MUJTMFc4U0Ymd2lkZ2V0TmFtZT1zcF9hdGYmYWN0aW9uPWNsaWNrUmVkaXJlY3QmZG9Ob3RMb2dDbGljaz10cnVl

      1. Hallo Wolle,
        ich habe mir deine Bibliothek mal etwas angeschaut.
        Es gibt für Ausgänge eigentlich den Mode Output und OUTPUT_OPEN_DRAIN.
        Für Eingänge INPUT UND INPUT_PULLUP.
        Wenn man bei der Initialisierung diese erweiterten Modi angibt werden diese von deiner Bibliothek nicht berücksichtigt.
        sprich wenn ein Ausgang OUTPUT_OPEN_DRAIN IST, wird die Funktion setPin und setPinX dies nicht berücksichtigen.
        Sollte Init zusammen mit den anderen Methoden diese Modi unterstützen könnte man direkt bei der Initialisierung zum Beispiel INPUTPULLUP oder Opendrainverwenden.
        Übrigens ich finde es toll wie du Dich mit dem I2C-Expander befasst hast und meinen Tipp für die Lösung von Adresskonflikten mit Bausteinen welche selbst zu wenig Adressen anbieten zu lösen.

        Gruß
        Joachim Scheffel

        1. Vielen Dank für den Hinweis – das muss ich mir nochmal in Ruhe anschauen, was ein kleines bisschen dauern kann.

        2. INPUT_PULLUP kann jetzt mit der setPinMode oder setPortMode Funktion verwendet werden. Habe es gerade implementiert.

  27. … achso, eins habe ich noch vergesssen. Deine Projekte und die Dokumentation dazu sind echt toll!! Hut ab, wo nimmst du die Zeit her!? Bin gerne in deinem Archiv und finde immer neue Anregungen zum Arduino Basteln.

    Schöne Grüße
    Enno

    1. Vielen Dank! Ja, das mit der Zeit ist tatsächlich so eine Sache, die Familie ist nicht immer begeistert…

  28. Hallo Wolfgang,
    vielen Dank für deine schnelle Rückmeldung!
    Mußte feststellen, dass die MCP23017 Bibliothek nicht ordnungsgemäß installiert war. Ich habe über die Arduino IDE neu installiert. Die Arduino IDE neu gestartet und alles ist gut!
    Schöne Grüße aus Wilhelmshaven
    Enno

  29. Hallo Wolfgang,

    ich habe deine MCP23017 Bibliothek erfolgreich eingesetzt und es funktionierte wunderbar.
    Jetzt wollte ich etwas am Arduino Programm ändern, aber der Compiler zeigt fatal error: MCP23017.h: No such file or directory compilation terminated.
    Ich arbeite z. Zt. mit der Arduino Version 1.6.4 hier funktioniert die Programmierung der ESP-01 Module sehr gut.
    Was mache ich falsch? ich habe die MCP23017 Bibliothek über Sketch, Include Libary installiert.
    Schöne Grüße aus Wilhelmshaven und vielen Dank für deine Hilfe
    Enno Jürgens

    1. Hallo Enno, ich habe eben nochmal einen Beispielsketch compiliert (Board: ESP8266-01) und es funktionierte. Vielleicht nimmst du auch mal einen unveränderten Beispielsketch? Funktioniert das? Du könntest mir deinen Sketch schicken, dann compiliere ich ihn mal bei mir: wolfgang.ewald@wolles-elektronikkiste.de.

      Wenn du die Bibliothek über die Arduino IDE installiert hast, dann kannst du eigentlich nichts falsch machen. Du könntest aber nochmal schauen, ob sie tatsächlich ordentlich installiert ist. Entweder über die Bibliotheksverwaltung -> installierte Bibliotheken oder über Datei -> Beispiel. Und wenn du sie frisch installiert hast, dann einmal die Arduino IDE neu starten. Viele Grüße in den Norden! Wolfgang

  30. Hallo Wolfgang,

    danke für deine Bibliothek. Dank dieser funktionieren endlich meine Interrupts :-)!

    Eine Frage zum Verständnis bleibt mir dennoch:
    Du bestimmst den gedrückten Pin durch
    eventPin = log(intFlagReg)/log(2);

    Bei log handelt es sich ja um den Logarithmus mit Basis 10, oder?
    Wieso muss man durch log(2) teilen?

    Bei mir kommt der korrekte Pin raus, ich weiss jedoch nicht wieso.
    Per Taschenrechner wäre ja z. Bsp. bei gedrücktem Taster 3 -> intFlagReg = 100

    -> log(100) = 2
    Im Arduino Code funktioniert das so aber nicht, da muss ich (wie du es im Sketch gemacht hast) durch log(2) teilen um den korrekten Pin zu berechnen.

    Ich würde mich über eine Erläuterung freuen :-)!

    1. Hallo Alex,

      die Logarithmusfunfunktion log ist zur Basis e (2,718…), sollte also eigentlich besser ln genannt werden. z.B. Beispiel ist log(100) = 4.605….

      Um bei dem Beispiel Taster 3 (man beginnt bei 0 zu zählen!) zu bleiben: intFlagReg ist dann 0b1000 = 8 und nicht 1000. Man braucht also den Logarithmus zur Basis 2. In anderen Worten: zwei hoch wieviel ist 8? Und da kann (oder muss) man dann Umwege gehen.

      Dabei macht man sich zunutze, dass log(2^a) = a x log(2).

      Wir suchen im Beispiel: 2^x = 8
      log(2^x) = log(8)
      x log(2) = log(8)
      x = log(8) / log(2)

  31. Hallo Wolfgang,
    ich mache meine ersten Schritt mit Arduino und stelle deshalb eventuell dumme Fragen.
    Mein Arduino Mega 2560 liefert leider beim Interrupt-Betrieb selten die richtigen Werte für
    intFlagReg = myMCP.getIntFlag(B); // fast immer 0
    intCapReg = myMCP.getIntCap(B);

    Was kann dafür der Grund sein?

    Ich habe dann versucht die Variablen als globale
    volatile byte …
    anzulegen und die Abfragen in der Interruptroutine zumachen.
    Aber das geht nicht, es wird nie in loop() in if(event == true) abgezweigt.
    Weshalb nicht?
    Darf ich deine Library-Funktionen nicht im der Interruptroutine aufrufen?

    Gruß
    Axel

    1. Hallo Axel,
      ich habe mir den Sketch angeschaut. Deine Vermutung geht schon in die richtige Richtung. Es liegt aber nicht speziell daran, dass du Library Funktionen in der Interrupt Serivs Routine aufrufst, sondern überhaupt man darf generell keine komplexeren Funktionen aufrufen. Interrupt Service Routinen müssen so kurz sein wie möglich. Typischerweise nutzt man sie nur, um einen Schalter umzulegen, also so etwas wie event = true. Und alle in der Interruptroutine verwendeten Variablen müssen als volatile deklariert werden, sonst werden sie verschluckt.

      Interrupts können ganz schön tricky sein. Vor allem hier, weil man ja zwei Interrupts hat, nämlich den auf dem MCP23017 und der, der auf dem Arduino dadurch ausgelöst wird. Da kann es zum Beispiel schnell passieren, dass man, bevor man den Interrupt am Arduino gelöscht hat, schon der nächste am MCP23017 ausgelöst wird. Wenn du ihn dann am Arduino gelöscht hast, wartet der Arduino auf das nächste RISING oder FALLING (je nach Interruptpolung), kann dann aber lange warten, weil der MCP23017 Interrupt ja schon aktiv ist! Ich hoffe, du verstehst was ich meine…

  32. version=1.3.4
    Interrupt handling: Multiple

    ioConA |= (0<<INTPOL); // INTODR
    ioConB |= (0<<INTPOL);

    Or zero does not clear the bit.

    1. Of course this needs to be

      ioConA &= ~(1<<INTPOL);

      to clear the bit.

      I will change this immediately. I must have been VERY tired or distracted when I added this rubbish! Thanks.

  33. Dear Wolfgang,

    I’am using your library for the MCP23017 and first of all let me thank you for your great work on this.
    But I’ve got a little question.
    In the source code in the write and read functions there is an delay(1) of 1 millisecond.
    Can you tell why this delay.
    This is making the library (relative) slow.

    1. Dear Antonie,

      thank you. These delays make no sense. They are leftovers from tests I did with the library when I developed it. Then I simply forgot to take them out! Great that you found this. You find the version without the delays on Github. Please try.

      Best regards, Wolfgang

      1. Dear Wolfgang,

        Thanks for the heads up.
        I’ve removed them myself in the source code and using the MCP23017 on 1.7Mhz on a Teensy 3.5
        With the togglePin function in a loop I get a squarewave of about 5.3 Khz.

        But I also see that in the setPin or togglePin function you always first set the pin or port direction before sending the GPIO data.
        I don’t believe that this is necessary.
        If you remove „writeMCP23017(IODIRA, ioDirA);“ in the togglePin and setPin function you double the speed.

        Best regards, Antonie

  34. Naja, zuallererst dachte ich an das Timing.
    Bezüglich der Ausgänge: Wenn ich ein Rollo runter fahre muss ich 2 Pins setzen (1: Motor an, 2: Richtung runter)
    Ich würde dann nicht die Pins einzeln setzen, sondern vorher bestimmen, wie der Gesamtzustand an einem
    MCP23017 sein muss. So wie ich das sehe, ist das ein gewaltiger Geschwindigkeitsvorteil. Einmal alles schreiben
    geht schneller als zwei Pins nacheinander zu setzen.

    Bezüglich der Eingänge habe ich noch keinen Plan.
    Derzeit polle ich die Eingänge immer alle 10ms, würde das aber ändern zugunsten der IRQ Funktionalität.
    Im Prinzip sind alle 32 Eingänge immer auf HIGH (Pull-UP) und ich würde einen IRQ auslösen, sobald
    mindestens einer auf LOW gegangen ist.
    Dann würde ich alle 32 Eingänge im Abstand von 10ms-20ms pollen und Tastendrücke auswerten.
    (Kann ja sein, dass ein Langklick an einem Eingang erfolgt und währenddessen an einem anderen
    Eingang ein Doppelklick ankommt)
    Sobald alle Klicks abgearbeitet sind und alle Pins auf High liegen, würde wieder auf einen IRQ warten.
    Im Prinzip müsste ich die IRQ Programmierung des MCP23017 nicht ändern, weil ja nach reden Read
    der IRQ gelöscht wird und wenn ich das letzte Mal gelesen habe, sind ja alle Eingänge auf High.
    Theoretisch könnte ich das auch für jeden der beiden MCP23017 getrennt machen.

    Ach ja, im Datenblatt steht, dass der MCP23017 auch 1,7MHz I2C kann, hast Du das mal getestet.
    Oder anders: Wie lange dauert es die beiden Input Register zu lesen?

    Ach ja, die Haussteuerung habe ich wie gesagt seit 2007 und hier findest Du auch ein Bild davon:
    https://wiki.carluccio.de/index.php?title=Licht-_und_Rolladensteuerung

    1. Mit Interrupts zu arbeiten klingt auf jeden Fall besser als ständig zu pollen. Man muss ein bisschen aufpassen mit den Interrupts, damit einem nichts verloren geht. Wenn du das Interrupt Capture Register abfragst, wo der Interrupt stattgefunden hat, löscht das den Interrupt und das System ist sozusagen wieder scharf. Wenn dann ein neuer Interrupt auf dem MCP23017 kommt, aber du hast den Microcontroller noch nicht wieder scharf geschaltet, dann kann was verloren gehen. Deswegen ist die Idee, in der Zwischenzeit zu pollen, eine gute!
      Die Geschwindigkeit der I2C Übertragung habe ich nicht getestet. Viel Erfolg mit dem Projekt!

  35. Hi Dario,
    zu a): auch wenn du mit einem uint_32 arbeitest, musst du ja irgendwann diesen Wert wieder aufspalten, wenn du ihn an die MCP23017 verschickst. Du kannst die dafür natürlich eine Funktion basteln, die das „Aufdröseln“. Also würde ich sagen, es ist reine Geschmackssache.
    zu b): auch hier kann man natürlich die eingesammelten Werte zu einem uint_32 zusammenbasteln. Ob das sinnvoll (einfacher) ist hängt davon ab was du mit den Werten machst. Und ja, du müsstest dann für djeden MCP23017 ein Objekt anlegen. Wenn du nur einen IRQ pro MCP23017 nimmst, musst du die mirror Funktion nehmen, um die beiden Interruptausgänge zusammenzulegen. Und du musst prüfen, ob der Interrupt auf dem Port A oder B war. Kein Problem, aber wollte es erwähnen.
    Array von Objekten möglich: gute Frage, habe ich noch nicht ausprobiert.
    Aber ich muss noch sagen: 4 MCP23017 – also 64 Ein-/Ausgänge, ich bin beeindruckt! Viel Erfolg beim Verkabeln!

  36. Hallo Wolfgang,
    bin soeben auf Deine Seite gestoßen und erstmal 2 Stunden darin versunken.
    Sehr schön geschrieben und sehr ansprechend, Danke.

    Nun meine Frage:
    Ich habe eine Schaltung, die seit 2007 mein Haus steuert.
    Sie besteht aus einem ATMega8, 4x 74HC166, 4x 74HCT595 und 4x ULN2801A.
    Der ATMega liest kontinuierlich den Zustand der 32 Taster ein,
    ermittelt Klick, Doppelklick und Langklick und steuert dann die 32 Ausgänge.

    Das will ich nun netzwerkfähig machen und dabei auch den MCP23017 einsetzen, insbesondere,
    weil ich dann nicht mehr Pollen muss, sondern den IRQ nutzen kann. Außerdem sind es weniger Bauteile.

    Jetzt würde ich 4 MCP23017 nehmen, 2 als Eingang, 2 als Ausgang.
    Was wäre jetzt programmtechnisch die sinnvollste Lösung?
    a) um 32 Ausgänge zu schreiben
    (kann ich ein uint_32 geschickt rausschieben, oder sollte ich zwei uint_16 nehmen?)
    b) um 32 Eingänge zu lesen?
    (Hier würde ich zwei IRQs, einen von jedem MCP23017 nehmen und zwei Objekte anlegen)
    BTW: Geht ein Array von Objekten?

  37. Hallo Wolfgang,
    ich finde den Artikel sehr interessant und würde die Lösung gern benutzen. Dazu habe ich 2 Fragen:
    – ich verwende in meiner Lösung 5 MCP 23017, die mit MCP1….MCP5 benannt sind, mit der Bibliothek „Adafruit_MCP23017.h“ am Arduino mega (MCP4 und 5 laufen als Eingänge). Für MCP4 und 5 würde ich gern deine Interrupt-Lösung einbauen. Kann ich anstatt „myMCP“ MCP4 bzw. MCP5 verwenden und ist dann die Bibliothek „MCP2317.h“ erforderlich?
    -wird der RESET-Eingang verwendet (liegt bei mir fest auf +5V)
    Vielen Dank im Voraus.
    Viele Grüße
    Lutz

    1. Hallo Lutz,

      1) Du bist (abgesehen von Sonderzeichen) völlig frei, wie du die Objekte benennst, die du mithilfe der Bibliotheken erzeugst.
      2) Wenn du die Funktionen aus der MCP23017_WE Bibliothek nutzen willst, musst du auch zwingendermaßen MCP23017.h einbinden. Du würdest schon bei der Objekterzeugung scheitern, wenn du das nicht tust.
      3) In der Initfunktion wird die Resetfunktion einmal aufgerufen. Das habe ich gemacht um einen definierten Zustand zu erzeugen, wenn man seinen Sketch ändert und hochlädt. In dem Fall kann es sein, dass sich der MCP23017 alle vorherigen Einstellungen merkt. Du kannst darauf verzichten und müsstest dann aber evtl. nach dem Hochladen eines neuen Sketches den MCP23017 kurz vom Strom nehmen. Und du musst trotzdem bei der Initialisierung einen Resetpin definieren. Nimm dafür einen ungenutzten Pin. VG, Wolfgang

    2. Hallo, ich habe mir das mit dem reset nochmal angeschaut und eine neue Version der Bibliothek veröffentlicht, bei der man sich aussuchen kann, ob man die Resetfunktion haben will oder nicht. Die Initfunktion enthält Anweisungen, die auch ohne Reset dafür sorgen, dass der MCP23017 bei Programm(neu-)start einen definierten Zustand hat.

      1. Hallo Wolfgang,
        ich habe mal angefangen, zu probieren und bin in folgende Falle getappt: Ich hatte auch die MCP23017-Libary von Bernard Lemasle eingebunden und der hat seine Libary-Datei auch MCP23017.h genannt. Das führte erst mal zu Fehlermeldungen. Erst nach Deinstallation des Verzeichnisses konte ich die Dein Programm erfolgreich hochladen. (Es gibt auch noch eine Version von Rob Tillaart mit dem gleichen Problem.) Vielleicht könntest Du über eine Umbenennung ( wie das Verzeichnis) nachdenken. – Adafruit hat das auch gemacht.
        VG Lutz

        1. Hallo Lutz, da hast du absolut Recht. Das war keine schlaue Wahl. War aber auch meine erste Bibliothek. Bei allen danach habe ich Name_WE.cpp und Name_WE.h verwendet. Das einzige, was mich davon abhält den Namen zu ändern, ist dass es nicht rückwärtskompatibel ist. Leute, die das Update herunterladen werden zunächst einmal sehen, dass die Bibliothek nicht mehr funktioniert mit ihrem Code. Und nicht jeder wird die Fehlermeldungen verstehen.

    1. Hi Nampico. Danke für’s Feedback und den Link. Und schöne Grüße nach Kiel – da hab ich studiert.

  38. Hallo Wolfgng,

    auch von mir gibt es ein Sternchen für Deinen Blog und für die Bibliothek.

    Besonders gelungen finde ich, dass Du sehr gut nachvollziehbare Beispiele inklusive der Schaltung lieferst.
    Auch hervorzuheben ist die Ausführlichkeit Deiner Erläuterungen und die sehr gelungene Aufbereitung der Interrupt-Einstellungen.

    Wenn Du statt der sequentiellen Adressierung der Register auf die Adressierung in separaten Bänken gehen würdest, könnte man die Bibliothek auch für den MCP23008 nehmen (dann immer Bank A als Parameter).

    Gruß Jörg

    1. Hallo Jörg,
      danke für den Stern und das positive Feedback. Was die sequentielle Adressierung angeht hast du recht. Ich hatte mir, als ich die Bibliothek geschrieben habe, überlegt welchen Weg ich gehe. Dabei hatte ich den MCP23008 aber nicht im Blick. Und zu dem Zeitpunkt erschien es mir einfacher, beide Bänke gemeinsam anzusprechen. Anders als die anderen Vorschläge, die du mir noch separat geschickt hast (nochmal danke dafür), ist das schon ein etwas größerer Eingriff in die Bibliothek. Da muss ich mal schauen, ob bzw. wann ich das angehe.
      Viele Grüße, Wolle

  39. Hallo Christoph,

    mit diesem Kommentar hast du mir wirklich eine große Freude gemacht! Und danke für den Stern – der erste!

    Viele Grüße, Wolle

  40. Hallo Wolfgang,

    hab ein kleines Projekt gestartet, in dem der PCF8574 zum Einsatz kommt. Der ESP32 Controller arbeitet mit 3V, LCD und Relaiskarte mit 5V. Hab diese Spannungen einfach mal ganz naiv parallel verwendet und es hat nur so halb funktioniert. Also hab ich Herrn G. gefragt und bin so deinen Blog gekommen. Dein Tipp einen Level Konverter zwischen zu schalten war genau das, was ich gebraucht habe. Den hab ich dann auch gleich bestellt zusammen mit einem MCP23017. Von Github hab ich mir auch deine Bibliothek herunter geladen und für dich ein Sternchen hinterlassen als kleines Dankeschön! Du machst dir viel Mühe mit deinem Blog und du hast sehr interessante Themen, die ich gerne mal ausprobieren möchte, wenn ich die Zeit dazu finde…
    Jedenfalls, danke für deine Hilfe, super Blog, mach weiter so!

    Mit freundlichen Grüßen aus Graz
    Christoph Mayer

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert