Port Expander MCP23017 - [Teil 3]

Wir nehmen den MCP23017 unter die Lupe - [Teil 3]

Im ersten Teil unserer Reihe über den Port Expander haben wir mit Hilfe einer Adafruit Bibliothek unseren Arduino Uno in der IDE programmiert. Dabei haben uns die Programmierer von Adafruit die Schwerstarbeit mit den Registern abgenommen.

Im zweiten Teil haben wir den gleichen Versuchsaufbau mit dem Raspberry Pi verwendet und die simulierte „Alarmanlage“ mit Python programmiert. Hier haben wir das Modul smbus kennen gelernt, dass uns Zugang zum I2C-Bus gewährt. Die im Programm verwendeten Register-Nummern habe ich dabei aus dem Datenblatt entnommen, um Sie nicht gleich mit dem 48-seitigen „datasheet“ in englischer Sprache zu erschlagen oder abzuschrecken.

Jetzt im dritten Teil wollen wir dann doch einen Blick in das Datenblatt werfen, um die vielen Möglichkeiten des MCP23017 zu entdecken. Als Versuchsaufbau dient uns eine 4x7-Segment-Anzeige mit 36 „Beinchen“.



Auf dem Steckbrett sieht das Ganze sehr unübersichtlich aus, denn 32 Anschlüsse müssen mit den GPIO Pins von zwei MCP23017 verbunden werden, und vier Anschlüsse mit GND. Diese Verdrahtung würde ja nicht auf dem Steckbrett, sondern unterhalb der Anzeige erfolgen und in einem Gehäuse verschwinden.

Es gibt im Sortiment auch fertige Module mit eingebauten Chips, bei denen sich die Verdrahtung auf wenige Anschlüsse für Stromversorgung und Datenübertragung reduziert:

4 Bit Digital Tube LED Display Modul I2C mit Clock Display für Arduino und Raspberry Pi

MAX7219 Led Modul 8 Bit 7-Segmentanzeige LED Display für Arduino und Raspberry Pi

Aber wir wollen ja gerade die vielen Anschlüsse an unseren beiden MCP23017 „bewältigen“. Also, los geht’s. Wir stürzen uns auf die 4x 7-Segment-Anzeige plus Dezimalpunkt mit 2x 18 Pins.


In der folgenden Tabelle habe ich zunächst links im Bild die offizielle Bezeichnung der 7 Segmente mit den Buchstanden a bis g dargestellt. Ein achter Pin wird für den Dezimalpunkt DP verwendet.

Von mir selbst gewählt ist die Zählweise der Pins von links nach rechts mit den Abkürzungen o=oben und u=unten.

1. Ziffer

f = o1

g = o2

a = o3

b = o4

e = u1

d = u2

c = u3

DP = u4

GND = o5

2. Ziffer

f = o7

a = o8

b = o9

g = u7

e = u5

d = u6

c = u8

DP = u9

GND = o6

3. Ziffer

f = o10

g = o11

a = o12

b = o13

e = u10

d = u11

c = u12

DP = u13

GND = o14

4. Ziffer

f = o16

a = o17

b = o18

e = u14

d = u15

g = u16

c = u17

DP= u18

GND = o15


Die Pins der ersten und zweiten Ziffer habe ich an den MCP23017 mit der Adresse 0x20, also A0 bis A2 an GND, und die dritte und vierte Ziffer an den Port Expander mit der Adresse 0x21, also A0 an 3,3V, angeschlossen. Empfehlung: Zunächst die vier Pins 5, 6, 14 und 15 der oberen Reihe mit GND verbinden. Wenn man die GND Pins erst einmal gefunden und angeschlossen hat, findet man die Pins der einzelnen Segmente sehr schnell – einfach kurz an 3,3 V anschließen.  Bei höheren Spannungen empfehle ich den üblichen Vorwiderstand zur Strombegrenzung zu verwenden.

Die Verbindungen habe ich wie folgt gesteckt: Segment a an GPA0, Segment b an GPA1 usw. bis Dezimalpunkt an GPA7, dann die nächste Ziffer an GPB0 bis 7, usw.

Die übrigen Anschlüsse des MCP23017 bleiben wie vorher: Reset über 10 kOhm-Widerstand an VDD, VDD (Pin9) an 3,3V, VSS (Pin10) an GND, SCL (Pin12) an Pin5 des Raspi bzw. A5 des Arduino sowie SDA (Pin13) an Pin3 des Raspi bzw. A4 des Arduino.

Hier exemplarisch das Schaltbild für die erste 7-Segment-Ziffernanzeige an GPA:


Nun werfen wir einen Blick in das berühmt berüchtigte Datenblatt. Hier noch einmal der Link.

Auf Seite 12 finden wir eine Tabelle mit den Register-Adressen, die wir zum Teil schon verwendet haben. Die Namen sind Abkürzungen für den Verwendungszweck des jeweiligen Registers. Wir erkennen schnell die elf Registerpaare für PortA und PortB, die sich jeweils im letzten Buchstaben unterscheiden.

Kennengelernt haben wir im zweiten Teil schon IODIRA, GPIOA und GPPUA bzw. statt A ein B am Ende. Es macht Sinn, die Registeradressen am Anfang des Programms als Variable gleichen Namens festzulegen. In der Tabelle werden zwei Möglichkeiten für die Registeradressen gezeigt. Wir verwenden die Voreinstellung in der mittleren Spalte mit der fortlaufenden Nummerierung.


Ab Seite 12 im Datenblatt werden die einzelnen Register erklärt. Ich habe von dort die vollen Namen der Register kopiert. Für uns sind am Anfang nur die fettgedruckten Namen von Bedeutung. Die übrigen Register werden von fortgeschrittenen Programmierern für die sogenannten Interrupts verwendet.


In unserer Schaltung verwenden wir ja zwei Port Expander. Deshalb definieren wir in unserem Programm zu Beginn die zwei verwendeten Adressen 0x20 und 0x21 als DEVICE0 und DEVICE1. Damit werden die beiden MCP23017 adressiert. Die Registeradressen unterscheiden sich nicht. Diesen Block kann man leicht in andere Programme kopieren. Hier ändert sich nichts.

Dann erklären wir alle GPIOs zu Ausgängen. Dafür schreiben wir in die IODIR-Register 0x00, also Bit=0 für Ausgang, Bit=1 für Eingang wird diesmal nicht gebraucht.

Über das Register GPIOA bzw. GPIOB hatten wir die Zustände der GPIOs als Eingang gelesen. Nun wollen wir diese ja als Ausgang benutzen. Aber halt: Dafür beschreibt man nicht die GPIO-Register! Das wäre ja zu leicht. Für die Ausgabe werden die sogenannten Latch-Register OLATA und OLATB verwendet.

Zunächst testen wir kurz das 7-Segment-Display, indem wir alle Segmente sowie die Dezimalpunkte einschalten, also lauter Einsen im Dualsystem. Angezeigt wird dann „8.“ Danach löschen wir die Anzeige, also lauter Nullen im Dualsystem. Wie man im Programm erkennt, ist es dem Computer völlig egal, in welchem Zahlenformat wir die Angaben übergeben. Wir müssen nur die richtige Nomenklatur verwenden, also 0x vor den Hexadezimalzahlen und 0b vor den Dualzahlen, die uns ja am einfachsten die 8 Bits und damit den Zustand der GPIOs anzeigen.

Unter dem Variablennamen num ist ein sogenanntes „Dictionary“ (engl. für Wörterbuch) definiert. Bei diesem Datentyp in der Programmiersprache Python wird jeweils einem Schlüssel (key) ein bestimmter Wert (value) zugeordnet. Der Schlüssel ermöglicht den direkten Zugriff auf die zugeordneten Daten. Unsere Schlüssel sind das Leerzeichen und die Ziffern 0 bis 9, denen jeweils die Binärzahl zuordnet ist, die dazu die richtigen Segmente ansteuert. In anderen Programmiersprachen müsste man die Daten in einem Array abspeichern und über einen Index auf die Daten zugreifen. Das geht in Python einfacher.

Und in der Endlosschleife werden Uhrzeit und Datum aktualisiert, dann Stunden, Minuten, Tag und Monat jeweils in 2 einzelne Ziffern aufgeteilt und abwechselnd zur Anzeige gebracht.

Python-Programm mit 2 MCP23017 (0x20 und 0x21):

 #! /usr/bin/python3
#
#

import smbus # I2C-Bus
from time import localtime, sleep # Zeitfunktionen

bus=smbus.SMBus(1) # I2C-Bus 1 (pin 3 und 5)

DEVICE0 = 0x20 # Device Adresse (A0, A1 und A2 low)
DEVICE1 = 0x21 # Device Adresse (A0 high, A1 und A2 low)
IODIRA = 0x00 # Pin Register für die Richtung von GPA
IODIRB = 0x01 # Pin Register für die Richtung von GPB
GPIOA = 0x12 # Register für Eingabe GPA
GPIOB = 0x13 # Register für Eingabe GPB
OLATA = 0x14 # Register für Ausgabe GPA
OLATB = 0x15 # Register für Ausgabe GPB

bus.write_byte_data(DEVICE0,IODIRA,0x00) # alle GPA pins als Ausgang
bus.write_byte_data(DEVICE0,IODIRB,0x00) # alle GPB pins als Ausgang
bus.write_byte_data(DEVICE1,IODIRA,0x00) # alle GPA pins als Ausgang
bus.write_byte_data(DEVICE1,IODIRB,0x00) # alle GPB pins als Ausgang

bus.write_byte_data(DEVICE0,OLATA,0x00) # alle GPA pins auf 0
bus.write_byte_data(DEVICE0,OLATB,0x00) # alle GPB pins auf 0
bus.write_byte_data(DEVICE1,OLATA,0x00) # alle GPA pins auf 0
bus.write_byte_data(DEVICE1,OLATB,0x00) # alle GPB pins auf 0

bus.write_byte_data(DEVICE0,OLATB,0b11111111) # alle GPA pins zeigen 8.
bus.write_byte_data(DEVICE0,OLATA,0b11111111) # alle GPA pins zeigen 8.
bus.write_byte_data(DEVICE1,OLATB,0b11111111) # alle GPA pins zeigen 8.
bus.write_byte_data(DEVICE1,OLATA,0b11111111) # alle GPA pins zeigen 8.
sleep(1)
bus.write_byte_data(DEVICE0,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE0,OLATA,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATA,0) # alle GPA pins zeigen nichts an.

num = {' ':(0b00000000),
0:(0b00111111),
1:(0b00000110),
2:(0b01011011),
3:(0b01001111),
4:(0b01100110),
5:(0b01101101),
6:(0b01111100),
7:(0b00000111),
8:(0b01111111),
9:(0b01101111)}

while True:
DTG = localtime()
Tag = DTG.tm_mday
Monat = DTG.tm_mon
Stunde = DTG.tm_hour
Minute = DTG.tm_min
Tag1 = int(Tag/10)
Tag2 = Tag%10
Monat1 = int(Monat/10)
Monat2 = Monat%10
Stunde1 = int(Stunde/10)
Stunde2 = Stunde%10
Minute1 = int(Minute/10)
Minute2 = Minute%10

bus.write_byte_data(DEVICE0,OLATB,num[Tag1]) # alle GPA pins zeigen 1. Stelle Tag an
bus.write_byte_data(DEVICE0,OLATA,num[Tag2]+128) # alle GPA pins zeigen 2.Stelle Tag an
bus.write_byte_data(DEVICE1,OLATB,num[Monat1]) # alle GPA pins zeigen 1.Stelle Monat an
bus.write_byte_data(DEVICE1,OLATA,num[Monat2]+128) # alle GPA pins zeigen 2.Stelle Monat an
sleep(0.84)

bus.write_byte_data(DEVICE0,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE0,OLATA,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATA,0) # alle GPA pins zeigen nichts an.
sleep(0.1)

bus.write_byte_data(DEVICE0,OLATB,num[Stunde1]) # alle GPA pins zeigen 1. Stelle Tag an
bus.write_byte_data(DEVICE0,OLATA,num[Stunde2]+128) # alle GPA pins zeigen 2.Stelle Tag an
bus.write_byte_data(DEVICE1,OLATB,num[Minute1]) # alle GPA pins zeigen 1.Stelle Monat an
bus.write_byte_data(DEVICE1,OLATA,num[Minute2]) # alle GPA pins zeigen 2.Stelle Monat an
sleep(0.84)

bus.write_byte_data(DEVICE0,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE0,OLATA,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATB,0) # alle GPA pins zeigen nichts an.
bus.write_byte_data(DEVICE1,OLATA,0) # alle GPA pins zeigen nichts an.
sleep(0.1)

Damit geht diese kleine Reihe zum MCP23017 zu Ende. Wir haben gesehen, wie wir beim Arduino mit Hilfe der Adafruit-Bibliothek und beim Raspberry Pi mit Hilfe des Moduls smbus und dem Datenblatt Register des Port Expanders lesen oder beschreiben können. Noch mehr Möglichkeiten bietet der MCP23017 bei der Nutzung von Interrupts, die wir hier nicht betrachtet haben.

Alles, was wir hier über die Register gelernt haben, gilt auch für die SPI-Variante des Port Expanders MCP23S17.

Warum hat sich eigentlich niemand gewundert, dass für die I2C-Adressen nur 7 Bits zur Verfügung stehen? Wir haben doch gesehen, dass 1 Byte = 8 Bits sind.

Dazu sagt Wikipedia:

„Eine Standard-I²C-Adresse ist das erste vom Master gesendete Byte, wobei die ersten sieben Bit die eigentliche Adresse darstellen und das achte Bit (R/W-Bit) dem Slave mitteilt, ob er Daten vom Master empfangen soll (LOW) oder Daten an den Master zu übertragen hat (HIGH). I²C nutzt daher einen Adressraum von 7 Bit, was bis zu 112 Knoten auf einem Bus erlaubt (16 der 128 möglichen Adressen sind für Sonderzwecke reserviert).“

Auf vielfachen Wunsch geht es hier zum Download.

1 Kommentar

Heiko Lehmann

Heiko Lehmann

Hallo an das tolle Team von AZ-Delivery,
tolle Beiträge. lese ich immer wieder gern.
Der MCP23017 ist schon ein tolles “Steinchen”. Habe damit auch schon einige Schaltungen gebaut.
Es fehlen ja doch immer irgendwie ein oder zwei Pin´s.
Zur Ansteuerung einer 7-Segment Anzeige, 8 Stellig, benutze ich den MAX7219.
Ist schon klar das ihr hier den MCP23017 erklären wolltet.
Vieleicht schreibt ihr auch nochmal einen Blogbeitrag über den MAX7219.
Oder ihr habt es schon und ich hab ihn noch nicht gefunden.

Schöne Grüße aus dem Norden
und ganz wichtig MACHT WEITER SO

Einen Kommentar hinterlassen

Alle Kommentare werden vor der Veröffentlichung moderiert