GPIO beim Raspberry Pi

Prof. Jürgen Plate

GPIO beim Raspberry Pi per Programm ansteuern

im Abschnitt Raspberry Pi: GPIO per Shell-Kommando ansteuern wurde nicht nur gezeigt, wie man mit Hilfe des Pseudo-Verzeichnisses /sys/class/gpio/ auf die GPIO-Pins zugreifen kann, sondern auch der "General Purpose Input Output" besprochen und die Steckerbelegung aufgelistet. Gegebenfalls müssen Sie dort nachsehen. Im Grunde kann man die dort besprochenen Methoden direkt aus einem in C oder einer anderen Programmiersprache geschriebenen Programm nutzen, indem die entsprechenden Dateizugriffe in der jeweiligen Sprache realisiert werden. Neben dieser Methode, di gleich anschließend behandelt wird, können aber auch diverse Bibliotheken eingesetzt werden, die das Leben erleichtern.

Achtung: Trotz des Kommandos chmod 666 ... wird kein Zugriffsrecht für others zugelassen. Wenn andere Benutzeraccounts als der Standarduser "pi" auf die GPIO-Pins zugreifen sollen, müssen Sie zusätzlich in die Gruppe gpio eingetragen werden:

sudo usermod -a -G gpio <Username>

Außer Konkurrenz: Python

Nicht umsonst hat das Bard den Nachnamen "PI", was für "Python" steht. Daher ist ein entsprechendes Bibliothes-Modul, RPi.GPIO, per Default in Raspbian bereits installiert. Die Bibliothek muss in jedem Python-Programm importiert werden, in dem sie genutzt werden soll, z. B. durch import RPi.GPIO as GPIO . Auch hier gilt übrigens, dass die Python-Programme für den GPIO mit Root-Rechten laufen müssen - was bedeutet, dass nur der root-User sie ausführen kann. Bei Programmen in Script-Sprachen ist das Setzen des SUID-Bits übrigens eine ganz schlechte Idee, weil man so jedem Hacker die Tore weit öffnet. In einer Computerzeitschrift wurde daher die Idee geäußert, einen Wrapper in C zu schreiben, dessen Executable mit Root-Rechten versehen werden kann und seinerseits das Python-Programm aufruft. Abgesehen davon, dass das genauso unsicher ist: wenn man schon bei C angelangt ist, kann man gleich alles in C erledigen (siehe unten). Weil die Python-Programmierung in diversen Dokumenten und Büchern rund um den Raspberry Pi beschrieben ist, beschränke ich mich hier auf ein einfaches Beispiel, um das Prinzip aufzuzeigen:

import RPi.GPIO as GPIO

# use P1 header pin numbering convention
GPIO.setmode(GPIO.BOARD)

# Set up the GPIO channels - one input and one output
GPIO.setup(11, GPIO.IN)
GPIO.setup(12, GPIO.OUT)

# Input from pin 11
input_value = GPIO.input(11)

# Output to pin 12
GPIO.output(12, GPIO.HIGH)

# The same script as above but using BCM GPIO 00..nn numbers
GPIO.setmode(GPIO.BCM)

# Set up the GPIO channels - one input and one output
GPIO.setup(17, GPIO.IN)
GPIO.setup(18, GPIO.OUT)

# Input from pin 11
input_value = GPIO.input(17)

# Output to pin 12
GPIO.output(18, GPIO.HIGH)

# Clean um GPIO settings
GPIO.cleanup()
Anmerkung: Weil es immer wieder gefragt wird, soll hier der Unterschied bei der Pin-Bezeichnung klargestellt werden: Hier ein komplettes Programmbeispiel, das mit vier LEDs ein zufälliges Blinkmuster zeigt.
import RPi.GPIO as GPIO
import time              # fuer sleep()
import random            # fuer random()

# BCM-Bezeichnung der Pins verwenden
GPIO.setmode(GPIO.BCM)

LED = [17,27,22,18] # GPIOs fuer die LEDs
NumLED = len(LED)   # Anzahl aktiver LEDs
Repeat = 5          # Muster wird Repeat mal wiederholt
Ontime = 0.3        # Leuchtdauer der LED

# GPIOs auf Ausgang setzen und LED ausschalten
for i in LED:
  GPIO.setup(i, GPIO.OUT, initial=0)

# Zufaellig blinkern
while True:
  for i in range(Repeat*NumLED):
    j = random.randint(0,NumLED - 1)
    GPIO.output(LED[j], True)
    time.sleep(Ontime)
    GPIO.output(LED[j], False)

Viele Programm erledigen keine Aufräumarbeiten wenn sie mit Strg-C unterbrochen werden. Da die Pin-Einstellungen auch nach dem Programmende erhalten bleiben, kann es vorkommen, dass die benutzten Pins bei einer anderen Anwendung anschließend nicht funktioniert. Man sollte also beim Unterbrechen des Programms die Funktion GPIO.cleanup() auszuführen. Der Keyboard-Interrupt läßt sich nutzen, um das Programm ordnungsgemäß zu beenden, wie folgendes Beispiel zeigt:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(18,GPIO.OUT)

try:
  while True:
    GPIO.output(18,1)
    time.sleep(0.5)
    GPIO.output(18,0)
    time.sleep(0.5)

except KeyboardInterrupt:
  GPIO.cleanup()
  print "Bye"

In Python kann auch ein interner Pulldown eingeschaltet werden mit

# Pulldown-Widerstand (gegen Masse)
GPIO.setup(XX, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
                  oder
# Pullup-Widerstand (gegen + 3,3 V)
GPIO.setup(XX, GPIO.IN, pull_up_down = GPIO.PUD_UP)

Die Bibliothek enthält auch die Möglichkeit, beispielsweise eine LED mittels Pulsweiten-Modulation zu steuern. Dabei könnten Tastverhältnis und Frequenz geändert werden. Das erste Beispiel dimmt eine LED an GPIO 18 erst auf und dann ab. Als Frequenz wird hier 50 Hz genommen, damit keine Interferenzen mit der normalen elektrischen Beleuchtung entstehen.

import RPi.GPIO as GPIO
import time

# BCM-Bezeichnung der Pins verwenden
GPIO.setmode(GPIO.BCM)

LED = 18                  # GPIO 18 auf Ausgang setzen
GPIO.setup(LED, GPIO.OUT)

# PWM einschalten
pwm = GPIO.PWM(LED, 50); 
pwm.start(0)

try:
  while True:
    # langsam aufdimmen
    for c in range(0, 101, 2):
      pwm.ChangeDutyCycle(c)
      time.sleep(0.1)
    # langsam runterdimmen
    for c in range(100, -1, -2):
      pwm.ChangeDutyCycle(c)
      time.sleep(0.1)

# Abbruch durch Taste Strg-C
except KeyboardInterrupt:
  pwm.stop()
  GPIO.cleanup()
Das zweite Beispiel spielt etwas mit Tastverhältnis und Frequenz.
import RPi.GPIO as GPIO
import time

# BCM-Bezeichnung der Pins verwenden
GPIO.setmode(GPIO.BCM)

LED = 18                  # GPIO 18 auf Ausgang setzen
GPIO.setup(LED, GPIO.OUT)

# PWM einschalten 50% Tastverhaeltnis
pwm = GPIO.PWM(LED, 50);
pwm.start(50)
time.sleep(5.0)

try:
  while True:
    # change duty cycle to 10%
    pwm.ChangeDutyCycle(10.0)
    time.sleep(5.0)
    # change frequency to 10 Hz
    pwm.ChangeFrequency(10.0)
    time.sleep(5.0)
    # change duty cycle to 80%
    pwm.ChangeDutyCycle(80.0)
    time.sleep(5.0)
    # change frequency to 50 Hz
    pwm.ChangeFrequency(50.0)
    time.sleep(5.0)

# Abbruch durch Taste Strg-C
except KeyboardInterrupt:
  pwm.stop()
  GPIO.cleanup()
Das letzte Beispiel zeigt eine praktisch nutzbare Anwendung: Es wird eine flackernde Kerze mit einer LED simuliert.
import RPi.GPIO as GPIO
import time
from random import randrange

GPIO.setmode(GPIO.BCM)

LED = 18
GPIO.setup(LED, GPIO.OUT)

# PWM einschalten
pwm = GPIO.PWM(LED, 200)
pwm.start(100)
time.sleep(5.0)
  
while True:
  pwm.ChangeDutyCycle(randrange(0, 100))
  time.sleep(randrange(1, 10) * 0.01)

Weitere Dokumentation finden Sie unter

C und das /sys-Dateisystem

Mit dieser Methode werden die im Abschnitt Raspberry Pi: GPIO per Shell-Kommando ansteuern beschiebenen Methoden in C implementiert. Die GPIOs können, wie beschrieben, direkt über ein System virtueller Dateien angesprochen werden. Auch hier gilt: Dies kann jedoch nur mit root-Rechten erledigt werden. Das folgende Programm definiert die benötigten Funktionen und Konstanten. Als Beispiel verwende ich wieder das Lauflicht, so dass Sie Parallelen zwischen Shell- und C-Version ziehen können. Mit der Demo- bzw. Testplatine kann die Portbelegung sichtbar gemacht werden. Das Programm kann durch einen High-Pegel an GPIO-Pin 17 beendet werden (Taste auf der Testplatine):

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* Symbolische Namen fuer die Datenrichtung und die Daten  */
#define IN  0
#define OUT 1

#define LOW  0
#define HIGH 1

/* Datenpuffer fuer die GPIO-Funktionen */
#define MAXBUFFER 100

/* GPIO-Pin aktivieren
 * Schreiben der Pinnummer nach /sys/class/gpio/export
 * Ergebnis: 0 = O.K., -1 = Fehler
 */
int gpio_export(int pin)
  {
  char buffer[MAXBUFFER];    /* Output Buffer   */
  ssize_t bytes;             /* Datensatzlaenge */
  int fd;                    /* Filedescriptor  */
  int res;                   /* Ergebnis von write */

  fd = open("/sys/class/gpio/export", O_WRONLY);
  if (fd < 0)
    {
    perror("Kann nicht auf export schreiben!\n");
    return(-1);
    }
  bytes = snprintf(buffer, MAXBUFFER, "%d", pin);
  res = write(fd, buffer, bytes);
  if (res < 0)
    {
    perror("Kann Pin nicht aktivieren (write)!\n");
    return(-1);
    }
  close(fd);
  return(0);
  }

/* GPIO-Pin deaktivieren
 * Schreiben der Pinnummer nach /sys/class/gpio/unexport
 * Ergebnis: 0 = O.K., -1 = Fehler
 */
int gpio_unexport(int pin)
  {
  char buffer[MAXBUFFER];    /* Output Buffer   */
  ssize_t bytes;             /* Datensatzlaenge */
  int fd;                    /* Filedescriptor  */
  int res;                   /* Ergebnis von write */

  fd = open("/sys/class/gpio/unexport", O_WRONLY);
  if (fd < 0)
    {
    perror("Kann nicht auf unexport schreiben!\n");
    return(-1);
    }
  bytes = snprintf(buffer, MAXBUFFER, "%d", pin);
  res = write(fd, buffer, bytes);
  if (res < 0)
    {
    perror("Kann Pin nicht deaktivieren (write)!\n");
    return(-1);
    }
  close(fd);
  return(0);
  }

/* Datenrichtung GPIO-Pin festlegen
 * Schreiben Pinnummer nach /sys/class/gpioXX/direction
 * Richtung dir: 0 = Lesen, 1 = Schreiben
 * Ergebnis: 0 = O.K., -1 = Fehler
 */
int gpio_direction(int pin, int dir)
  {
  char path[MAXBUFFER];      /* Buffer fuer Pfad   */
  int fd;                    /* Filedescriptor     */
  int res;                   /* Ergebnis von write */

  snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/direction", pin);
  fd = open(path, O_WRONLY);
  if (fd < 0)
    {
    perror("Kann Datenrichtung nicht setzen (open)!\n");
    return(-1);
    }
  switch (dir)
    {
    case IN : res = write(fd,"in",2); break;
    case OUT: res = write(fd,"out",3); break;
    }
  if (res < 0)
    {
    perror("Kann Datenrichtung nicht setzen (write)!\n");
    return(-1);
    }
  close(fd);
  return(0);
  }

/* vom GPIO-Pin lesen
 * Ergebnis: -1 = Fehler, 0/1 = Portstatus
 */
int gpio_read(int pin)
  {
  char path[MAXBUFFER];         /* Buffer fuer Pfad     */
  int fd;                       /* Filedescriptor       */
  char result[MAXBUFFER] = {0}; /* Buffer fuer Ergebnis */

  snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin);
  fd = open(path, O_RDONLY);
  if (fd < 0)
    {
    perror("Kann vom GPIO nicht lesen (open)!\n");
    return(-1);
    }
  if (read(fd, result, 3) < 0)
    {
    perror("Kann vom GPIO nicht lesen (read)!\n");
    return(-1);
    }
  close(fd);
  return(atoi(result));
  }

/* auf GPIO schreiben
 * Ergebnis: -1 = Fehler, 0 = O.K.
 */
int gpio_write(int pin, int value)
  {
  char path[MAXBUFFER];      /* Buffer fuer Pfad   */
  int fd;                    /* Filedescriptor     */
  int res;                   /* Ergebnis von write */

  snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin);
  fd = open(path, O_WRONLY);
  if (fd < 0)
    {
    perror("Kann auf GPIO nicht schreiben (open)!\n");
    return(-1);
    }
  switch (value)
    {
    case LOW : res = write(fd,"0",1); break;
    case HIGH: res = write(fd,"1",1); break;
    }
  if (res < 0)
    {
    perror("Kann auf GPIO nicht schreiben (write)!\n");
    return(-1);
    }
  close(fd);
  return(0);
  }

/*
 * Delay (warten), Zeitangabe in Millisekunden
 */
int delay(unsigned long millis)
  {
  struct timespec ts;
  int err;

  ts.tv_sec = millis / 1000;
  ts.tv_nsec = (millis % 1000) * 1000000L;
  err = nanosleep(&ts, (struct timespec *)NULL);
  return (err);
  }

int main(void)
  {
  int pin, val;

  /* Enable GPIO Output Pins     */
  for (pin = 22; pin <= 25; pin++)
    { if (gpio_export(pin) < 0)
        return(1);
    }
  /* Set GPIO Output directions */
  for (pin = 22; pin <= 25; pin++)
    { if (gpio_direction(pin, OUT) < 0)
        return(2);
    }
  /* Enable GPIO Input Pin */
  gpio_export(17);
  /* Set GPIO Input direction */
  gpio_direction(17,IN);

pin = 22;
val = 0;
while(val == 0)
  {
  gpio_write(pin, LOW);
  pin++;
  if (pin == 26)
    { pin = 22; }
  gpio_write(pin, HIGH);
  delay(500);
  val = gpio_read(17);
  }

  /* Switch off */
  gpio_write(pin, LOW);

  /* Disable GPIO pins   */
  for (pin = 22; pin <= 25; pin++)
    { if (gpio_unexport(pin) < 0)
        return(1);
    }
  if (gpio_unexport(17) < 0)
    return(1);

  return(0);
  }

Das Programm wird mittels gcc -Wall -o led-lauf led-lauf.c übersetzt und kann dann vom root-User gestartet werden. Wird mit dem Kommando chmod u+s led-lauf das Binary mit dem SUID-Bit versehen, kann es auch von "Normalbenutzern" gestartet werden, ohne dass die Dateien unterhalb von /sys/class/gpio/ explizit Schreib- und Leserecht für die Benutzer erhalten müssen. Das ist ein Vorteil gegenüber den Script-Lösungen.

Außerdem ist die C-Variante um mehr als den Faktor 12 schneller als die Shell-Version. Wo bei der Shell-Lösung gerade mal ca. 3500 Portänderungen pro Sekunde erreichbar sind, schafft die C-Variante mehr als 40000 Pegelwechsel pro Sekunde. Das ist natürlich nur der Fall, wenn nur vom GPIO gelesen wird. Je nach Verarbeitungsaufwand der Daten sinkt natürlich auch die maximal erreichbare Lesefrequenz.

Die C-Bibliothek für GPIO

Macht man aus den oben beschriebenen Funktionen eine kleine Library mit Header-Datei, wird das Programm selbst auch relativ kurz und übersichtlich. Im Programm wird dann nur die Headerdatei mittels #include "gpiolib.h" eingelesen. Das Compilieren erfolgt mit z. B. gcc -Wall -o led-lauf led-lauf.c gpiolib.c. Die Bibliothek ist unter folgenden Links abrufbar:

Flanken am GPIO erkennen

Bisher wurden immer nur die Pegel an den GPIO-Pins abgefragt - sogenanntes Polling. Oft soll aber auf einen Flanke, also einen Pegelwechsel, gewartet werden. Mit den oben aufgeführten Funktionen ginge das nur durch ständige Abfrage eines Pins. Wenn Sie so ein Programm schreiben und in einer Schleife einen Pin immer wieder lesen, werden Sie bemerken, dass Ihr Programm nahezu die volle CPU-Leistung in Anspruch nimmt, was nicht so toll ist. Es wird also ein Flankendetektor gebraucht. Dass es so etwas geben muss, erkennt man daran, dass bei jedem GPIO neben "value" auch eine Pseudovariable namens "edge" (Engl. für Flanke) gibt. Diese kann mit drei verschiednen Werten belegt werden:

Ist die Flanke gesetzt, generiert der GPIO beim Pegelwechsel einen Iterrupt, der aber beim Betriebssystem-Kern landet. Mit Unterstützung der C-Funktion poll() kann im Anwenderprogramm auf eine Nachricht vom Betriebssystem warten lassen. Der Name "poll" ist insofern nicht ganz treffen, als das Betriebssystem den laufenden Prozess so lange schlafen legt, bis das Ereignis eingetroffen ist, was wesentlich ressourcenschonender ist, als das echte Polling im Anwenderprogramm. Programme, die nicht-blockierende I/O verwenden, Setzen oft die Systemaufrufe poll() oder select ein, um mehrere Datenströme zu überwachen. Beide arbeiten ähnlich: Mit ihrer Hilfe kann ein Prozess ermitteln, ob er ohne zu blockieren aus einer oder mehreren offenen Dateien lesen oder drin schreiben kann. Hier wird poll() verwendet, um beispielsweise auf Daten von der Pseudodatei /sys/class/gpio/gpio27/value zu warten. Beide Funktionen haben auch einen Timeout-Parameter, der es erlaubt unabhängig von der Eingabe nach einer vorgegebneen Zeit mit der Programmausführung fortzufahren.

Der Systemaufruf poll() besitzt drei Parameter:

#include <poll.h>
   ...
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
Der erste Parameter fds ist ein Array aus Strukturen, welche für jede zu überwachende Datei die notwendigen Daten enthalten. Die Strukturen sind recht einfach aufgebaut:
struct pollfd {
        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */
    };
events und revents sind Bitmasken, welche die zu erwartenden Ereignisse spezifizieren (siehe Manualpage). Der zweite Parameter nfds enthält die Anzahl der Array-Elemente und timeout die maximale Wartezeit in Millisekunden. Nach dem Aufruf wartet poll() auf das oder die gewünschten Ereignisse und kehrt mit entsprechenden Einträgen in fds[i].revents zurück. Tat sich in den Dateien nichts, liefert poll() eine Null zurück. Für timeout wird normalerweise ein positiver Wert übergeben. Ist timeout = 0, kehrt poll() sofort zurück, gegebenenfalls ohne Ergebnis. Wird für timeout ein negativer Wert angegeben, wartet poll() ohne Timeout auf auf ein Ereignis - schlimmstanfalls ewig.

Für die Flankenerkennung werden neben der Headerdatei poll.h noch zwie weitere Funktionen definiert:

Die Funktion gpio_edge() ähnelt stark den bisherigen Funktionen:

int gpio_edge(unsigned int pin, char edge)
  {
  char path[gpio_MAXBUF];    /* Buffer fuer Pfad   */
  int fd;                    /* Filedescriptor     */

  snprintf(path, gpio_MAXBUF, "/sys/class/gpio/gpio%d/edge", pin);

  fd = open(path, O_WRONLY | O_NONBLOCK );
  if (fd < 0)
    {
    perror("gpio_edge: Kann auf GPIO nicht schreiben (open)!\n");
    return(-1);
    }

  switch (edge)
    {
    case 'r': strncpy(path,"rising",8); break;
    case 'f': strncpy(path,"falling",8); break;
    case 'b': strncpy(path,"both",8); break;
    case 'n': strncpy(path,"none",8); break;
    default: return(-2);
    }
  write(fd, path, strlen(path) + 1);
  close(fd);
  return 0;
  }

Die Funktion gpio_wait() öffnet den angegebenen GPIO-Value zum nicht blockierenden Lesen. Das Steuer-Array für poll() hat nur ein Element (man hätte auch eine "normale" Variable nehmen können, aber vielleicht wollen Sie ja später auch mehrere GPIO-Pins abfragen). Es wird nun mit den notwendigen Daten versehen: zum Einen das Device-Handle und zm Anderen der gewünschte Event, der in diesem Fall durch die Konstante POLLPRI repräsentieret wird. POLLPRI bedeutet, dass es sich nicht um einen Standardlesevorgang handelt, sondern ein davon abweichendes Ereignis (die Manualpage sagt hier : "... there is urgent data to read ...") - eben die Flanke. Vor dem Aufruf von poll() müssen eventuell noch am GPIO anstehende Daten eliminiert werden, sonst würde poll() sofort beendet und "alte" Info liefern. Dies geschieht durch lseek() und read().

Nach dem Aufruf von poll() geht es entweder weiter, wie der Timeout abgelaufen ist oder weil eien Flanke auftrat. Neben der Fehlerbehandlung sind die Werte 0 oder der Wert des GPIO (größer 0) interessant. Da der Pin nur 0 oder 1 liefern kann, die 0 aber schon für "Timeout" steht, wurde hier zum Portwert 1 addiert (1 + atoi(buf)) - so erhält man 1 für Port = 0 und 2 für Port = 1. Zum Abschluss (und bei den Fehler-Exits) wird der GPIO-Value wieder geschlossen und so frei gegeben.

int gpio_wait(unsigned int pin, int timeout)
  {
  char path[gpio_MAXBUF];    /* Buffer fuer Pfad   */
  int fd;                    /* Filedescriptor     */
  struct pollfd polldat[1];  /* Variable fuer poll() */
  char buf[gpio_MAXBUF];     /* Lesepuffer */
  int rc;                    /* Hilfsvariablen */

  /* GPIO-Pin dauerhaft oeffnen */
  snprintf(path, gpio_MAXBUF, "/sys/class/gpio/gpio%d/value", pin);
  fd = open(path, O_RDONLY | O_NONBLOCK );
  if (fd < 0)
    {
    perror("gpio_wait: Kann von GPIO nicht lesen (open)!\n");
    return(-1);
    }
  /* poll() vorbereiten */
  memset((void*)buf, 0, sizeof(buf));
  memset((void*)polldat, 0, sizeof(polldat));
  polldat[0].fd = fd;
  polldat[0].events = POLLPRI;
  /* eventuell anstehende Interrupts loeschen */
  lseek(fd, 0, SEEK_SET);
  rc = read(fd, buf, gpio_MAXBUF - 1);

  rc = poll(polldat, 1, timeout);
  if (rc < 0)
    { /* poll() failed! */
    perror("gpio_wait: Poll-Aufruf ging schief!\n");
    close(fd);
    return(-1);
    }
  if (rc == 0)
    { /* poll() timeout! */
    close(fd);
    return(0);
    }
  if (polldat[0].revents & POLLPRI)
    {
    if (rc < 0)
      { /* read() failed! */
      perror("gpio_wait: Kann von GPIO nicht lesen (read)!\n");
      close(fd);
      return(-2);
      }
    /* printf("poll() GPIO %d interrupt occurred: %s\n", pin, buf); */
    close(fd);
    return(1 + atoi(buf));
    }
  close(fd);
  return(-1);
  }

Das Testprogramm ist kurz und knackig. Es stützt sich auf die Bibliothek und richtet zunächst den GPIO27 als Eingabe mit fallender Flanke ein. Danach wird in einer Schleife auf die Flanke oder einen Timeout gewartet und die entsprechende Info ausgegeben. Für das Beenden des Programms mit STRG-C wird noch ein Signalhandler hinzugefügt (siehe C-Skript, Signale). Wenn die Signal-Bearbeitungsroutine finish() aufgerufen wird, gibt sie eine Meldung aus und setzt danach den Wert der Variablen loop auf 0. Gegebenenfalls kommen noch weitere Meldungen, weil poll() abgebrochen wurde.

/* Compile with: gcc -Wall -o edge edge.c gpiolib.c */

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

#include "gpiolib.h"

/* globale Variable - zum Beenden der Schleife in main */
int loop = 1;

/* Signalhandler fuer STRG-C */
void  finish(int sig)
  {
  printf("Signal %d empfangen. Programm wird beendet.\n", sig);
  loop = 0;
  }

/* ZUM TESTEN: Flanke an GPIO 27 = Pin 13  erkennen */
int main(void)
  {
  int ret;
  struct sigaction sig_struct;

  /* Signalhandler fuer STRG-C einrichten */
  sig_struct.sa_handler = finish;
  sigemptyset(&sig_struct.sa_mask);
  sig_struct.sa_flags = 0;
  sigaction(SIGINT,&sig_struct,NULL);

  /* GPIO konfigurieren */
  gpio_export(27);
  gpio_direction(27, IN);
  gpio_edge(27, 'f');

  /* Flankenerkennung */
  while(loop)
    {
    ret = gpio_wait(27, 5000);
    if(ret < 0)
      printf("*** Error poll(): %d\n", ret);
    else if(ret == 0)
      printf("*** Timeout\n");
    else
      printf("*** Edge detected: %d\n", ret);
    }
  gpio_unexport(27);
  return 0;
  }

Für einen Probelauf schließen Sie am GPIO27 einen Pullup-Widerstand (10 kΩ bis 100 kΩ) gegen 3,3 V und einen Taster gegen Masse an. Jeder Tastendruck liefert meist gleich mehrere Flanken, weil der Taster prellt (und deshalb bekommen Sie auch 1 und 2 zurück). Die Bearbeitung innerhalb von poll() dauert übrigens ca. 60 - 100 Mikrosekunden. Immerhin geht nun kein Pegelwechsel am Eingang verloren. Ein Praxisanwendung wäre z. B. ein Geigerzähler, der ja immer nur sehr kurze Impulse liefert. Mit gpio_wait() können nun die Impulse für einen bestimmten Zeitraum gezählt werden und damit hat man ein Mass für die Strahlung.

C und die bcm2835-Library

Mike McCauley hat eine C-Library geschrieben, die nach dem Baustein benannt ist: "bcm2835". Sie bietet zahlreichen Funktionen für die Ansteuerung des GPIO in C, darunter auch solche, die nicht über das Sys-Dateisystem erreichbar sind. Die Bibliothek kann von www.airspayce.com/mikem/bcm2835/ heruntergeladen werden. Danach erfolgt die Compilierung und Installation mit den folgenden Kommandos (die aktuelle Version wird mit bcm2835-1.xx.tar.gz bezeichnet:

# Archiv entpacken
tar zxvf bcm2835-1.xx.tar.gz
# ins Verzeichnis wechseln
cd bcm2835-1.xx
# Uebersetzen
./configure
make
# kurzer Test
sudo make check
# installieren
sudo make install

Danach kann die Bibliothek verwendet werden. Im Quellcode muss die Headerdatei mit #include <bcm2835.h> eingebunden werden. Beim Compilieren mit dem gcc wird die Library angegeben, indem man an die Kommandozeile ein "-l bcm2835" anhängt (siehe Beispiel unten). Die Bibliothek ändert natürlich nichts an der Tatsache, dass das Programm nur mit root-Rechten läuft. Das Beispiel von oben ist schnell umgeschrieben:

/* Compilieren: gcc -Wall -o led_bcm led_bcm.c -l bcm2835 */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <bcm2835.h>

int main(void)
  {
  uint8_t pin, val;  /* Pinnummer und Ergebniswert */

  /* Zum Testen kann man den folgenden Funktionsaufruf einbauen.
   * In diesem Fall wird nicht auf den GPIO zugegriffen
   * bcm2835_set_debug(1);
   */

   /* Library initialisieren */
   if (!bcm2835_init()) return 1;

   /* Enable GPIO Output Pins (Export und Richtungswahl) */
   for (pin = 22; pin <= 25; pin++)
     bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_OUTP);

  /* Enable GPIO Input Pin  (Export und Richtungswahl)*/
  bcm2835_gpio_fsel(17, BCM2835_GPIO_FSEL_INPT);

  pin = 22;
  val = 0;

  /* Start Lauflicht */
  while(val == 0)
    {
    /* LED aus */
    bcm2835_gpio_write(pin, LOW);
    pin++;
    if (pin == 26) pin = 22;
    /* naechste LED an */
    bcm2835_gpio_write(pin, HIGH);
    /* 1/2 s warten */
    usleep(500 * 1000);
    /* Input abfragen */
    val = bcm2835_gpio_lev(17);
    }

  /* Switch off */
  bcm2835_gpio_write(pin, LOW);

  return(0);
  }
Die bcm2835-Library verwendet in den mitgelieferten Beispielen für alle GPIO-Pins eigene Konstante, welche die Pinnummer der zweireihigen Steckerleiste adressieren (GPIO 17 ist dann beispielsweise RPI_GPIO_P1_11. Da ich mich in allen Beispielen auf die GPIO-Bezeichnungen beziehe, habe ich die Konstanten nicht im Programm verwendet.

Die Library bietet mehr Möglichkeiten als die Kommandozeile, so kann zum Beispiel der interne Pullup-Widerstand geschaltet werden. Das folgende Programmfragment zeigt, wie so etwas im Programm aussehen könnte:

switch (pullup) 
  {
  case PULL_UP:   bcm2835_gpio_set_pud(pin, BCM2835_GPIO_PUD_UP); break;
  case PULL_DOWN: bcm2835_gpio_set_pud(pin, BCM2835_GPIO_PUD_DOWN); break;
  case NO_PULL:   bcm2835_gpio_set_pud(pin, BCM2835_GPIO_PUD_OFF); break;
  default:        bcm2835_gpio_set_pud(pin, BCM2835_GPIO_PUD_OFF); break;
  }

Die Library ermöglicht sogar Pulsweitenmodulation (PWM) an einem der GPIO-Pins, und zwar am Pin 12 der Steckerleiste, GPIO-Pin 18. Dies ist übrigens der einzige verwendbare PWM-Ausgang. Er wird vom PWM-Channel 0 gesteuert. Compiliert wird mit gcc -Wall -o pwm pwm.c -l bcm2835; zum Starten muss man Root sein.

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <bcm2835.h>

int main(void)
  {
  int richtung = 1;  /* Richtungsinfo fuer PWM-Ausgabe */
  int data = 1;      /* PWM-Tastverhaeltnis */

  if (!bcm2835_init()) return 1;
  /* Output-Pin auf "Alternate Function 5" setzen, damit der PWM-Channel 0
   * dort ausgeben kann; Pin 18 ist der einzige auf der Steckerleiste
   * herausgefuehrte PWM-Pin
   */
  bcm2835_gpio_fsel(18, BCM2835_GPIO_FSEL_ALT5);

  /* Mit Taktteiler = 16, Range = 1024 und Markspace-Mode
   * betraegt die Puls-Wiederholfrequenz 1.2 MHz/1024 ~ 1,2 kHz
   */
  bcm2835_pwm_set_clock(16);
  bcm2835_pwm_set_mode(0, 1, 1);
  bcm2835_pwm_set_range(0, 1024);

  /* Tastverhaeltnis zwischen 1/1024 and 1023/1024 variieren */
  for (;;)
    {
    if (data <= 1)    richtung = +1;
    if (data >= 1023) richtung = -1;
    data = data + richtung;
    bcm2835_pwm_set_data(0, data);
    /* Debug-Ausgabe */
    printf("%d\n",data);
    /*  10 ms warten */
    usleep(10 * 1000);
    }
  /* hier kommen wir nie hin */
  bcm2835_close();
  return 0;
  }

Die umfangreiche Dokumentation und natürlich das Library-Paket finden Sie unter www.airspayce.com/mikem/bcm2835/.

C und die wiringPi-Library

Wer es lieber im Stil der Arduino-Programme hat, ist mit wiringPi gut bedient. Die Homepage des Projekts ist https://projects.drogon.net/raspberry-pi/wiringpi/. Um die Library zu installieren gehen Sie zur URL https://git.drogon.net/?p=wiringPi;a=summary und klicken in der obersten Zeile des Logs auf "snapshot". Damit könen Sie dann einen Tar-Ball der letzten Version herunterladen, beispilsweise die Datei wiringPi-f18c8f7.tar.gz (der Name ändert sich von Version zu Version). Zum Installieren wird das Paket ausgepackt und dann übersetzt (als root-User!):

tar xvfz wiringPi-f18c8f7.tar.gz
cd wiringPi-f18c8f7
./build
Ob alles geklappt hat, können Sie oberflächlich testen mit:
gpio -v
gpio readall
Das letzte Kommando liefert eine Liste der Bezeichnungen und Zustände aller Pins in wiringPi und auch gleich die verschiedenen Zuordnungen:
+----------+-Rev2-+------+--------+------+-------+
| wiringPi | GPIO | Phys | Name   | Mode | Value |
+----------+------+------+--------+------+-------+
|      0   |  17  |  11  | GPIO 0 | IN   | Low   |
|      1   |  18  |  12  | GPIO 1 | ALT5 | Low   |
|      2   |  27  |  13  | GPIO 2 | IN   | Low   |
|      3   |  22  |  15  | GPIO 3 | OUT  | Low   |
|      4   |  23  |  16  | GPIO 4 | OUT  | Low   |
|      5   |  24  |  18  | GPIO 5 | OUT  | Low   |
|      6   |  25  |  22  | GPIO 6 | OUT  | Low   |
|      7   |   4  |   7  | GPIO 7 | IN   | Low   |
|      8   |   2  |   3  | SDA    | IN   | High  |
|      9   |   3  |   5  | SCL    | IN   | High  |
|     10   |   8  |  24  | CE0    | IN   | Low   |
|     11   |   7  |  26  | CE1    | IN   | Low   |
|     12   |  10  |  19  | MOSI   | IN   | Low   |
|     13   |   9  |  21  | MISO   | IN   | Low   |
|     14   |  11  |  23  | SCLK   | IN   | Low   |
|     15   |  14  |   8  | TxD    | ALT0 | High  |
|     16   |  15  |  10  | RxD    | ALT0 | High  |
|     17   |  28  |   3  | GPIO 8 | IN   | Low   |
|     18   |  29  |   4  | GPIO 9 | IN   | Low   |
|     19   |  30  |   5  | GPIO10 | IN   | Low   |
|     20   |  31  |   6  | GPIO11 | IN   | Low   |
+----------+------+------+--------+------+-------+
Mit dem gpio-Programm kann die Ansteuerung der GPIOs auf der Kommandoebene erfolgen (oder per System-Aufruf aus diversen Programmiersprachen heraus - siehe unten).

Das Compilieren des Programms erfolgt wie oben, nur mit einer anderen Library: gcc -Wall -o xxx xxx.c -lwiringPi. Diesmal zur Abwechslung ein kurzes Programm, das bekannte blink.c:

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

int main (void)
  {
  /* Initialisierung der Bibliothek */
  if (wiringPiSetup() == -1) return 1;

  pinMode(3, OUTPUT);           /* BCM_GPIO pin 22 */

  for (;;)
    {
    digitalWrite(3, 1);       /* ein */
    usleep(500 * 1000);       /*  1/2 s warten */
    digitalWrite(3, 0);       /* aus */
    delay (500);              /*  1/2 s warten */
    }
  return 0;
  }

Das gpio-Programm

Das Programm gpio steht nach der Installation von wirigPi (siehe oben) zur Verfügung. Es sollte SUID root sein, sodass man keine root-Rechte benötigt, um auf den GPIO zuzugreifen. Neben dem reinen Portzugriff ermöglicht das Programm auch das Ansteuern verschiedener Zusatzboards und der SPI- und I2C-Schnittstelle. Eine Info über die Anwendung und die zahlreichen Parameter erhalten Sie mittels man gpio.

Sehr wichtig ist der Parameter -g, der dafür sorgt, dass nicht die Pinnummern von wiringPi zum Ansteuern genutzt werden, sondern die des BMC-Chips, die ich immer verwende. Wegen des Umfangs des Kommandos, soll an dieser Stelle nur ein kurzer Überblick erfolgen. Die allgemeine Form des Kommandos zum initialisieren der Pins ist:

gpio mode [-g] <Pin> <Modus>
<Pin> ist die Nummer des Pins auf der Steckleiste die in wiringPi festgelegt ist bzw, bei Anwendung von -g die BCM_Pinnummer. Der Parameter <Modus> legt das Verhalten des angesprochenen Pins fest: Kombiniert können die Angaben mit:

Zum Setzen eines Ausgabepins dient das write-Kommand:

gpio [-g] write <Pin> 0
gpio [-g] write <Pin> 1
Der PWM-Pin verträgt Werte zwischen 0 und 1023:
gpio [-g] pwm <Pin> <Wert>
Das Lesen eines Eingangs-Pins erfolgt mit dem read-Kommando:
gpio [-g] read <Pin>

GPIO-Zugriff mit Perl

Mit einer Script-Sprache wie Perl fühlt man sich auch in der Web-Umgebung wieder heimisch. Es eröffnet Ihnen die Möglichkeit, die GPIO-Zugriffe aus der Ferne über ein Webformular oder dergleichen zu tätigen - und damit sind Sie schon mitten drin im "Internet der Dinge". Es spricht aber nichts dagegen, sich mit etwas Mühe seine CGI-Programme auch in C zu schreiben (siehe Common Gateway Interface in C). Mehr zu CGI (Common Gateway Interface) in Perl finden Sie unter Perl und CGI.

Natürlich könnte man von Perl aus wie in der Shell das sys-Dateisystem nutzen und mit den Dateifunktionen von Perl die GPIO-Ports lesen und setzen. Das wäre aber ähnlich langsam wie die Shell-Zugriffe, was bei manchen Anwendungen auch nichts ausmachen sollte.

Dankenswerter Weise hat Mike McCauley auch ein Perl-Modul geschrieben, das sich auf die oben beschriebene C-Library stützt. Sie müssen zusätzlich zum Perl-Modul also auch die bcm2835-Library installieren. Dann wird das Modul Device::BCM2835 vom CPAN-Archiv heruntergeladen: search.cpan.org/~mikem/Device-BCM2835-1.0/lib/Device/BCM2835.pm. Diese Webseite enhält auch die Kurz-Dokumentation. In Grunde werden die C-Funktionen der Library ziemlich genau auf Perl-Funktionen abgebildet.

Für die Installation kann das CPAN-Kommando verwendet werden. Meist ist es aber bequemer den Tar-Ball zu entpacken und dann zu installieren:

tar xvfz Device-BCM2835-1.0.tar.gz
cd Device-BCM2835-1.0
perl Makefile.PL
make
make install

Das Perl-Beispiel realisiert, wie schon das vorhergehende, einen einfachen Blinker. Auch hier kommt nur der root-User in den Genuss, was bei CGI-Scripten ein Sicherheitsproblem darstellt:

use Device::BCM2835;
use strict;

# Fuer das Testen auf -Nicht-Pi-Hardware kann "set_debug()" verwendet werden
# Device::BCM2835::set_debug(1);

# Library initialisieren
Device::BCM2835::init() || die "Kann Library nicht initialisieren";

# Setze Pin 15 (GPIO 22) als Ausgabe-Pin
Device::BCM2835::gpio_fsel(&Device::BCM2835::RPI_GPIO_P1_15, 
                            &Device::BCM2835::BCM2835_GPIO_FSEL_OUTP);
while (1)
  {
  # LED an
  Device::BCM2835::gpio_write(&Device::BCM2835::RPI_GPIO_P1_15, 1);
  Device::BCM2835::delay(500); # 1/2 s Pause
  # LED aus
  Device::BCM2835::gpio_write(&Device::BCM2835::RPI_GPIO_P1_15, 0);
  Device::BCM2835::delay(500); # 1/2 s Pause
  }
Verglichen mit der Python-Version ist das Perl-Modul etwas schneller. Läßt man die delay_Aufrufe weg, erhält man ein Rechtecksignal von ca. 35 kHz.

GPIO-Zugriff mit PHP

PHP ist die inzwischen wohl häufigste Script-Sprache für Web-Anwendungen. Ein Modul bzw. eine Klassenbibliothek, die so maschinennah zugreifen wie C, habe ich bisher nicht gefunden. Die meisten PHP-Scripte machen es wie die Shell-Beispiele im Abschnitt Raspberry Pi: GPIO per Shell-Kommando ansteuern. Das hat zumindest den Vorteil, dass der Export der Pins und das Setzen der Zugriffsrechte für Normaluser per Start-Script in rc.local erfolgen kann.

Etliche Beispiele aus dem Netz machen es auch ganz pragmatisch und greifen auf die Shell-Ebene via shell_exec() zu, z. B.:

shell_exec("echo \"1\" > /sys/class/gpio/gpio$pin/value");
Haben Sie die gut versteckte Variable $pin entdeckt? Auch wenn es dann eine Mikrosekunde länger dauert, ziehe ich das Kommando gerne auseinander (Lesbarkeit/Wartbarkeit vor Schnelligkeit). Dabei verschwinden auch die Backslashes:
shell_exec('echo "1" > /sys/class/gpio/gpio' . $pin . '/value');

Ist wiringPi installiert kann anstelle von cat und echo das Kommando gpio verwendet werden. Dazu ein Beispiel (Der Klammeraffe @ vor dem Funktionsaufruf unterdrückt etwaige Fehlermeldungen.):

$val = trim(@shell_exec("/usr/local/bin/gpio -g read 23"));
$val = trim(@shell_exec("/usr/local/bin/gpio -g write 23 1"));
Schön und schnell ist das aber immer noch nicht. Schliesslich muss das Betriebssystem für shell_exe einen Kindprozess erzeugen, die Shell laden und dann das Kommando ausführen.

deshalb ist es sinnvoller, wie beim ersten C-Beispiel ganz oben, die Dateischnittstelle zu verwenden. Linux kommt uns ja mit seinem Konzept, das "alles Datei ist" sehr entgegen. Und PHP hat sehr kompakte und einfache Dateifunktionen, wobei eigentlich nur zwei Funktionen gebraucht werden, file_put_contents() und file_get_contents().

Für alle folgenden Funktionen wird eine globale Konstante mit dem Dateipfad eingerichtet. Zum Testen auf Nicht-Pi-Systemen kann man dann diesen Pfad leicht umbiegen:

define("GPIOPATH","/sys/class/gpio/");
Wenn davon ausgegangen wird, dass die benötigten Pins schon exportiert sind und daher die entsprechenden (Pseudo-)Dateien unterhalb von /sys bereits existieren, könnte man sofort loslegen. Zur Sicherheit, sollte das PHP-Script aber doch prüfen, ob die Pins wirklich verfügbar sind:
function gpio_is_exported($pin)
  // prueft, ob der Pin exportiert ist
  {
  return file_exists(GPIOPATH.'gpio'.$pin.'');
  }

function gpio_get_direction($pin)
  // gibt die eingestellte Richtung zurueck
  {
  $Dir = '';
  if (gpio_is_exported($pin))
    { $Dir = trim(file_get_contents(GPIOPATH.'gpio'.$pin.'/direction')); }
  return $Dir;
  }

function gpio_set_direction($pin, $direction)
  // Stellt die Richtung ein ($direction: 'in' oder 'out')
  {
  if (gpio_is_exported($pin)
       && ($direction == 'in' || $direction == 'out'))
    {
    file_put_contents(GPIOPATH.'gpio'.$pin.'/direction', $direction);
    return true;
    }
  else
    { return false; }
  }

Für die Ein- und Ausgabe von Pin-Werten kann man dann nach dem gleichen Schema die beiden folgenden Funktionen schreiben:

function gpio_get_value($pin)
  // gibt den augenblicklichen Wert (0/1) des Pins zurueck
  // oder einen leeren String im Fehlerfall
  {
  $Val = '';
  if (gpio_is_exported($pin) && gpio_get_direction($pin) == 'in')
    { $Val = trim(file_get_contents(GPIOPATH.'gpio'.$pin.'/value')); }
  return $Val;
  }

function gpio_set_value($pin, $value)
  // gibt den Wert in $value (0/1) auf dem Pin aus
  {
  if (gpio_is_exported($pin)
       && gpio_get_direction($pin) == 'out'
        && ($value == '0' || $value == '1'))
    {
    file_put_contents(GPIOPATH.'gpio'.$pin.'/value', $value);
    return true;
    }
  else
    { return false; }
  }
Mit diesen fünf Funktionen könnten Sie zwar Anwendungen für die GPIO-Pins schreiben, beim Einsatz als Webinterface spielen jedoch die Zugriffsrechte des Webserver-Users eine Rolle. Dieser User (meist www-data) hat wesentlich weniger Rechte als der Standard-User pi.

Siehe auch


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