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
Pinout des MCP23017

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() erzeugt euer MCP23017 Objekt. Ihr müsst die I2C Adresse übergeben. Zusätzlich könnt ihr den Reset Pin übergeben (wenn ihr die Resetfunktion braucht) und / oder ein Wire Objekt. Mit Letzterem könnt ihr zum Beispiel beide I2C Busse eines ESP32 nutzen. 
  • 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(){ 
  Wire.begin();
  myMCP.Init();  
  myMCP.setPortMode(B11111101, A);  // Port A: all pins are OUTPUT except pin 1
  myMCP.setPortMode(B11111111, 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(B11110000, B); // B4 - B7 switched on
  delay(wT);
  myMCP.setPort(B01011110, 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(B11110000, B01101111,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

Ihr könnt dieselbe Schaltung wie oben verwenden und in Verbindung mit dem folgenden Sketch ein bisschen mit diesen Funktionen herumspielen. Setzt Ihr an einen LOW geschalteten Ausgang eine externe Spannung in der Höhe des HIGH Levels, dann werdet Ihr sehen, dass im GPIO Register ein HIGH steht. Im GPIO Register steht also der tatsächliche logische Level und nicht der vorgegebene. 

#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
byte portStatus;
bool pinStatus;

void setup(){ 
  Serial.begin(9600);
  Wire.begin();
  myMCP.Init();  
  myMCP.setPortMode(B11111111, A);  // Port A: all pins are OUTPUT
  myMCP.setPortMode(B11111111, B);  // Port B: all pins are OUTPUT
  myMCP.setPort(B10010011,A);  // 
}

void loop(){ 
  portStatus = myMCP.getPort(A);
  Serial.print("Status GPIO A: ");
  Serial.println(portStatus, BIN);
  pinStatus = myMCP.getPin(7, A);
  Serial.print("Status Port A, Pin 7: ");
  Serial.println(pinStatus, BIN);
  portStatus = myMCP.getPort(B);
  Serial.print("Status GPIO B: ");
  Serial.println(portStatus, BIN);
  pinStatus = myMCP.getPin(0, B);
  Serial.print("Status Port B, Pin 0: ");
  Serial.println(pinStatus, BIN);
  Serial.println("-------------------------------------");
  delay(5000);
} 

 

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

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

Das könnt ihr ja noch im obigen Sketch mit einbauen und ausprobieren. 

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)
#define RESET_PIN 5  
int interruptPin = 3;
volatile bool event; 
byte intCapReg; 

/* 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);

void setup(){ 
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), eventHappened, RISING);
  Serial.begin(9600);
  Wire.begin();
  myMCP.Init(); 
  myMCP.setPortMode(B11111111,A);
  myMCP.setPort(B11111111, A); // just an LED test
  delay(1000); 
  myMCP.setAllPins(A, OFF);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH); // set INTA and INTB active-high
  delay(10);
  myMCP.setInterruptOnChangePort(B11111111, B); //set all B pins as interrrupt Pins
  event=false;
}  

void loop(){ 
  intCapReg = myMCP.getIntCap(B); // ensures that existing interrupts are cleared
  if(event){
    delay(200);
    byte intFlagReg, eventPin; 
    intFlagReg = myMCP.getIntFlag(B);
    eventPin = log(intFlagReg)/log(2);
    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(1000);
    event = false; 
  }
}

void eventHappened(){
  event = true;
}

 

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

Hinweis 1: Bei schnellem Drücken des Tasters wird der HIGH-LOW Interrupt „verschluckt“. 

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 28 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. Besonders relevant wird die Problematik bei den Interrupts-on-DefVal-Deviation. 

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)
#define RESET_PIN 5 
int interruptPin = 3;
volatile bool event = false;
byte intCapReg; 

/* 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);

void setup(){ 
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), eventHappened, RISING);
  Serial.begin(9600);
  Wire.begin();
  myMCP.Init();
  myMCP.setPortMode(B11111111,A);
  myMCP.setPort(B11111111, A); // just an LED test 
  delay(1000); 
  myMCP.setAllPins(A, OFF);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH);
  delay(10);
  myMCP.setInterruptOnDefValDevPort(B11111111, B, B00001111); // interrupt pins, port, DEFVALB
  myMCP.setPortPullUp(B00001111, B); // pull-up for B0-B3
  event=false;
}  

void loop(){ 
  intCapReg = myMCP.getIntCap(B);
  if(event){
    delay(200);
    byte intFlagReg, eventPin; 
    intFlagReg = myMCP.getIntFlag(B);
    eventPin = log(intFlagReg)/log(2);
    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(1000);
    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. Eine gute Übersicht über die MCP23XXX Familie findet Ihr hier

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. 

54 thoughts on “Portexpander MCP23017

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

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

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

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

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

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

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

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

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

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

  11. 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…

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

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

  14. 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!

  15. 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!

  16. 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?

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

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

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

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