Kampis Elektroecke

I/O Zugriff

Raspberry Pi

In diesem Artikel zeige ich euch wie ihr auf die einfachen GPIO Funktionen des SoC zugreifen könnt. Wir werden schauen wie man einen Eingang ausließt und einen Ausgang schalten kann um damit eine LED zum blinken zu bringen. Ausgangsbasis für diesen Artikel ist die Ansteuerung der GPIO auf elinux.org.

Los geht´s – Zugriff auf den internen Speicher:

Wer schon einmal einen Mikrocontroller programmiert hat wird wissen, dass sämtliche Funktionen eines Mikrocontrollers über Register gesteuert werden. Bei dem SoC auf dem Raspberry Pi ist das nicht anders. Für den Anfang versuchen wir mal den Pin 17 des Raspberry Pi zu setzen um eine LED einzuschalten. Laut dem Datenblatt sind diese Register für das Schalten eines IO zuständig:

  • GPIO Function Select
  • GPIO Pin Output Set
  • GPIO Pin Output Clear

Das Function Select-Register ist gleich mehrfach vorhanden. Eine Adresse deckt immer 10 GPIO ab. Bei den Set– und Clear-Registern werden die 53 I/O des SoC gleichmäßig aufgeteilt. Zusammengefasst müssen also für den GPIO 17 folgende Register gesetzt werden

  • GPIO Function Select 1
  • GPIO Pin Output Set 0
  • GPIO Pin Output Clear 0

Mit diesen Informationen kann man sich nun an die Arbeit machen und schauen wie die GPIO geschaltet werden. Die Register sind im physischen Speicher des Prozessors integriert und dieser physische Speicher ist über das Gerät /dev/mem ansprechbar. Jetzt muss einfach nur das selbe gemacht werden wie bei einem Mikrocontroller…

  1. Register adressieren
  2. Inhalt in das Register schreiben

Leider ist das bei einem Chip mit einem Betriebssystem nicht ganz so einfach, da der Kernel die Verwaltung des Speichers übernimmt. In diesem Fall muss man sich mit Hilfe der mmap-Funktion Zugriff auf den Speicher verschaffen. Mit Hilfe dieser Funktion kann man einen bestimmten Bereich im Speicher markieren, den man dann auslesen oder beschreiben kann.

Schauen wir uns das ganze mal an…

Als erstes lege ich mir eine Struktur an, welches als Speicher für den Peripheriezugriff dient:

struct BCM2835_Peripherie {
	unsigned long Adress;
	int Memory;
	void *Map;
	volatile unsigned int *Addr;
};

In diesem Struct sind alle notwendigen Variablen für den Speicherzugriff untergebracht:

Member Funktion
Address Hier wird die Adresse der zu beschreibenden Peripherie gespeichert.
Memory Der Filedescriptor für den Zugriff auf /dev/mem.
*Map Der Rückgabezeiger der mmap()-Funktion, der auf den markierten Speicherbereich zeigt.
*Addr Ein Zeiger, der nachher auf eine bestimmte Adresse innerhalb des markierten Bereiches zeigt.

Für einen Zugriff auf die GPIO lege ich eine Variable mit dem Namen GPIO vom Typ BCM2835_Peripherie an, sprich eine Kopie des eben angelegten Structs mit einem anderen Namen. In die Variable Adress dieser Kopie schreibe ich nun die Adresse des GPIO Controllers, welche am Anfang durch ein Define-Befehl definiert wurde:

#define Peripherie_Basis 0x20000000
#define GPIO_Basis (Peripherie_Basis + 0x200000)
struct BCM2835_Peripherie GPIO = {GPIO_Basis};

Wie ergeben sich die Adressen?

Auf S. 5 des Datenblattes erkennt man, dass die Startadresse der IO-Peripherals, also der I/O-Peripherie, bei 0x20000000 liegt.


Achtung:

Da man sich Zugriff auf den physischen Speicher verschafft, muss man auch auf die Adressen des physischen Speichers schauen! Die Beschreibung der GPIO findet man anschließend auf S. 89 des Datenblattes und auf S. 90 erkennt man, dass die erste Adresse des GPIO-Controllers bei 0x7E200000 liegt. Bei dieser Adresse handelt es sich um eine Bus-Adresse. Um diese Adresse mit einem Speicherzugriff ansprechen zu können muss die Bus-Adresse in eine physische Adresse umgerechnet werden!


Auf S. 5 des Datenblattes sieht man, dass die Bus-Adressen der IO-Peripherals bei 0x7E000000 startet. Der Rest ist einfaches rechnen…

  • Physische Startadresse: 0x20000000
  • Startadresse des Buses: 0x7E000000
  • Virtuelle Adresse der I/O-Peripherals: 0x7E200000
  • Differenz: 0x200000

Diese Differenz muss nun einfach auf die physische Startadresse drauf gerechnet werden und man erhält die Startadresse des GPIO-Controllers: 0x20200000.

Mit dem Befehl

struct BCM2835_Peripherie GPIO = {GPIO_Basis};

beschreibe ich jetzt die erste Stelle der Struktur BCM2835_Peripherie, also die Variable Adress, mit dem Wert aus dem Define GPIO_Basis, sprich der Startadresse des GPIO-Controllers. Jetzt kann der Speicherzugriff erfolgen, welcher über die unten gezeigte Funktion realisiert wird:

int RAM_Map(struct BCM2835_Peripherie *Peripherie)
{
   Peripherie->Memory = open("/dev/mem", O_RDWR | O_SYNC);

   if(Peripherie->Memory < 0)
   {
      printf("Oeffnen von /dev/mem fehlgeschlagen!\n");
      return -1;
   }

   Peripherie->Map = mmap(
      NULL,	
      BLOCK_SIZE,
      PROT_READ | PROT_WRITE,						
      MAP_SHARED,	
      Peripherie->Memory,
      Peripherie->Adresse
   );

   if(Peripherie->Map < 0)
   {
      printf("Mapping fehlgeschlagen!\n");
      return -1;
   }

   Peripherie->Addr = (volatile unsigned int *)Peripherie->Map;

   return 0;
}

Diese Funktion erwartet als Übergabeparameter die Adresse einer Struktur vom Typ BCM2832_Peripherie. Aus dieser Struktur bezieht die Funktion dann alle wichtigen Informationen für den Zugriff, bzw. legt die erhaltenen Informationen darin ab. Soll z. B. der Speicherzugriff für die GPIO realisiert werden, kann die Funktion wie folgt aufgerufen werden:

struct BCM2835_Peripherie GPIO = {GPIO_Basis};

if(RAM_Map(&GPIO) != 0)
{
	printf("Error!\n);
}

Als erstes wird ein Zugriff auf den Speicher des Prozessors hergestellt. Hierfür wird mit Hilfe des open-Systemcalls das Device /dev/mem geöffnet:

Peripherie->Memory = open("/dev/mem", O_RDWR | O_SYNC);
if(Peripherie->Memory < 0)
{
   printf("Oeffnen von /dev/mem fehlgeschlagen!\n");
   return -1;
}

Der zurückgegebene Filedescriptor wird in der Variable Memory der übergebenen Struktur gespeichert. Die anschließende if-Abfrage prüft dann noch ob der Zugriff erfolgreich war und gibt ggf. einen Fehler zurück. Nach einem erfolgreichen Zugriff wird der Speicherbereich markiert und der Rückgabezeiger, welcher auf den markierten Bereich zeigt, wird in dem Zeiger *Map des übergebenen Structs gespeichert:

Peripherie->Map = mmap(
	NULL,
	BLOCK_SIZE,
	PROT_READ | PROT_WRITE,
	MAP_SHARED,						
	Peripherie->Memory,					
	Peripherie->Adresse
);

Diese Funktion ist größtenteils aus dem Manual übernommen. Das einzige was angepasst werden muss ist der Filedescriptor (Peripherie->Memory) und die Adresse die markiert werden soll (Peripherie->Adresse). Danach wird der markierte Bereich nur noch in einen Integer Zeiger umgewandelt und in dem Zeiger *Addr der übergebenen Struktur gespeichert:

Peripherie->Addr = (volatile unsigned int *)Peripherie->Map;

Jetzt ist der Bereich markiert und man kann über den Zeiger *Addr des übergebenen Structs (z. B. GPIO.Addr) auf beliebige Adressen zugreifen. Dabei geht man immer von entsprechenden Startadresse aus, die in die Funktion RAM_Map übergeben wurde.

Beispiel:

#define Peripherie_Basis 0x20000000
#define GPIO_Basis (Peripherie_Basis + 0x200000)
struct BCM2835_Peripherie GPIO = {GPIO_Basis};	

RAM_Map(&GPIO);

Bei diesem Beispiel wird die Startadresse des GPIO-Controllers in der ersten Variable (Adress) des Struct GPIO gespeichert und die Adresse der Struktur anschließend in die Funktion RAM_Map übergeben. Die Funktion RAM_Map markiert nun den entsprechenden Speicherbereich und legt einen Adresszeiger an, welcher in der Struktur GPIO unter dem Zeiger *Addr verfügbar ist. Dieser Zeiger zeigt nun auf die Adresse 0x20200000, sprich der Adresse des GPIO-Controllers, welche jetzt als Adresse 0 angenommen werden kann. Auf diese Weise kann ein Zeiger für jede Peripherie (I2C, UART, SPI, etc.) innerhalb des Prozessors erstellt werden. Diese Zeiger zeigen dann immer auf die Startadressen der Peripherie und so kann jede Peripherie angesprochen werden.

Zugriff auf einen I/O:

Jetzt versuchen wir den GPIO 17 einzuschalten. Wie wir bereits wissen, müssen wir dafür als erstes das Register GPIO Function Select 1 beschreiben, damit wir den I/O als Ausgang deklarieren. Dieses Register hat laut Datenblatt die physische Adresse 0x20200004. Da wir die Adresse 0x20200000 als Startadresse definiert haben, könnte man nun meinen, dass man die Startadresse um 4 erhöhen muss um auf die richtige Registeradresse zu kommen. Dies ist aber so nicht richtig, da wir mit der Registeradresse nur einen Zeiger ansprechen und dieser nur von Register zu Register wandern kann. Da ein Register aber 4 Byte belegt (32-Bit System), besitzt das erste Register die Adresse 0, das zweite Register die Adresse 4, das dritte Register die Adresse 8, etc:

Zugriff_IO(2)
Die Adresse für das Register wäre also die Adresse GPIO.Addr + 1 und in dieses Register muss nun an der entsprechenden Stelle das Bitmuster 0x01 rein geschrieben werden um den Pin als Ausgang zu definieren:

Zugriff_IO(1)
Auf S. 92 erkennt man außerdem, dass der GPIO 17 die Bits 21 – 23 belegt, weswegen das Bitmuster an diese Stelle geschoben werden muss:

*(GPIO.Addr + 0x01) = 0x01 << 0x15;

Wichtig:

Da hier lediglich die Adresse um eins erhöht werden soll, darf das * vor dem Namen nicht vergessen werden.


Jetzt wird der Pin noch auf gesetzt um die LED zu aktivieren. Die Vorgehensweise ist exakt gleich, nur das es diesmal nur zwei Register gibt von denen jedes Bit für einen I/O steht:

*(GPIO.Addr + 0x07) = 0x01 << 0x11;

Jetzt muss nur noch die Schaltung aufgebaut werden und dann kann der Code getestet werden:

Zugriff_IO(3)
Zum testen wir das Programm kompiliert und anschließend gestartet:

$ gcc GPIO17.c -o GPIO17
$ ./GPIO17

Die LED sollte nun aufleuchten und wenn dies der Fall ist, habt ihr es geschafft die Hardware innerhalb des Prozessors direkt anzusprechen um einen I/O zu schalten.

Auslesen eines GPIO:

Da wir nun bereits wissen wie ein I/O geschaltet wird, wollen wir uns mal anschauen wie wir den Zustand eines GPIO einlesen können. Auch in diesem Beispiel nehme ich wieder den GPIO 17, nur das er dieses mal nicht als Ausgang, sondern als Eingang geschaltet wird. Als erstes muss das wieder das GPIO Function Select 1 Register beschrieben werden, nur das diese mal das Bitmuster 0x00 sein muss, d.h. die drei Bits müssen gelöscht werden:

*(GPIO.Addr + 0x01) &= ~(0x07 << 0x15);

Diese Codezeile schiebt das Bitmuster 0x07 um 21 Stellen nach links und invertiert es dann. Das Ergebnis ist dann folgendes Bitmuster:

00000000111000000000000000000000 → 11111111000111111111111111111111

Dieses Bitmuster wird nun über eine Und-Verknüpfung mit dem Registerinhalt verknüpft und das Ergebnis wird dann wieder in das Register geschrieben. Als Resultat bleiben alle Bits, außer die Bits für den GPIO 17, erhalten und die drei Bits für den GPIO 17 werden gelöscht (da die Und-Verknüpfung mit einer 0 immer 0 ist). Jetzt, da der Pin als Eingang deklariert wurde, kann der Status eingelesen werden, welcher im GPIO Pin Level-Register zu finden ist.

Ähnlich der Set– oder Clear-Register hat dieses Register wieder für jeden I/O ein Bit und jedes Bit repräsentiert den Status des jeweiligen I/O:

Zugriff_IO(4)
Auch von diesem Register gibt es zwei Stück und da wir den GPIO 17 auslesen wollen, müssen wir das erste Register auslesen:

Status = *(GPIO.Addr + 0x0D);

Jetzt ist in der Variable Status der aktuelle Registerinhalt gespeichert. Als nächstes wird das Bit für den GPIO 17 herausgefiltert. Dies geschieht wieder mit einer Und-Verknüpfung:

Status &= (0x01 << 0x11);

Im letzten Schritt schiebe ich das Bitmuster noch an den Anfang zurück, sodass ich je nach Status eine 0 oder 1 als Wert bekomme:

Status = Status >> 0x11;

Nun ist in der Variable Status der aktuelle Zustand des GPIO 17 gespeichert und kann in der Konsole ausgegeben werden:

printf("Status von GPIO 17: %d\n", Status);

3 Kommentare

  1. Hallo danke für den tollen Beitrag.
    Können auch die Interruptregister für das GPIO auf diese weise angesprochen bzw. konfiguriert werden?

    1. Hallo Michel,

      ja, theoretisch kannst du die so konfigurieren. Allerdings wird es nicht funktionieren, da du Interrupts beim Betriebssystem anmelden musst und dem Betriebssystem auch sagen musst was es dann tun soll (sprich eine ISR definieren).

      Gruß
      Daniel

  2. Hallo,
    ich mache ein kleines Projekt, in dem ich 21 LED’s ansteuern will.
    Geht das so direkt am Raspberry oder brauche ich weitere Elektronik ?
    Gruß Rainer

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert