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.
2. Technische Daten
| Merkmal | Wert beziehungsweise Bedeutung |
|---|---|
| Baustein | MCP23017 – 16-Bit-I/O-Expander mit I²C-Schnittstelle |
| Betriebsspannung | 1,8 V bis 5,5 V |
| I²C-Taktraten | 100 kHz, 400 kHz und bis 1,7 MHz; die zulässige Höchstgeschwindigkeit hängt auch von der Betriebsspannung ab |
| GPIO-Aufteilung | PORTA: GPA0…GPA7; PORTB: GPB0…GPB7 |
| Adressauswahl | Drei Hardwarepins A0, A1 und A2; dadurch acht Adressen 0x20…0x27 |
| Zustand nach Reset | Richtungsregister grundsätzlich auf Eingang; Pull-ups und Interrupts aus; Ausgangslatches auf 0 |
| Interne Pull-ups | Einzeln zuschaltbar; typischer Pull-up-Strom laut Datenblatt etwa 75 µA bei 5 V |
| Interruptausgänge | INTA und INTB; aktiv HIGH, aktiv LOW oder Open Drain; getrennt oder gespiegelt |
| Maximalwert je Ausgang | 25 mA Source oder Sink als absoluter Grenzwert, nicht als empfohlener Dauerstrom |
| Garantierte Ausgangswerte | Bei 4,5 V unter anderem LOW bei 8 mA und HIGH bei 3 mA spezifiziert |
| Gesamtstromgrenzen | Maximal 150 mA über VSS und 125 mA über VDD; ebenfalls absolute Grenzwerte |
| Gehäuse | Unter anderem 28-poliges SPDIP, SOIC, SSOP und QFN |
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.
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.
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.
| MCP23017 | ESP32 / Beschaltung | Hinweis |
|---|---|---|
| VDD | 3,3 V | So bleiben I²C- und GPIO-Pegel zum ESP32 kompatibel. |
| VSS | GND | Gemeinsame Masse ist zwingend erforderlich. |
| SDA | GPIO21, gelb | Auf dem CJMCU-2317 über 10 kΩ nach VCC gezogen. |
| SCL | GPIO22, grün | Auf dem CJMCU-2317 über 10 kΩ nach VCC gezogen. |
| A0, A1, A2 | GND | Ergibt 0x20. Die Adresspins dürfen nicht offen bleiben. |
| RESET | Über etwa 10 kΩ nach 3,3 V | RESET ist aktiv LOW und muss einen definierten Pegel erhalten. |
| INTA | beispielsweise GPIO27 | Bei Open Drain mit etwa 10 kΩ nach 3,3 V ziehen. |
| INTB | optional weiterer ESP32-GPIO | Kann getrennt genutzt oder mit INTA gespiegelt werden. |
3.1 I²C-Adressen
| A2 | A1 | A0 | Adresse |
|---|---|---|---|
| 0 | 0 | 0 | 0x20 |
| 0 | 0 | 1 | 0x21 |
| 0 | 1 | 0 | 0x22 |
| 0 | 1 | 1 | 0x23 |
| 1 | 0 | 0 | 0x24 |
| 1 | 0 | 1 | 0x25 |
| 1 | 1 | 0 | 0x26 |
| 1 | 1 | 1 | 0x27 |
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-Bit | Wirkung |
|---|---|
1 | Pin ist Eingang; der Ausgangstreiber ist hochohmig. |
0 | Pin 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.
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.
GPIOA/GPIOBlesen: tatsächlichen Pegel an den Portpins lesen.OLATA/OLATBlesen: gespeicherten Sollwert der Ausgangslatches lesen.GPIOA/GPIOBschreiben: schreibt praktisch in das Ausgangslatch.OLATA/OLATBschreiben: setzt gezielt die Ausgangslatches.
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 A | Port B | Register | Aufgabe |
|---|---|---|---|
| 0x00 | 0x01 | IODIRA / IODIRB | Richtung: 1 = Eingang, 0 = Ausgang |
| 0x02 | 0x03 | IPOLA / IPOLB | Eingangspolarität invertieren |
| 0x04 | 0x05 | GPINTENA / GPINTENB | Interrupt für einzelne Pins freigeben |
| 0x06 | 0x07 | DEFVALA / DEFVALB | Sollwert für Interruptvergleich |
| 0x08 | 0x09 | INTCONA / INTCONB | Vergleich gegen früheren Wert oder DEFVAL |
| 0x0A | 0x0B | IOCON | Bank, Spiegelung, Open Drain, Polarität |
| 0x0C | 0x0D | GPPUA / GPPUB | Interne Pull-ups einschalten |
| 0x0E | 0x0F | INTFA / INTFB | Interruptquelle anzeigen |
| 0x10 | 0x11 | INTCAPA / INTCAPB | Zustand beim Interrupt erfassen und Interrupt löschen |
| 0x12 | 0x13 | GPIOA / GPIOB | Aktuelle Pinpegel lesen |
| 0x14 | 0x15 | OLATA / OLATB | Ausgangslatches lesen und schreiben |
5.1 Wichtige IOCON-Bits
| Bit | Name | Bedeutung |
|---|---|---|
| 7 | BANK | 0 = A-/B-Register abwechselnd; 1 = nach Ports getrennt |
| 6 | MIRROR | 1 = INTA und INTB melden Ereignisse beider Ports |
| 5 | SEQOP | 1 = automatische Adressfortschaltung deaktiviert |
| 2 | ODR | 1 = Interruptausgänge arbeiten als Open Drain |
| 1 | INTPOL | Aktive 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.
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.
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-Bit | Vergleich | Einsatz |
|---|---|---|
| 0 | Aktueller Eingang gegen den zuvor erfassten Portzustand | Interrupt bei jeder Änderung |
| 1 | Aktueller Eingang gegen DEFVAL | Interrupt bei Abweichung von einem Sollwert |
7.2 Ablauf
- GPINTEN schaltet die Überwachung je Pin ein.
- INTCON legt die Vergleichsart fest.
- DEFVAL enthält bei Bedarf den Sollzustand.
- Bei einer Abweichung wird INTA oder INTB aktiv.
- INTF zeigt die auslösenden Pins.
- INTCAP enthält den Portzustand im Moment des Ereignisses.
- Das Lesen von INTCAP oder GPIO löscht den Interrupt des betreffenden Ports.
7.3 INTA und INTB
- Standardmäßig gehört INTA zu PORTA und INTB zu PORTB.
- Mit MIRROR = 1 melden beide Ausgänge Ereignisse beider Ports.
- Push-Pull aktiv LOW oder aktiv HIGH ist möglich.
- Open Drain erlaubt einen gemeinsamen Interruptdraht und saubere Pegelanpassung.
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
- ESP32-Entwicklungsboard
- MCP23017 oder Breakout-Modul
- sieben Taster
- sieben LEDs mit Vorwiderständen, beispielsweise 680 Ω bis 1 kΩ bei 3,3 V
- Pull-ups für SDA, SCL und Open-Drain-INTA, falls nicht auf dem Modul vorhanden
- 10-kΩ-Widerstand für RESET nach 3,3 V
8.2 Verdrahtung
| Funktion | Pins | Beschaltung |
|---|---|---|
| Taster 1…7 | GPA0…GPA6 | Je ein Taster zwischen Pin und GND; interner Pull-up ein |
| LED 1…7 | GPB0…GPB6 | Pin → Vorwiderstand → LED → GND |
| Interrupt | INTA | Zu 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
- Warum zeigt ein nicht gedrückter Taster eine 1?
- Welche Bits ändern sich in GPIOA?
- Wie speichert OLATB den LED-Zustand?
- Was steht im INTCAPA-Register beim Ereignis?
- Warum wird im IRQ-Handler nur ein Merker gesetzt?
- Welche Tasterprellungen sind in den Ausgaben sichtbar?
9. MCP23017 und PCF8574 im Vergleich
| Eigenschaft | MCP23017 | PCF8574 |
|---|---|---|
| Portpins | 16 | 8 |
| Portprinzip | Echte Richtungsregister; Push-Pull-Ausgänge | Quasi-bidirektionale Pins ohne separates Richtungsregister |
| Eingang konfigurieren | IODIR-Bit auf 1 setzen | Eine 1 an den Pin schreiben und anschließend lesen |
| Ausgang HIGH | Aktiv getriebener HIGH-Pegel | Schwache interne Stromquelle nach dem Umschaltimpuls |
| Interne Pull-ups | Je Eingang über GPPU zuschaltbar | Keine frei konfigurierbaren Pull-up-Widerstände |
| Eingangsinvertierung | Bitweise mit IPOL | Kein eigenes Register |
| Interruptausgänge | INTA und INTB; umfangreich konfigurierbar | Ein gemeinsamer INT-Ausgang |
| Interruptquelle | INTF und INTCAP | Port lesen und alten mit neuem Zustand vergleichen |
| Registerumfang | Richtung, Polarität, Pull-up, Interrupt, GPIO und Latch getrennt | Einfaches Byte-Lese-/Schreibprinzip |
| Geeignet für | Komplexere Bedienfelder und gemischte Ein-/Ausgänge | Einfache Erweiterungen und didaktischer I²C-Einstieg |
10. Typische Fehler und Fehlersuche
| Beobachtung | Mögliche Ursache | Lösung |
|---|---|---|
| Scan findet nichts | SDA/SCL vertauscht, keine Masse, RESET LOW, fehlende Pull-ups | Spannungen, RESET und Leitungen prüfen |
| Adresse ist nicht 0x20 | A0, A1 oder A2 liegen HIGH oder schweben | Adresspins auf definierte Pegel legen |
| Adresse bleibt unverändert oder wechselt unerwartet | Fehlerhaftes Dupont-Kabel oder schlechter Kontakt auf dem Breadboard; die Litze kann unzuverlässig mit Stecker oder Buchse verpresst sein | Schaltung spannungsfrei machen, Kabel mit dem Multimeter auf Durchgang prüfen und dabei vorsichtig bewegen; auffällige Kabel aussortieren |
| Eingang wechselt zufällig | Pin ist offen | GPPU einschalten oder externen Widerstand verwenden |
| Andere Ausgänge ändern sich mit | Restliches Ausgangsbyte überschrieben | Schattenregister beziehungsweise OLAT verwenden |
| Interrupt bleibt aktiv | GPIO/INTCAP nicht gelesen oder DEFVAL-Abweichung besteht weiter | INTF prüfen und INTCAP des richtigen Ports lesen |
| Viele Interrupts je Tastendruck | Tasterprellen | Software- oder Hardware-Entprellung |
| MicroPython wird instabil | I²C oder Ausgaben direkt im IRQ-Handler | Im Handler nur Merker setzen |
| GPA7/GPB7 nicht als Eingang nutzbar | Aktuelle Datenblattbeschränkung | Diese Pins als Ausgänge verwenden |
| Relais oder Motor unzuverlässig | GPIO überlastet, keine Freilaufdiode | Treiberstufe, separate Lastversorgung und Freilaufdiode |
11. Zusammenfassung
- Der MCP23017 erweitert einen I²C-Mikrocontroller um zwei 8-Bit-Ports.
- IODIR legt die Richtung eindeutig fest.
- Eingänge besitzen einzeln schaltbare schwache Pull-ups.
- GPIO liest den tatsächlichen Pinpegel, OLAT den gespeicherten Ausgangswert.
- INTA und INTB melden Änderungen oder Abweichungen von einem Sollwert.
- INTF identifiziert die Quelle; INTCAP speichert den Zustand beim Ereignis.
- Die 25-mA-Angabe ist ein absoluter Grenzwert und kein normaler Betriebsstrom.
- GPA7 und GPB7 werden nach aktuellem Datenblatt vorsichtshalber nur als Ausgänge eingeplant.
12. Quellen
- Microchip-Produktseite MCP23017
- Microchip-Datenblatt DS20001952D
- MicroPython: machine.I2C
- Adafruit CircuitPython MCP230xx
Stand der technischen Prüfung: 27.07.2026. Maßgeblich ist stets das Datenblatt der tatsächlich eingesetzten Bauteilrevision.