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

  • 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, 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.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:

#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) 
#include <Wire.h>
#include <MCP23017.h> 
MCP23017 myMCP(MCP_ADDRESS,5); 
int wT = 1000; // wT = waiting time

void setup(){ 
  Wire.begin();
  myMCP.Init();  
  myMCP.setPortMode(B11111101, A);  // Port A: alles auf OUTPUT bis auf PIN 1
  myMCP.setPortMode(B11111111, B);  // Port B: alles auf OUTPUT
  delay(wT);
  myMCP.setAllPins(A, ON); // alle LEDs an Port A leuchten bis auf Pin 1
  delay(wT);
  myMCP.setPinX(1, A, OUTPUT, HIGH); // Port A, Pin 1 geht an
  delay(wT); 
  myMCP.setPort(B11110000, B); // B4 - B7 leuchten
  delay(wT);
  myMCP.setPort(B01011110, A); // A0,A5,A7 gehen aus
  delay(wT);
  myMCP.setPinX(0,B,OUTPUT,HIGH); // B0 geht an
  delay(wT);
  myMCP.setPinX(4,B,OUTPUT,LOW); // B4 geht aus
  delay(wT);
  myMCP.setAllPins(A, HIGH); // A0 - A7 gehen an
  delay(wT);
  myMCP.setPin(3, A, LOW); // A3 geht aus
  delay(wT);
  myMCP.setPortX(B11110000, B01101111,B); // an B leuchten nur B5,B6
  delay(wT);
  myMCP.setPinMode(0,B,OUTPUT); // B0 auf OUTPUT
  for(int i=0; i<5; i++){  // B0 blinkt
    myMCP.togglePin(0,B); 
    delay(200);
    myMCP.togglePin(0,B);
    delay(200);
  }
  for(int i=0; i<5; i++){ // B7 blinkt
    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. 

#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) 
#include <Wire.h>
#include <MCP23017.h> 
MCP23017 myMCP(MCP_ADDRESS,5); 
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: alles auf OUTPUT
  myMCP.setPortMode(B11111111, B);  // Port B: alles auf 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

#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) 
#include <Wire.h>
#include <MCP23017.h> 
int interruptPin = 3;
volatile bool event; 
byte intCapReg; 

MCP23017 myMCP(MCP_ADDRESS,5); // 5 = ResetPin

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); // kurzer LED Test
  delay(1000); 
  myMCP.setAllPins(A, OFF);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH);
  delay(10);
  myMCP.setInterruptOnChangePort(B11111111, B);
  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){
      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

#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW) 
#include <Wire.h>
#include <MCP23017.h> 
int interruptPin = 3;
volatile bool event = false;
byte intCapReg; 

MCP23017 myMCP(MCP_ADDRESS,5); // 5 = ResetPin

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); // kurzer LED Test
  delay(1000); 
  myMCP.setAllPins(A, OFF);
  delay(1000);
  myMCP.setInterruptPinPol(HIGH);
  delay(10);
  myMCP.setInterruptOnDefValDevPort(B11111111, B, B00001111); // IntPins, Port, DEFVAL
  myMCP.setPortPullUp(B00001111, B);
  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){
      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

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

26 thoughts on “Portexpander MCP23017

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

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

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

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

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

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

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

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

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

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

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

Schreibe einen Kommentar

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