Raspberry-Pi-Projekte: I2C-IO-Expander mit MCP23017

Prof. Jürgen Plate, Christoph Holzleitner, Christoph Sporer, Tobias Steinhart

Raspberry Pi: I2C-IO-Expander mit MCP23017

Allgemeines

Der Raspberry Pi besitzt nicht allzuviele Ein- und Ausgabeleitungen an seinem GPIO-Port. So kommt man schnell in Bedränglis, wenn mehr als ein paar digitale Leitungen benötigt werden. Abhilfe bieten die beiden IO-Busse I2C und SPI. Für den ersteren wird mit dem Baustein MCP23017SP von Microchip ein güstiger und allgemein verwendbarer IO-Expander angeboten, der 16 Ein- oder Ausgabeleitungen bietet. Der Chip hat zusätzlich einen Interrupt-Ausgang und es können bis zu acht MCP23017 an einem Bus betrieben werden (also max. 128 Leitungen). Die Ausgangstreiber könen bis zu 25 mA Strom liefern, was z. B. für den direkten Anschluss von LEDs ausreicht. Für höhere Ströme sind entsprechende Treiberbausteine notwendig. Außerdem arbeitet der Baustein bereits ab 1,8 V Versorgungsspannung einwandfrei, was für den RasPi ideal ist.

Hardware

Das folgende Bild zeigt den Schaltplan der I2C-Erweiterungsplatine mit dem Baustein MCP23017SP. Die 16 digitalen Ports sind auf die Anschlussleisten JP1 und JP2 herausgeführt. Zusätzlich werden die Signale über zwei Treiber 74HC541 auf Anzeige-LEDs geführt, so dass sich die Ein- und Ausgabe auch visuell verfolgen läßt. Über eine Adressierung von bis zu acht der genannten Bausteine über einen 3-Bit-Dippschalter SW1 kann die Subadresse des Bausteins eingestellt werden. So steht einer möglichen Erweiterung auf acht Platinen nichts im Wege. Die LEDs leichten jeweils bei einem High-Signal. Die Verbindung zum I2C-Bus stellt JP3 her, der genauso belegt ist, wie bei den anderen I2C-Projekten.

Das Bild unten zeigt das Platinenlayout. Die IO-Ports befinden sich an der Platinenoberseite, der Anschluß zum Raspberry Pi am linken Rand. Wegen des ggf. etwas höheren Strombedarfs (wenn alle Ports auf "1" stehen und alle LEDs leuchten) kann die Platine auch aus einer externen Stromquelle versorgt werden.

Da der Baustein MCP23017SP nicht über Pull-Down-Widerstände verfügt, werden alle als Eingang deklarierten Pins über einen Pull-Up-Widerstand auf HIGH-Pegel gezogen (Negative Logik), um einen undefinierten Zustand oder das Auffangen von Störungen auszuschließen. Die Eingangspins müssen dann also aktiv gegen Masse gezogen werden.

Die Ein- und Ausgaberegister werden im Datenblatt als "GPIO" bezeichnet, was nichts mit dem GPIO-Port des Raspberry Pi zu tun hat. Es handelt sich einfach um einen 16 Bit breiten, bidirektionaler Allzweck-Port, der funktionell in zwei 8-Bit-Ports aufgeteilt ist. Er besteht aus den Daten-Ports (GPIOA und GPIOB), zuschaltbaren internen Pull-up-Widerständen und den Ausgangszwischenspeichern (OLATA und OLATB). Das Lesen der GPIO-Register liest den Wert auf den Eingansleitungen. Das Lesen der OLAT-Register liest nur den Inhalt der Ausgabe-Latches, nicht der tatsächliche Wert am Port. Schreiben in die GPIO-Register verursacht in Wirklichkeit das Schreiben in die OLAT-Latches. Schreiben in die OLAT-Latches bewirkt die Ausgabe des Wertes über die zugehörigen Ausgangstreiber an den Pins des Bausteins. Bei Pins, die als Eingänge konfiguriert wurden, ist jedoch der zugehörige Ausgangstreiber hochohmig geschaltet.

Der Baustein besitzt 11 Registerpaare, die über den I2C-Bus angesprochen werden können, wobei es drei Modi gibt. Die folgende Tabelle zeigt die Adressen der Register des MCP23017SP.

Die drei Modi sind:

Die Adressierung der Baustein-Register hat aber nichts mit der Adressierung auf dem I2C-Bus zu tun, sondern es besteht beispielsweise ein Schreibvorgang aus zwei aufeinanderfolgenden I2C-Schreibvogängen: zuerst wird die Registeradresse ("OP" im folgenden Bild) und dann das Register-Datenbyte übermittelt. Das folgende Bild zeigt die Adressierung der Register des MCP23017SP:

An dieser Stelle wird nur ein kurzer Überblick der Register gegeben. Wer alle Möglichkeiten des Baustein ausschöpfen will, kommt um das Studium des Datenblatts nicht herum.

IODIRA, IODIRB: steuert die Richtung der Daten. Wenn ein Bit gesetzt ist, wird der entsprechenden Pin ein Eingang. Ist ein Bit gelöscht, wird der entsprechende Pin ein Ausgang.

IPOLA, IPOLB: Dieses Register erlaubt es dem Benutzer, die Polarität der entsprechenden GPIO-Port-Bits zu konfigurieren. Wenn ein Bit gesetzt ist, wird das entsprechende GPIO-Register-Bit auf den invertierten Wert des Eingangspins gesetzt.

GPINTENA, GPINTENB:Diese Register steuern die Interrupt-On-Change-Funktion für jeden Pin. Ist ein Bit gesetzt, wird der entsprechende Pin für Interrupt-On-Change freigegeben. Es müssen zusätzlich die DEFVAL- und INTCON-Register konfiguriert werden.

DEFVALA, DEFVALB: Die Standardvorgabe für einen Eingangspin wird in den DEFVAL-Registern eingetragen. Ist der Pin als Interrupt-Pin aktiviert (über GPINTEN und INTCON) wird der Eingangswert mit den DEFVAL-Registern verglichen. Sobald ein dazu entgegengesetzten Wert beim zugehörige Pin auftritt, wird ein Interrupt ausgelöst.

INTCONA, INTCONB: Die INTCON-Register legen fest, wie der zugehörige Pin-Wert für die Interrupt-On-Change-Funktion verglichen wird. Wenn ein Bit gesetzt ist, wird der entsprechende I/O-Pin gegen das zugeordnete Bit im DEFVAL-Register verglichen. Ist das entsprechende Bit gelöscht, führt ein Wechsel des Eingangspegels zum Interrupt.

GPPUA, GPPUB: Die GPPU-Register steuern die Pull-Up-Widerstände für die Port-Pins. Ist ein Bit gesetzt und der entsprechende Stift als Eingang konfiguriert, wird der entsprechende Port-Pin intern mit einem 100-Kiloohm-Widerstand auf Vcc gezogen.

INTFA, INTFB: Die INTF-Register spiegeln die Interrupt-Bedingung auf den Port-Pins wieder, die für Unterbrechungen über die GPINTEN-Register konfiguriert wurden. Ein gesetztes Bit zeigt an, dass der zugehörige Pin einen Interrupt ausgelöst hat. Dieses Register kann nur gelesen werden. Schreibvorgänge in diese Register werden ignoriert.

INTCAPA, INTCAPB: Die INTCAP-Register erfassen den GPIO-Port-Wert zum Zeitpunkt des Interrupts. Das Register kann nur gelesen werden und es wird nur beim Auftreten eines Interrupts aktualisiert. Die Registerwerte bleiben unverändert, bis der Interrupt über das Lesen von INTCAP oder GPIO gelöscht.

GPIOA, GPIOB: Die GPIO-Register geben den Wert der Portleitungen zurück. Ein Lesen der Register entspricht dem Lesen der Portleitungen. Ein Schreiben in diese Register entspricht dem Schreiben in die Ausgangs-Latches (OLAT).

OLATA, OLATB: Die OLAT-Register erlauben den Zugriff auf die als Ausgang konfigurierten Pins. Ein Lesen von diesen Registern liefert den zuletzt hinein geschriebenen Wert, nicht die an den Pins aliegenden Daten. Ein Schreiben auf diese Register ändert die Ausgangs-Latches, die ihrerseits diejenigen Pins beeinflussen, die als Ausgänge konfiguriert sind.

IOCON: Dieses Register enthält etliche Bits für die Konfiguration des Bausteins: Das Bit 7, das BANK-Bit, ändert die Adresszuordnung der einzelnen Register (siehe Tabelle oben).

Sobald sich durch die Änderung des BANK-Bits die Adresszuordnung ändert (das geschieht sofort, nachdem das IOCON-Byte in das Device getaktet wurde) gilt die neue Zuordnung. Der Adresszeiger kann dann ggf. auf eine ungültige Stelle zeigen. Das kann der Fall sein, wenn mit dem oben erwähnten automatischen Inkrement des Adresspointers gearbeitet wird. Es könnte beispielsweise folgendes Szenario auftreten:

Bit 6, das MIRROR-Bit, steuert das Verhalten der INTA- und INTB-Pins:

Bit 5, das SEQOP-Bit, steuert das Auto-Inkrement des Adress-Pointers. Ist SEQOP = 1, ist die Inkrement-Funktion gesperrt, bei SEQOP = 0 wird der Adress-Pointers automatisch erhöht.

Bit 4, das DISSLW-Bit, steuert die Anstiegsgeschwindigkeit (Slew-Rate) auf der SDA-Leitung. Ist DISSLW = 1, wird die Slew_Rate nicht beeinflusst, ist DISSLW = 0, steuert der Baustein die SDA-Anstiegsgeschwindigkeit bei der steigenden Flanke.

Bit 3, das HAEN-Bit, aktiviert bzw. deaktiviert die Hardware-Adressierung bei der SPI-Variante des Bausteins, dem MCP23S17. Spielt hier keine Rolle.

Bit 2, das ODR-Bit, aktiviert oder deaktiviert die INT-Pins für Open-Drain-Konfiguration. Im Open-Drain-Modus ist die Schaltungstechnische Verknüpfung mit den entsperechenden Ausgängen anderer Bausteine möglich. Das Löschen dieses Bits überschreibt das INTPOL-Bit (Bit 1). Ist ODR = 1, wird der Ausgang auf open drain geschaltet und das INTPOL-Bit überschrieben. Ist ODR = 0, ist der Ausgang ein "normaler" Gegentakt-Ausgang und das INTPOL-Bit legt die Polarität fest.

Bit 1, das INTPOL-Bit, legt die Polarität der INT-Pins fest. Dieses Bit ist nur aktiv, wenn das ODR-Bit auf 0 steht. INTPOL = 1 bedeutet active-high, INTPOL = 0 active-low.

Bit 0 ist nicht implementiert, es liefert immer den Wert "0".

Software

Vorausgesetzt wird, dass die I2C-Schnittstelle bereits auf dem Raspberry Pi aktiviert wurde, wie es unter RasPi_I2C.html beschrieben ist. Auch die Zugriffrechte sollten für den User "pi" und, bei Einsatz auf einer Webseite, auch für den User "www-data" passend gesetzt sein. Die Einrichtung des Web-Interface ist unter RasPi_http.html beschrieben.

Zunächst überprüfen Sie, ob der Baustein überhaupt gefunden wird:

i2cdetect -y 1
Die Ausgabe sollte etwa folgendermaßen aussehen:
    0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Es wurde also ein I2C-Chip an der Adresse 0x20 gefunden.

Ein erster Test kann mit dem Tool i2cset erfolgen. Per Default sind alle Pins als Eingang geschaltet. Das ist sinnvoll, denn da kann nichts kaputt gemacht werden - egal, was an den Expander angeschlossen ist. Entweder es treffen zei Eingänge aufeinander oder ein Ausgang und ein Eingang. Für den ersten Versuch werden alle Pins von PORT A (GPA) als Ausgänge deklarieren. Hierfür ist die Adresse IODIRA zuständig:

i2cset -y 1 0x20 0x00 0x00
OLATA oder auch GPIOA sind nun für das Schalten der Ausgänge zuständig. Also wird das LSB von PORT A über OLATA gesetzt:
i2cset -y 1 0x20 0x14 0x01
Es sollte nun eine LED auf dem Board leuchten. Das Auslesen von beispielsweise dem Port B erfolgt mit i2cget:
i2cget -y 1 0x20 0x15
255
Auf diese Weise können Sie nun etwas weiter spielen. Wenn Sie genug haben, kann es an das Programmieren gehen. Noch ein Hinweis: Beim Programmieren mit Python muss ggegenenfalls das Modul python-smbus nachinstalliert werden mit:
sudo apt-get install python-smbus

Programmierung in Python

Das erste Python-Programm verwendet vom Port A nur das Bit 7 als Eingang für eine Taste. Wird sie gedrückt, blinken nacheinander alle acht am Port B angeschlossenen LEDs. Für die Verzögerungszeit brauct man neben der smbus_Bibliothek noch time.

import smbus
import time

bus = smbus.SMBus(1) # Pi Rev 2

DEVICE = 0x20 # Device Adresse (A0-A2)
IODIRA = 0x00 # Pin Register fuer die Richtung
IODIRB = 0x01 # Pin Register fuer die Richtung
OLATB = 0x15  # Register fuer Ausgabe (GPB)
GPIOA = 0x12  # Register fuer Eingabe (GPA)

# Definiere GPA-Pin 7 als Input (10000000 = 0x80)
# (0 bedeutet Output, 1 bedeutet Input)
bus.write_byte_data(DEVICE,IODIRA,0x80)

# Definiere alle GPB-Pins als Output (00000000 = 0x00)
# und setze alle Output bits auf 0
bus.write_byte_data(DEVICE,IODIRB,0x00)
bus.write_byte_data(DEVICE,OLATB,0)

# Funktion, die alle LEDs aufleuchten laesst.
def blink():
  for Count in range(0,8):
    # nacheinader alle LEDs blitzen lassen
    LED = 1 << Count
    bus.write_byte_data(DEVICE,OLATB,LED)
    time.sleep(1)
    # LED ausschalten
    bus.write_byte_data(DEVICE,OLATB,0)

# Schleife, die auf Tastendruck wartet und dann Action macht
while True:
  # Status von GPIOA Register auslesen
  Taste = bus.read_byte_data(DEVICE,GPIOA)
  if (Taste & 0x80) > 0:
    blink()

Das folgende Python-Programm macht das Setzen, Rücksetzen und Abfragen von Portleitungen über die Kommandozeile einfacher als mit i2cset und i2sget. Dazu wird das Python-Programm mit drei Optionen aufgerufen, wobei die unten angegebenen Reihenfolge einzuhalten ist:

expander.py -b <bank> -o <output> -s <state>'
Dabei sind folgende Angaben möglich:
#!/usr/bin/python
# Einfaches Python Kommandozeilen-Tool fuer den MCP23017 I2C IO Expander

import smbus
import sys
import getopt
 
# RasPi Revision 1: bus = smbus.SMBus(0) 
# RasPi Revision 2: bus = smbus.SMBus(1)
bus = smbus.SMBus(1) 

# I2C address of MCP23017
address = 0x20

bus.write_byte_data(address,0x00,0x00) # Set all of bank A to outputs 
bus.write_byte_data(address,0x01,0x00) # Set all of bank B to outputs 

# Let them know how it works
def usage():
   print 'Usage: mcp23017.py -b <bank> -o <output> -s <state>'
   print 'bank ::= a|b; output ::= 0|1|2|3|5|6|7; state ::= 0|1|r;'

# Handle the command line arguments
def main():
   try:
      opts, args = getopt.getopt(sys.argv[1:],"hb:o:s:",["bank=","output=","state="])

      if not opts:
        usage()
        sys.exit(2)

   except getopt.GetoptError:
      usage()
      sys.exit(2)

   for opt, arg in opts:
      if opt == '-h':
         usage()
         sys.exit()
      elif opt in ("-b", "--bank"):
         bank = arg
      elif opt in ("-o", "--output"):
         output = int(arg)
      elif opt in ("-s", "--state"):
         state = arg

# Set the correct register for the banks
   if bank == "a" :
    register = 0x12
   elif bank == "b" :
    register = 0x13
   else:
    print "Error! Bank must be a or b"
    sys.exit()

# Read current values from the IO Expander
   value =  bus.read_byte_data(address,register) 

# Shift the bits for the register value, checking if they are already set first
   if state == "1":
    if (value >> output) & 1 :
     print "Output GP"+bank.upper()+str(output), "is already high."
     sys.exit()
    else:
      value += (1 << output)
   elif state == "0":
    if (value >> output) & 1 :
     value -= (1 << output)
    else:
     print "Output GP"+bank.upper()+str(output), "is already low."
     sys.exit()
   elif state == "r":
    if (value >> output) & 1 :
     print "high"
    else:
     print "low"
    sys.exit()
   else:
    print "Error! state must be r, 1 or 0"
    sys.exit()

# Now write to the IO expander
   bus.write_byte_data(address,register,value)
 
# Tell them what we did
   print "Output GP"+bank.upper()+str(output), "changed to", state

if __name__ == "__main__":
   main()

Programmierung in C

Für den Zugriff auf das Expander-Board wurde eine einfache Library, bestehend aus C- und Headerdatei erstellt. In der Headerdatei wird zuerst abgefragt, ob der Name des Headers bereits existiert. Wenn nein, wird der Inhalt der Headerdatei eingesetzt. Andernfalls springt der Präprozessor direkt zum Ende der Headerdatei. Es kann nämlich vorkommen, dass eine Headerdatei mehrfach eingebunden wird (mehrere Include-Anweisungen in verschiedenen Quelldateien). Durch dieses Standardkonstrukt werden Duplikate (und damit Compiler-Fehlermeldungen) vermieden.

In der Headerdatei (Endung ".h") werden alle Konstanten, globale Variablen und Funktionsprototypen der Bibliothek aufgeführt. Das folgende Listign dient auch gleichzeitig als Dokumentation der Funktionen. Für den Expander-Datentyp wurde eine Struktur entworfen, die alle wichtigen Informationen aufnimmt, die I2C-Bus-Adresse des Bausteines, die Datenrichtung für bei de Ports und den Devicenamen des I2C-Device ("/dev/i2c-1" für Bus 1). Diese Struktur wird von allen Funktionen verwendet.

#ifndef _EXPANDERLIB_H_
#define _EXPANDERLIB_H_

/* Richtungsregister von Port A und Port B */
#define IODIRA 0x00
#define IODIRB 0x01

/* Datenregister Port A und Port B */
#define GPIOA 0x12
#define GPIOB 0x13

/* Register um Logik-Polaritaet umzustellen */
#define IPOLA 0x02
#define IPOLB 0x03

/* Interne Pull-Up-Widerstände einschalten */
#define GPPUA 0x0C
#define GPPUB 0x0D

/* Pins am Port */
#define P1 0x01
#define P2 0x02
#define P3 0x04
#define P4 0x08
#define P5 0x10
#define P6 0x20
#define P7 0x40
#define P8 0x80


/* Struktur fuer den Expander-Datentyp */
struct expander
  {
  int address;      /* I2C-Bus-Adresse des Bausteines      */
  int directionA;   /* Datenrichtung Port A                */
  int directionB;   /* Datenrichtung Port B                */
  char* I2CBus;     /* I2C-Device ("/dev/i2c-1" für Bus 1) */
  };
/* Expander-Datentyp */
typedef struct expander mcp23017;

/* Init des Expanders; gibt den Expander zurück
 * address: I2C-Busadresse des Bausteines (i2cdetect -y 1)
 * directionA/B: Richtungen der Ports
 * I2CBus: Pfad zum I2CBus ("/dev/i2c-1" für Bus 1)
 */
mcp23017 init_mcp23017(int address, int directionA, int directionB, char* I2CBus);

/* Datenrichtung der Ports festlegen
 * richtungsregister: muss "IODIRA" oder "IODIRB" sein!
 * value: Zuordnung der Bits (Input: 1, Output: 0)
 * Bei den Eingangspins wird der Pullup-Widerstand eingeschaltet und die Logik umgekehrt
 */
void setdir_mcp23017(mcp23017 expander, int richtungsregister, int value);

/* Oeffnet den Bus und gibt Filedescriptor zurueck
 * (write_mcp23017 und read_mcp23017 übernehmen das selbst)
 */
int open_mcp23017(mcp23017 expander);

/* Schreibt in ein Register des Expanders
 * reg: Register in das geschrieben werden soll
 * value: Byte das geschrieben werden soll
 */
void write_mcp23017(mcp23017 expander, int reg, int value);

/* Liest Register des Expanders
 * reg: Register, das ausgelesen wird;
 * gibt ausgelesenen Registerwert zurück
 */
int read_mcp23017(mcp23017 expander, int reg);

#endif /* EXPANDERLIB_H */

Der Code dazu befindet sich in einer C-Quelldatei gleichen Namens (Endung ".c"). Diese kann einfach beim Compilieren des eigentlichen Programms mit angegeben werden (siehe unten). Es ist aber auch möglich, eine Objectdatei zu erzeugen (Endung ".o"), die dann zum erzeugten Binärcode des eigentlichen Programms hinzugelinkt werden kann.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


#include "expander_lib.h"

/* defined in <linux/i2c-dev.h>
#define I2C_SLAVE 0x703 */


/* Init des Expanders; gibt den Expander zurück
 * address: I2C-Busadresse des Bausteines (i2cdetect -y 1)
 * directionA/B: Richtungen der Ports
 * I2CBus: Pfad zum I2CBus ("/dev/i2c-1" für Bus 1)
 */
mcp23017 init_mcp23017(int address, int directionA, int directionB, char* I2CBus)
  {
  int fd;             /* Filehandle */
  mcp23017 expander;  /* Rueckgabedaten */

  /* Structure mit Daten fuellen */
  expander.address = address;
  expander.directionA = directionA;
  expander.directionB = directionB;
  expander.I2CBus = I2CBus;

  // Port-Richtung (Eingabe/Ausgabe) setzen
  fd = open_mcp23017(expander);
  setdir_mcp23017(expander, IODIRA, expander.directionA);
  setdir_mcp23017(expander, IODIRB, expander.directionB);
  close(fd);
  return expander;
  }

/* Datenrichtung der Ports festlegen
 * richtungsregister: muss "IODIRA" oder "IODIRB" sein!
 * value: Zuordnung der Bits (Input: 1, Output: 0)
 * Bei den Eingangspins wird der Pullup-Widerstand eingeschaltet und die Logik umgekehrt
 */
void setdir_mcp23017(mcp23017 expander, int richtungsregister, int value)
  {
  if(richtungsregister == IODIRA)
    {
    /* Datenrichtung schreiben */
    write_mcp23017(expander, IODIRA, value);
    /* Pull-Up-Widerstaende einschalten Port A */
    write_mcp23017(expander, GPPUA, value);
    /* Logik umkehren */
    write_mcp23017(expander, IPOLA, value);
    }
  else if(richtungsregister == IODIRB)
    {
    /* Datenrichtung schreiben */
    write_mcp23017(expander, IODIRB, value);
    /* Pull-Up-Widerstaende einschalten Port B */
    write_mcp23017(expander, GPPUB, value);
    /*Logik umkehren */
    write_mcp23017(expander, IPOLB, value);
    }
  else
    {
    printf("Richtungsregister falsch!\n");
    exit(1);
    }
  }

/* Oeffnet den Bus und gibt Filedescriptor zurueck
 * (write_mcp23017 und read_mcp23017 übernehmen das selbst)
 */
int open_mcp23017(mcp23017 expander)
  {
  int fd;
  if ((fd = open(expander.I2CBus, O_RDWR)) < 0)
    {
    printf("Failed to open the i2c bus\n");
    exit(1);
    }

  /* Spezifizieren der Adresse des slave device */
  if (ioctl(fd, I2C_SLAVE, expander.address) < 0)
    {
    printf("Failed to acquire bus access and/or talk to slave\n");
    exit(1);
    }
  return fd;
  }

/* Schreibt in ein Register des Expanders
 * reg: Register in das geschrieben werden soll
 * value: Byte das geschrieben werden soll
 */
void write_mcp23017(mcp23017 expander, int reg, int value)
  {
  int fd;
  fd = open_mcp23017(expander);
  if(i2c_smbus_write_byte_data(fd,reg,value) < 0)
    {
    printf("Failed to write to the i2c bus\n");
    exit(1);
    }
  close(fd);
  }

/* Liest Register des Expanders
 * reg: Register, das ausgelesen wird;
 * gibt ausgelesenen Registerwert zurück
 */
int read_mcp23017(mcp23017 expander, int reg)
   {
   int value,fd;
   fd = open_mcp23017(expander);
   if((value = i2c_smbus_read_byte_data(fd, reg)) < 0)
     {
     printf("Failed to read from the i2c bus\n");
     close(fd);
     exit(1);
     return 0;
     }
   else
     {
     close(fd);
     return value;
     }
  }
Wenn es so einfach ist wie hier, wo nur zwei C-Quellen angegeben werden müssen, muss kein Makefile und keine Objectdatei erzeugt werden, es genügt ein einfacher Compileraufruf:
gcc -Wall -o lauflicht lauflicht.c expander_lib.c

Das Lauflicht selbst bedarf an sich keiner Beschreibung mehr. Man kennt es aus der Fernsehserie der 80er Jahre, wo ein Auto die Hauptrolle spielte (und ein Sänger die Nebenrolle). Das Programm soll nur zeigen, wie die oben aufgeführte Bibliothek eingebunden werden kann:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "expander_lib.h"

int main(int argc, char *argv[])
  {
  mcp23017 expander;  /* Verwaltungs-Structure */
  int data = 0x01;    /* Ausgabewert */
  int down = 0;       /* Richtungsangabe */

  expander = init_mcp23017(0x20,0,0,"/dev/i2c-1");

  while(1)
    {
    /* beide Ports identisch "bedienen" */
    write_mcp23017(expander,GPIOA,data);
    write_mcp23017(expander,GPIOB,data);

    if (data == 0x80) /* ganz links - umdrehen */
      down = 1;
    if (data == 0x01) /* ganz rechts - umdrehen */
      down = 0;

    if (down)
       data = data >> 1;
    else
       data = data << 1;
    usleep(100000); /* 100 ms Pause */
    }

  return 0;
  }

Sollen anstelle der LEDs gleich größere Lasten, beispielsweise Relais, geschaltet werden, könnte man das Layout des Boards ändern uns statt der LED-Treiber Bausteine des Typs ULN2803 einsetzen, die dann bis zu 0,5 A schalten können. Man kann aber auch eine fertige Relaiskarte (z. B. den Pollin-Bausatz "PC-Relaiskarte K8IO", Bestellnummer: 710722, 12,95 Euro) an das Expander-Board anschließen.

Programmierung in C mit WiringPi

Bei WiringPi gibt es nicht nur eine Bibliothek für den I2C-Bus, sondern auch für den MCP23017. Sie müssen nur die beiden folgenden Includes in Ihr Programm aufnehmen:

#include <wiringPi.h>
#include <mcp23017.h>
Die Initialisierung erfolgt am Programmanfang durch:
wiringPiSetup();
mcp23017Setup(int pinBase, int i2cAddress);
Der Wert der Variablen pinBase kann eine beliebige Zahl größer 64 sein und die I2C-Adresse (i2cAddress) ist die Adresse des MCP23017 auf dem Bus (in der Regel 0x20, je nach Adresseinstellung). Wenn Sie beispielsweise die pinBase = 100 setzen, dann entsprechen im Programm die Pins 100 bis 107 dem Bits 0 bis 7 von Port A und die Pins 108 bis 115 den Bits 0 bis 7 von Port B.

Das folgende Programm gibt eine 8-Bit-Binärzahl aus und pausiert, wenn die Taste am MSB von Port B gedrückt wird. Die ersten 8 Pins sind als Ausgänge konfiguriert und der letzte ist ein Eingang mit aktiviertem internen Pull-up-Widerstand (die Taste schliesst gegen Masse).

#include <stdio.h>
#include <wiringPi.h>
#include <mcp23017.h>

/* Pin-Base */
#define BASE 100

/* Baustein-Adresse */
#define I2CADDR 0x20

int main (void)
  {
  int i, bit;

  wiringPiSetup();
  mcp23017Setup (BASE, I2CADDR);

  /* Ausgabepins definieren */
  for (i = 0; i < 8; ++i)
    pinMode (BASE + i, OUTPUT);

  /* Eingangspin definieren */
  pinMode         (BASE + 15, INPUT);
  pullUpDnControl (BASE + 15, PUD_UP);

  /* Endlosschleife */
  for (;;)
    {
    /* Ausgabe 8 Bit */
    for (bit = 0; bit < 8; ++bit)
      digitalWrite (BASE + bit, (1 << bit));
    delay (1);
    while (digitalRead (BASE + 15) == 0)
      /* Taste gedrueckt */
      delay (1);
    }
  return 0;
  }

Links


Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung: