MCP23017

Technische Daten · Python-/MicroPython-Programmierung · Verhalten der Ein- und Ausgänge · Interrupts · Einsatzbeispiel

1. Überblick

Der MCP23017 ist ein über I²C gesteuerter Portexpander von Microchip. Er erweitert einen Mikrocontroller um zwei digitale 8-Bit-Ports: PORTA mit GPA0 bis GPA7 und PORTB mit GPB0 bis GPB7. Die Pins werden über Register einzeln als Eingang oder Ausgang konfiguriert.

16 GPIO-PinsZwei funktionell getrennte 8-Bit-Ports.
I²C7-Bit-Adressen von 0x20 bis 0x27.
RichtungsregisterJeder nutzbare GPIO wird gezielt als Eingang oder Ausgang festgelegt.
Pull-upsFür Eingänge einzeln zuschaltbare schwache Pull-up-Widerstände.
InterruptsINTA und INTB mit Capture-, Flag- und Vergleichsregistern.
1,8 bis 5,5 VFür ESP32-Projekte zweckmäßig mit 3,3 V betreiben.
Didaktischer Vorteil: Im Gegensatz zum PCF8574 lässt sich beim MCP23017 sehr deutlich zeigen, dass Richtung, Pull-up, Ausgangslatch, Eingangswert und Interruptquelle getrennte Funktionen und Register sind.

2. Technische Daten

MerkmalWert beziehungsweise Bedeutung
BausteinMCP23017 – 16-Bit-I/O-Expander mit I²C-Schnittstelle
Betriebsspannung1,8 V bis 5,5 V
I²C-Taktraten100 kHz, 400 kHz und bis 1,7 MHz; die zulässige Höchstgeschwindigkeit hängt auch von der Betriebsspannung ab
GPIO-AufteilungPORTA: GPA0…GPA7; PORTB: GPB0…GPB7
AdressauswahlDrei Hardwarepins A0, A1 und A2; dadurch acht Adressen 0x20…0x27
Zustand nach ResetRichtungsregister grundsätzlich auf Eingang; Pull-ups und Interrupts aus; Ausgangslatches auf 0
Interne Pull-upsEinzeln zuschaltbar; typischer Pull-up-Strom laut Datenblatt etwa 75 µA bei 5 V
InterruptausgängeINTA und INTB; aktiv HIGH, aktiv LOW oder Open Drain; getrennt oder gespiegelt
Maximalwert je Ausgang25 mA Source oder Sink als absoluter Grenzwert, nicht als empfohlener Dauerstrom
Garantierte AusgangswerteBei 4,5 V unter anderem LOW bei 8 mA und HIGH bei 3 mA spezifiziert
GesamtstromgrenzenMaximal 150 mA über VSS und 125 mA über VDD; ebenfalls absolute Grenzwerte
GehäuseUnter anderem 28-poliges SPDIP, SOIC, SSOP und QFN
25 mA sind kein sinnvoller Planungswert. Die 25 mA pro Pin stehen im Abschnitt der absoluten Grenzwerte. Für robuste Unterrichtsschaltungen sollte der Strom deutlich kleiner bleiben. LEDs werden mit Vorwiderstand betrieben. Relais, Motoren, Magnetventile, größere Anzeigen oder LED-Matrizen benötigen Treiber, beispielsweise Transistoren, MOSFETs oder einen ULN2803A.
Besonderheit GPA7 und GPB7: Das aktuelle Microchip-Datenblatt DS20001952D kennzeichnet diese beiden Pins beim MCP23017 als nur als Ausgang verwendbar. Im älteren Datenblatt DS20001952C wurden noch alle 16 GPIOs als bidirektional beschrieben. Für neue und zuverlässig reproduzierbare Schaltungen sollten GPA7 und GPB7 deshalb nur als Ausgänge eingeplant werden, sofern die konkrete Chiprevision nicht eindeutig geprüft wurde.

3. Anschluss an einen ESP32

Pinlayout des vorhandenen CJMCU-Moduls

Das Modul führt die Anschlüsse für die I²C-Variante MCP23017 und die SPI-Variante MCP23S17 gemeinsam heraus. Für den MCP23017 gelten links SDA und SCL; die alternativ aufgedruckten Bezeichnungen SI, SCK, SO und CS gehören zur SPI-Variante.

Die linke Reihe enthält von oben nach unten A2, A1, A0, RESET, zwei bei I²C nicht verwendete Anschlüsse, SDA, SCL, GND und VCC. Rechts liegen die Versorgung, die beiden Interruptausgänge und die Portpins. Bei den paarweisen Beschriftungen steht links jeweils Port B und rechts Port A, beispielsweise B0/A0 bis B7/A7.

Beschriftung ITB/ITA: Gemeint sind INTB und INTA. Für unseren Test wird INTB mit dem vorgesehenen ESP32-Interruptpin verbunden.
Pinlayout des CJMCU-Moduls für MCP23017 und MCP23S17

Schaltplan und Bestückungsseite des CJMCU-2317

Der Modulschaltplan zeigt die gemeinsame Auslegung für MCP23017 (I²C) und MCP23S17 (SPI). Für unseren MCP23017 sind die Leitungen SDA (gelb) und SCL (grün) maßgeblich.

R1 und R2 sind jeweils mit 10 kΩ angegeben und ziehen SDA und SCL nach VCC. Das vorhandene Modul besitzt damit bereits die notwendigen I²C-Pull-up-Widerstände. Zusätzliche Pull-ups sind beim einzelnen Modul normalerweise nicht erforderlich.

Wichtig: Das sind Pull-up-Widerstände zwischen Signalleitung und VCC, keine in Reihe liegenden Vorwiderstände. Das Modul wird am ESP32 mit 3,3 V betrieben, damit auch die I²C-Pegel 3,3 V betragen.
Schaltplan des CJMCU-2317 mit 10-Kiloohm-Pull-ups an SDA und SCL

Die Aufnahme der Bestückungsseite hilft bei der Identifikation des tatsächlich verwendeten Moduls. Oberhalb des MCP23017 ist das mit 103 beschriftete Widerstandsnetzwerk zu sehen. Die beiden ebenfalls mit 103 gekennzeichneten Widerstände unterhalb des Bausteins entsprechen jeweils 10 kΩ.

Beide Abbildungen sind auf der Seite verkleinert. Ein Klick öffnet das jeweilige Bild in Originalgröße.

Bestückungsseite des CJMCU-2317 mit MCP23017 und Widerständen
MCP23017ESP32 / BeschaltungHinweis
VDD3,3 VSo bleiben I²C- und GPIO-Pegel zum ESP32 kompatibel.
VSSGNDGemeinsame Masse ist zwingend erforderlich.
SDAGPIO21, gelbAuf dem CJMCU-2317 über 10 kΩ nach VCC gezogen.
SCLGPIO22, grünAuf dem CJMCU-2317 über 10 kΩ nach VCC gezogen.
A0, A1, A2GNDErgibt 0x20. Die Adresspins dürfen nicht offen bleiben.
RESETÜber etwa 10 kΩ nach 3,3 VRESET ist aktiv LOW und muss einen definierten Pegel erhalten.
INTAbeispielsweise GPIO27Bei Open Drain mit etwa 10 kΩ nach 3,3 V ziehen.
INTBoptional weiterer ESP32-GPIOKann getrennt genutzt oder mit INTA gespiegelt werden.
ESP32 MCP23017 ───── ──────── 3,3 V ─────────────────────── VDD GND ─────────────────────── VSS GPIO21 / SDA ──────────────── SDA GPIO22 / SCL ──────────────── SCL GPIO27 / IRQ ◄─────────────── INTA │ 3,3 V ── 10 kΩ ──┘ Open-Drain-Pull-up 3,3 V ── 10 kΩ ───────────── RESET GND ──────────────────────── A0 GND ──────────────────────── A1 GND ──────────────────────── A2 → Adresse 0x20 SDA ── 4,7 kΩ ── 3,3 V SCL ── 4,7 kΩ ── 3,3 V nur nötig, wenn auf dem Modul keine Pull-ups vorhanden sind

3.1 I²C-Adressen

A2A1A0Adresse
0000x20
0010x21
0100x22
0110x23
1000x24
1010x25
1100x26
1110x27

Mit acht MCP23017 lassen sich theoretisch bis zu 128 Portleitungen adressieren. Buskapazität, Leitungslänge, Pull-up-Widerstände und Stromversorgung setzen in der Praxis Grenzen.

4. Verhalten der Ein- und Ausgänge

4.1 Richtungsregister

IODIR-BitWirkung
1Pin ist Eingang; der Ausgangstreiber ist hochohmig.
0Pin ist Ausgang; das Ausgangslatch bestimmt den Pegel.

Nach einem Reset stehen IODIRA und IODIRB auf 0xFF. Die Pins sind damit grundsätzlich zunächst Eingänge. Die Einschränkung für GPA7 und GPB7 muss zusätzlich berücksichtigt werden.

4.2 Eingänge

Ein Eingang ist hochohmig. Ohne externe Beschaltung oder zugeschalteten Pull-up kann er einen undefinierten Zustand annehmen. Über GPPUA und GPPUB wird für jeden Eingang einzeln ein schwacher Pull-up aktiviert. Interne Pull-down-Widerstände besitzt der MCP23017 nicht.

Typischer Tasteranschluss: Taster zwischen GPIO und GND; internen Pull-up einschalten. Nicht gedrückt wird eine logische 1 gelesen, gedrückt eine logische 0.

4.3 Eingangspolarität

IPOLA und IPOLB können gelesene Eingangswerte bitweise invertieren. Das verändert nicht den elektrischen Pegel am Pin, sondern nur den im GPIO-Register sichtbaren Wert.

4.4 Ausgänge und Ausgangslatches

Die Ausgänge sind echte Push-Pull-Ausgänge. Sie können aktiv nach LOW und nach HIGH schalten.

Ein gespeicherter Schattenwert verhindert, dass beim Ändern eines einzelnen Bits versehentlich andere Ausgangsbits verändert werden.

4.5 Umschalten der Richtung

Wird ein Pin vom Eingang zum Ausgang umgeschaltet, liegt sofort der bereits im Ausgangslatch gespeicherte Wert am Pin an. Bei empfindlichen Aktoren sollte deshalb zunächst OLAT auf einen sicheren Anfangswert gesetzt und erst danach die Richtung auf Ausgang geändert werden.

5. Wichtige Register bei IOCON.BANK = 0

Port APort BRegisterAufgabe
0x000x01IODIRA / IODIRBRichtung: 1 = Eingang, 0 = Ausgang
0x020x03IPOLA / IPOLBEingangspolarität invertieren
0x040x05GPINTENA / GPINTENBInterrupt für einzelne Pins freigeben
0x060x07DEFVALA / DEFVALBSollwert für Interruptvergleich
0x080x09INTCONA / INTCONBVergleich gegen früheren Wert oder DEFVAL
0x0A0x0BIOCONBank, Spiegelung, Open Drain, Polarität
0x0C0x0DGPPUA / GPPUBInterne Pull-ups einschalten
0x0E0x0FINTFA / INTFBInterruptquelle anzeigen
0x100x11INTCAPA / INTCAPBZustand beim Interrupt erfassen und Interrupt löschen
0x120x13GPIOA / GPIOBAktuelle Pinpegel lesen
0x140x15OLATA / OLATBAusgangslatches lesen und schreiben

5.1 Wichtige IOCON-Bits

BitNameBedeutung
7BANK0 = A-/B-Register abwechselnd; 1 = nach Ports getrennt
6MIRROR1 = INTA und INTB melden Ereignisse beider Ports
5SEQOP1 = automatische Adressfortschaltung deaktiviert
2ODR1 = Interruptausgänge arbeiten als Open Drain
1INTPOLAktive HIGH- oder LOW-Polarität im Push-Pull-Betrieb

6. Python- und MicroPython-Programmierung

6.1 I²C-Bus erzeugen und Baustein suchen

Für die praktische Fehlersuche steht ein bewusst einfacher Scanner bereit. Er gibt fortlaufend nur die gefundenen I²C-Adressen aus und vermeidet damit ablenkende Ergebnisse aus einem vollständigen Modultest: i2c-scanner.py im Code-Viewer öffnen.

Immer zuerst scannen: Vor jedem Test eines I²C-Moduls wird zunächst i2c-scanner.py ausgeführt. Der eigentliche Modultest beginnt erst, wenn die erwartete Adresse zuverlässig angezeigt wird.
from machine import Pin, I2C

i2c = I2C(
    0,
    sda=Pin(21),
    scl=Pin(22),
    freq=100_000,
)

print([hex(adresse) for adresse in i2c.scan()])
# Bei A0 = A1 = A2 = GND muss 0x20 erscheinen.

6.2 Direkter Registerzugriff

ADRESSE = 0x20
IODIRA = 0x00
GPPUA = 0x0C
GPIOA = 0x12

i2c.writeto_mem(ADRESSE, IODIRA, bytes((0xFF,)))
i2c.writeto_mem(ADRESSE, GPPUA, bytes((0x7F,)))

wert = i2c.readfrom_mem(ADRESSE, GPIOA, 1)[0]
print("{:08b}".format(wert))

6.3 Mitgelieferte MicroPython-Bibliothek

Die Bibliothek kapselt die Registerzugriffe in verständliche Methoden und benötigt keine zusätzliche Installation.

Direkter Start: Wird mcp23017.py in Thonny direkt ausgeführt, startet der Block if __name__ == "__main__": automatisch einen Selbsttest an Adresse 0x20. Bei erfolgreicher Initialisierung werden PORTA und PORTB gelesen. Schlägt die Initialisierung fehl, folgt unmittelbar ein I²C-Scan. Beim Import mit from mcp23017 import MCP23017 wird der Selbsttest nicht ausgeführt.
"""
mcp23017.py
Kleine MicroPython-Bibliothek für den MCP23017 am ESP32.

Registerbelegung: IOCON.BANK = 0
Standardadresse: 0x20 bei A0 = A1 = A2 = GND

Wichtiger Hinweis:
Das aktuelle Microchip-Datenblatt DS20001952D kennzeichnet GPA7 und
GPB7 beim MCP23017 als reine Ausgänge. Ältere Datenblattstände
beschrieben diese Pins als bidirektional. Mit strict_gp7=True wird
deshalb eine Konfiguration dieser beiden Pins als Eingang verhindert.
"""

class MCP23017:
    INPUT = 1
    OUTPUT = 0

    IODIRA = 0x00
    IODIRB = 0x01
    IPOLA = 0x02
    IPOLB = 0x03
    GPINTENA = 0x04
    GPINTENB = 0x05
    DEFVALA = 0x06
    DEFVALB = 0x07
    INTCONA = 0x08
    INTCONB = 0x09
    IOCON = 0x0A
    GPPUA = 0x0C
    GPPUB = 0x0D
    INTFA = 0x0E
    INTFB = 0x0F
    INTCAPA = 0x10
    INTCAPB = 0x11
    GPIOA = 0x12
    GPIOB = 0x13
    OLATA = 0x14
    OLATB = 0x15

    def __init__(self, i2c, address=0x20, strict_gp7=True):
        if not 0x20 <= address <= 0x27:
            raise ValueError("Die MCP23017-Adresse muss zwischen 0x20 und 0x27 liegen.")

        self.i2c = i2c
        self.address = address
        self.strict_gp7 = strict_gp7

        # BANK = 0, normale fortlaufende Registeranordnung
        self._write_register(self.IOCON, 0x00)

        self._olat = [
            self._read_register(self.OLATA),
            self._read_register(self.OLATB),
        ]

    def _write_register(self, register, value):
        self.i2c.writeto_mem(
            self.address,
            register,
            bytes((value & 0xFF,))
        )

    def _read_register(self, register):
        return self.i2c.readfrom_mem(
            self.address,
            register,
            1
        )[0]

    def _change_bit(self, register, bit, value):
        data = self._read_register(register)

        if value:
            data |= 1 << bit
        else:
            data &= ~(1 << bit)

        self._write_register(register, data)

    @staticmethod
    def _pin_data(pin):
        if not 0 <= pin <= 15:
            raise ValueError("Die Pinnummer muss zwischen 0 und 15 liegen.")

        port = 0 if pin < 8 else 1
        bit = pin if pin < 8 else pin - 8
        return port, bit

    @staticmethod
    def _port_index(port):
        if port in ("A", "a", 0):
            return 0
        if port in ("B", "b", 1):
            return 1
        raise ValueError("Der Port muss 'A', 'B', 0 oder 1 sein.")

    def pin_mode(self, pin, mode):
        port, bit = self._pin_data(pin)

        if mode not in (self.INPUT, self.OUTPUT):
            raise ValueError("Als Modus ist MCP23017.INPUT oder MCP23017.OUTPUT zulässig.")

        if self.strict_gp7 and mode == self.INPUT and pin in (7, 15):
            raise ValueError(
                "GPA7 und GPB7 werden nach aktuellem Datenblatt "
                "beim MCP23017 nur als Ausgänge verwendet."
            )

        self._change_bit(self.IODIRA + port, bit, mode == self.INPUT)

    def pullup(self, pin, enabled=True):
        port, bit = self._pin_data(pin)
        self._change_bit(self.GPPUA + port, bit, enabled)

    def invert_input(self, pin, enabled=True):
        port, bit = self._pin_data(pin)
        self._change_bit(self.IPOLA + port, bit, enabled)

    def write(self, pin, value):
        port, bit = self._pin_data(pin)

        if value:
            self._olat[port] |= 1 << bit
        else:
            self._olat[port] &= ~(1 << bit)

        self._write_register(self.OLATA + port, self._olat[port])

    def read(self, pin):
        port, bit = self._pin_data(pin)
        data = self._read_register(self.GPIOA + port)
        return 1 if data & (1 << bit) else 0

    def write_port(self, port, value):
        port = self._port_index(port)
        self._olat[port] = value & 0xFF
        self._write_register(self.OLATA + port, self._olat[port])

    def read_port(self, port):
        port = self._port_index(port)
        return self._read_register(self.GPIOA + port)

    def interrupt_enable(self, pin, enabled=True):
        port, bit = self._pin_data(pin)
        self._change_bit(self.GPINTENA + port, bit, enabled)

    def interrupt_on_change(self, pin):
        """Interrupt bei Änderung gegenüber dem zuvor gelesenen Zustand."""
        port, bit = self._pin_data(pin)
        self._change_bit(self.INTCONA + port, bit, False)
        self.interrupt_enable(pin, True)

    def interrupt_on_default(self, pin, default_value):
        """Interrupt bei Abweichung vom angegebenen Sollwert."""
        port, bit = self._pin_data(pin)
        self._change_bit(self.DEFVALA + port, bit, bool(default_value))
        self._change_bit(self.INTCONA + port, bit, True)
        self.interrupt_enable(pin, True)

    def configure_interrupt_outputs(
        self,
        mirror=False,
        open_drain=True,
        active_high=False
    ):
        value = self._read_register(self.IOCON)

        # BANK = 0 beibehalten
        value &= ~(1 << 7)

        if mirror:
            value |= 1 << 6
        else:
            value &= ~(1 << 6)

        if open_drain:
            value |= 1 << 2
        else:
            value &= ~(1 << 2)

        if active_high:
            value |= 1 << 1
        else:
            value &= ~(1 << 1)

        self._write_register(self.IOCON, value)

    def interrupt_flags(self, port):
        port = self._port_index(port)
        return self._read_register(self.INTFA + port)

    def interrupt_capture(self, port):
        """
        Liest den beim Interrupt gespeicherten Portzustand.
        Das Lesen von INTCAP löscht den Interrupt dieses Ports.
        """
        port = self._port_index(port)
        return self._read_register(self.INTCAPA + port)

    def clear_interrupt(self, port):
        return self.interrupt_capture(port)


def _selbsttest():
    """Initialisiert den MCP23017 beim direkten Start dieser Datei."""
    from machine import I2C, Pin

    SDA_PIN = 21  # GPIO21 - SDA - gelb
    SCL_PIN = 22  # GPIO22 - SCL - grün
    ADRESSE = 0x20

    i2c = I2C(
        0,
        sda=Pin(SDA_PIN),
        scl=Pin(SCL_PIN),
        freq=100_000,
    )

    print("MCP23017-Selbsttest an Adresse", hex(ADRESSE))

    try:
        mcp = MCP23017(i2c, address=ADRESSE)
        print("Initialisierung erfolgreich.")
        print("Port A: {:08b}".format(mcp.read_port("A")))
        print("Port B: {:08b}".format(mcp.read_port("B")))
    except Exception as fehler:
        print("Initialisierung fehlgeschlagen:", fehler)
        print(
            "I2C-Scan:",
            [hex(adresse) for adresse in i2c.scan()],
        )


if __name__ == "__main__":
    _selbsttest()

6.4 Kurzes Beispiel ohne Interrupt

from machine import Pin, I2C
from time import sleep_ms
from mcp23017 import MCP23017

i2c = I2C(0, sda=Pin(21), scl=Pin(22), freq=100_000)
mcp = MCP23017(i2c, address=0x20)

mcp.pin_mode(0, MCP23017.INPUT)
mcp.pullup(0, True)

mcp.pin_mode(8, MCP23017.OUTPUT)

while True:
    taster = mcp.read(0)
    mcp.write(8, not taster)
    sleep_ms(20)

6.5 CircuitPython oder Raspberry Pi

Für CircuitPython und CPython auf Einplatinencomputern existiert die Adafruit-Bibliothek Adafruit_CircuitPython_MCP230xx.

import board
import busio
from digitalio import Direction, Pull
from adafruit_mcp230xx.mcp23017 import MCP23017

i2c = busio.I2C(board.SCL, board.SDA)
mcp = MCP23017(i2c, address=0x20)

taster = mcp.get_pin(0)
taster.direction = Direction.INPUT
taster.pull = Pull.UP

led = mcp.get_pin(8)
led.direction = Direction.OUTPUT
led.value = not taster.value

7. Interrupts

Mit Interrupts muss der Mikrocontroller die Eingänge nicht ständig abfragen. Ändert sich ein freigegebener Eingang, aktiviert der MCP23017 INTA oder INTB. Der ESP32 liest erst dann die zugehörigen Register.

7.1 Vergleichsarten

INTCON-BitVergleichEinsatz
0Aktueller Eingang gegen den zuvor erfassten PortzustandInterrupt bei jeder Änderung
1Aktueller Eingang gegen DEFVALInterrupt bei Abweichung von einem Sollwert

7.2 Ablauf

  1. GPINTEN schaltet die Überwachung je Pin ein.
  2. INTCON legt die Vergleichsart fest.
  3. DEFVAL enthält bei Bedarf den Sollzustand.
  4. Bei einer Abweichung wird INTA oder INTB aktiv.
  5. INTF zeigt die auslösenden Pins.
  6. INTCAP enthält den Portzustand im Moment des Ereignisses.
  7. Das Lesen von INTCAP oder GPIO löscht den Interrupt des betreffenden Ports.
INTCAP ist besonders wertvoll: Der beim Ereignis erfasste Zustand bleibt zunächst erhalten, auch wenn sich der Eingang danach erneut ändert.

7.3 INTA und INTB

Keine I²C-Kommunikation direkt in der ESP32-Interruptfunktion. Die Interruptfunktion sollte nur einen Merker setzen. Das Lesen des MCP23017 erfolgt anschließend in der normalen Hauptschleife.
interrupt_steht_an = False

def irq_handler(pin):
    global interrupt_steht_an
    interrupt_steht_an = True

irq = Pin(27, Pin.IN, Pin.PULL_UP)
irq.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler)

while True:
    if interrupt_steht_an:
        interrupt_steht_an = False
        capture = mcp.interrupt_capture("A")
        print("Zustand beim Interrupt:", bin(capture))

7.4 Taster entprellen

Mechanische Taster erzeugen häufig mehrere schnelle Pegelwechsel. Eine kurze Software-Entprellzeit von ungefähr 20 bis 40 ms ist für einfache Bedienfelder oft ausreichend.

8. Einsatzbeispiel: Taster- und LED-Bedienfeld

Sieben Taster sind an GPA0 bis GPA6 angeschlossen. Sie verwenden die internen Pull-ups und schalten gegen GND. Sieben LEDs liegen über Vorwiderstände an GPB0 bis GPB6. Jeder Tastendruck schaltet die zugehörige LED um.

GPA7 wird bewusst nicht als Eingang verwendet. GPB7 kann bei Bedarf als zusätzlicher Ausgang eingesetzt werden.

8.1 Bauteile

8.2 Verdrahtung

FunktionPinsBeschaltung
Taster 1…7GPA0…GPA6Je ein Taster zwischen Pin und GND; interner Pull-up ein
LED 1…7GPB0…GPB6Pin → Vorwiderstand → LED → GND
InterruptINTAZu ESP32-GPIO27; 10-kΩ-Pull-up nach 3,3 V

8.3 Vollständiges MicroPython-Beispiel

"""
mcp23017_interrupt_beispiel.py

Einsatzbeispiel:
- sieben Taster an GPA0 bis GPA6 gegen GND
- interne Pull-ups des MCP23017
- sieben LEDs an GPB0 bis GPB6 über Vorwiderstände
- INTA als Open-Drain-Interrupt an ESP32-GPIO27
- I2C: SDA = GPIO21, SCL = GPIO22

Jeder Tastendruck schaltet die zugehörige LED um.
"""

from machine import Pin, I2C
from time import sleep_ms
from mcp23017 import MCP23017


SDA_PIN = 21
SCL_PIN = 22
INT_PIN = 27
ADRESSE = 0x20

i2c = I2C(
    0,
    sda=Pin(SDA_PIN),
    scl=Pin(SCL_PIN),
    freq=100_000,
)

gefundene_adressen = i2c.scan()
print("Gefundene I2C-Adressen:", [hex(a) for a in gefundene_adressen])

if ADRESSE not in gefundene_adressen:
    raise RuntimeError(
        "MCP23017 nicht unter 0x20 gefunden. "
        "Verdrahtung und Adresspins A0 bis A2 prüfen."
    )

mcp = MCP23017(i2c, address=ADRESSE)

# GPA0 bis GPA6: Tastereingänge mit internen Pull-ups
for pin in range(0, 7):
    mcp.pin_mode(pin, MCP23017.INPUT)
    mcp.pullup(pin, True)
    mcp.interrupt_on_change(pin)

# GPB0 bis GPB6: LED-Ausgänge
for pin in range(8, 15):
    mcp.pin_mode(pin, MCP23017.OUTPUT)
    mcp.write(pin, 0)

# INTA und INTB arbeiten getrennt.
# INTA ist Open Drain und wird extern nach 3,3 V hochgezogen.
mcp.configure_interrupt_outputs(
    mirror=False,
    open_drain=True,
    active_high=False,
)

mcp.clear_interrupt("A")
letzter_tasterzustand = mcp.read_port("A") | 0x80
led_zustand = 0
interrupt_steht_an = False


def interrupt_handler(pin):
    """
    In einer Interruptfunktion keine I2C-Kommunikation durchführen.
    Es wird nur ein Merker gesetzt.
    """
    global interrupt_steht_an
    interrupt_steht_an = True


interrupt_eingang = Pin(INT_PIN, Pin.IN, Pin.PULL_UP)
interrupt_eingang.irq(
    trigger=Pin.IRQ_FALLING,
    handler=interrupt_handler,
)

print("Bereit: Taster an GPA0 bis GPA6 betätigen.")

while True:
    if interrupt_steht_an:
        interrupt_steht_an = False

        # Lesen von INTCAPA löscht den Interrupt von Port A.
        capture = mcp.interrupt_capture("A")

        # Kurze Entprellzeit, danach stabilen Zustand lesen.
        sleep_ms(25)
        aktueller_tasterzustand = mcp.read_port("A") | 0x80

        # Eine 1-zu-0-Flanke entspricht einem gedrückten Taster.
        gedrueckt = letzter_tasterzustand & (~aktueller_tasterzustand & 0xFF)

        for bit in range(7):
            if gedrueckt & (1 << bit):
                led_zustand ^= 1 << bit
                print(
                    "Taster GPA{} gedrückt – LED GPB{} ist jetzt {}.".format(
                        bit,
                        bit,
                        "EIN" if led_zustand & (1 << bit) else "AUS",
                    )
                )

        mcp.write_port("B", led_zustand)
        letzter_tasterzustand = aktueller_tasterzustand

        print(
            "INTCAPA = {:08b}, GPIOA = {:08b}, GPIOB = {:08b}".format(
                capture,
                aktueller_tasterzustand,
                led_zustand,
            )
        )

    sleep_ms(5)

8.4 Unterrichtliche Auswertung

9. MCP23017 und PCF8574 im Vergleich

EigenschaftMCP23017PCF8574
Portpins168
PortprinzipEchte Richtungsregister; Push-Pull-AusgängeQuasi-bidirektionale Pins ohne separates Richtungsregister
Eingang konfigurierenIODIR-Bit auf 1 setzenEine 1 an den Pin schreiben und anschließend lesen
Ausgang HIGHAktiv getriebener HIGH-PegelSchwache interne Stromquelle nach dem Umschaltimpuls
Interne Pull-upsJe Eingang über GPPU zuschaltbarKeine frei konfigurierbaren Pull-up-Widerstände
EingangsinvertierungBitweise mit IPOLKein eigenes Register
InterruptausgängeINTA und INTB; umfangreich konfigurierbarEin gemeinsamer INT-Ausgang
InterruptquelleINTF und INTCAPPort lesen und alten mit neuem Zustand vergleichen
RegisterumfangRichtung, Polarität, Pull-up, Interrupt, GPIO und Latch getrenntEinfaches Byte-Lese-/Schreibprinzip
Geeignet fürKomplexere Bedienfelder und gemischte Ein-/AusgängeEinfache Erweiterungen und didaktischer I²C-Einstieg

10. Typische Fehler und Fehlersuche

BeobachtungMögliche UrsacheLösung
Scan findet nichtsSDA/SCL vertauscht, keine Masse, RESET LOW, fehlende Pull-upsSpannungen, RESET und Leitungen prüfen
Adresse ist nicht 0x20A0, A1 oder A2 liegen HIGH oder schwebenAdresspins auf definierte Pegel legen
Adresse bleibt unverändert oder wechselt unerwartetFehlerhaftes Dupont-Kabel oder schlechter Kontakt auf dem Breadboard; die Litze kann unzuverlässig mit Stecker oder Buchse verpresst seinSchaltung spannungsfrei machen, Kabel mit dem Multimeter auf Durchgang prüfen und dabei vorsichtig bewegen; auffällige Kabel aussortieren
Eingang wechselt zufälligPin ist offenGPPU einschalten oder externen Widerstand verwenden
Andere Ausgänge ändern sich mitRestliches Ausgangsbyte überschriebenSchattenregister beziehungsweise OLAT verwenden
Interrupt bleibt aktivGPIO/INTCAP nicht gelesen oder DEFVAL-Abweichung besteht weiterINTF prüfen und INTCAP des richtigen Ports lesen
Viele Interrupts je TastendruckTasterprellenSoftware- oder Hardware-Entprellung
MicroPython wird instabilI²C oder Ausgaben direkt im IRQ-HandlerIm Handler nur Merker setzen
GPA7/GPB7 nicht als Eingang nutzbarAktuelle DatenblattbeschränkungDiese Pins als Ausgänge verwenden
Relais oder Motor unzuverlässigGPIO überlastet, keine FreilaufdiodeTreiberstufe, separate Lastversorgung und Freilaufdiode

11. Zusammenfassung

12. Quellen

  1. Microchip-Produktseite MCP23017
  2. Microchip-Datenblatt DS20001952D
  3. MicroPython: machine.I2C
  4. Adafruit CircuitPython MCP230xx

Stand der technischen Prüfung: 27.07.2026. Maßgeblich ist stets das Datenblatt der tatsächlich eingesetzten Bauteilrevision.