Arduino und I2C - MCP23017


Gepostet September 2017, Kategorie: I2C & SPI


Arduino und I2C - MCP23017

Der MCP23017 ist ein 16-Bit I/0-Expander, der über das serielle Protokoll I2C angesteuert wird. In diesem Post geht es darum, wie dieser Chip vom Arduino angesprochen werden kann.


Inhaltsverzeichnis


    Der MCP23017 ist ein 16-Bit I/O-Expander, der über das serielle Protokoll I2C angesteuert wird. Dieser Chip besitzt 16 Pins, die als Aus- und Eingänge unabhängig voneinander programmiert werden können. Die Betriebsspannung kann sich im Bereich von 1,8 bis 5,5 V befinden. Das bedeutet, dass die Stromversorgung des Arduinos (5 V) verwendet werden kann. Die I2C-Schnittstelle kann ebenfalls auf einem 5 V-Pegel betrieben werden.

    Datenblatt: http://ww1.microchip.com/downloads/en/DeviceDoc/20001952C.pdf

    Grundaufbau

    Foto des Grundaufbaus Schaltplan des Grundaufbaus

    Der MCP23017 ist an die Stromversorgung des Arduinos angeschlossen. Außerdem werden die beiden I2C-Verbindungen zwischen Arduino und MCP23017 hergestellt. Über die Verbindung SDA werden die Daten zwischen Master (Arduino) und Slave (MCP23017) und in die umgekehrte Richtung übertragen. Über die Verbindung SCL wird das Clock-Signal vom Master an die Slaves übergeben.

    Die 16 GPIOs des MCP23017 werden wie im Bild zu sehen durchnummeriert. An Pin 0 (GPIO_A0) wird eine LED mit Vorwiderstand angeschlossen und an Pin 8 (GPIO_B0) wird ein Taster angeschlossen. Grundsätzlich kann frei gewählt werden, an welche Pins diese beiden Komponenten angeschlossen werden. Jedoch müssen diese Pins in der Programmierung korrekt eingestellt werden.

    Der Reset-Pin des MCP23017 wird auf 5 V gelegt, um einen Reset zu unterbinden. Mit Hilfe der Pins A0, A1 und A2 kann die Adresse des IC eingestellt werden. Da alle diese Pins auf Masse liegen, ist die Adresse 0 (bzw. 0x20, wenn die Arduino-Bibliothek nicht verwendet wird).

    Programmierung mit Arduino-Bibliothek

    Die verwendete Arduino-Bibliothek kann unter dem folgenden Link heruntergeladen werden: https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

    Das folgende Beispielprogramm stellt die Verbindung zwischen Arduino und MCP23017 her. Der Pin, an den der Button angeschlossen ist, wird als Eingang eingestellt und zusätzlich der interne Pullup des Moduls aktiviert. Der LED-Pin wird als Ausgang eingestellt, damit die LED ein- und ausgeschalten werden kann.

    Der Wert des Taster-Pins wird mit Hilfe des Befehls mcp.digitalRead(BTN) ausgelesen und in der Variable state zwischengespeichert. Mit Hilfe des Befehls mcp.digitalWrite() wird der LED-Pin an- oder ausgeschalten. Dies ist abhängig vom Zustand des Taster-Pins.

    Durch die Negation der Variable state leuchtet die LED, wenn der Taster gedrückt ist.

    Was ist uint8_t ?

    #include <Wire.h>
    #include "Adafruit_MCP23017.h"
     
    const uint8_t LED = 0; // LED an GPIO_A0
    const uint8_t BTN = 8; // Taster an GPIO_B0
    const uint8_t ADDRESS = 0; // Adresse des MCP, Einstellen mit A0, A1, A2
     
    Adafruit_MCP23017 mcp;
     
    void setup() {  
      mcp.begin(ADDRESS);
     
      // Button-Pin als Eingang mit Pullup
      mcp.pinMode(BTN, INPUT);
      mcp.pullUp(BTN, HIGH);
     
      // LED-Pin als Ausgang
      mcp.pinMode(LED, OUTPUT);
    }
     
    void loop() {
      // Button auslesen
      uint8_t state = mcp.digitalRead(BTN);
     
      // LED setzen, Negation des Zustandes
      mcp.digitalWrite(LED, ! state);
    }

    Bedeutung der Adresse

    Jeder I2C-Slave besitzt eine Adresse, mit der dieser vom Master über die I2C-Schnittstelle angesprochen werden kann. Damit mehrere MCP23017-ICs an einen I2C-Bus angeschlossen werden können, kann über die Pins A0, A1 und A2 diese Adresse variiert werden. Dies ist in der folgenden Tabelle zu sehen. (L…Low-Pegel, H…High-Pegel)

    A2 A1 A0 Adresse
    L L L 0x20
    L L H 0x21
    L H L 0x22
    L H H 0x23
    H L L 0x24
    H L H 0x25
    H H L 0x26
    H H H 0x27

    Wird nun, wie im folgenden Bild zu sehen, die Adresse des MCP23017 auf 0x24 geändert. So kann mit dem aktuellen Programm keine I2C-Verbindung aufgebaut werden - die LED reagiert nicht mehr auf den Tastendruck.

    Verändern der Adresse - Schaltplan

    Um die Verbindung wieder aufbauen zu können, muss das Programm angepasst werden:

    const uint8_t ADDRESS = 4;

    Blinkende LED

    Um die Funktionsweise der I2C-Schnittstelle zu verstehen wird im Folgenden auf die Verwendung der Arduino-Bibliothek verzichtet. Im ersten Beispiel soll eine LED zum Blinken gebracht werden, die an Pin 0 (GPIO_A0) angeschlossen ist.

    Hierfür wird die vorinstallierte Arduino-Bibliothek Wire (siehe https://www.arduino.cc/en/Reference/Wire) verwendet, die die Ansteuerung der I2C-Schnittstelle übernimmt. Würde man auch auf diese Bibliothek verzichten wollen, so müssten direkt die Register und Interrupts des ATmega328p verwendet werden.

    #include <Wire.h>
     
    const uint8_t MCP_ADDRESS = 0x20;
    const uint8_t MCP_GPIOA = 0x12;
    const uint8_t MCP_IODIRA = 0x00;
     
    void setup() {
      Wire.begin();
      writeRegister(MCP_IODIRA, 0b11111110);
    }
     
    void loop() {
      writeRegister(MCP_GPIOA, 0b00000001);
      delay(500);
      writeRegister(MCP_GPIOA, 0b00000000);
      delay(500);
    }
     
    void writeRegister(uint8_t address, uint8_t value) {
      Wire.beginTransmission(MCP_ADDRESS);
      Wire.write(address);
      Wire.write(value);
      Wire.endTransmission();
    }

    Für die direkte Programmierung der I2C-Schnittstelle wird die vollständige Adresse des MCP23017 benötigt. Diese Adresse wird in der Konstanten MCP_ADDRESS abgespeichert.

    Der MCP23017 besitzt eine Vielzahl von Registern, die über I2C geschrieben oder gelesen werden können. Jedem Register ist eine Adresse zugeordnet, sodass das Register über I2C ausgewählt werden kann. Eine Übersicht der Register befindet sich im Datenblatt in Tabelle 3-5 auf Seite 17. Die Register werden im Folgenden ab Seite 18 im Datenblatt einzeln beschrieben.

    In der Konstanten MCP_IODIRA befindet sich die Adresse des IODIRA-Registers. Dieses Register wird benötigt, um einzustellen, ob es sich bei einem Pin um ein Ein- oder Ausgang handelt. Jedem Bit des Wertes dieses Registers ist ein GPIO_A-Pin zugeordnet. Ist dieses Bit 1, so wird dieser Pin als Eingang genutzt. Ist das Bit 0, so wird der Pin als Ausgang genutzt.

    Durch den Befehl writeRegister(MCP_IODIRA, 0b11111110); wird dieses Register auf den Wert 0b11111110 gesetzt. Das bedeutet nur GPIO_A0 wird als Ausgang geschalten, die anderen Pins bleiben Eingänge.

    In der Konstanten MCP_GPIOA ist die Adresse des GPIOA-Registers abgelegt. Mit Hilfe dieses Register können unter anderem Ausgänge geschalten werden. Hier gilt ebenfalls, dass jedem Bit des Register ein Pin des MCP23017 zugeordnet ist.

    Daher kann durch den Befehl writeRegister(MCP_GPIOA, 0b00000001); die LED angeschaltet werden und durch den Befehl writeRegister(MCP_GPIOA, 0b00000000); wird diese wieder ausgeschalten.

    Die Funktionsweise der Funktion writeRegister() soll erst im nächsten Abschnitt zusammen mit der Funktionsweise von I2C beschrieben werden.

    Vertiefende Aufgabe
    Das Ziel dieser Aufgabe soll es sein, das Verständnis der Verwendung der Register des MCP23017 zu verbessern. Es soll mit einer zweiten LED ein Wechselblinker programmiert werden. Diese LED soll an GPIO_B3 (PIN_11) angeschlossen werden.

    Schaltplan
    Benötigte Register
    Lösung

    Das I2C-Protokoll

    Die Pullup-Widerstände

    Die I2C-Schnittstelle benötigt zwei Leitung, über die die Daten und das Clock-Signal übertragen werden. Über die Datenleitung SDA werden Daten vom Master in den Client und in umgekehrte Reihenfolge gesendet. Aus diesem Grund müssen die Pins dieser Datenleitung sogenannte Open-Drain-Ausgänge (Offener Kollektor) sein.

    Damit die Leitungen trotzdem ein Spannungslevel besitzen, werden die Pullup-Widerstände benötigt.

    I2C Aufbau
    (Quelle: https://cdn.sparkfun.com/assets/5/f/5/a/1/51adff65ce395ff71a000000.png)

    Werden diese Pullup-Widerstände nicht verwendet, dann wirken nur die internen Pullup-Widerstände des Arduinos. Dies hat zur Folge, dass das Signal nicht mehr sauber übertragen wird, wie im folgenden Bildschirmfoto zu sehen ist.
    Ohne Pullup-Widerstände

    Werden die -Widerstände eingesetzt, so ist das Signal deutlich sauberer.
    Mit Pullup-Widerstände

    Schreiben eines Registers

    In diesem Abschnitt wird beschrieben, wie die Funktion writeRegister() funktioniert. Dabei wird der Signalverlauf der beiden I2C-Leitung beschrieben.

    Der Befehl, welcher über I2C während der Aufnahme des folgenden Bildes übertragen wurde, schaltet die LED, welche sich an GPIO_A0 befindet ein. Das bedeutet, dass das Register, welches sich an der Adresse 0x12 befindet mit dem Wert 0x01 beschrieben werden muss.
    Schreiben eines Registers

    Durch den Befehl Wire.beginTransmission(0x20) wird zuerst die Startsequenz vom Master gesendet. Anschließend folgt das Übertragen der Adresse, des Slaves, der angesprochen werden soll. In diesem Fall ist das die Adresse 0x20. Anschließend wird ein Bit übertragen, welches angibt, ob auf dem Slave geschrieben wird, oder gelesen werden soll. Dieses Bit ist 0, da anschließend geschrieben wird.

    Im nächsten Schritt des Protokolls wird ein Acknowledge (ACK, Bestätigung) vom Slave verlangt. Das bedeutet, der Slave muss nun die Leitung auf Masse ziehen, wenn dieser durch die vorangegangen Adresse angesprochen wurde. Es ist im Signalverlauf des Channel A zu erkennen, dass der Slave die Leitung auf Masse zieht. Dieser zusätzliche Einbruch des Signals erhält man, wenn man einen zusätzlichen Widerstand in die Datenleitung einbringt ().

    Im folgenden werden vom Master an den Client die Daten übertragen. Als erstes wird die Adresse des Registers (0x12) übertragen. Dies geschieht im Arduino-Programm durch den Befehl Wire.write(0x12). Nach diesem Byte werden die Daten wieder durch den Slave bestätigt (ACK).

    Anschließend folgt das Byte, welches in das Register geschrieben werden soll. Diese Daten werden ebenfalls durch den Befehl Wire.write(0x01) gesendet. Nach dem ACK des Slave wird die I2C-Nachricht durch den Befehl Wire.endTransmission() beendet.

    Auslesen von Eingängen

    Aktivieren der internen Pullup-Widerstände

    Der erste Schritt, der zum Auslesen des Status des Buttons getan werden muss, ist das Aktivieren der internen Pullup-Widerstände der Pins. Die Register GPPUA und GPPUB werden hierfür benötigt (siehe Datenblatt Seite 22).

    const uint8_t MCP_GPPUA = 0x0C;
    const uint8_t MCP_GPPUB = 0x0D;

    Da der Taster an GPIO_B0 angeschlossen ist, so muss der Wert des GPPUB-Registers so gewählt werden, dass das Bit 0 1 ist, damit der Pullup-Widerstand des Tasters aktiviert ist.

    Durch den folgenden Befehl werden alle internen Pullup-Widerstände des Port B angeschalten.

    writeRegister(MCP_GPPUB, 0b11111111);

    Senden des GPIOB-Registers über UART

    Durch die Funktion readRegister() kann ein Register des MCP23017 vom Arduino über die I2C-Schnittstelle ausgelesen werden. Die Beschreibung der Funktionsweise dieser Funktion erfolgt im folgenden Abschnitt.

    Das folgende Programm sendet in regelmäßigen Abständen den Wert des GPIOB-Registers über UART. Der Wert des Registers wird durch den Befehl Serial.println(readRegister(MCP_GPIOB), BIN); im Binärformat ausgegeben.

    #include <Wire.h>
     
    const uint8_t MCP_ADDRESS = 0x20;
    const uint8_t MCP_IODIRA = 0x00;
    const uint8_t MCP_IODIRB = 0x01;
    const uint8_t MCP_GPPUA = 0x0C;
    const uint8_t MCP_GPPUB = 0x0D;
    const uint8_t MCP_GPIOA = 0x12;
    const uint8_t MCP_GPIOB = 0x13;
     
    void setup() {
      Wire.begin();
      writeRegister(MCP_IODIRB, 0b11111111);
      writeRegister(MCP_GPPUB, 0b11111111);
     
      Serial.begin(9600);
    }
     
    void loop() {
      Serial.println(readRegister(MCP_GPIOB), BIN);
      delay(250);
    }
     
    void writeRegister(uint8_t address, uint8_t value) {
      Wire.beginTransmission(MCP_ADDRESS);
      Wire.write(address);
      Wire.write(value);
      Wire.endTransmission();
    }
     
    uint8_t readRegister(uint8_t address) {
      Wire.beginTransmission(MCP_ADDRESS);
      Wire.write(address);
      Wire.endTransmission();
     
      Wire.requestFrom(MCP_ADDRESS, (uint8_t) 1);
      return Wire.read();
    }

    Über die seriellen Monitor können die folgenden Daten empfangen werden.
    Ausgabe über den seriellen Monitor

    Wenn der Taster nicht gedrückt ist, so kommt es zur Ausgabe 11110111. Es ist zu erkennen, dass durch die internen Pullup-Widerstände sich alle Pins auf H-Pegel befinden. Eine Ausnahme ist der Pin B3, an dem eine zweite LED angeschlossen ist, die diesen Pin auf Masse zieht.

    Wird der Taster gedrückt, so wird die Ausgabe 11110110 ausgegeben. Durch das Drücken des Tasters wird zusätzlich der Pin B0 auf L-Pegel gezogen.

    Lesen eines Registers - I2C-Protokoll

    Im folgenden wird der Signalverlauf der I2C-Verbindungen analysiert, der während des Ausführens der Funktion readRegister() aufgenommen wurde.

    Signalverlauf während des Lesens eines Registers

    Das Lesen des Registers findet mit Hilfe von zwei verschiedenen I2C-Nachrichten statt. Die erste Nachricht ist eine typische WRITE-Nachricht, wie sie auch schon beim Schreiben eines Registers verwendet wurde. Jedoch wird hier nur ein Byte innerhalb der I2C-Nachricht übertragen. Dieses Byte enthält die Adresse des Registers, welches gelesen werden soll.

    Diese I2C-Nachricht wird im Arduino-Programm durch die folgenden Befehlszeilen gesendet.

    Wire.beginTransmission(MCP_ADDRESS);
    Wire.write(address);
    Wire.endTransmission();

    Im Anschluss an diese I2C-Nachricht wird eine READ-Nachricht übertragen. Hierfür wird wie bei I2C üblich zuerst die Adresse des Slaves über die I2C-Verbindungen gesendet. Das R/W-Bit ist bei dieser Nachricht H, sodass gelesen wird. Nachdem der Slave die Adresse bestätigt (ACK), sendet dieser die verlangten Daten. Im gezeigten Signalverlauf wird das Byte 0x7f (= 0b11110111) übertragen. Im Signalverlauf ist zu erkennen, dass der Client die I2C-Leitung auf Masse zieht, wenn eine 0 übertragen wird.

    Wurde dieses Byte übertragen, so wird dieses vom Master nicht bestätigt (NACK). Das signalisiert dem Slave das Ende der Nachricht und das keine weiteren Bytes übertragen werden sollen.

    Die I2C-READ-Nachricht wird durch den Befehl Wire.requestFrom(MCP_ADDRESS, (uint8_t) 1); übertragen bzw. angefragt. Das zweite Argument dieser Anweisung steht für die Anzahl der Bytes, die vom Slave an den Master übertragen werden sollen.

    Durch die Anweisung Wire.read() wird ein Byte aus dem Dateneingangsbuffer des Masters abgerufen.

    LED durch Taster schalten

    In diesem Abschnitt ist das Programm zu sehen, welches die Funktionalität des ersten Beispielprogrammes besitzt. Das bedeutet eine LED soll angeschaltet werden, wenn ein Taster gedrückt wird.

    #include <Wire.h>
     
    const uint8_t MCP_ADDRESS = 0x20;
    const uint8_t MCP_IODIRA = 0x00;
    const uint8_t MCP_IODIRB = 0x01;
    const uint8_t MCP_GPPUA = 0x0C;
    const uint8_t MCP_GPPUB = 0x0D;
    const uint8_t MCP_GPIOA = 0x12;
    const uint8_t MCP_GPIOB = 0x13;
     
    void setup() {
      Wire.begin();
      writeRegister(MCP_IODIRA, 0b11111110);
      writeRegister(MCP_IODIRB, 0b11111111);
      writeRegister(MCP_GPPUB, 0b11111111);
    }
     
    void loop() {
      uint8_t reg = readRegister(MCP_GPIOB);
     
      if (reg & 0b00000001) {
        writeRegister(MCP_GPIOA, 0b00000000);
      } else {
        writeRegister(MCP_GPIOA, 0b00000001);
      }
    }
     
    void writeRegister(uint8_t address, uint8_t value) {
      Wire.beginTransmission(MCP_ADDRESS);
      Wire.write(address);
      Wire.write(value);
      Wire.endTransmission();
    }
     
    uint8_t readRegister(uint8_t address) {
      Wire.beginTransmission(MCP_ADDRESS);
      Wire.write(address);
      Wire.endTransmission();
     
      Wire.requestFrom(MCP_ADDRESS, (uint8_t) 1);
      return Wire.read();
    }
     
    Wetterdaten Eilenburg
    Temperatur: 14.6 °C
    Luftdruck: 1030.1 hPa
    Luftfeuchte: 68 %
    [mehr]